Compare commits
51 Commits
Author | SHA1 | Date | |
---|---|---|---|
22c293eb58 | |||
8e10d8aefb | |||
3c100af8d2 | |||
0ccc15089b | |||
ab839658ba | |||
a970a6ef8a | |||
|
146bdc347b | ||
3456e95135 | |||
40b987911c | |||
3107b78aa0 | |||
030c1c519f | |||
127815dd1f | |||
3fbec5ea29 | |||
9fd1b4bfed | |||
4fa19ce27a | |||
efdad72708 | |||
5955385e9c | |||
f94756e615 | |||
00bbb51384 | |||
20e2498f39 | |||
a23c74a70a | |||
941f487292 | |||
8bc64da119 | |||
4ad99b6707 | |||
c5df97ebe8 | |||
b81aed66b6 | |||
77f1965113 | |||
122e16b697 | |||
a34b0f5edf | |||
32574bdd23 | |||
d6f74de034 | |||
821d27863d | |||
f2678b5f88 | |||
a215aac2f6 | |||
6d547299cd | |||
40aeec7bea | |||
e8fe0efcdb | |||
6f4a88737d | |||
0587d73472 | |||
3c0375891a | |||
ee592451b5 | |||
cec02602a0 | |||
f9c474a53f | |||
9ffbd09477 | |||
cc95b79447 | |||
adeabcec6b | |||
f4a568c3ca | |||
b1df2f7a12 | |||
3b7bf12da8 | |||
8dc3cc969e | |||
|
d8e31f8b46 |
99
README
99
README
@ -27,11 +27,31 @@ Implemented subcommands:
|
||||
- bad mark the current revision as bad - containing the change sought
|
||||
- good mark the current revision as good - older than the change sought
|
||||
- reset terminate the bisect operation and return to the original revision
|
||||
branch[es] [-d] [-s] [<branch_name>]
|
||||
- with no arguments, list branches with '*' by the current one
|
||||
- with -d, delete <branch>
|
||||
- otherwise, create a new branch from the current one
|
||||
branch[es]
|
||||
- branch: list branches with '*' by the current one
|
||||
- branch -d name: delete branch <name>
|
||||
- branch [-s] name [source[@rev]]: create branch <name>
|
||||
- if <source> is given it is resolved as a reference name (can be 'trunk',
|
||||
or a tag or branch name)
|
||||
- if <source> is not given the HEAD of the current working-copy URL is used.
|
||||
- also switch to the new branch if -s is given
|
||||
checkout [options] [url] [wc_path]
|
||||
- if a URL is given that ends with /trunk and no working copy path is given,
|
||||
then the last directory component before "/trunk" is used as the working
|
||||
copy path
|
||||
clean [-x] {-n|-f} [path...]
|
||||
- remove (or list) unversioned items
|
||||
options:
|
||||
-x, --ignore-ignores
|
||||
- remove/list unversioned items that are ignored by Subversion
|
||||
-n, --dry-run
|
||||
- perform a dry-run, i.e. list files that would be removed but do not
|
||||
actually remove them
|
||||
-f, --force
|
||||
- perform the actual removal of unversioned files
|
||||
commit
|
||||
- removes trailing whitespace (including end-of-line characters) from commit
|
||||
messages before committing if neither -m nor -F arguments are given
|
||||
diff
|
||||
- allow specifying ref1..ref2 syntax to show the diff between two references
|
||||
- references can be tag names, branch names, or 'trunk'
|
||||
@ -49,43 +69,57 @@ Implemented subcommands:
|
||||
merge <branch>
|
||||
- merge branch <branch> into the current WC path
|
||||
- falls back to Subversion "merge" if <branch> doesn't exist
|
||||
revert <path[s]>
|
||||
- revert all affected files under given target path(s)
|
||||
root
|
||||
- output root URL (for use on shell such as "svn log $(svn root)/tags")
|
||||
stash [command]
|
||||
stash [options] [file...]
|
||||
- allow temporarily saving changes to the working copy without committing
|
||||
- the stashes behaves as a "stack" where "save" pushes a new stash object
|
||||
and "pop" pops the newest one from the top of the stack
|
||||
commands:
|
||||
save [-k] (default if not specified):
|
||||
- save changes as a "stash" object
|
||||
- revert changes from working copy unless -k (keep working copy) given
|
||||
- this only works with text files, not binary files
|
||||
list:
|
||||
- the stashes behaves as a "stack" where stashing pushes a new stash object
|
||||
and popping removes the newest one from the top of the stack
|
||||
- binary files are ignored (a warning is printed) and not stashed
|
||||
options:
|
||||
-e, --externals
|
||||
- also stash changes in externals (if no explicit targets given)
|
||||
- this option is implicitly on if the configuration value
|
||||
'stash_externals' is set to True
|
||||
--noexternals
|
||||
- reverse --externals (or the configuration value 'stash_externals')
|
||||
-k, --keep
|
||||
- create the stash object, but keep the changes locally as well
|
||||
- with --pop, do not remove the stash object after reapplying it
|
||||
-p, --patch
|
||||
- interactively prompt for whether to stash each hunk
|
||||
--list
|
||||
- show a list of all stash objects
|
||||
pop [-k] [id]:
|
||||
--pop [id]
|
||||
- apply the stash object <id> back to the working copy
|
||||
- the stash object is removed unless -k (keep) given
|
||||
- <id> defaults to the newest stash object created
|
||||
show [id]:
|
||||
--show [id]
|
||||
- display the diff stored in stash with ID <id>
|
||||
- <id> defaults to the newest stash object created
|
||||
drop [id]:
|
||||
--drop [id]
|
||||
- delete stash object <id>
|
||||
- <id> defaults to the newest stash object created
|
||||
- if none of --list, --pop, --show, or --drop is given, a new stash object
|
||||
is created containing the chosen differences
|
||||
- if file is given, only the changes from the listed files will be stashed
|
||||
switch <short_name>
|
||||
- switch to 'trunk', branch name, or tag name without having to specify
|
||||
the full URL
|
||||
- falls back to Subversion "switch" if <short_name> doesn't exist
|
||||
tag[s]
|
||||
- tag: list tags
|
||||
- tag -d <name>: delete tag <name>
|
||||
- tag -m <old> <new>: rename tag <old> to <new>
|
||||
- tag <name> [source_ref[@rev]]: create tag <name>; if source_ref is given
|
||||
it is resolved as a reference name (can be 'trunk', or another tag or a
|
||||
branch name); if source is not given the HEAD of the current working-
|
||||
copy URL is used
|
||||
url
|
||||
- output repository URL of current working directory
|
||||
- tag [-v]: list tags
|
||||
- with -v (verbose), lists tag origin and creation date
|
||||
- tag -d name: delete tag <name>
|
||||
- tag -m old new: rename tag <old> to <new>
|
||||
- tag name [source[@rev]]: create tag <name>
|
||||
- if <source> is given it is resolved as a reference name (can be 'trunk',
|
||||
or another tag or a branch name)
|
||||
- if <source> is not given the HEAD of the current working-copy URL is used.
|
||||
url [file]
|
||||
- output repository URL of file [default: '.' (current working directory)]
|
||||
users
|
||||
- show a list of contributing users to a SVN path
|
||||
watch-lock
|
||||
@ -138,6 +172,10 @@ Available configuration variables:
|
||||
for newly added files which should not automatically have the
|
||||
svn:executable property added for them even if the files are
|
||||
executable. The default value is ['.c', '.cc', '.h', '.txt'].
|
||||
ignore_symlinks: True or False to hide unversioned symlinks from appearing
|
||||
in status output (default: False)
|
||||
stash_externals: True or False to enable/disable whether '-e' is implicitly
|
||||
on for 'stash' subcommand. Defaults to False.
|
||||
|
||||
Configuration Examples:
|
||||
pager = 'less -FRXi' # enable case-insensitive searching in less
|
||||
@ -153,6 +191,17 @@ Configuration Examples:
|
||||
Author: Josh Holtrop
|
||||
|
||||
History:
|
||||
v1.7 - 2013-08-16
|
||||
- add "clean" subcommand handler
|
||||
- bugfix: revert subcommand handler: revert deleted items
|
||||
v1.6 - 2013-07-24
|
||||
- rework 'stash' subcommand to operate on individual hunks
|
||||
- add 'revert' subcommand handler
|
||||
v1.5 - 2012-11-08
|
||||
- add -v (verbose) flag to 'tag' subcommand
|
||||
- allow optional source argument to 'branch' subcommand
|
||||
- remove repository prefix URL from diff --summarize output
|
||||
- Add .exe extension to svn binary name if platform is Windows (not cygwin)
|
||||
v1.4 - 2012-08-23
|
||||
- allow optional source/revision argument when creating a tag
|
||||
- handle ctrl+c better when interacting with a pager
|
||||
|
629
jsvn
629
jsvn
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python2
|
||||
|
||||
# Josh's SVN wrapper script
|
||||
#
|
||||
@ -15,8 +15,12 @@ import datetime
|
||||
import types
|
||||
import getopt
|
||||
import signal
|
||||
import platform
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
STATUS_LINE_REGEX = r'[ACDIMRX?!~ ][CM ][L ][+ ][SX ][KOTB ]..(.+)'
|
||||
COMMIT_IGNORE_LINE = "--This line, and those below, will be ignored--"
|
||||
|
||||
###########################################################################
|
||||
# Subcommand Handler Return Values #
|
||||
@ -65,13 +69,15 @@ def get_config(svn):
|
||||
'pager': '',
|
||||
'use_pager': True,
|
||||
'use_color': True,
|
||||
'ignore_symlinks': False,
|
||||
'aliases': {
|
||||
# default jsvn aliases
|
||||
'tags': 'tag',
|
||||
'branches': 'branch'},
|
||||
'svn': '',
|
||||
'ignore_executable_extensions':
|
||||
['.c', '.cc', '.h', '.txt']
|
||||
['.c', '.cc', '.h', '.txt'],
|
||||
'stash_externals': False,
|
||||
}
|
||||
|
||||
global_user_config_fname = os.path.expanduser('~/.jsvn')
|
||||
@ -90,7 +96,7 @@ class LogEntry(object):
|
||||
self.revision = 0
|
||||
self.user = ''
|
||||
self.date = ''
|
||||
self.lines = ''
|
||||
self.lines_text = ''
|
||||
self.message_lines = 0
|
||||
self.changed_paths = []
|
||||
self.message = []
|
||||
@ -397,7 +403,7 @@ def filter_update(pout, out):
|
||||
continue
|
||||
if re.match(r'\s*$', line):
|
||||
continue
|
||||
if re.match(r'External at revision ', line):
|
||||
if re.match(r'(External at|Updated external to) revision ', line):
|
||||
if external_printed:
|
||||
out.write(line)
|
||||
continue
|
||||
@ -406,6 +412,9 @@ def filter_update(pout, out):
|
||||
out.write('\n')
|
||||
out.write(line)
|
||||
continue
|
||||
if re.match(r'^Removed external ', line):
|
||||
out.write(line)
|
||||
continue
|
||||
|
||||
# anything not matched yet will cause an external to be shown
|
||||
if not external_printed:
|
||||
@ -454,16 +463,19 @@ def filter_status(line, out):
|
||||
ansi_reset(out)
|
||||
out.write('\n')
|
||||
|
||||
def get_unknowns(svn):
|
||||
def get_unknowns(svn, config):
|
||||
unknowns = []
|
||||
pout = Popen([svn, 'status'], stdout=PIPE).stdout
|
||||
for line in iter(pout.readline, ''):
|
||||
m = re.match(r'\? (.*)$', line)
|
||||
if m is not None:
|
||||
if not (config['ignore_symlinks'] and os.path.islink(m.group(1))):
|
||||
unknowns.append(m.group(1))
|
||||
return unknowns
|
||||
|
||||
def descendant_path(child, parent):
|
||||
if parent == '.':
|
||||
parent = os.getcwd()
|
||||
if child[0] != '/' and parent[0] == '/':
|
||||
child = os.getcwd() + '/' + child
|
||||
elif child[0] == '/' and parent[0] != '/':
|
||||
@ -534,7 +546,10 @@ def find_branched_revision(svn, branch_url, branch_path, base_path):
|
||||
new_path, old_path, rev = m.group(1, 2, 3)
|
||||
if new_path == search_path:
|
||||
if old_path == base_path:
|
||||
try:
|
||||
p.kill()
|
||||
except OSError:
|
||||
pass
|
||||
return (int(rev), old_path)
|
||||
search_path = old_path
|
||||
return (-1, '')
|
||||
@ -557,6 +572,21 @@ def filter_add_output(fh, out, svn, config):
|
||||
out.write(line)
|
||||
out.write('\n')
|
||||
|
||||
def relpath(path):
|
||||
cwdprefix = os.getcwd() + '/'
|
||||
if path.startswith(cwdprefix):
|
||||
return path[len(cwdprefix):]
|
||||
return path
|
||||
|
||||
def get_editor():
|
||||
if 'EDITOR' in os.environ and os.environ['EDITOR'] != '':
|
||||
return os.environ['EDITOR']
|
||||
for p_ent in os.environ['PATH'].split(':'):
|
||||
editor_path = os.path.join(p_ent, 'editor')
|
||||
if os.path.isfile(editor_path):
|
||||
return editor_path
|
||||
return 'vim'
|
||||
|
||||
###########################################################################
|
||||
# Subcommand Handlers #
|
||||
###########################################################################
|
||||
@ -574,7 +604,7 @@ def add_h(argv, svn, out, config):
|
||||
# for each target specified, check if there are unversioned items
|
||||
# underneath it (for directories) and add them as well
|
||||
# if none are found, fall back to the native svn add
|
||||
unknowns = get_unknowns(svn)
|
||||
unknowns = get_unknowns(svn, config)
|
||||
for path in argv:
|
||||
if path == '.':
|
||||
path = os.getcwd()
|
||||
@ -678,6 +708,9 @@ Operations:
|
||||
update_h(['update', '-r%d' % rev], svn, out, config)
|
||||
return RET_OK
|
||||
|
||||
# branch # list branches
|
||||
# branch -d name # delete branch <name>
|
||||
# branch [-s] name [source[@rev]] # create branch <name> [from <source> [at revision <rev>]]
|
||||
def branch_h(argv, svn, out, config):
|
||||
argv = argv[1:] # strip 'branch' command
|
||||
options, args = getopt.getopt(argv, 'ds')
|
||||
@ -686,7 +719,19 @@ def branch_h(argv, svn, out, config):
|
||||
if origin == '' or root == '':
|
||||
sys.stderr.write("Could not determine origin/root URL\n")
|
||||
return RET_ERR
|
||||
if len(options) == 0 and len(args) == 0:
|
||||
do_switch = False
|
||||
for opt, val in options:
|
||||
if opt == '-d':
|
||||
# delete branch
|
||||
if len(args) < 1:
|
||||
sys.stderr.write('Must supply branch name\n')
|
||||
return RET_ERR
|
||||
Popen([svn, 'rm', root + '/branches/' + args[0], '-m',
|
||||
"Removed branch '%s'" % args[0]], stdout=out).wait()
|
||||
return RET_OK
|
||||
elif opt == '-s':
|
||||
do_switch = True
|
||||
if len(args) == 0:
|
||||
bl = ['trunk'] + get_svn_branch_list(svn)
|
||||
current = origin.split('/')[-1]
|
||||
bl.sort()
|
||||
@ -701,26 +746,27 @@ def branch_h(argv, svn, out, config):
|
||||
ansi_reset(out)
|
||||
out.write('\n')
|
||||
return RET_OK
|
||||
if len(args) == 0:
|
||||
sys.stderr.write('Error: must supply branch name\n')
|
||||
return RET_ERR
|
||||
branch_name = args[0]
|
||||
do_switch = False
|
||||
for opt, val in options:
|
||||
if opt == '-d':
|
||||
# delete branch
|
||||
Popen([svn, 'rm', root + '/branches/' + branch_name, '-m',
|
||||
"Removed branch '%s'" % branch_name], stdout=out).wait()
|
||||
return RET_OK
|
||||
elif opt == '-s':
|
||||
do_switch = True
|
||||
bl = get_svn_branch_list(svn)
|
||||
if branch_name in bl:
|
||||
sys.stderr.write('Error: branch %s already exists\n' % branch_name)
|
||||
return RET_ERR
|
||||
comment = "Created '%s' branch" % branch_name
|
||||
branch_path = root + '/branches/' + branch_name
|
||||
Popen([svn, 'copy', origin, branch_path, '-m', comment], stdout=out).wait()
|
||||
branch_source = origin
|
||||
if len(args) >= 2:
|
||||
source = args[1]
|
||||
m = re.match(r'(.*?)(@\d+)?$', source)
|
||||
if m is not None:
|
||||
ref_name, rev_str = m.group(1, 2)
|
||||
url, path = resolve_reference(svn, ref_name)
|
||||
if url != '':
|
||||
branch_source = url
|
||||
if rev_str is not None:
|
||||
branch_source += rev_str
|
||||
else:
|
||||
branch_source = source
|
||||
Popen([svn, 'copy', branch_source, branch_path, '-m', comment], stdout=out).wait()
|
||||
if do_switch:
|
||||
return switch_h(['switch', branch_name], svn, out, config)
|
||||
return RET_OK
|
||||
@ -731,9 +777,10 @@ def branch_h(argv, svn, out, config):
|
||||
# tag name [source[@rev]] # create tag <name> [from source [at revision rev]]
|
||||
def tag_h(argv, svn, out, config):
|
||||
argv = argv[1:] # strip command
|
||||
options, args = getopt.getopt(argv, 'dm')
|
||||
options, args = getopt.getopt(argv, 'dmv')
|
||||
origin = get_svn_top_level(svn)
|
||||
root = get_svn_root_url(svn)
|
||||
verbose = False
|
||||
if origin == '' or root == '':
|
||||
sys.stderr.write("Could not determine origin/root URL\n")
|
||||
return RET_ERR
|
||||
@ -764,13 +811,50 @@ def tag_h(argv, svn, out, config):
|
||||
'-m', "Renamed tag '%s' to '%s'" % (old_tag_name, tag_name)],
|
||||
stdout=out).wait()
|
||||
return RET_OK
|
||||
elif opt == '-v':
|
||||
verbose = True
|
||||
else:
|
||||
sys.stderr.write('Unrecognized option to "tag" command\n')
|
||||
return RET_ERR
|
||||
if len(args) == 0:
|
||||
tag_list.sort()
|
||||
max_tagname_length = 0
|
||||
if verbose:
|
||||
for t in tag_list:
|
||||
out.write(t + '\n')
|
||||
if len(t) > max_tagname_length:
|
||||
max_tagname_length = len(t)
|
||||
for t in tag_list:
|
||||
if verbose:
|
||||
out.write(('%%-%ds' % max_tagname_length) % t)
|
||||
date = ''
|
||||
origin = ''
|
||||
rev = ''
|
||||
pout = Popen([svn, 'log', '-v', '--stop-on-copy', '%s/tags/%s' % (root, t)],
|
||||
stdout=PIPE).stdout
|
||||
for line in iter(pout.readline, ''):
|
||||
m = re.match(r'r\d+\s*\|[^|]+\|\s*(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2})', line)
|
||||
if m is not None:
|
||||
date = m.group(1)
|
||||
m = re.match(r'\s\s\sA\s.*\/tags\/%s\s\(from\s(.*):(\d+)' % t, line)
|
||||
if m is not None:
|
||||
origin = re.sub(r'.*\/(trunk|tags|branches)\b', r'\1', m.group(1))
|
||||
rev = m.group(2)
|
||||
if date != '':
|
||||
out.write(' ')
|
||||
ansi_color(out, 'magenta')
|
||||
out.write(date)
|
||||
ansi_reset(out)
|
||||
if origin != '':
|
||||
out.write(' ')
|
||||
ansi_color(out, 'yellow')
|
||||
out.write(origin)
|
||||
ansi_reset(out)
|
||||
ansi_color(out, 'blue', bold=True)
|
||||
out.write('@' + rev)
|
||||
ansi_reset(out)
|
||||
else:
|
||||
out.write(t)
|
||||
out.write('\n')
|
||||
return RET_OK
|
||||
tag_name = args[0]
|
||||
if tag_name in tag_list:
|
||||
@ -989,8 +1073,10 @@ def diff_h(argv, svn, out, config):
|
||||
argv = argv[:i] + [url1, url2] + argv[i + 1:]
|
||||
break
|
||||
pout = Popen([svn] + argv, stdout=PIPE).stdout
|
||||
root_url = get_svn_root_url(svn)
|
||||
for line in iter(pout.readline, ''):
|
||||
if doing_summarize:
|
||||
line = line.replace(root_url, '')
|
||||
filter_status(line, out)
|
||||
else:
|
||||
colordiff(out, line)
|
||||
@ -1067,6 +1153,9 @@ def log_h(argv, svn, out, config):
|
||||
if match != target:
|
||||
return False
|
||||
return True
|
||||
if pretty not in ("default", "oneline"):
|
||||
sys.stderr.write("Error: unknown --pretty option: '%s'\n" % pretty)
|
||||
return RET_ERR
|
||||
pout = Popen([svn] + argv, stdout=PIPE).stdout
|
||||
while True:
|
||||
le = LogEntry(pout)
|
||||
@ -1107,12 +1196,15 @@ def status_h(argv, svn, out, config):
|
||||
continue
|
||||
|
||||
# look for lines that should be ignored
|
||||
if re.match(STATUS_LINE_REGEX, line):
|
||||
m = re.match(STATUS_LINE_REGEX, line)
|
||||
if m is not None:
|
||||
action = line[0]
|
||||
if action == 'X':
|
||||
continue # don't print directory externals
|
||||
elif line.startswith(' X '):
|
||||
continue # don't print unmodified file externals
|
||||
elif action == '?' and config['ignore_symlinks'] and os.path.islink(m.group(1)):
|
||||
continue
|
||||
|
||||
# anything not matched yet will cause an external to be shown
|
||||
if not external_printed:
|
||||
@ -1121,6 +1213,14 @@ def status_h(argv, svn, out, config):
|
||||
out.write("External '%s':\n" % external)
|
||||
external_printed = True
|
||||
|
||||
# trim working directory from paths in externals
|
||||
if external != '':
|
||||
m = re.match(STATUS_LINE_REGEX, line)
|
||||
if m is not None:
|
||||
path = m.group(1)
|
||||
if path.startswith(os.getcwd() + os.sep):
|
||||
line = line[:8] + path[len(os.getcwd() + os.sep):]
|
||||
|
||||
# look for lines to highlight
|
||||
if re.match(STATUS_LINE_REGEX, line):
|
||||
filter_status(line, out)
|
||||
@ -1139,103 +1239,251 @@ def externals_h(argv, svn, out, config):
|
||||
out.write(line[8:])
|
||||
return RET_OK
|
||||
|
||||
def stash_h(argv, svn, out, config):
|
||||
argv = argv[1:] # strip 'stash' command
|
||||
action = 'save'
|
||||
if len(argv) >= 1:
|
||||
if not argv[0].startswith('-'):
|
||||
action = argv[0]
|
||||
argv = argv[1:]
|
||||
# now argv only contains options/arguments to the stash subcommand itself
|
||||
if action == 'save':
|
||||
keep_wc = False
|
||||
options, args = getopt.getopt(argv, 'k')
|
||||
for opt, val in options:
|
||||
if opt == '-k':
|
||||
keep_wc = True
|
||||
def revert_h(argv, svn, out, config):
|
||||
argv = argv[1:] # strip off command
|
||||
if len(argv) == 0:
|
||||
# do not handle if no targets are passed
|
||||
return RET_REEXEC
|
||||
if len(filter(lambda x: x.startswith('-'), argv)) != 0:
|
||||
# do not handle if any options are passed
|
||||
Popen([svn, 'revert'] + argv).wait()
|
||||
return RET_OK
|
||||
did_something = False
|
||||
for i, target in enumerate(argv):
|
||||
if target.endswith('/'):
|
||||
argv[i] = target[:-1]
|
||||
p = Popen([svn, 'status'], stdout=PIPE)
|
||||
modified_files = []
|
||||
for line in iter(p.stdout.readline, ''):
|
||||
m = re.match(STATUS_LINE_REGEX, line)
|
||||
if m is not None:
|
||||
action = line[0]
|
||||
prop_action = line[1]
|
||||
if action in ('A', 'M', 'C', 'D', '!') or prop_action == 'M':
|
||||
fname = m.group(1)
|
||||
if action in ('D', '!'):
|
||||
modified_files.append((action, fname))
|
||||
else:
|
||||
modified_files.insert(0, (action, fname))
|
||||
for action, fname in modified_files:
|
||||
for target in argv:
|
||||
if fname.startswith(os.getcwd() + os.sep):
|
||||
fname = fname[len(os.getcwd() + os.sep):]
|
||||
if target == '.' or target == fname or fname.startswith(target + os.sep):
|
||||
cmd = [svn, "revert"]
|
||||
if action == 'A':
|
||||
cmd += ["--depth", "infinity"]
|
||||
cmd.append(fname)
|
||||
Popen(cmd).wait()
|
||||
did_something = True
|
||||
break
|
||||
return RET_OK if did_something else RET_REEXEC
|
||||
|
||||
def get_svn_contents_to_stash(targets, svn, out, keep, patch, externals):
|
||||
s_fd, s_fname = tempfile.mkstemp(prefix = 'svn.stash.')
|
||||
r_fd, r_fname = tempfile.mkstemp(prefix = 'svn.stash.')
|
||||
os.close(s_fd)
|
||||
os.close(r_fd)
|
||||
s_fh = open(s_fname, 'w')
|
||||
r_fh = open(r_fname, 'w')
|
||||
|
||||
external_list = []
|
||||
directories_added = []
|
||||
status_cmd = [svn, 'status']
|
||||
if not externals:
|
||||
status_cmd += ['--ignore-externals']
|
||||
status_proc = Popen(status_cmd, stdout=PIPE)
|
||||
for line in iter(status_proc.stdout.readline, ''):
|
||||
m = re.match(r"Performing status on external item at '(.*)':", line)
|
||||
if m is not None:
|
||||
external_list.append(m.group(1))
|
||||
m = re.match(STATUS_LINE_REGEX, line)
|
||||
if m is not None:
|
||||
path = m.group(1)
|
||||
if line[0] == 'A':
|
||||
if os.path.isdir(path):
|
||||
directories_added.append(relpath(path))
|
||||
|
||||
if externals and len(targets) == 0:
|
||||
targets = ['.'] + external_list
|
||||
|
||||
svars = {
|
||||
'revert_list': [],
|
||||
'skip_all': not patch,
|
||||
'skip_file': True,
|
||||
'answer': 'y',
|
||||
'index_header': '',
|
||||
'hunk_buildup': '',
|
||||
'index_fname': '',
|
||||
'prompted_for_index': False,
|
||||
'quit': False,
|
||||
'wrote_index_sf': False,
|
||||
'wrote_index_rf': False,
|
||||
'binary_file': False,
|
||||
'n_insertions': 0,
|
||||
'n_deletions': 0,
|
||||
}
|
||||
def update_answer():
|
||||
if not patch:
|
||||
svars['answer'] = 'y'
|
||||
return
|
||||
if svars['skip_file'] or svars['skip_all']:
|
||||
return
|
||||
if not svars['prompted_for_index']:
|
||||
for line in svars['index_header'].rstrip().split('\n'):
|
||||
colordiff(out, line)
|
||||
svars['prompted_for_index'] = True
|
||||
for li in svars['hunk_buildup'].rstrip().split('\n'):
|
||||
colordiff(out, li)
|
||||
answer = ''
|
||||
answers = ('y', 'n', 'yf', 'nf', 'ya', 'na', 'q', '?')
|
||||
while answer not in answers:
|
||||
ansi_color(out, 'magenta', bold=True)
|
||||
out.write('Stash this hunk (%s)? ' % ','.join(answers))
|
||||
ansi_reset(out)
|
||||
answer = sys.stdin.readline().rstrip().lower()
|
||||
if answer == '?':
|
||||
out.write('''y: yes, stash this hunk
|
||||
n: no, do not stash this hunk
|
||||
yf: yes, and stash every hunk from the rest of this file
|
||||
nf: no, and do not stash any hunk from the rest of this file
|
||||
ya: yes, and stash every remaining hunk
|
||||
na: no, and do not stash any remaining hunks
|
||||
q: quit and abort stash
|
||||
?: show this help
|
||||
''')
|
||||
answer = ''
|
||||
if answer == 'q':
|
||||
svars['quit'] = True
|
||||
elif answer[1:] == 'a':
|
||||
svars['skip_all'] = True
|
||||
elif answer[1:] == 'f':
|
||||
svars['skip_file'] = True
|
||||
svars['answer'] = answer[:1]
|
||||
|
||||
def flush_hunk():
|
||||
if svars['hunk_buildup'] != '':
|
||||
update_answer()
|
||||
if svars['answer'] == 'y':
|
||||
if not svars['wrote_index_sf']:
|
||||
s_fh.write(svars['index_header'])
|
||||
svars['wrote_index_sf'] = True
|
||||
s_fh.write(svars['hunk_buildup'])
|
||||
elif svars['answer'] == 'n':
|
||||
if not svars['wrote_index_rf']:
|
||||
r_fh.write(svars['index_header'])
|
||||
svars['wrote_index_rf'] = True
|
||||
r_fh.write(svars['hunk_buildup'])
|
||||
svars['hunk_buildup'] = ''
|
||||
|
||||
def flush_file(new_file_name):
|
||||
if svars['binary_file']:
|
||||
ansi_color(out, 'yellow', bold=True)
|
||||
out.write('Warning: not stashing binary file %s' % svars['index_fname'])
|
||||
ansi_reset(out)
|
||||
out.write('\n')
|
||||
else:
|
||||
flush_hunk()
|
||||
if svars['wrote_index_sf']:
|
||||
svars['revert_list'].append(svars['index_fname'])
|
||||
svars['skip_file'] = False
|
||||
svars['hunk_buildup'] = ''
|
||||
svars['index_fname'] = new_file_name
|
||||
svars['binary_file'] = False
|
||||
svars['wrote_index_sf'] = False
|
||||
svars['wrote_index_rf'] = False
|
||||
svars['prompted_for_index'] = False
|
||||
|
||||
diff_proc = Popen([svn, 'diff'] + targets, stdout=PIPE)
|
||||
for line in iter(diff_proc.stdout.readline, ''):
|
||||
m = re.match(r'Index: (.*)', line)
|
||||
if m is not None:
|
||||
flush_file(m.group(1))
|
||||
svars['index_header'] = line
|
||||
elif (re.match(r'=+$', line) or
|
||||
re.match(r'--- ', line) or
|
||||
re.match(r'\+\+\+ ', line)):
|
||||
svars['index_header'] += line
|
||||
elif (re.match(r'@@ ', line) or re.match(r'Property.changes.on:', line)):
|
||||
flush_hunk()
|
||||
svars['hunk_buildup'] = line
|
||||
elif re.match(r'Cannot display: file.marked.as.a.binary.type', line):
|
||||
svars['binary_file'] = True
|
||||
else:
|
||||
svars['hunk_buildup'] += line
|
||||
if line.startswith('+'):
|
||||
svars['n_insertions'] += 1
|
||||
elif line.startswith('-'):
|
||||
svars['n_deletions'] += 1
|
||||
if svars['quit']:
|
||||
break
|
||||
if not svars['quit']:
|
||||
flush_file('')
|
||||
|
||||
for da in directories_added:
|
||||
stash_da = len(targets) == 0
|
||||
for t in targets:
|
||||
if descendant_path(da, t):
|
||||
stash_da = True
|
||||
break
|
||||
if stash_da:
|
||||
s_fh.write('#dir: %s\n' % da)
|
||||
if not da in svars['revert_list']:
|
||||
svars['revert_list'].append(da)
|
||||
|
||||
s_fh.close()
|
||||
r_fh.close()
|
||||
if svars['quit']:
|
||||
svars['revert_list'] = []
|
||||
|
||||
return s_fname, r_fname, svars['revert_list'], svars['n_insertions'], svars['n_deletions'], directories_added
|
||||
|
||||
def stash_save_h(args, svn, out, config, keep, patch, externals):
|
||||
owd = os.getcwd()
|
||||
wc_dir = get_svn_wc_root(svn)
|
||||
os.chdir(wc_dir)
|
||||
s_fname, r_fname, revert_list, n_insertions, n_deletions, directories_added = \
|
||||
get_svn_contents_to_stash(args, svn, out, keep, patch, externals)
|
||||
if len(revert_list) == 0:
|
||||
out.write('No changes stashed.\n')
|
||||
else:
|
||||
if not keep:
|
||||
for rf in reversed(revert_list):
|
||||
Popen([svn, 'revert', rf], stdout=PIPE).wait()
|
||||
if rf in directories_added and len(os.listdir(rf)) == 0:
|
||||
os.rmdir(rf)
|
||||
if r_fname != '':
|
||||
Popen([svn, 'patch', r_fname], stdout=PIPE).wait()
|
||||
if s_fname != '':
|
||||
stash_idx = get_next_stash_idx(svn)
|
||||
stash_fname = get_stash_fname(svn, stash_idx)
|
||||
fh = open(stash_fname, 'w')
|
||||
proc = Popen([svn, 'diff'], stdout=PIPE)
|
||||
wrote_something = False
|
||||
found_binary_file = False
|
||||
n_insertions = 0
|
||||
n_deletions = 0
|
||||
for line in iter(proc.stdout.readline, ''):
|
||||
if re.match(r'Cannot.display..file.marked.as.a.binary.type',
|
||||
line, flags=re.I):
|
||||
found_binary_file = True
|
||||
if line.startswith('-') and not line.startswith('---'):
|
||||
n_deletions += 1
|
||||
if line.startswith('+') and not line.startswith('+++'):
|
||||
n_insertions += 1
|
||||
fh.write(line)
|
||||
wrote_something = True
|
||||
proc.wait()
|
||||
if found_binary_file:
|
||||
out.write('Error: cannot stash with changes to binary files\n')
|
||||
fh.close()
|
||||
os.unlink(stash_fname)
|
||||
elif not wrote_something:
|
||||
out.write('Error: no changes to stash!\n')
|
||||
fh.close()
|
||||
os.unlink(stash_fname)
|
||||
else:
|
||||
# the stash is good, now revert the working copy changes
|
||||
status_lines = Popen([svn, 'status', '--ignore-externals'],
|
||||
stdout=PIPE).communicate()[0].split('\n')
|
||||
files_changed = {}
|
||||
for line in reversed(status_lines):
|
||||
m = re.match(STATUS_LINE_REGEX, line)
|
||||
if m is not None:
|
||||
target = m.group(1)
|
||||
action = line[0]
|
||||
prop_action = line[1]
|
||||
if (action in ('A', 'M', 'D')
|
||||
or prop_action == 'M'):
|
||||
if action != ' ':
|
||||
files_changed[target] = action
|
||||
else:
|
||||
files_changed[target] = prop_action
|
||||
if not keep_wc:
|
||||
# do the actual revert if -k not given
|
||||
Popen([svn, 'revert', target], stdout=PIPE).wait()
|
||||
if action == 'A':
|
||||
# a file was added, so to stash it we must
|
||||
# remove it in addition to reverting the add
|
||||
if os.path.isfile(target):
|
||||
os.unlink(target)
|
||||
elif os.path.isdir(target):
|
||||
if len(os.listdir(target)) == 0:
|
||||
os.rmdir(target)
|
||||
else:
|
||||
raise ValueError('unhandled target type')
|
||||
stash_fh = open(stash_fname, 'w')
|
||||
s_fh = open(s_fname, 'r')
|
||||
for line in iter(s_fh.readline, ''):
|
||||
stash_fh.write(line)
|
||||
s_fh.close()
|
||||
# write stash info
|
||||
if len(files_changed) == 1:
|
||||
fname = files_changed.keys()[0]
|
||||
fh.write('#info: %s: %s\n' % (files_changed[fname], fname))
|
||||
else:
|
||||
num_actions = {'A': 0, 'M': 0, 'D': 0}
|
||||
for k in files_changed:
|
||||
if files_changed[k] in num_actions:
|
||||
num_actions[files_changed[k]] += 1
|
||||
for a in num_actions:
|
||||
if num_actions[a] > 0:
|
||||
fh.write('#info: %s: %d\n' % (a, num_actions[a]))
|
||||
if n_deletions > 0:
|
||||
fh.write('#info: -%d\n' % n_deletions)
|
||||
stash_fh.write('#info: -%d\n' % n_deletions)
|
||||
if n_insertions > 0:
|
||||
fh.write('#info: +%d\n' % n_insertions)
|
||||
stash_fh.write('#info: +%d\n' % n_insertions)
|
||||
if len(revert_list) == 1:
|
||||
stash_fh.write('#info: F: %s\n' % revert_list[0])
|
||||
else:
|
||||
stash_fh.write('#info: F: %d files\n' % len(revert_list))
|
||||
now = datetime.datetime.now()
|
||||
fh.write('#info: @%04d-%02d-%02d %02d:%02d\n' %
|
||||
stash_fh.write('#info: @%04d-%02d-%02d %02d:%02d\n' %
|
||||
(now.year, now.month, now.day, now.hour, now.minute))
|
||||
fh.close()
|
||||
stash_fh.close()
|
||||
out.write('Created stash %d\n' % stash_idx)
|
||||
if s_fname != '':
|
||||
os.unlink(s_fname)
|
||||
if r_fname != '':
|
||||
os.unlink(r_fname)
|
||||
os.chdir(owd)
|
||||
elif action == 'list':
|
||||
return RET_OK
|
||||
|
||||
def stash_list_h(argv, svn, out, config):
|
||||
stash_ids = get_stash_ids(svn)
|
||||
for si in reversed(stash_ids):
|
||||
ins_text = ''
|
||||
@ -1243,6 +1491,7 @@ def stash_h(argv, svn, out, config):
|
||||
add_text = ''
|
||||
modify_text = ''
|
||||
delete_text = ''
|
||||
summary_text = ''
|
||||
date = ''
|
||||
stash_fname = get_stash_fname(svn, si)
|
||||
fh = open(stash_fname, 'r')
|
||||
@ -1256,6 +1505,8 @@ def stash_h(argv, svn, out, config):
|
||||
modify_text = info
|
||||
elif info.startswith('D:'):
|
||||
delete_text = info
|
||||
elif info.startswith('F:'):
|
||||
summary_text = info[3:]
|
||||
elif info.startswith('-'):
|
||||
del_text = info
|
||||
elif info.startswith('+'):
|
||||
@ -1269,6 +1520,7 @@ def stash_h(argv, svn, out, config):
|
||||
(add_text, 'green'),
|
||||
(modify_text, 'yellow'),
|
||||
(delete_text, 'red'),
|
||||
(summary_text, 'magenta'),
|
||||
]
|
||||
for elem, color in elements:
|
||||
if elem != '':
|
||||
@ -1290,12 +1542,9 @@ def stash_h(argv, svn, out, config):
|
||||
ansi_reset(out)
|
||||
out.write(')')
|
||||
out.write('\n')
|
||||
elif action == 'pop':
|
||||
keep = False
|
||||
options, args = getopt.getopt(argv, 'k')
|
||||
for opt, val in options:
|
||||
if opt == '-k':
|
||||
keep = True
|
||||
return RET_OK
|
||||
|
||||
def stash_pop_h(args, svn, out, config, keep):
|
||||
owd = os.getcwd()
|
||||
wc_dir = get_svn_wc_root(svn)
|
||||
os.chdir(wc_dir)
|
||||
@ -1305,6 +1554,12 @@ def stash_h(argv, svn, out, config):
|
||||
if len(args) >= 1:
|
||||
stash_idx = int(args[0])
|
||||
stash_fname = get_stash_fname(svn, stash_idx)
|
||||
fh = open(stash_fname, 'r')
|
||||
for line in iter(fh.readline, ''):
|
||||
m = re.match('#dir: (.*)', line)
|
||||
if m is not None:
|
||||
Popen([svn, 'mkdir', m.group(1)]).wait()
|
||||
fh.close()
|
||||
p = Popen([svn, 'patch', stash_fname], stdout=PIPE)
|
||||
filter_update(p.stdout, out)
|
||||
rc = p.wait()
|
||||
@ -1317,7 +1572,9 @@ def stash_h(argv, svn, out, config):
|
||||
else:
|
||||
out.write('No stashes to pop\n')
|
||||
os.chdir(owd)
|
||||
elif action == 'show':
|
||||
return RET_OK
|
||||
|
||||
def stash_show_h(argv, svn, out, config):
|
||||
stash_ids = get_stash_ids(svn)
|
||||
if len(stash_ids) > 0:
|
||||
stash_id = stash_ids[-1]
|
||||
@ -1327,6 +1584,13 @@ def stash_h(argv, svn, out, config):
|
||||
stash_fname = get_stash_fname(svn, stash_id)
|
||||
fd = open(stash_fname, 'r')
|
||||
for line in iter(fd.readline, ''):
|
||||
m = re.match('#dir: (.*)', line)
|
||||
if m is not None:
|
||||
ansi_color(out, 'magenta', bold=True)
|
||||
out.write('New Directory: %s' % m.group(1))
|
||||
ansi_reset(out)
|
||||
out.write('\n')
|
||||
continue
|
||||
if not re.match('#info:', line):
|
||||
colordiff(out, line)
|
||||
fd.close()
|
||||
@ -1334,7 +1598,9 @@ def stash_h(argv, svn, out, config):
|
||||
out.write('Invalid stash ID\n')
|
||||
else:
|
||||
out.write('No stashes to show\n')
|
||||
elif action == 'drop':
|
||||
return RET_OK
|
||||
|
||||
def stash_drop_h(argv, svn, out, config):
|
||||
stash_ids = get_stash_ids(svn)
|
||||
if len(stash_ids) > 0:
|
||||
stash_id = stash_ids[-1]
|
||||
@ -1345,10 +1611,44 @@ def stash_h(argv, svn, out, config):
|
||||
out.write('Dropped stash %d\n' % stash_id)
|
||||
else:
|
||||
out.write('No stashes to drop\n')
|
||||
else:
|
||||
out.write('Unknown action "%s"\n' % action)
|
||||
return RET_OK
|
||||
|
||||
def stash_h(argv, svn, out, config):
|
||||
argv = argv[1:] # strip 'stash' command
|
||||
opts, args = getopt.getopt(argv, 'ekp',
|
||||
['list', 'pop', 'show', 'drop', 'externals', 'noexternals',
|
||||
'keep', 'patch'])
|
||||
keep = False
|
||||
patch = False
|
||||
externals = config['stash_externals']
|
||||
operation = 'save'
|
||||
for opt, arg in opts:
|
||||
if opt == '--list':
|
||||
operation = 'list'
|
||||
elif opt == '--pop':
|
||||
operation = 'pop'
|
||||
elif opt == '--show':
|
||||
operation = 'show'
|
||||
elif opt == '--drop':
|
||||
operation = 'drop'
|
||||
elif opt in ('-k', '--keep'):
|
||||
keep = True
|
||||
elif opt in ('-p', '--patch'):
|
||||
patch = True
|
||||
elif opt in ('-e', '--externals'):
|
||||
externals = True
|
||||
elif opt == '--noexternals':
|
||||
externals = False
|
||||
if operation == 'list':
|
||||
return stash_list_h(args, svn, out, config)
|
||||
elif operation == 'pop':
|
||||
return stash_pop_h(args, svn, out, config, keep)
|
||||
elif operation == 'show':
|
||||
return stash_show_h(args, svn, out, config)
|
||||
elif operation == 'drop':
|
||||
return stash_drop_h(args, svn, out, config)
|
||||
return stash_save_h(args, svn, out, config, keep, patch, externals)
|
||||
|
||||
def root_h(argv, svn, out, config):
|
||||
out.write(get_svn_root_url(svn) + '\n')
|
||||
return RET_OK
|
||||
@ -1360,6 +1660,89 @@ def url_h(argv, svn, out, config):
|
||||
out.write(get_svn_url(svn, path) + '\n')
|
||||
return RET_OK
|
||||
|
||||
def checkout_h(argv, svn, out, config):
|
||||
co_url = argv[-1].rstrip('/')
|
||||
if re.search(r'://.*/trunk$', co_url):
|
||||
argv += [co_url.split("/")[-2]]
|
||||
Popen([svn] + argv, stdout=out).wait()
|
||||
return RET_OK
|
||||
|
||||
def clean_h(argv, svn, out, config):
|
||||
argv = argv[1:] # strip command
|
||||
opts, args = getopt.getopt(argv, 'fnx',
|
||||
['force', 'dry-run', 'ignore-ignores'])
|
||||
force = False
|
||||
dry_run = False
|
||||
ignore_ignores = False
|
||||
for opt, arg in opts:
|
||||
if opt in ('-f', '--force'):
|
||||
force = True
|
||||
elif opt in ('-n', '--dry-run'):
|
||||
dry_run = True
|
||||
elif opt in ('-x', '--ignore-ignores'):
|
||||
ignore_ignores = True
|
||||
if not force and not dry_run:
|
||||
sys.stderr.write('Error: specify either -n or -f\n')
|
||||
return RET_ERR
|
||||
if force and dry_run:
|
||||
sys.stderr.write('Error: specify only one of -n or -f\n')
|
||||
return RET_ERR
|
||||
status_args = args
|
||||
if ignore_ignores:
|
||||
status_args.append('--no-ignore')
|
||||
clean_paths = []
|
||||
pout = Popen([svn, 'status'] + status_args, stdout=PIPE).stdout
|
||||
for line in iter(pout.readline, ''):
|
||||
m = re.match(STATUS_LINE_REGEX, line)
|
||||
if m is not None:
|
||||
action = line[0]
|
||||
if action in ('?', 'I'):
|
||||
clean_paths.append(m.group(1))
|
||||
for cp in clean_paths:
|
||||
if dry_run:
|
||||
out.write("Would remove %s\n" % cp)
|
||||
if force:
|
||||
if os.path.islink(cp):
|
||||
os.unlink(cp)
|
||||
elif os.path.isdir(cp):
|
||||
shutil.rmtree(cp)
|
||||
elif os.path.isfile(cp):
|
||||
os.unlink(cp)
|
||||
return RET_OK
|
||||
|
||||
def commit_h(argv, svn, out, config):
|
||||
argv = argv[1:] # strip command
|
||||
for arg in argv:
|
||||
if re.search(r'^-[Fm]', arg):
|
||||
# Do not handle the commit if the user supplied a -m or -F.
|
||||
return RET_REEXEC
|
||||
commit_file_fd, commit_file_fname = tempfile.mkstemp('.tmp', 'svn-commit')
|
||||
os.close(commit_file_fd)
|
||||
commit_file_fh = open(commit_file_fname, 'w')
|
||||
commit_file_fh.write("\n%s\n\n" % COMMIT_IGNORE_LINE)
|
||||
pout = Popen([svn, 'status'], stdout=PIPE).stdout
|
||||
commit_file_fh.write(pout.read())
|
||||
commit_file_fh.close()
|
||||
Popen([get_editor(), commit_file_fname]).wait()
|
||||
commit_file_fh = open(commit_file_fname, 'r')
|
||||
commit_file_contents = commit_file_fh.read()
|
||||
commit_file_fh.close()
|
||||
commit_message = ""
|
||||
for line in commit_file_contents.splitlines():
|
||||
if line == COMMIT_IGNORE_LINE:
|
||||
break
|
||||
commit_message += line + "\n"
|
||||
commit_message = commit_message.rstrip()
|
||||
if commit_message == "":
|
||||
out.write("Aborting commit due to empty commit message.\n")
|
||||
else:
|
||||
commit_file_fh = open(commit_file_fname, 'w')
|
||||
commit_file_fh.write(commit_message)
|
||||
commit_file_fh.close()
|
||||
Popen([svn, 'commit'] + argv + ['-F', commit_file_fname]).wait()
|
||||
os.unlink(commit_file_fname)
|
||||
return RET_OK
|
||||
|
||||
###########################################################################
|
||||
# Main #
|
||||
###########################################################################
|
||||
@ -1456,6 +1839,9 @@ def do_cmd(argv, realsvn, config, expand=True):
|
||||
'add': add_h,
|
||||
'bisect': bisect_h,
|
||||
'branch': branch_h,
|
||||
'checkout': checkout_h,
|
||||
'clean': clean_h,
|
||||
'commit': commit_h,
|
||||
'externals': externals_h,
|
||||
'switch': switch_h,
|
||||
'merge': merge_h,
|
||||
@ -1471,6 +1857,7 @@ def do_cmd(argv, realsvn, config, expand=True):
|
||||
'lockable': lockable_h,
|
||||
'status': status_h,
|
||||
'stash': stash_h,
|
||||
'revert': revert_h,
|
||||
}
|
||||
|
||||
do_native_exec = True
|
||||
@ -1491,8 +1878,12 @@ def do_cmd(argv, realsvn, config, expand=True):
|
||||
pager_proc.wait()
|
||||
|
||||
def main(argv):
|
||||
# Determine the name of the real svn binary
|
||||
svn_bin_name = 'svn'
|
||||
if platform.system() == 'Windows':
|
||||
svn_bin_name = 'svn.exe'
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
realsvn = find_in_path('svn')
|
||||
realsvn = find_in_path( svn_bin_name )
|
||||
config = get_config(realsvn)
|
||||
if config['svn']:
|
||||
realsvn = config['svn']
|
||||
|
Loading…
x
Reference in New Issue
Block a user