mirror of https://github.com/bjeanes/dotfiles.git
672 lines
19 KiB
Python
Executable File
672 lines
19 KiB
Python
Executable File
#!/usr/bin/env python
|
|
"""
|
|
Usage: vcprompt [options]
|
|
|
|
Version control information in your prompt.
|
|
|
|
Options:
|
|
-f, --format FORMAT The format string to use.
|
|
-p, --path PATH The path to run vcprompt on.
|
|
-d, --max-depth DEPTH The maximum number of directories to traverse.
|
|
-s, --systems SYSTEMS The version control systems to use.
|
|
-u, --unknown UNKNOWN The "unknown" value.
|
|
-v, --version Show program's version number and exit
|
|
-h, --help Show this help message and exit
|
|
|
|
VCS-specific formatting:
|
|
These options can be used for VCS-specific prompt formatting.
|
|
|
|
--format-bzr FORMAT Bazaar
|
|
--format-cvs FORMAT CVS
|
|
--format-darcs FORMAT Darcs
|
|
--format-fossil FORMAT Fossil
|
|
--format-git FORMAT Git
|
|
--format-hg FORMAT Mercurial
|
|
--format-svn FORMAT Subversion
|
|
|
|
Internal options:
|
|
These options are used internally or for testing. Use of these is not
|
|
recommended.
|
|
|
|
--values OPTIONS Prints the values of the given `OPTIONS` to stdout.
|
|
--without-environment Ignore any environment variables and fall back to
|
|
just the provided (or default) values. If used, this
|
|
option must come before any other provided options.
|
|
"""
|
|
|
|
from __future__ import with_statement
|
|
from subprocess import Popen, PIPE
|
|
import optparse
|
|
import os
|
|
import re
|
|
import sqlite3
|
|
import sys
|
|
|
|
__version__ = (0, 1, 5)
|
|
|
|
|
|
# check to make sure the '--without-environment' flag is called first
|
|
# this could be done in a callback, but we'd have to keep a note of every
|
|
# option which is affected by this flag
|
|
if '--without-environment' in sys.argv and \
|
|
sys.argv[1] != '--without-environment':
|
|
output = "The '--without-environment' option must come before any "
|
|
print "%s other options." % output
|
|
sys.exit(1)
|
|
|
|
# we need to get this in early because callbacks are always called after
|
|
# every other option is already set, regardless of when the callback option
|
|
# is actually used in the script
|
|
if len(sys.argv) > 1 and sys.argv[1] == '--without-environment':
|
|
for k in os.environ.keys():
|
|
if k.startswith('VCPROMPT'):
|
|
del os.environ[k]
|
|
del sys.argv[1]
|
|
|
|
|
|
DEPTH = os.environ.get('VCPROMPT_DEPTH', 0)
|
|
FORMAT = os.environ.get('VCPROMPT_FORMAT', '%s:%b')
|
|
UNKNOWN = os.environ.get('VCPROMPT_UNKNOWN', '(unknown)')
|
|
SYSTEMS = []
|
|
|
|
|
|
def helper(*args, **kwargs):
|
|
"""
|
|
Prints the module's docstring.
|
|
|
|
Doing this kills two birds with one stone: it adds PEP 257
|
|
compliance and allows us to stop using optparse's built-in
|
|
help flag.
|
|
|
|
"""
|
|
print __doc__.strip()
|
|
sys.exit(0)
|
|
|
|
|
|
def sorted_alpha(sortme, unique=True):
|
|
"""
|
|
Sorts a string alphabetically, but does (kind of) the opposite of
|
|
the built-in ``sorted`` function.
|
|
|
|
Sorted order is:
|
|
|
|
* lowercase
|
|
* uppercase
|
|
* digits
|
|
* punctuation
|
|
|
|
Example:
|
|
|
|
>>> sorted_alpha("aSD??fc?!")
|
|
afcSD?!
|
|
|
|
"""
|
|
upper, lower, digit, punctuation, passed = ([], [], [], [], [])
|
|
passed = []
|
|
for char in sortme:
|
|
# skip duplicates
|
|
if unique and char in passed:
|
|
continue
|
|
|
|
if char.isupper():
|
|
upper.append(char)
|
|
elif char.islower():
|
|
lower.append(char)
|
|
elif char.isdigit():
|
|
digit.append(char)
|
|
else:
|
|
punctuation.append(char)
|
|
|
|
passed.append(char)
|
|
|
|
return ''.join(lower + upper + digit + punctuation)
|
|
|
|
|
|
def systems():
|
|
"""Prints all available systems to stdout."""
|
|
for system in SYSTEMS:
|
|
name = getattr(system, 'name', system.__name__)
|
|
desc = getattr(system, 'description', '')
|
|
output = "%s: %s" % (name, desc)
|
|
print output
|
|
sys.exit(0)
|
|
|
|
|
|
def values(option, opt, value, parser, *args, **kwargs):
|
|
"""
|
|
Prints the given values to stdout.
|
|
|
|
This function is *private* and should not be relied on.
|
|
"""
|
|
for option in parser.rargs:
|
|
if option == 'SYSTEMS':
|
|
systems()
|
|
if option in globals().keys():
|
|
print globals()[option]
|
|
sys.exit(0)
|
|
|
|
|
|
def vcs(name, description):
|
|
"""
|
|
Adds the given ``name`` and ``description`` as attributes on the
|
|
wrapped function.
|
|
|
|
Arguments:
|
|
|
|
``name``
|
|
The display name for the system. E.g. "Mercurial" or
|
|
"Subversion".
|
|
|
|
``description``
|
|
The description for the system. E.g.:
|
|
"The fast version continue system".
|
|
|
|
"""
|
|
|
|
def wrapped(function):
|
|
function.name = name
|
|
function.description = name
|
|
SYSTEMS.append(function)
|
|
return function
|
|
return wrapped
|
|
|
|
|
|
def version(*args):
|
|
"""
|
|
Convenience function for printing a version number.
|
|
"""
|
|
print 'vcprompt %s' % '.'.join(map(str, __version__))
|
|
sys.exit(0)
|
|
|
|
|
|
def vcprompt(path, formats, unknown, *args, **kwargs):
|
|
"""
|
|
Returns a formatted version control string for use in a shell prompt
|
|
or elsewhere.
|
|
|
|
Arguments:
|
|
|
|
``path``
|
|
A path for vcprompt to check for version control systems
|
|
|
|
Defaults to the current working directory.
|
|
|
|
``formats``
|
|
A dictionary mapping version control systems to formatting
|
|
strings to use for said system.
|
|
|
|
The default format key should be named 'default'.
|
|
|
|
``unknown``
|
|
The string to use for when ``vcprompt`` cannot determine a
|
|
value.
|
|
|
|
Keyword Arguments:
|
|
|
|
``depth``
|
|
The maximum number of directories that ``vcprompt`` will traverse
|
|
in order to find a version control system.
|
|
|
|
Defaults to 0 (no limit).
|
|
|
|
|
|
``systems``
|
|
|
|
A list of systems for ``vcprompt`` to search for over the given
|
|
``path``.
|
|
|
|
Defaults to all systems.
|
|
|
|
"""
|
|
|
|
paths = os.path.abspath(os.path.expanduser(path)).split('/')
|
|
depth = kwargs.get('depth', 0)
|
|
systems = kwargs.get('systems', None)
|
|
prompt = None
|
|
count = 0
|
|
|
|
while paths:
|
|
path = '/'.join(paths)
|
|
paths.pop()
|
|
for vcs in SYSTEMS:
|
|
if not systems or systems and vcs.__name__ in systems:
|
|
format = formats[vcs.__name__] or formats['default']
|
|
prompt = vcs(path, format, unknown)
|
|
if prompt:
|
|
return prompt
|
|
if depth:
|
|
if count == depth:
|
|
break
|
|
count += 1
|
|
return ''
|
|
|
|
|
|
def main():
|
|
# parser
|
|
parser = optparse.OptionParser()
|
|
|
|
# dump the provided --help option
|
|
parser.remove_option('--help')
|
|
|
|
# our own --help flag
|
|
parser.add_option('-h', '--help', action='callback', callback=helper)
|
|
|
|
# format
|
|
parser.add_option('-f', '--format', dest='format', default=FORMAT)
|
|
|
|
# path
|
|
parser.add_option('-p', '--path', dest='path', default='.')
|
|
|
|
# max depth
|
|
parser.add_option('-d', '--max-depth', dest='depth', type='int',
|
|
default=DEPTH)
|
|
# systems
|
|
parser.add_option('-s', '--systems', dest='systems', action='append')
|
|
|
|
# unknown
|
|
parser.add_option('-u', '--unknown', dest='unknown', default=UNKNOWN)
|
|
|
|
# version
|
|
parser.add_option('-v', '--version', action='callback', callback=version)
|
|
|
|
# values
|
|
parser.add_option('--values', dest='values', action='callback',
|
|
callback=values)
|
|
|
|
# vcs-specific formatting
|
|
for system in SYSTEMS:
|
|
default = 'VCPROMPT_FORMAT_%s' % system.__name__.upper()
|
|
default = os.environ.get(default, None)
|
|
dest = 'format-%s' % system.__name__
|
|
flag = '--%s' % dest
|
|
parser.add_option(flag, dest=dest, default=default)
|
|
|
|
|
|
# parse!
|
|
options, args = parser.parse_args()
|
|
|
|
# break out formats into their own dictionary
|
|
formats = {}
|
|
for k, v in options.__dict__.items():
|
|
if k.startswith('format'):
|
|
k = k.split('format-')[-1]
|
|
if k == 'format':
|
|
k = 'default'
|
|
formats[k] = v
|
|
|
|
output = vcprompt(depth=options.depth, formats=formats, path=options.path,
|
|
systems=options.systems, unknown=options.unknown)
|
|
|
|
return output
|
|
|
|
|
|
@vcs('Bazaar', 'The Bazaar version control system')
|
|
def bzr(path, string, unknown):
|
|
file = os.path.join(path, '.bzr/branch/last-revision')
|
|
if not os.path.exists(file):
|
|
return None
|
|
|
|
branch = revision = hash = status = unknown
|
|
|
|
# local revision or global hash
|
|
if re.search('%(r|h)', string):
|
|
with open(file, 'r') as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
revision = line.split()[0]
|
|
hash = line.rsplit('-', 1)[0][:7]
|
|
break
|
|
|
|
|
|
# status
|
|
if '%i' in string:
|
|
command = 'bzr status'
|
|
process = Popen(command.split(), stdout=PIPE)
|
|
output = process.communicate()[0]
|
|
returncode = process.returncode
|
|
|
|
if not returncode:
|
|
# the list of headers in 'bzr status' output
|
|
headers = {'added': 'A',
|
|
'modified': 'M',
|
|
'removed': 'R',
|
|
'renamed': 'V',
|
|
'kind changed': 'K',
|
|
'unknown': '?'}
|
|
headers_regex = '%s:' % '|'.join(headers.keys())
|
|
|
|
status = ''
|
|
for line in output.split('\n'):
|
|
line = line.strip()
|
|
if re.match(headers_regex, line):
|
|
header = line.split(':')[0]
|
|
status = '%s%s' % (status, headers[header])
|
|
|
|
status = sorted_alpha(status)
|
|
|
|
# formatting
|
|
string = string.replace('%b', os.path.basename(path))
|
|
string = string.replace('%h', hash)
|
|
string = string.replace('%r', revision)
|
|
string = string.replace('%i', status)
|
|
string = string.replace('%s', 'bzr')
|
|
return string
|
|
|
|
|
|
@vcs('CVS', 'Concurrent Versions System.')
|
|
def cvs(path, string, unknown):
|
|
# Stabbing in the dark here
|
|
# TODO make this not suck
|
|
file = os.path.join(path, 'CVS/')
|
|
if not os.path.exists(file):
|
|
return None
|
|
|
|
branch = revision = status = unknown
|
|
|
|
string = string.replace('%s', 'cvs')
|
|
string = string.replace('%b', branch)
|
|
string = string.replace('%i', status)
|
|
string = string.replace('%h', revision)
|
|
string = string.replace('%r', revision)
|
|
return string
|
|
|
|
|
|
@vcs('Darcs', 'Distributed. Interactive. Smart.')
|
|
def darcs(path, string, unknown):
|
|
# It's almost a given that everything in here is
|
|
# going to be wrong
|
|
file = os.path.join(path, '_darcs/hashed_inventory')
|
|
if not os.path.exists(file):
|
|
return None
|
|
|
|
hash = branch = status = unknown
|
|
|
|
# hash
|
|
if re.search('%(h|r)', string):
|
|
with open(file, 'r') as f:
|
|
for line in f:
|
|
size, hash = line.strip().split('-')
|
|
hash = hash[:7]
|
|
break
|
|
|
|
# branch
|
|
# darcs doesn't have in-repo local branching (yet), so just use
|
|
# the directory name for now
|
|
# see also: http://bugs.darcs.net/issue555
|
|
branch = os.path.basename(path)
|
|
|
|
# status
|
|
if '%i' in string:
|
|
status = ''
|
|
command = 'darcs whatsnew -l'
|
|
process = Popen(command.split(), stdout=PIPE, stderr=PIPE)
|
|
output = process.communicate()[0]
|
|
returncode = process.returncode
|
|
|
|
if not returncode:
|
|
for line in output.split('\n'):
|
|
code = line.split(' ')[0]
|
|
status = '%s%s' % (status, code)
|
|
status = sorted_alpha(status)
|
|
|
|
# formatting
|
|
string = string.replace('%b', branch)
|
|
string = string.replace('%h', hash)
|
|
string = string.replace('%r', hash)
|
|
string = string.replace('%i', status)
|
|
string = string.replace('%s', 'darcs')
|
|
return string
|
|
|
|
|
|
@vcs('Fossil', 'The Fossil version control system.')
|
|
def fossil(path, string, unknown):
|
|
file = os.path.join(path, '_FOSSIL_')
|
|
if not os.path.exists(file):
|
|
return None
|
|
|
|
branch = hash = status = unknown
|
|
|
|
# all this just to get the repository file :(
|
|
repository = None
|
|
try:
|
|
query = "SELECT value FROM vvar where name = 'repository'"
|
|
conn = sqlite3.connect(file)
|
|
c = conn.cursor()
|
|
c.execute(query)
|
|
repository = c.fetchone()[0]
|
|
except sqlite3.OperationalError:
|
|
pass
|
|
finally:
|
|
conn.close()
|
|
|
|
# we can grab the latest hash the manifest.uuid file
|
|
manifest = os.path.join(path, 'manifest.uuid')
|
|
if os.path.exists(manifest):
|
|
with open(manifest, 'r') as f:
|
|
for line in f:
|
|
hash = line.strip()[:7]
|
|
break
|
|
|
|
# branch
|
|
if hash != unknown:
|
|
try:
|
|
query = """SELECT value FROM tagxref WHERE rid =
|
|
(SELECT rid FROM blob WHERE uuid LIKE '%s%%')
|
|
AND value is not NULL LIMIT 1 """ % hash
|
|
conn = sqlite3.connect(repository)
|
|
c = conn.cursor()
|
|
c.execute(query)
|
|
branch = c.fetchone()[0]
|
|
except (sqlite3.OperationalError, TypeError):
|
|
pass
|
|
finally:
|
|
conn.close()
|
|
|
|
# status
|
|
if '%i' in string:
|
|
# new files
|
|
command = 'fossil extra'
|
|
process = Popen(command.split(), stdout=PIPE)
|
|
output = process.communicate()[0]
|
|
returncode = process.returncode
|
|
|
|
if not returncode and output:
|
|
if status == unknown:
|
|
status = ''
|
|
status = "%s?" % status
|
|
|
|
command = 'fossil changes'
|
|
process = Popen(command.split(), stdout=PIPE)
|
|
output = process.communicate()[0]
|
|
returncode = process.returncode
|
|
|
|
if not returncode:
|
|
if status == unknown:
|
|
status = ''
|
|
headers = {'EDITED': 'M',
|
|
'MISSING': 'R'}
|
|
for line in output.split('\n'):
|
|
line = line.strip()
|
|
if line:
|
|
status = "%s%s" % (status, line[0])
|
|
|
|
status = sorted_alpha(status)
|
|
|
|
# parse out formatting string
|
|
string = string.replace('%b', branch)
|
|
string = string.replace('%h', hash)
|
|
string = string.replace('%r', hash)
|
|
string = string.replace('%i', status)
|
|
string = string.replace('%s', 'fossil')
|
|
return string
|
|
|
|
|
|
@vcs('Git', 'The fast version control system.')
|
|
def git(path, string, unknown):
|
|
file = os.path.join(path, '.git/')
|
|
if not os.path.exists(file):
|
|
return None
|
|
|
|
branch = hash = status = unknown
|
|
# the current branch is required to get the hash
|
|
if re.search('%(b|r|h)', string):
|
|
branch_file = os.path.join(file, 'HEAD')
|
|
with open(branch_file, 'r') as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if re.match('^ref: refs/heads/', line.strip()):
|
|
branch = (line.split('/')[-1] or unknown).strip()
|
|
break
|
|
|
|
# hash/revision
|
|
if re.search('%(r|h)', string) and branch != unknown:
|
|
hash_file = os.path.join(file, 'refs/heads/%s' % branch)
|
|
if os.path.exists(hash_file):
|
|
with open(hash_file, 'r') as f:
|
|
for line in f:
|
|
hash = line.strip()[0:7]
|
|
break
|
|
|
|
# status
|
|
if '%i' in string:
|
|
command = 'git status --short -z'
|
|
process = Popen(command.split(), stdout=PIPE, stderr=PIPE)
|
|
output = process.communicate()[0]
|
|
returncode = process.returncode
|
|
|
|
if not returncode:
|
|
status = 'clean'
|
|
status_codes = []
|
|
for line in output.split('\n'):
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
status_codes.append(line.split()[0])
|
|
|
|
if status_codes:
|
|
status = sorted_alpha(status_codes)
|
|
|
|
# formatting
|
|
string = string.replace('%b', branch)
|
|
string = string.replace('%h', hash)
|
|
string = string.replace('%r', hash)
|
|
string = string.replace('%i', status)
|
|
string = string.replace('%s', 'git')
|
|
return string
|
|
|
|
|
|
@vcs('Mercurial', 'The Mercurial version control system.')
|
|
def hg(path, string, unknown):
|
|
file = os.path.join(path, '.hg/branchheads.cache')
|
|
if not os.path.exists(file):
|
|
return None
|
|
|
|
branch = revision = hash = status = unknown
|
|
|
|
# changeset ID or global hash
|
|
if re.search('%(r|h)', string):
|
|
cache_file = os.path.join(path, '.hg/tags.cache')
|
|
if os.path.exists(cache_file):
|
|
with open(cache_file, 'r') as f:
|
|
for line in f:
|
|
revision, hash = line.strip().split()
|
|
hash = hash[:7]
|
|
break
|
|
|
|
# branch
|
|
if re.search('%(r|h|b)', string):
|
|
count = 0
|
|
with open(file, 'r') as f:
|
|
for line in f:
|
|
hash, revision_branch = line.split()
|
|
hash = hash[:7]
|
|
if count > 0:
|
|
branch = revision_branch
|
|
break
|
|
else:
|
|
revision = revision_branch
|
|
count += 1
|
|
|
|
# status
|
|
if '%i' in string:
|
|
command = 'hg status'
|
|
process = Popen(command.split(), stdout=PIPE)
|
|
output = process.communicate()[0]
|
|
returncode = process.returncode
|
|
if not returncode:
|
|
status = ''
|
|
for line in output.split('\n'):
|
|
code = line.strip().split(' ')[0]
|
|
status = '%s%s' % (status, code)
|
|
|
|
# sort the string to make it all pretty like
|
|
status = sorted_alpha(status)
|
|
|
|
string = string.replace('%b', branch)
|
|
string = string.replace('%h', hash)
|
|
string = string.replace('%r', revision)
|
|
string = string.replace('%i', status)
|
|
string = string.replace('%s', 'hg')
|
|
return string
|
|
|
|
|
|
@vcs('Subversion', 'The Subversion version control system.')
|
|
def svn(path, string, unknown):
|
|
file = os.path.join(path, '.svn/entries')
|
|
if not os.path.exists(file):
|
|
return None
|
|
|
|
branch = revision = status = unknown
|
|
|
|
# branch
|
|
command = 'svn info %s' % path
|
|
process = Popen(command.split(), stdout=PIPE, stderr=PIPE)
|
|
output = process.communicate()[0]
|
|
returncode = process.returncode
|
|
|
|
if not returncode:
|
|
# compile some regexes
|
|
branch_regex = re.compile('((tags|branches)|trunk)')
|
|
revision_regex = re.compile('^Revision: (?P<revision>\d+)')
|
|
|
|
for line in output.split('\n'):
|
|
# branch
|
|
if '%b' in string:
|
|
if re.match('URL:', line):
|
|
matches = re.search(branch_regex, line)
|
|
if matches:
|
|
branch = matches.groups(0)[0]
|
|
|
|
# revision/hash
|
|
if re.search('%(r|h)', string):
|
|
if re.match('Revision:', line):
|
|
matches = re.search(revision_regex, line)
|
|
if 'revision' in matches.groupdict():
|
|
revision = matches.group('revision')
|
|
|
|
# status
|
|
if '%i' in string:
|
|
command = 'svn status'
|
|
process = Popen(command, shell=True, stdout=PIPE)
|
|
output = process.communicate()[0]
|
|
returncode = process.returncode
|
|
|
|
if not returncode:
|
|
status = ''
|
|
for line in output.split('\n'):
|
|
code = line.strip().split(' ')[0]
|
|
status = '%s%s' % (status, code)
|
|
|
|
status = sorted_alpha(status)
|
|
|
|
# formatting
|
|
string = string.replace('%r', revision)
|
|
string = string.replace('%h', revision)
|
|
string = string.replace('%b', branch)
|
|
string = string.replace('%i', status)
|
|
string = string.replace('%s', 'svn')
|
|
return string
|
|
|
|
|
|
if __name__ == '__main__':
|
|
print main() |