Compare commits

...

46 Commits
v1.5 ... master

Author SHA1 Message Date
22c293eb58 run under python2 for now 2016-12-04 15:28:36 -05:00
8e10d8aefb fix log subcommand error for missing lines_text attribute of LogEntry 2016-08-11 16:29:16 -04:00
3c100af8d2 commit: end "Aborting commit" message with a newline 2016-01-04 15:08:41 -05:00
0ccc15089b clean: properly handle removing symbolic links 2015-12-01 13:11:26 -05:00
ab839658ba add a "commit" handler to rstrip commit messages 2015-09-30 17:11:25 -04:00
a970a6ef8a ignore "Updated external to revision XXX" lines if they are the only ones present 2015-09-14 16:46:49 -04:00
Mike_Zwagerman
146bdc347b Stripped the trailing forward slash(s) '/' from the end of the checkout url before checking for 'trunk$' 2015-08-25 20:58:02 -04:00
3456e95135 add a checkout handler that defaults the working copy path to the last directory component before "/trunk" 2015-08-17 12:24:21 -04:00
40b987911c update "add" to be aware of "ignore_symlinks" 2015-03-11 15:18:51 -04:00
3107b78aa0 add "ignore_symlinks" configuration flag 2015-03-11 15:15:40 -04:00
030c1c519f revert directories added by copying recursively 2014-10-08 11:52:40 -04:00
127815dd1f revert: revert files with "C"onflicts 2014-08-13 16:23:47 -04:00
3fbec5ea29 update: handle "Removed external '...'" messages better 2014-08-08 08:35:48 -04:00
9fd1b4bfed log: add error message for unknown --pretty option 2014-03-18 09:36:58 -04:00
4fa19ce27a revert bugfix: revert deleted items in order by status but added/modified items in reverse order 2013-10-24 10:30:52 -04:00
efdad72708 add v1.7 release notes 2013-08-16 15:23:06 -04:00
5955385e9c also apply "revert" to deleted items 2013-08-16 15:21:16 -04:00
f94756e615 add "clean" subcommand handler 2013-07-29 14:34:11 -04:00
00bbb51384 v1.6 2013-07-24 15:48:49 -04:00
20e2498f39 handle OSErrors possibly raised when kill()ing subprocess 2013-05-23 10:22:01 -04:00
a23c74a70a revert: do smart revert on items with modified properties as well 2013-03-20 10:11:06 -04:00
941f487292 revert: revert items in reverse-order to handle added directories 2013-03-20 10:08:28 -04:00
8bc64da119 stash: remove added directories when stashing if they are empty 2013-03-15 16:32:41 -04:00
4ad99b6707 stash: revert on save in reverse order to handle nested added directories 2013-03-15 16:24:40 -04:00
c5df97ebe8 stash: recreate added directories on pop; handle in show as well 2013-03-15 16:22:44 -04:00
b81aed66b6 stash: annotate stashes with created directories 2013-03-15 16:15:48 -04:00
77f1965113 stash: bugfix: respect options that follow operation (ex: --pop -k) 2013-03-15 15:42:42 -04:00
122e16b697 Merge branch 'stash-rework' 2013-03-11 22:36:31 -04:00
a34b0f5edf stash: add info for file name changed or # of files changed 2013-03-11 22:34:44 -04:00
32574bdd23 stash: re-add # of insertions/deletions to stash info 2013-03-11 22:25:30 -04:00
d6f74de034 stash: add --noexternals option, stash_externals configuration flag 2013-03-11 22:21:05 -04:00
821d27863d stash: update README for stash rework 2013-03-11 22:12:36 -04:00
f2678b5f88 stash: add -e/--externals option to also stash externals 2013-03-11 22:06:18 -04:00
a215aac2f6 stash: color the patch prompt line 2013-03-11 21:31:05 -04:00
6d547299cd stash: colorize Index line while patch-prompting 2013-03-11 21:26:03 -04:00
40aeec7bea stash: properly display new Index line when file skipped 2013-03-11 21:24:42 -04:00
e8fe0efcdb stash: rework bugfixes 2013-03-11 21:22:03 -04:00
6f4a88737d stash: respect -k flag again 2013-03-11 20:51:56 -04:00
0587d73472 stash: rework to do per-hunk processing 2013-03-11 20:49:11 -04:00
3c0375891a stash: add --keep, -p, --patch options 2013-03-11 13:50:42 -04:00
ee592451b5 change stash subcommands to require "--" prefix 2013-03-11 13:24:52 -04:00
cec02602a0 status: use relative paths for items in externals 2013-02-28 10:14:04 -05:00
f9c474a53f break up stash subcommand handlers into their own functions 2013-02-12 16:30:24 -05:00
9ffbd09477 revert: support smart recursive reversion for externals 2013-02-12 15:40:02 -05:00
cc95b79447 add "revert" subcommand handler 2012-12-04 15:33:22 -05:00
adeabcec6b update documentation for url subcommand 2012-11-08 16:25:22 -05:00
2 changed files with 608 additions and 238 deletions

84
README
View File

@ -35,6 +35,23 @@ Implemented subcommands:
or a tag or branch name) or a tag or branch name)
- if <source> is not given the HEAD of the current working-copy URL is used. - 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 - 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 diff
- allow specifying ref1..ref2 syntax to show the diff between two references - allow specifying ref1..ref2 syntax to show the diff between two references
- references can be tag names, branch names, or 'trunk' - references can be tag names, branch names, or 'trunk'
@ -52,29 +69,42 @@ Implemented subcommands:
merge <branch> merge <branch>
- merge branch <branch> into the current WC path - merge branch <branch> into the current WC path
- falls back to Subversion "merge" if <branch> doesn't exist - falls back to Subversion "merge" if <branch> doesn't exist
revert <path[s]>
- revert all affected files under given target path(s)
root root
- output root URL (for use on shell such as "svn log $(svn root)/tags") - 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 - allow temporarily saving changes to the working copy without committing
- the stashes behaves as a "stack" where "save" pushes a new stash object - the stashes behaves as a "stack" where stashing pushes a new stash object
and "pop" pops the newest one from the top of the stack and popping removes the newest one from the top of the stack
commands: - binary files are ignored (a warning is printed) and not stashed
save [-k] (default if not specified): options:
- save changes as a "stash" object -e, --externals
- revert changes from working copy unless -k (keep working copy) given - also stash changes in externals (if no explicit targets given)
- this only works with text files, not binary files - this option is implicitly on if the configuration value
list: 'stash_externals' is set to True
- show a list of all stash objects --noexternals
pop [-k] [id]: - reverse --externals (or the configuration value 'stash_externals')
- apply the stash object <id> back to the working copy -k, --keep
- the stash object is removed unless -k (keep) given - create the stash object, but keep the changes locally as well
- <id> defaults to the newest stash object created - with --pop, do not remove the stash object after reapplying it
show [id]: -p, --patch
- display the diff stored in stash with ID <id> - interactively prompt for whether to stash each hunk
- <id> defaults to the newest stash object created --list
drop [id]: - show a list of all stash objects
- delete stash object <id> --pop [id]
- <id> defaults to the newest stash object created - 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]
- display the diff stored in stash with ID <id>
- <id> defaults to the newest stash object created
--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 <short_name>
- switch to 'trunk', branch name, or tag name without having to specify - switch to 'trunk', branch name, or tag name without having to specify
the full URL the full URL
@ -88,8 +118,8 @@ Implemented subcommands:
- if <source> is given it is resolved as a reference name (can be 'trunk', - if <source> is given it is resolved as a reference name (can be 'trunk',
or another tag or a branch name) or another tag or a branch name)
- if <source> is not given the HEAD of the current working-copy URL is used. - if <source> is not given the HEAD of the current working-copy URL is used.
url url [file]
- output repository URL of current working directory - output repository URL of file [default: '.' (current working directory)]
users users
- show a list of contributing users to a SVN path - show a list of contributing users to a SVN path
watch-lock watch-lock
@ -142,6 +172,10 @@ Available configuration variables:
for newly added files which should not automatically have the for newly added files which should not automatically have the
svn:executable property added for them even if the files are svn:executable property added for them even if the files are
executable. The default value is ['.c', '.cc', '.h', '.txt']. 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: Configuration Examples:
pager = 'less -FRXi' # enable case-insensitive searching in less pager = 'less -FRXi' # enable case-insensitive searching in less
@ -157,6 +191,12 @@ Configuration Examples:
Author: Josh Holtrop Author: Josh Holtrop
History: 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 v1.5 - 2012-11-08
- add -v (verbose) flag to 'tag' subcommand - add -v (verbose) flag to 'tag' subcommand
- allow optional source argument to 'branch' subcommand - allow optional source argument to 'branch' subcommand

762
jsvn
View File

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python2
# Josh's SVN wrapper script # Josh's SVN wrapper script
# #
@ -16,8 +16,11 @@ import types
import getopt import getopt
import signal import signal
import platform import platform
import tempfile
import shutil
STATUS_LINE_REGEX = r'[ACDIMRX?!~ ][CM ][L ][+ ][SX ][KOTB ]..(.+)' STATUS_LINE_REGEX = r'[ACDIMRX?!~ ][CM ][L ][+ ][SX ][KOTB ]..(.+)'
COMMIT_IGNORE_LINE = "--This line, and those below, will be ignored--"
########################################################################### ###########################################################################
# Subcommand Handler Return Values # # Subcommand Handler Return Values #
@ -66,13 +69,15 @@ def get_config(svn):
'pager': '', 'pager': '',
'use_pager': True, 'use_pager': True,
'use_color': True, 'use_color': True,
'ignore_symlinks': False,
'aliases': { 'aliases': {
# default jsvn aliases # default jsvn aliases
'tags': 'tag', 'tags': 'tag',
'branches': 'branch'}, 'branches': 'branch'},
'svn': '', 'svn': '',
'ignore_executable_extensions': 'ignore_executable_extensions':
['.c', '.cc', '.h', '.txt'] ['.c', '.cc', '.h', '.txt'],
'stash_externals': False,
} }
global_user_config_fname = os.path.expanduser('~/.jsvn') global_user_config_fname = os.path.expanduser('~/.jsvn')
@ -91,7 +96,7 @@ class LogEntry(object):
self.revision = 0 self.revision = 0
self.user = '' self.user = ''
self.date = '' self.date = ''
self.lines = '' self.lines_text = ''
self.message_lines = 0 self.message_lines = 0
self.changed_paths = [] self.changed_paths = []
self.message = [] self.message = []
@ -398,7 +403,7 @@ def filter_update(pout, out):
continue continue
if re.match(r'\s*$', line): if re.match(r'\s*$', line):
continue continue
if re.match(r'External at revision ', line): if re.match(r'(External at|Updated external to) revision ', line):
if external_printed: if external_printed:
out.write(line) out.write(line)
continue continue
@ -407,6 +412,9 @@ def filter_update(pout, out):
out.write('\n') out.write('\n')
out.write(line) out.write(line)
continue continue
if re.match(r'^Removed external ', line):
out.write(line)
continue
# anything not matched yet will cause an external to be shown # anything not matched yet will cause an external to be shown
if not external_printed: if not external_printed:
@ -455,16 +463,19 @@ def filter_status(line, out):
ansi_reset(out) ansi_reset(out)
out.write('\n') out.write('\n')
def get_unknowns(svn): def get_unknowns(svn, config):
unknowns = [] unknowns = []
pout = Popen([svn, 'status'], stdout=PIPE).stdout pout = Popen([svn, 'status'], stdout=PIPE).stdout
for line in iter(pout.readline, ''): for line in iter(pout.readline, ''):
m = re.match(r'\? (.*)$', line) m = re.match(r'\? (.*)$', line)
if m is not None: if m is not None:
unknowns.append(m.group(1)) if not (config['ignore_symlinks'] and os.path.islink(m.group(1))):
unknowns.append(m.group(1))
return unknowns return unknowns
def descendant_path(child, parent): def descendant_path(child, parent):
if parent == '.':
parent = os.getcwd()
if child[0] != '/' and parent[0] == '/': if child[0] != '/' and parent[0] == '/':
child = os.getcwd() + '/' + child child = os.getcwd() + '/' + child
elif child[0] == '/' and parent[0] != '/': elif child[0] == '/' and parent[0] != '/':
@ -535,7 +546,10 @@ def find_branched_revision(svn, branch_url, branch_path, base_path):
new_path, old_path, rev = m.group(1, 2, 3) new_path, old_path, rev = m.group(1, 2, 3)
if new_path == search_path: if new_path == search_path:
if old_path == base_path: if old_path == base_path:
p.kill() try:
p.kill()
except OSError:
pass
return (int(rev), old_path) return (int(rev), old_path)
search_path = old_path search_path = old_path
return (-1, '') return (-1, '')
@ -558,6 +572,21 @@ def filter_add_output(fh, out, svn, config):
out.write(line) out.write(line)
out.write('\n') 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 # # Subcommand Handlers #
########################################################################### ###########################################################################
@ -575,7 +604,7 @@ def add_h(argv, svn, out, config):
# for each target specified, check if there are unversioned items # for each target specified, check if there are unversioned items
# underneath it (for directories) and add them as well # underneath it (for directories) and add them as well
# if none are found, fall back to the native svn add # if none are found, fall back to the native svn add
unknowns = get_unknowns(svn) unknowns = get_unknowns(svn, config)
for path in argv: for path in argv:
if path == '.': if path == '.':
path = os.getcwd() path = os.getcwd()
@ -1124,6 +1153,9 @@ def log_h(argv, svn, out, config):
if match != target: if match != target:
return False return False
return True 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 pout = Popen([svn] + argv, stdout=PIPE).stdout
while True: while True:
le = LogEntry(pout) le = LogEntry(pout)
@ -1164,12 +1196,15 @@ def status_h(argv, svn, out, config):
continue continue
# look for lines that should be ignored # 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] action = line[0]
if action == 'X': if action == 'X':
continue # don't print directory externals continue # don't print directory externals
elif line.startswith(' X '): elif line.startswith(' X '):
continue # don't print unmodified file externals 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 # anything not matched yet will cause an external to be shown
if not external_printed: if not external_printed:
@ -1178,6 +1213,14 @@ def status_h(argv, svn, out, config):
out.write("External '%s':\n" % external) out.write("External '%s':\n" % external)
external_printed = True 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 # look for lines to highlight
if re.match(STATUS_LINE_REGEX, line): if re.match(STATUS_LINE_REGEX, line):
filter_status(line, out) filter_status(line, out)
@ -1196,215 +1239,415 @@ def externals_h(argv, svn, out, config):
out.write(line[8:]) out.write(line[8:])
return RET_OK return RET_OK
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)
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 n_deletions > 0:
stash_fh.write('#info: -%d\n' % n_deletions)
if n_insertions > 0:
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()
stash_fh.write('#info: @%04d-%02d-%02d %02d:%02d\n' %
(now.year, now.month, now.day, now.hour, now.minute))
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)
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 = ''
del_text = ''
add_text = ''
modify_text = ''
delete_text = ''
summary_text = ''
date = ''
stash_fname = get_stash_fname(svn, si)
fh = open(stash_fname, 'r')
for line in iter(fh.readline, ''):
m = re.match(r'#info: (.*)$', line)
if m is not None:
info = m.group(1)
if info.startswith('A:'):
add_text = info
elif info.startswith('M:'):
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('+'):
ins_text = info
elif info.startswith('@'):
date = info[1:]
fh.close()
out.write('%-3d' % si)
elements = [
(date, 'cyan'),
(add_text, 'green'),
(modify_text, 'yellow'),
(delete_text, 'red'),
(summary_text, 'magenta'),
]
for elem, color in elements:
if elem != '':
out.write(' ')
ansi_color(out, color)
out.write(elem)
ansi_reset(out)
if del_text != '' or ins_text != '':
out.write(' (')
if del_text != '':
ansi_color(out, 'red')
out.write(del_text)
ansi_reset(out)
if ins_text != '':
if del_text != '':
out.write(' ')
ansi_color(out, 'green')
out.write(ins_text)
ansi_reset(out)
out.write(')')
out.write('\n')
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)
stash_ids = get_stash_ids(svn)
if len(stash_ids) > 0:
stash_idx = stash_ids[-1]
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()
if rc == 0:
if not keep:
os.unlink(stash_fname)
out.write('Popped stash %d\n' % stash_idx)
else:
out.write('Error popping stash %d\n' % stash_idx)
else:
out.write('No stashes to pop\n')
os.chdir(owd)
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]
if len(argv) >= 1:
stash_id = int(argv[0])
if stash_id in stash_ids:
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()
else:
out.write('Invalid stash ID\n')
else:
out.write('No stashes to show\n')
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]
if len(argv) >= 1:
stash_id = int(argv[0])
stash_fname = get_stash_fname(svn, stash_id)
os.unlink(stash_fname)
out.write('Dropped stash %d\n' % stash_id)
else:
out.write('No stashes to drop\n')
return RET_OK
def stash_h(argv, svn, out, config): def stash_h(argv, svn, out, config):
argv = argv[1:] # strip 'stash' command argv = argv[1:] # strip 'stash' command
action = 'save' opts, args = getopt.getopt(argv, 'ekp',
if len(argv) >= 1: ['list', 'pop', 'show', 'drop', 'externals', 'noexternals',
if not argv[0].startswith('-'): 'keep', 'patch'])
action = argv[0] keep = False
argv = argv[1:] patch = False
# now argv only contains options/arguments to the stash subcommand itself externals = config['stash_externals']
if action == 'save': operation = 'save'
keep_wc = False for opt, arg in opts:
options, args = getopt.getopt(argv, 'k') if opt == '--list':
for opt, val in options: operation = 'list'
if opt == '-k': elif opt == '--pop':
keep_wc = True operation = 'pop'
owd = os.getcwd() elif opt == '--show':
wc_dir = get_svn_wc_root(svn) operation = 'show'
os.chdir(wc_dir) elif opt == '--drop':
stash_idx = get_next_stash_idx(svn) operation = 'drop'
stash_fname = get_stash_fname(svn, stash_idx) elif opt in ('-k', '--keep'):
fh = open(stash_fname, 'w') keep = True
proc = Popen([svn, 'diff'], stdout=PIPE) elif opt in ('-p', '--patch'):
wrote_something = False patch = True
found_binary_file = False elif opt in ('-e', '--externals'):
n_insertions = 0 externals = True
n_deletions = 0 elif opt == '--noexternals':
for line in iter(proc.stdout.readline, ''): externals = False
if re.match(r'Cannot.display..file.marked.as.a.binary.type', if operation == 'list':
line, flags=re.I): return stash_list_h(args, svn, out, config)
found_binary_file = True elif operation == 'pop':
if line.startswith('-') and not line.startswith('---'): return stash_pop_h(args, svn, out, config, keep)
n_deletions += 1 elif operation == 'show':
if line.startswith('+') and not line.startswith('+++'): return stash_show_h(args, svn, out, config)
n_insertions += 1 elif operation == 'drop':
fh.write(line) return stash_drop_h(args, svn, out, config)
wrote_something = True return stash_save_h(args, svn, out, config, keep, patch, externals)
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')
# 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)
if n_insertions > 0:
fh.write('#info: +%d\n' % n_insertions)
now = datetime.datetime.now()
fh.write('#info: @%04d-%02d-%02d %02d:%02d\n' %
(now.year, now.month, now.day, now.hour, now.minute))
fh.close()
out.write('Created stash %d\n' % stash_idx)
os.chdir(owd)
elif action == 'list':
stash_ids = get_stash_ids(svn)
for si in reversed(stash_ids):
ins_text = ''
del_text = ''
add_text = ''
modify_text = ''
delete_text = ''
date = ''
stash_fname = get_stash_fname(svn, si)
fh = open(stash_fname, 'r')
for line in iter(fh.readline, ''):
m = re.match(r'#info: (.*)$', line)
if m is not None:
info = m.group(1)
if info.startswith('A:'):
add_text = info
elif info.startswith('M:'):
modify_text = info
elif info.startswith('D:'):
delete_text = info
elif info.startswith('-'):
del_text = info
elif info.startswith('+'):
ins_text = info
elif info.startswith('@'):
date = info[1:]
fh.close()
out.write('%-3d' % si)
elements = [
(date, 'cyan'),
(add_text, 'green'),
(modify_text, 'yellow'),
(delete_text, 'red'),
]
for elem, color in elements:
if elem != '':
out.write(' ')
ansi_color(out, color)
out.write(elem)
ansi_reset(out)
if del_text != '' or ins_text != '':
out.write(' (')
if del_text != '':
ansi_color(out, 'red')
out.write(del_text)
ansi_reset(out)
if ins_text != '':
if del_text != '':
out.write(' ')
ansi_color(out, 'green')
out.write(ins_text)
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
owd = os.getcwd()
wc_dir = get_svn_wc_root(svn)
os.chdir(wc_dir)
stash_ids = get_stash_ids(svn)
if len(stash_ids) > 0:
stash_idx = stash_ids[-1]
if len(args) >= 1:
stash_idx = int(args[0])
stash_fname = get_stash_fname(svn, stash_idx)
p = Popen([svn, 'patch', stash_fname], stdout=PIPE)
filter_update(p.stdout, out)
rc = p.wait()
if rc == 0:
if not keep:
os.unlink(stash_fname)
out.write('Popped stash %d\n' % stash_idx)
else:
out.write('Error popping stash %d\n' % stash_idx)
else:
out.write('No stashes to pop\n')
os.chdir(owd)
elif action == 'show':
stash_ids = get_stash_ids(svn)
if len(stash_ids) > 0:
stash_id = stash_ids[-1]
if len(argv) >= 1:
stash_id = int(argv[0])
if stash_id in stash_ids:
stash_fname = get_stash_fname(svn, stash_id)
fd = open(stash_fname, 'r')
for line in iter(fd.readline, ''):
if not re.match('#info:', line):
colordiff(out, line)
fd.close()
else:
out.write('Invalid stash ID\n')
else:
out.write('No stashes to show\n')
elif action == 'drop':
stash_ids = get_stash_ids(svn)
if len(stash_ids) > 0:
stash_id = stash_ids[-1]
if len(argv) >= 1:
stash_id = int(argv[0])
stash_fname = get_stash_fname(svn, stash_id)
os.unlink(stash_fname)
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 root_h(argv, svn, out, config): def root_h(argv, svn, out, config):
out.write(get_svn_root_url(svn) + '\n') out.write(get_svn_root_url(svn) + '\n')
@ -1417,6 +1660,89 @@ def url_h(argv, svn, out, config):
out.write(get_svn_url(svn, path) + '\n') out.write(get_svn_url(svn, path) + '\n')
return RET_OK 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 # # Main #
########################################################################### ###########################################################################
@ -1513,6 +1839,9 @@ def do_cmd(argv, realsvn, config, expand=True):
'add': add_h, 'add': add_h,
'bisect': bisect_h, 'bisect': bisect_h,
'branch': branch_h, 'branch': branch_h,
'checkout': checkout_h,
'clean': clean_h,
'commit': commit_h,
'externals': externals_h, 'externals': externals_h,
'switch': switch_h, 'switch': switch_h,
'merge': merge_h, 'merge': merge_h,
@ -1528,6 +1857,7 @@ def do_cmd(argv, realsvn, config, expand=True):
'lockable': lockable_h, 'lockable': lockable_h,
'status': status_h, 'status': status_h,
'stash': stash_h, 'stash': stash_h,
'revert': revert_h,
} }
do_native_exec = True do_native_exec = True