Merge branch 'stash-rework'
This commit is contained in:
commit
122e16b697
37
README
37
README
@ -56,27 +56,38 @@ Implemented subcommands:
|
|||||||
- revert all affected files under given target 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
|
||||||
|
--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
|
- show a list of all stash objects
|
||||||
pop [-k] [id]:
|
--pop [id]
|
||||||
- apply the stash object <id> back to the working copy
|
- apply the stash object <id> back to the working copy
|
||||||
- the stash object is removed unless -k (keep) given
|
- the stash object is removed unless -k (keep) given
|
||||||
- <id> defaults to the newest stash object created
|
- <id> defaults to the newest stash object created
|
||||||
show [id]:
|
--show [id]
|
||||||
- display the diff stored in stash with ID <id>
|
- display the diff stored in stash with ID <id>
|
||||||
- <id> defaults to the newest stash object created
|
- <id> defaults to the newest stash object created
|
||||||
drop [id]:
|
--drop [id]
|
||||||
- delete stash object <id>
|
- delete stash object <id>
|
||||||
- <id> defaults to the newest stash object created
|
- <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
|
||||||
@ -144,6 +155,8 @@ 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'].
|
||||||
|
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
|
||||||
|
297
jsvn
297
jsvn
@ -16,6 +16,7 @@ import types
|
|||||||
import getopt
|
import getopt
|
||||||
import signal
|
import signal
|
||||||
import platform
|
import platform
|
||||||
|
import tempfile
|
||||||
|
|
||||||
STATUS_LINE_REGEX = r'[ACDIMRX?!~ ][CM ][L ][+ ][SX ][KOTB ]..(.+)'
|
STATUS_LINE_REGEX = r'[ACDIMRX?!~ ][CM ][L ][+ ][SX ][KOTB ]..(.+)'
|
||||||
|
|
||||||
@ -72,7 +73,8 @@ def get_config(svn):
|
|||||||
'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')
|
||||||
@ -1233,93 +1235,182 @@ def revert_h(argv, svn, out, config):
|
|||||||
break
|
break
|
||||||
return RET_OK if did_something else RET_REEXEC
|
return RET_OK if did_something else RET_REEXEC
|
||||||
|
|
||||||
def stash_save_h(argv, svn, out, config):
|
def get_svn_contents_to_stash(targets, svn, out, keep, patch, externals):
|
||||||
keep_wc = False
|
s_fd, s_fname = tempfile.mkstemp(prefix = 'svn.stash.')
|
||||||
options, args = getopt.getopt(argv, 'k')
|
r_fd, r_fname = tempfile.mkstemp(prefix = 'svn.stash.')
|
||||||
for opt, val in options:
|
os.close(s_fd)
|
||||||
if opt == '-k':
|
os.close(r_fd)
|
||||||
keep_wc = True
|
s_fh = open(s_fname, 'w')
|
||||||
|
r_fh = open(r_fname, 'w')
|
||||||
|
|
||||||
|
if externals and len(targets) == 0:
|
||||||
|
targets = ['.']
|
||||||
|
proc = Popen([svn, 'status'], stdout=PIPE)
|
||||||
|
for line in iter(proc.stdout.readline, ''):
|
||||||
|
m = re.match(r"Performing status on external item at '(.*)':", line)
|
||||||
|
if m is not None:
|
||||||
|
targets.append(m.group(1))
|
||||||
|
|
||||||
|
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('')
|
||||||
|
|
||||||
|
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']
|
||||||
|
|
||||||
|
def stash_save_h(args, svn, out, config, keep, patch, externals):
|
||||||
owd = os.getcwd()
|
owd = os.getcwd()
|
||||||
wc_dir = get_svn_wc_root(svn)
|
wc_dir = get_svn_wc_root(svn)
|
||||||
os.chdir(wc_dir)
|
os.chdir(wc_dir)
|
||||||
|
s_fname, r_fname, revert_list, n_insertions, n_deletions = \
|
||||||
|
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 revert_list:
|
||||||
|
Popen([svn, 'revert', rf], stdout=PIPE).wait()
|
||||||
|
if r_fname != '':
|
||||||
|
Popen([svn, 'patch', r_fname], stdout=PIPE).wait()
|
||||||
|
if s_fname != '':
|
||||||
stash_idx = get_next_stash_idx(svn)
|
stash_idx = get_next_stash_idx(svn)
|
||||||
stash_fname = get_stash_fname(svn, stash_idx)
|
stash_fname = get_stash_fname(svn, stash_idx)
|
||||||
fh = open(stash_fname, 'w')
|
stash_fh = open(stash_fname, 'w')
|
||||||
proc = Popen([svn, 'diff'], stdout=PIPE)
|
s_fh = open(s_fname, 'r')
|
||||||
wrote_something = False
|
for line in iter(s_fh.readline, ''):
|
||||||
found_binary_file = False
|
stash_fh.write(line)
|
||||||
n_insertions = 0
|
s_fh.close()
|
||||||
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')
|
|
||||||
# write stash info
|
# 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:
|
if n_deletions > 0:
|
||||||
fh.write('#info: -%d\n' % n_deletions)
|
stash_fh.write('#info: -%d\n' % n_deletions)
|
||||||
if n_insertions > 0:
|
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()
|
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))
|
(now.year, now.month, now.day, now.hour, now.minute))
|
||||||
fh.close()
|
stash_fh.close()
|
||||||
out.write('Created stash %d\n' % stash_idx)
|
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)
|
os.chdir(owd)
|
||||||
return RET_OK
|
return RET_OK
|
||||||
|
|
||||||
@ -1331,6 +1422,7 @@ def stash_list_h(argv, svn, out, config):
|
|||||||
add_text = ''
|
add_text = ''
|
||||||
modify_text = ''
|
modify_text = ''
|
||||||
delete_text = ''
|
delete_text = ''
|
||||||
|
summary_text = ''
|
||||||
date = ''
|
date = ''
|
||||||
stash_fname = get_stash_fname(svn, si)
|
stash_fname = get_stash_fname(svn, si)
|
||||||
fh = open(stash_fname, 'r')
|
fh = open(stash_fname, 'r')
|
||||||
@ -1344,6 +1436,8 @@ def stash_list_h(argv, svn, out, config):
|
|||||||
modify_text = info
|
modify_text = info
|
||||||
elif info.startswith('D:'):
|
elif info.startswith('D:'):
|
||||||
delete_text = info
|
delete_text = info
|
||||||
|
elif info.startswith('F:'):
|
||||||
|
summary_text = info[3:]
|
||||||
elif info.startswith('-'):
|
elif info.startswith('-'):
|
||||||
del_text = info
|
del_text = info
|
||||||
elif info.startswith('+'):
|
elif info.startswith('+'):
|
||||||
@ -1357,6 +1451,7 @@ def stash_list_h(argv, svn, out, config):
|
|||||||
(add_text, 'green'),
|
(add_text, 'green'),
|
||||||
(modify_text, 'yellow'),
|
(modify_text, 'yellow'),
|
||||||
(delete_text, 'red'),
|
(delete_text, 'red'),
|
||||||
|
(summary_text, 'magenta'),
|
||||||
]
|
]
|
||||||
for elem, color in elements:
|
for elem, color in elements:
|
||||||
if elem != '':
|
if elem != '':
|
||||||
@ -1380,12 +1475,7 @@ def stash_list_h(argv, svn, out, config):
|
|||||||
out.write('\n')
|
out.write('\n')
|
||||||
return RET_OK
|
return RET_OK
|
||||||
|
|
||||||
def stash_pop_h(argv, svn, out, config):
|
def stash_pop_h(args, svn, out, config, keep):
|
||||||
keep = False
|
|
||||||
options, args = getopt.getopt(argv, 'k')
|
|
||||||
for opt, val in options:
|
|
||||||
if opt == '-k':
|
|
||||||
keep = True
|
|
||||||
owd = os.getcwd()
|
owd = os.getcwd()
|
||||||
wc_dir = get_svn_wc_root(svn)
|
wc_dir = get_svn_wc_root(svn)
|
||||||
os.chdir(wc_dir)
|
os.chdir(wc_dir)
|
||||||
@ -1443,23 +1533,30 @@ def stash_drop_h(argv, svn, out, config):
|
|||||||
|
|
||||||
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']
|
||||||
actions = {
|
for opt, arg in opts:
|
||||||
'save': stash_save_h,
|
if opt == '--list':
|
||||||
'list': stash_list_h,
|
return stash_list_h(args, svn, out, config)
|
||||||
'pop': stash_pop_h,
|
elif opt == '--pop':
|
||||||
'show': stash_show_h,
|
return stash_pop_h(args, svn, out, config, keep)
|
||||||
'drop': stash_drop_h
|
elif opt == '--show':
|
||||||
}
|
return stash_show_h(args, svn, out, config)
|
||||||
if action in actions:
|
elif opt == '--drop':
|
||||||
return actions[action](argv, svn, out, config)
|
return stash_drop_h(args, svn, out, config)
|
||||||
sys.stderr.write('Unknown action "%s"\n' % action)
|
elif opt in ('-k', '--keep'):
|
||||||
return RET_ERR
|
keep = True
|
||||||
|
elif opt in ('-p', '--patch'):
|
||||||
|
patch = True
|
||||||
|
elif opt in ('-e', '--externals'):
|
||||||
|
externals = True
|
||||||
|
elif opt == '--noexternals':
|
||||||
|
externals = False
|
||||||
|
return stash_save_h(args, svn, out, config, keep, patch, externals)
|
||||||
|
|
||||||
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')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user