Compare commits

...

18 Commits
v1.6 ... 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
2 changed files with 155 additions and 18 deletions

22
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'
@ -155,6 +172,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'].
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 stash_externals: True or False to enable/disable whether '-e' is implicitly
on for 'stash' subcommand. Defaults to False. on for 'stash' subcommand. Defaults to False.
@ -172,6 +191,9 @@ 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 v1.6 - 2013-07-24
- rework 'stash' subcommand to operate on individual hunks - rework 'stash' subcommand to operate on individual hunks
- add 'revert' subcommand handler - add 'revert' subcommand handler

151
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
# #
@ -17,8 +17,10 @@ import getopt
import signal import signal
import platform import platform
import tempfile 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 #
@ -67,6 +69,7 @@ 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',
@ -93,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 = []
@ -400,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
@ -409,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:
@ -457,13 +463,14 @@ 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):
@ -571,6 +578,15 @@ def relpath(path):
return path[len(cwdprefix):] return path[len(cwdprefix):]
return path 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 #
########################################################################### ###########################################################################
@ -588,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()
@ -1137,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)
@ -1177,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:
@ -1231,23 +1253,30 @@ def revert_h(argv, svn, out, config):
if target.endswith('/'): if target.endswith('/'):
argv[i] = target[:-1] argv[i] = target[:-1]
p = Popen([svn, 'status'], stdout=PIPE) p = Popen([svn, 'status'], stdout=PIPE)
status_lines = [] modified_files = []
for line in iter(p.stdout.readline, ''): for line in iter(p.stdout.readline, ''):
status_lines.append(line)
for line in reversed(status_lines):
m = re.match(STATUS_LINE_REGEX, line) m = re.match(STATUS_LINE_REGEX, line)
if m is not None: if m is not None:
action = line[0] action = line[0]
prop_action = line[1] prop_action = line[1]
if action in ('A', 'M', 'D') or prop_action == 'M': if action in ('A', 'M', 'C', 'D', '!') or prop_action == 'M':
fname = m.group(1) fname = m.group(1)
for target in argv: if action in ('D', '!'):
if fname.startswith(os.getcwd() + os.sep): modified_files.append((action, fname))
fname = fname[len(os.getcwd() + os.sep):] else:
if target == '.' or target == fname or fname.startswith(target + os.sep): modified_files.insert(0, (action, fname))
Popen([svn, 'revert', fname]).wait() for action, fname in modified_files:
did_something = True for target in argv:
break 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 return RET_OK if did_something else RET_REEXEC
def get_svn_contents_to_stash(targets, svn, out, keep, patch, externals): def get_svn_contents_to_stash(targets, svn, out, keep, patch, externals):
@ -1631,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 #
########################################################################### ###########################################################################
@ -1727,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,