Skip to content

Instantly share code, notes, and snippets.

@Tblue Tblue/svn-clean.py
Last active Jan 16, 2016

Embed
What would you like to do?
Lists and optionally removes untracked and/or ignored files in a SVN working copy.
#!/usr/bin/env python3
#
# List and optionally remove untracked and/or ignored files in a SVN working copy.
#
# Copyright (c) 2015-2016, Tilman Blumenbach
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import fnmatch
import os
import re
import shutil
import subprocess
import sys
from argparse import ArgumentParser
from xml.etree import ElementTree
FS_TYPE_DIRECTORY = os.sep
FS_TYPE_SYMLINK = '@'
FS_TYPE_UNKNOWN = ''
def fs_get_type(path):
if os.path.islink(path):
return FS_TYPE_SYMLINK
elif os.path.isdir(path):
return FS_TYPE_DIRECTORY
else:
return FS_TYPE_UNKNOWN
def get_untracked_files(targets, include_ignored, only_ignored, include_dirs):
if only_ignored:
include_ignored = True
status_data = subprocess.check_output(["svn", "st", "--xml", "--no-ignore", "--"] + targets)
status_xml = ElementTree.fromstring(status_data)
file_list = []
for entry in status_xml.findall("target/entry"):
path = entry.get("path")
status = entry.find("wc-status").get("item")
# Make sure paths start with either an empty path component (this is always the case for
# absolute paths) or a path component like "." or "..". We do this so that patterns like
# "*/dirname/*" also match directories which are directly contained in the target directory.
if not re.match(
r"(?:%s|%s)[%s%s]" % (
re.escape(os.curdir),
re.escape(os.pardir),
re.escape(os.sep),
re.escape(os.altsep) if os.altsep is not None else ""
),
path
):
path = os.path.join(os.curdir, path)
if (include_ignored and status == "ignored") or (not only_ignored and status == "unversioned"):
fs_type = fs_get_type(path)
if fs_type == FS_TYPE_DIRECTORY and not include_dirs:
continue
file_list.append((path, fs_type))
return file_list
def fnmatchcase_any(filename, patterns):
for pattern in patterns:
if fnmatch.fnmatchcase(filename, pattern):
return True
return False
def get_argparser():
argparser = ArgumentParser(
description="List and optionally remove untracked and/or ignored "
"files in a SVN working copy."
)
argparser.add_argument(
"-d", "--directories",
action="store_true",
help="Remove directories, too (and not just files)."
)
argparser.add_argument(
"-f", "--force",
action="store_true",
help="Actually remove the listed files."
)
argparser.add_argument(
"-n", "--dry-run",
action="store_true",
help="Do not remove files, just list them (default)."
)
argparser.add_argument(
"-x", "--ignored",
action="store_true",
help="Remove ignored files as well."
)
argparser.add_argument(
"-X", "--only-ignored",
action="store_true",
help="Only remove ignored files. Do not remove untracked files."
)
argparser.add_argument(
"-e", "--exclude",
action="append",
default=[],
metavar="PATTERN",
help="Do not remove files and directories matching the specified shell-style filename "
"pattern. May contain the usual wildcards `*', `?', `[seq]' and `[!seq]'. This "
"option can be specified multiple times to specify multiple exclusion patterns."
)
argparser.add_argument(
"path",
nargs="*",
default=[],
help="Paths to check for untracked/ignored files. Each path must be a working copy "
"(or a path inside a working copy). You can specify files here, but it makes more "
"sense to specify directories. If not given, the current working directory is used."
)
return argparser
def main():
args = get_argparser().parse_args()
if args.dry_run:
args.force = False
try:
file_list = get_untracked_files(args.path, args.ignored, args.only_ignored, args.directories)
except (OSError, subprocess.SubprocessError, ElementTree.ParseError) as e:
print("E: Could not get list of files: %s" % e, file=sys.stderr)
return 2
saw_files = False
for path, fs_type in file_list:
# Check if this path matches one of the exclusion patterns.
match_path = path
if fs_type == FS_TYPE_DIRECTORY:
# Directories should have a trailing slash so that they can be matched by exclusion
# patterns.
match_path += fs_type
if fnmatchcase_any(match_path, args.exclude):
# This path is excluded, skip it.
continue
saw_files = True
if args.force:
print("Removing %s%s" % (path, fs_type))
try:
if fs_type == FS_TYPE_DIRECTORY:
shutil.rmtree(path)
else:
os.unlink(path)
except (OSError, shutil.Error) as e:
print("E: Could not remove `%s': %s" % (path, e), file=sys.stderr)
return 3
else:
print("Would remove %s%s" % (path, fs_type))
if saw_files and not args.force:
print("\nDRY RUN: Use --force to actually remove the files listed above.", file=sys.stderr)
if __name__ == "__main__":
sys.exit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.