jsvn/jsvn

967 lines
32 KiB
Python
Executable File

#!/usr/bin/env python
# Josh's SVN wrapper script
#
# This wrapper script to Subversion supplements normal svn behavior by
# adding additional functionality or modifying the output of the default
# svn subcommands. Much of the functionality implemented here was inspired
# by the way that git works.
#
# It is recommended to put this script in your $PATH as 'jsvn' or something
# other than 'svn' and to make an alias svn='jsvn'.
# For example, this .bash_aliases stanza will check if 'jsvn' is in your
# path and automatically alias 'svn' to it if so:
# if [[ "$(which jsvn 2>/dev/null)" != "" ]]; then
# alias svn='jsvn'
# fi
#
# Implemented subcommands:
# add <file>...
# - add files as usual; add recursive contents of directories
# branch[es] [[-d] <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
# tag[s] [[-d] <tag_name>] | [-m <old_tag_name> <new_tag_name>]
# - with no arguments, list tags
# - with -d, delete <tag>
# - with -m, rename <old_tag_name> to <new_tag_name>
# - otherwise, create a new tag from the current branch
# 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
# merge <branch>
# - merge branch <branch> into the current WC path
# - falls back to Subversion "merge" if <branch> doesn't exist
# root
# - output root URL (for use on shell such as "svn log $(svn root)/tags")
# url
# - output repository URL of current working directory
# watch-lock
# - block until the lock on a file/URL is released
# users
# - show a list of contributing users to a SVN path
# binaries [--set-lock]
# - show a list of versioned binary files under the current path, with
# a prepended '*' for those with svn:needs-lock set
# - with --set-lock, set svn:needs-lock to '*' for binaries
# lockable [--remove] | [--status] <file[s]>
# - with no switches, set svn:needs-lock to '*' for file[s]
# - with --remove, remove svn:needs-lock' for file[s]
# - with --status, prepended '*' for those with svn:needs-lock set
# externals
# - print a list of the externals in the repository
# stash [command]
# - 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 (default if not specified):
# - save changes as a "stash" object and revert them from working copy
# - this currently only works with changes to already-versioned files
# list:
# - show a list of all stash objects
# pop [id]:
# - apply the stash object <id> back to the working copy
# - the stash object is removed if it was successfully applied
# - <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
#
# The following subcommands are executed using their native handler, but
# have their output simplified and/or colorized:
# - diff
# - log
# - status
# - update
#
# If the subcommand name begins with two leading underscores ("__"), the
# underscores will be stripped and the command will be handled by native
# Subversion without any jsvn processing.
#
# Configuration:
#
# jsvn will execute the file ~/.jsvn, if it exists, as a Python script.
# Variables written to will be used as configuration directives.
# Available configuration directives:
# use_color: True or False to enable/disable colorization of svn output
# use_pager: True or False to enable/disable automatic piping of svn
# output to a pager program
# pager: A string specifying the pager program (and args) to execute
# aliases['XXX']: A string or list defining the alias 'XXX'. A string
# can be used if the alias expands to a single argument. A
# list must be used to pass multiple arguments to svn.
#
# Configuration Examples:
# pager = 'less -FRXi' # enable case-insensitive searching in less
# aliases['revert'] = ['revert', '-R'] # default to recursive reverts
# aliases['s'] = ['status', '--ignore-externals']
# aliases['status'] = '__status' # ignore jsvn processing of status command
#
# Author: Josh Holtrop
#
# History:
# v1.0 - functional release on github
import sys
import os
import re
import time
from subprocess import *
import traceback
STATUS_LINE_REGEX = r'[ACDIMRX?!~ ][CM ][L ][+ ][SX ][KOTB ]'
###########################################################################
# Subcommand Handler Return Values #
###########################################################################
RET_OK = 0
RET_ERR = 1
RET_REEXEC = 2
###########################################################################
# ANSI escape color code values #
###########################################################################
COLORS = {
'black': 0,
'red': 1,
'green': 2,
'yellow': 3,
'blue': 4,
'magenta': 5,
'cyan': 6,
'white': 7,
}
using_color = False
###########################################################################
# Configuration #
###########################################################################
def get_config():
config = {
'pager': 'less -FRX',
'use_pager': True,
'use_color': True,
'aliases': {
# default Subversion aliases
'praise': 'blame',
'annotate': 'blame',
'ann': 'blame',
'cl': 'changelist',
'co': 'checkout',
'ci': 'commit',
'cp': 'copy',
'del': 'delete',
'remove': 'delete',
'rm': 'delete',
'di': 'diff',
'?': 'help',
'h': 'help',
'ls': 'list',
'mv': 'move',
'rename': 'move',
'ren': 'move',
'pdel': 'propdel',
'pd': 'propdel',
'pedit': 'propedit',
'pe': 'propedit',
'pget': 'propget',
'pg': 'propget',
'plist': 'proplist',
'pl': 'proplist',
'pset': 'propset',
'ps': 'propset',
'stat': 'status',
'st': 'status',
'sw': 'switch',
'up': 'update',
# default jsvn aliases
'tags': 'tag',
'branches': 'branch'},
'svn': '',
}
pth = os.path.expanduser('~/.jsvn')
if os.path.exists(pth):
fh = open(pth, 'r')
script = fh.read()
fh.close()
try:
exec(script, config)
except:
sys.stderr.write('Configuration file error in "%s":\n' % pth)
traceback.print_exception(sys.exc_info()[0], sys.exc_info()[1],
None)
tb = traceback.extract_tb(sys.exc_info()[2])
for ent in tb[1:]:
lineno, fn = ent[1:3]
sys.stderr.write(' File "%s", line %d, in %s\n'
% (pth, lineno, fn))
return config
def apply_aliases(config, argv):
if not argv[0] in config['aliases']:
return argv
alias = config['aliases'][argv[0]]
if type(alias) == str:
return [alias] + argv[1:]
elif type(alias) == list:
return alias + argv[1:]
sys.stderr.write('Unsupported type for alias "%s"\n' % alias)
return argv
###########################################################################
# Utility Functions #
###########################################################################
def ansi_color(out, fg=None, bg=None, bold=False):
if using_color:
bc = 1 if bold else 0
if fg is not None:
out.write('\033[%d;%dm' % (bc, 30 + COLORS[fg]))
if bg is not None:
out.write('\033[%d;%dm' % (bc, 40 + COLORS[bg]))
def ansi_reset(out):
if using_color:
out.write('\033[0m')
def colordiff(out, line):
if re.match(r'Index:\s', line):
ansi_color(out, 'yellow')
out.write(line)
ansi_reset(out)
return
if re.match(r'={67}', line):
ansi_color(out, 'yellow')
out.write(line)
ansi_reset(out)
return
if re.match(r'-', line):
ansi_color(out, 'red')
out.write(line)
ansi_reset(out)
return
elif re.match(r'\+', line):
ansi_color(out, 'green')
out.write(line)
ansi_reset(out)
return
m = re.match(r'(@@.*@@)(.*)', line)
if m is None:
m = re.match(r'(##.*##)(.*)', line)
if m is not None:
ansi_color(out, 'cyan')
out.write(m.group(1))
ansi_reset(out)
out.write(m.group(2))
out.write('\n')
return
out.write(line)
def findInPath(cmd):
path_entries = os.environ['PATH'].split(os.pathsep)
for p in path_entries:
full_path = os.path.join(p, cmd)
if os.path.exists(full_path):
return full_path
return ''
def getSVNURL(svn):
for line in Popen([svn, 'info'], stdout=PIPE).communicate()[0].split('\n'):
m = re.match(r'^URL:\s*(.*?)\s*$', line)
if m is not None:
return m.group(1)
return ''
def getSVNRoot(svn):
url = getSVNURL(svn)
parts = url.split('/')
for i in range(0, len(parts)):
if parts[i] in ('trunk', 'tags', 'branches'):
return '/'.join(parts[:i])
return ''
def get_svn_wc_root(svn):
for line in Popen([svn, 'info'], stdout=PIPE).communicate()[0].split('\n'):
m = re.match(r'Working Copy Root Path: (.*)$', line)
if m is not None:
return m.group(1)
return ''
def getSVNRelPath(svn):
url = getSVNURL(svn)
parts = url.split('/')
for i in range(0, len(parts) - 1):
if parts[i] == 'trunk' or i > 0 and parts[i-1] in ('tags', 'branches'):
return '/' + '/'.join(parts[i+1:])
return '/'
def getSVNTopLevel(svn):
url = getSVNURL(svn)
parts = url.split('/')
for i in range(0, len(parts)):
if parts[i] == 'trunk' or i > 0 and parts[i-1] in ('tags', 'branches'):
return '/'.join(parts[:i+1])
return ''
def getSVNBranchList(svn):
colist = []
root = getSVNRoot(svn)
lines = Popen([svn, 'ls', root + '/branches'],
stdout=PIPE, stderr=PIPE).communicate()[0].split('\n')
for line in lines:
if re.match(r'^\s*$', line) is None:
colist.append(re.sub(r'/$', '', line))
return colist
def getSVNTagList(svn):
colist = []
root = getSVNRoot(svn)
lines = Popen([svn, 'ls', root + '/tags'],
stdout=PIPE, stderr=PIPE).communicate()[0].split('\n')
for line in lines:
if re.match(r'^\s*$', line) is None:
colist.append(re.sub(r'/$', '', line))
return colist
def getSVNProperty(svn, prop, path):
return Popen([svn, 'propget', prop, path], stdout=PIPE).communicate()[0]
def setSVNProperty(svn, prop, val, path):
Popen([svn, 'propset', prop, val, path], stdout=PIPE).wait()
def delSVNProperty(svn, prop, path):
Popen([svn, 'propdel', prop, path], stdout=PIPE).wait()
def filter_update(pout, out):
external = ''
external_printed = True
any_external_printed = False
for line in iter(pout.readline, ''):
m = re.match(r"Fetching external item into '(.*)':", line)
if m is not None:
external = m.group(1)
external_printed = False
continue
if re.match(r'\s*$', line):
continue
if re.match(r'External at revision ', line):
if external_printed:
out.write(line)
continue
if re.match(r'(Updated.to|At) revision', line):
if any_external_printed:
out.write('\n')
out.write(line)
continue
# anything not matched yet will cause an external to be shown
if not external_printed:
out.write("\nExternal '%s':\n" % external)
external_printed = True
any_external_printed = True
if re.match(r'[ADUCGER ]{2}[B ][C ] ', line):
action = line[0]
if action == 'A':
ansi_color(out, 'green')
elif action == 'D':
ansi_color(out, 'red')
elif action == 'U':
ansi_color(out, 'cyan')
elif action == 'C':
ansi_color(out, 'yellow')
elif action == 'G':
ansi_color(out, 'magenta')
out.write(line)
ansi_reset(out)
continue
out.write(line)
def get_unknowns(svn):
unknowns = []
pout = Popen([svn, 'status'], stdout=PIPE).stdout
for line in iter(pout.readline, ''):
m = re.match(r'\? (.*)$', line)
if m is not None:
unknowns.append(m.group(1))
return unknowns
def descendant_path(child, parent):
if child[0] != '/' and parent[0] == '/':
child = os.getcwd() + '/' + child
elif child[0] == '/' and parent[0] != '/':
parent = os.getcwd() + '/' + parent
if child == parent:
return True
if child.startswith(parent):
if child[len(parent)] == '/':
return True
return False
def get_stashes_dir(svn):
stashes_dir = get_svn_wc_root(svn) + '/.svn/stashes'
if not os.path.isdir(stashes_dir):
os.mkdir(stashes_dir)
return stashes_dir
def get_stash_ids(svn):
stashes_dir = get_stashes_dir(svn)
stash_files = os.listdir(stashes_dir)
stash_ids = {}
for sf in stash_files:
m = re.match('stash\.(\d+)$', sf)
if m is not None:
stash_ids[int(m.group(1))] = 1
return sorted(stash_ids.keys())
def get_stash_fname(svn, idx):
return get_stashes_dir(svn) + '/stash.%d' % idx
def get_next_stash_idx(svn):
stash_ids = get_stash_ids(svn)
idx = 1
if len(stash_ids) > 0:
idx = stash_ids[-1] + 1
return idx
###########################################################################
# Subcommand Handlers #
###########################################################################
def add(argv, svn, out):
if len(argv) < 2:
# 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
return RET_REEXEC
# 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)
for path in argv[1:]:
if path == '.':
path = os.getcwd()
if path.endswith('/'):
path = path[:-1]
found_one = False
for u in unknowns:
if descendant_path(u, path):
Popen([svn, 'add', u], stdout=out).wait()
found_one = True
if not found_one:
Popen([svn, 'add', path], stdout=out).wait()
return RET_OK
def branch(argv, svn, out):
origin = getSVNTopLevel(svn)
root = getSVNRoot(svn)
if origin == '' or root == '':
sys.stderr.write("Could not determine origin/root URL\n")
return RET_ERR
if len(argv) < 2:
bl = ['trunk'] + getSVNBranchList(svn)
current = getSVNTopLevel(svn).split('/')[-1]
bl.sort()
for b in bl:
if b == current:
out.write('*')
ansi_color(out, 'green')
else:
out.write(' ')
out.write(b + '\n')
if b == current:
ansi_reset(out)
return RET_OK
branch_name = argv[-1]
if len(argv) >= 3 and argv[1] == "-d":
# delete branch in argv[2]
Popen([svn, 'rm', root + '/branches/' + argv[2], '-m',
"Removed branch '%s'" % branch_name], stdout=out).wait()
return RET_OK
comment = "Created '%s' branch" % branch_name
branch_path = root + '/branches/' + branch_name
Popen([svn, 'copy', origin, branch_path, '-m', comment], stdout=out).wait()
return RET_OK
def tag(argv, svn, out):
origin = getSVNTopLevel(svn)
root = getSVNRoot(svn)
if origin == '' or root == '':
sys.stderr.write("Could not determine origin/root URL\n")
return RET_ERR
tl = getSVNTagList(svn)
if len(argv) < 2:
tl.sort()
for t in tl:
out.write(t + '\n')
return RET_OK
tag_name = argv[-1]
if len(argv) == 4 and argv[1] == '-m':
old_tag_name = argv[2]
if not old_tag_name in tl:
sys.stderr.write('Tag %s not found!\n' % old_tag_name)
return RET_ERR
Popen([svn, 'mv',
root + '/tags/' + old_tag_name, root + '/tags/' + tag_name,
'-m', "Renamed tag '%s' to '%s'" % (old_tag_name, tag_name)],
stdout=out).wait()
return RET_OK
if len(argv) >= 3 and argv[1] == "-d":
if not tag_name in tl:
sys.stderr.write('Tag %s not found!\n' % tag_name)
return RET_ERR
# delete tag in argv[2]
Popen([svn, 'rm', root + '/tags/' + tag_name, '-m',
"Removed tag '%s'" % tag_name], stdout=out).wait()
return RET_OK
comment = "Created '%s' tag" % tag_name
tag_path = root + '/tags/' + tag_name
Popen([svn, 'copy', origin, tag_path, '-m', comment], stdout=out).wait()
return RET_OK
def switch(argv, svn, out):
if len(argv) < 2:
return RET_REEXEC
switched = False
root = getSVNRoot(svn)
path = getSVNRelPath(svn)
while True:
if argv[1] == 'trunk':
pout = Popen([svn, 'switch', root + '/trunk' + path],
stdout=PIPE).stdout
filter_update(pout, out)
switched = True
break
bl = getSVNBranchList(svn)
if argv[1] in bl:
pout = Popen([svn, 'switch', root + '/branches/' + argv[1] + path],
stdout=PIPE).stdout
filter_update(pout, out)
switched = True
break
tl = getSVNTagList(svn)
if argv[1] in tl:
pout = Popen([svn, 'switch', root + '/tags/' + argv[1] + path],
stdout=PIPE).stdout
filter_update(pout, out)
switched = True
break
# argument is not a tag/branch name
break
if switched:
url = getSVNURL(svn)
out.write('URL: %s\n' % url)
return RET_OK
pout = Popen([svn] + argv, stdout=PIPE).stdout
filter_update(pout, out)
return RET_OK
def merge(argv, svn, out):
if len(argv) < 2:
return RET_REEXEC
root = getSVNRoot(svn)
branches = getSVNBranchList(svn)
if not argv[1] in branches:
return RET_REEXEC
lines = Popen([svn, 'log', '--stop-on-copy', root + '/branches/' + argv[1]],
stdout=PIPE).communicate()[0].split('\n')
rev = 0
for line in lines:
m = re.match(r'^r(\d+)\s', line)
if m is not None:
rev = m.group(1)
if rev == 0:
sys.stderr.write('Could not get first branch revision\n')
return RET_ERR
path = getSVNRelPath(svn)
Popen([svn, 'merge', '-r%s:HEAD' % rev,
root + '/branches/' + argv[1] + path, '.'], stdout=out).wait()
return RET_OK
def watch_lock(argv, svn, out):
if len(argv) < 2:
return RET_ERR
path = argv[1]
if os.path.exists(path):
# Get the repository URL of the file being watched
p = Popen([svn, 'info', path], stdout=PIPE)
lines = p.communicate()[0].split('\n')
for line in lines:
m = re.match(r'URL: (.*)', line)
if m is not None:
path = m.group(1)
break
last_lock_owner = ''
while True:
lock_owner = ''
p = Popen([svn, 'info', path], stdout=PIPE)
lines = p.communicate()[0].split('\n')
for line in lines:
m = re.match(r'Lock\sOwner:\s*(.*)', line)
if m is not None:
lock_owner = m.group(1)
break
if lock_owner == '':
break
if lock_owner != last_lock_owner:
out.write('Locked by: %s\n' % lock_owner)
last_lock_owner = lock_owner
time.sleep(60)
out.write('''
_ _ _ _ _ _
| | | |_ __ | | ___ ___| | _____ __| | |
| | | | '_ \| |/ _ \ / __| |/ / _ \/ _` | |
| |_| | | | | | (_) | (__| < __/ (_| |_|
\___/|_| |_|_|\___/ \___|_|\_\___|\__,_(_)
''')
return RET_OK
def users(argv, svn, out):
path = '.'
if len(argv) > 1:
path = argv[1]
users = {}
p = Popen([svn, 'log', '-q', path], stdout=PIPE)
for line in iter(p.stdout.readline, ''):
m = re.match('r\d+\s*\|([^|]+)\|', line)
if m is not None:
user = m.group(1).strip()
if not user.lower() in users:
users[user.lower()] = [user, 1]
else:
users[user.lower()][1] += 1
values = users.values()
values.sort(key = lambda x: x[1], reverse = True)
for v in values:
out.write("%8d %s\n" % (v[1], v[0]))
return RET_OK
def binaries(argv, svn, out, base_path = '.'):
for ent in os.listdir(base_path):
if ent in ('.', '..', '.svn'):
continue
ent_path = os.sep.join([base_path, ent])
if os.path.isfile(ent_path):
mime_type = getSVNProperty(svn, 'svn:mime-type', ent_path)
if mime_type != '' and not re.match(r'text/.*', mime_type):
# we found a binary file
needs_lock = getSVNProperty(svn, 'svn:needs-lock', ent_path)
if needs_lock:
out.write('* ')
elif len(argv) >= 2 and argv[1] == '--set-lock':
setSVNProperty(svn, 'svn:needs-lock', '*', ent_path)
out.write('S ')
else:
out.write(' ')
out.write(ent_path)
out.write('\n')
elif os.path.isdir(ent_path):
binaries(argv, svn, out, os.sep.join([base_path, ent]))
return RET_OK
def lockable(argv, svn, out):
if len(argv) >= 2 and argv[1] == '--status':
for ob in argv[2:]:
ob_path = os.sep.join([base_path, ob])
needs_lock = getSVNProperty(svn, 'svn:needs-lock', ob_path)
if needs_lock:
out.write('* ')
else:
out.write(' ')
out.write(ob_path)
out.write('\n')
elif len(argv) >= 2 and argv[1] == '--remove':
for ob in argv[2:]:
ob_path = os.sep.join([base_path, ob])
delSVNProperty(svn, 'svn:needs-lock', ob_path)
else:
# note this is the default assumed operation
for ob in argv[1:]:
ob_path = os.sep.join([base_path, ob])
setSVNProperty(svn, 'svn:needs-lock', '*', ob_path)
return RET_OK
def diff(argv, svn, out):
pout = Popen([svn] + argv, stdout=PIPE).stdout
for line in iter(pout.readline, ''):
colordiff(out, line)
return RET_OK
def log(argv, svn, out):
mode = 'normal'
pout = Popen([svn] + argv, stdout=PIPE).stdout
for line in iter(pout.readline, ''):
if mode == 'normal' and re.match(r'(r\d+)\s+\|', line):
parts = line.split('|')
if len(parts) == 4:
ansi_color(out, 'blue', bold=True)
out.write(parts[0])
ansi_reset(out)
out.write('|')
ansi_color(out, 'cyan')
out.write(parts[1])
ansi_reset(out)
out.write('|')
ansi_color(out, 'magenta')
out.write(parts[2])
ansi_reset(out)
out.write('|')
out.write(parts[3])
else:
out.write(line)
elif mode == 'normal' and re.match(r'Changed.paths:', line):
out.write(line)
mode = 'cp'
elif mode == 'cp' and re.match(r' [ADM] ', line):
action = line[3]
if action == 'A':
ansi_color(out, 'green')
elif action == 'D':
ansi_color(out, 'red')
elif action == 'M':
ansi_color(out, 'yellow')
out.write(line)
ansi_reset(out)
elif re.match(r'-{72}', line):
ansi_color(out, 'yellow')
out.write(line)
ansi_reset(out)
mode = 'normal'
elif re.match(r'={67}', line):
ansi_color(out, 'yellow')
out.write(line)
ansi_reset(out)
mode = 'diff'
elif mode == 'diff':
colordiff(out, line)
elif re.match(r'Index:\s', line):
ansi_color(out, 'yellow')
out.write(line)
ansi_reset(out)
else:
out.write(line)
return RET_OK
def update(argv, svn, out):
pout = Popen([svn] + argv, stdout=PIPE).stdout
filter_update(pout, out)
return RET_OK
def status(argv, svn, out):
external = ''
external_printed = True
pout = Popen([svn] + argv, stdout=PIPE).stdout
for line in iter(pout.readline, ''):
m = re.match(r"Performing status on external item at '(.*)':", line)
if m is not None:
external = m.group(1)
external_printed = False
continue
if re.match(r'\s*$', line):
continue
# anything not matched yet will cause an external to be shown
if not external_printed:
out.write("\nExternal '%s':\n" % external)
external_printed = True
if re.match(STATUS_LINE_REGEX, line):
action = line[0]
if action == 'A' or action == 'M':
ansi_color(out, 'green')
elif action == 'C':
ansi_color(out, 'yellow')
elif action == 'D':
ansi_color(out, 'red')
elif action == 'R':
ansi_color(out, 'magenta')
elif action == 'X':
continue # don't print externals
out.write(line)
ansi_reset(out)
continue
out.write(line)
return RET_OK
def externals(argv, svn, out):
pout = Popen([svn, 'status'], stdout=PIPE).stdout
for line in iter(pout.readline, ''):
if re.match(STATUS_LINE_REGEX, line):
if line[0] == 'X':
out.write(line[8:])
return RET_OK
def stash(argv, svn, out):
action = 'save'
if len(argv) >= 2:
if not argv[1].startswith('-'):
action = argv[1]
if action == 'save':
owd = os.getcwd()
wc_dir = get_svn_wc_root(svn)
os.chdir(wc_dir)
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
for line in iter(proc.stdout.readline, ''):
if len(line) > 0:
wrote_something = True
fh.write(line)
proc.wait()
fh.close()
if wrote_something:
Popen([svn, 'revert', '--depth=infinity', '.'],
stdout=PIPE).wait()
out.write('Created stash %d\n' % stash_idx)
else:
out.write('Nothing to stash!\n')
os.unlink(stash_fname)
os.chdir(owd)
elif action == 'list':
stash_ids = get_stash_ids(svn)
for si in reversed(stash_ids):
out.write('%d\n' % si)
elif action == 'pop':
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(argv) >= 3:
stash_idx = int(argv[2])
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:
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) >= 3:
stash_id = int(argv[2])
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, ''):
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) >= 3:
stash_id = int(argv[2])
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(argv, svn, out):
out.write(getSVNRoot(svn) + '\n')
return RET_OK
def url(argv, svn, out):
out.write(getSVNURL(svn) + '\n')
return RET_OK
###########################################################################
# Main #
###########################################################################
def main(argv):
global using_color
config = get_config()
realsvn = config['svn'] if config['svn'] != '' else findInPath('svn')
out = sys.stdout
argv = apply_aliases(config, argv)
using_pager = False
using_color = sys.stdout.isatty() and config['use_color']
if sys.stdout.isatty() and config['use_pager']:
if (len(argv) >= 1 and argv[0] in
('blame', 'cat', 'diff', 'help', 'list', 'log',
'propget', 'proplist')):
pager = config['pager']
if 'PAGER' in os.environ and os.environ['PAGER'] != '':
pager = os.environ['PAGER']
pager_proc = Popen(pager, shell=True, stdin=PIPE)
out = pager_proc.stdin
using_pager = True
if realsvn == '':
sys.stderr.write("Error: 'svn' not found in path\n")
return 1
handlers = {
'add': add,
'branch': branch,
'externals': externals,
'switch': switch,
'merge': merge,
'tag': tag,
'diff': diff,
'log': log,
'root': root,
'update': update,
'url' : url,
'watch-lock': watch_lock,
'users': users,
'binaries': binaries,
'lockable': lockable,
'status': status,
'stash': stash,
}
do_normal_exec = True
if len(argv) >= 1:
if argv[0] in handlers:
r = handlers[argv[0]](argv, realsvn, out)
if r == RET_OK or r == RET_ERR:
do_normal_exec = False
elif argv[0].startswith('__'):
# allow double-underscore commands to execute the native
# subversion command (e.g. "__st")
argv[0] = argv[0][2:]
if do_normal_exec:
Popen([realsvn] + argv, stdout=out).wait()
if using_pager:
out.close()
pager_proc.wait()
return 0
if __name__ == "__main__":
rc = 0
try:
rc = main(sys.argv[1:])
except IOError:
pass
sys.exit(rc)