Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
22c293eb58 | |||
8e10d8aefb | |||
3c100af8d2 | |||
0ccc15089b | |||
ab839658ba | |||
a970a6ef8a | |||
|
146bdc347b | ||
3456e95135 | |||
40b987911c | |||
3107b78aa0 | |||
030c1c519f | |||
127815dd1f | |||
3fbec5ea29 | |||
9fd1b4bfed | |||
4fa19ce27a | |||
efdad72708 | |||
5955385e9c | |||
f94756e615 |
22
README
22
README
@ -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
|
||||||
|
137
jsvn
137
jsvn
@ -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,12 +463,13 @@ 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:
|
||||||
|
if not (config['ignore_symlinks'] and os.path.islink(m.group(1))):
|
||||||
unknowns.append(m.group(1))
|
unknowns.append(m.group(1))
|
||||||
return unknowns
|
return unknowns
|
||||||
|
|
||||||
@ -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,21 +1253,28 @@ 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)
|
||||||
|
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:
|
for target in argv:
|
||||||
if fname.startswith(os.getcwd() + os.sep):
|
if fname.startswith(os.getcwd() + os.sep):
|
||||||
fname = fname[len(os.getcwd() + os.sep):]
|
fname = fname[len(os.getcwd() + os.sep):]
|
||||||
if target == '.' or target == fname or fname.startswith(target + os.sep):
|
if target == '.' or target == fname or fname.startswith(target + os.sep):
|
||||||
Popen([svn, 'revert', fname]).wait()
|
cmd = [svn, "revert"]
|
||||||
|
if action == 'A':
|
||||||
|
cmd += ["--depth", "infinity"]
|
||||||
|
cmd.append(fname)
|
||||||
|
Popen(cmd).wait()
|
||||||
did_something = True
|
did_something = True
|
||||||
break
|
break
|
||||||
return RET_OK if did_something else RET_REEXEC
|
return RET_OK if did_something else RET_REEXEC
|
||||||
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user