Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 489 lines (386 sloc) 12.8 KB
#!/usr/bin/env python
#
# Easy jumping to branches and other refs, with history.
#
# Using git-j, you can easily jump to branches, jump back, or walk up
# your parent branches.
#
# git-j can be used in the following ways:
#
# git j <branchname> - Jumps to a branch
# git j - - Jumps to the last checked out branch
# git j .. - Jumps to the parent branch (stopping at master)
# git j history - Show the stored jump history
# git j <number> - Jump to the numbered item in the history
# git j alias name=branch - Store an alias for a branch or SHA
# git j alias name= - Removes a stored alias
# git j alias name - Shows the value of a stored alias
# git j aliases - Shows all aliases
# git j alias -g name=branch - Store a global alias for a branch or SHA
# git j alias -g name= - Removes a stored global alias
# git j alias -g name - Shows the value of a stored global alias
# git j aliases -g - Shows all global aliases
#
# On first usage, this will install a post-checkout hook to the repository.
# It will not overwrite an existing hook, but it must be present for git-j
# to do its job correctly.
#
#
# How is this useful? Let's give a scenario.
#
# Assume a branch setup with 'master', 'release-1.7.x', and 'my-feature'.
# In this example, I'll do the following:
#
# 1) Do some work on 1.7 (maybe I'm merging a few branches in).
# 2) Rebase my local feature branch on top of it (I like clean trees)
# 3) Merge it into 1.7.
# 4) Merge the latest 1.7 changes into master.
# 5) Go back to working on the 1.7 branch.
#
# Here we go:
#
# # This only needs to be done once. Just setting up my aliases.
# $ git j alias m=master
# $ git j alias 1.7=release-1.7.x
#
# $ git j 1.7
# # ... merge some branches in or do some work before moving into the
# # feature branch work.
# $ git j my-feature
# $ git rebase release-1.7.x
# $ git j ..
# $ git merge my-feature
# $ git j m
# $ git merge release-1.7.x
# $ git j -
# # Now I'm back on release-1.7.x, ready for more work
#
# This replaces:
#
# $ git checkout release-1.7.x
# # ... merge some branches in or do some work before moving into the
# # feature branch work.
# $ git checkout my-feature
# $ git rebase release-1.7.x
# $ git checkout release-1.7.x
# $ git merge my-feature
# $ git checkout master
# $ git merge release-1.7.x
# $ git checkout release-1.7.x
# # Now I'm back on release-1.7.x, ready for more work
#
#
# Also, if you're, say, dealing with 3 branches, and do:
#
# $ git j branch1
# # ...
# $ git j branch2
# # ...
# $ git j branch3
#
# And you want to go back 2 branches in your history to branch 1, you can
# just do:
#
# $ git j 2
#
#
# Aliases are a very useful feature, and some may be useful across all
# your repositories. To create a global alias, just pass -g when defining
# the alias:
#
# $ git j alias -g m=master
#
import ConfigParser
import hashlib
import os
import re
import subprocess
import sys
HOOK_SCRIPT = """
#!/bin/sh
if test $3 -eq 1; then
OLD_REF=`git name-rev --name-only $1`
NEW_REF=`git name-rev --name-only $2`
git j record-jump $OLD_REF $NEW_REF
fi
""".lstrip()
HOOK_SCRIPT_REVS = [
# Version 1 hook
'e1e383cc72a1224613e45b2c4c7b14a9',
# Version 2 hook
# - Checks the third argument to decide if checking out a branch.
'eacaac6c600fc2073358b8b58e3c3fe0',
]
git_dir = None
class History(object):
FILENAME = 'j-history'
MAX_HISTORY_LEN = 20
def __init__(self):
self.filename = os.path.join(git_dir, self.FILENAME)
if os.path.exists(self.filename):
with open(self.filename, 'r') as fp:
self.lines = [
line.strip()
for line in fp.readlines()
]
else:
self.lines = []
def push(self, ref):
if not self.lines or ref != self.lines[0]:
self.lines.insert(0, ref)
def pop(self):
try:
return self.lines.pop(0)
except IndexError:
return None
def save(self):
with open(self.filename, 'w') as fp:
fp.write('\n'.join([
line
for line in self.lines[:self.MAX_HISTORY_LEN]
]))
def __getitem__(self, i):
return self.lines[i]
def __iter__(self):
for line in self.lines:
yield line
class Config(object):
MAIN_SECTION = 'git-j'
ALIAS_KEY = '%s.alias' % MAIN_SECTION
ENCODED_ALIAS_RE = re.compile('\s*=\s*')
def __init__(self):
self._check_migrate_settings()
def get_aliases(self, is_global=False):
lines = self._run_git_config(['--get-all', self.ALIAS_KEY],
ignore_errors=(1,),
split_lines=True,
is_global=is_global)
if lines:
return [
self.ENCODED_ALIAS_RE.split(line.strip(), 1)
for line in lines
]
else:
return []
def get_alias(self, alias_name, default=None, is_global=False):
for key, value in self.get_aliases(is_global=is_global):
if key == alias_name:
return value
return default
def set_alias(self, alias_name, target_name, is_global=False):
if target_name:
self._run_git_config(
[
'--add', self.ALIAS_KEY,
'%s=%s' % (alias_name, target_name),
],
is_global=is_global)
def remove_alias(self, alias_name, is_global=False):
alias_value = self.get_alias(alias_name, is_global=is_global)
if alias_value:
self._run_git_config(
['--unset', self.ALIAS_KEY, re.escape(alias_value)],
is_global=is_global)
def _check_migrate_settings(self):
"""Migrates settings from an older j-config file.
Older versions of git-j stored aliases in a j-config file. This
will migrate any aliases from there into the Git config file.
"""
filename = os.path.join(git_dir, 'j-config')
if os.path.exists(filename):
print 'Migrating settings from j-config to .git/config...'
config = ConfigParser.RawConfigParser()
config.read(filename)
try:
aliases = config.items('aliases')
except ConfigParser.NoSectionError:
aliases = []
for key, value in aliases:
self.set_alias(key, value)
os.unlink(filename)
def _build_alias_key(self, alias_name):
return '%s.%s' % (self.ALIASES_SECTION, alias_name)
def _run_git_config(self, command, is_global=False, **kwargs):
cmd = ['git', 'config']
if is_global:
cmd.append('--global')
cmd += command
return execute(cmd, **kwargs)
def die(msg=None):
if msg:
sys.stderr.write(msg)
sys.exit(1)
def execute(command, split_lines=False, to_console=False, ignore_errors=(),
show_die_error=True):
"""
Utility function to execute a command and return the output.
"""
if to_console:
stdout = sys.stdout
stderr = sys.stderr
else:
stdout = subprocess.PIPE
stderr = subprocess.PIPE
p = subprocess.Popen(command,
stdin=subprocess.PIPE,
stdout=stdout,
stderr=stderr,
shell=False,
close_fds=True)
if to_console:
data = None
elif split_lines:
data = p.stdout.readlines()
else:
data = p.stdout.read()
rc = p.wait()
if rc and rc not in ignore_errors:
if show_die_error:
die('Failed to execute command: %s\n%s' % (command, data))
else:
die()
return data
def check_hook(filename):
with open(filename, 'r') as fp:
stored_hook = fp.read()
if stored_hook.strip() == HOOK_SCRIPT.strip():
return
md5 = hashlib.md5(stored_hook).hexdigest()
if md5 in HOOK_SCRIPT_REVS:
setup_hook(filename, upgrade=True)
else:
sys.stderr.write(
'You have an existing post-checkout hook at\n'
'%s\n'
'\n'
'This does not appear to be an older git-j hook. You\n'
'will need to somehow incorporate the following into the\n'
'hook:\n'
'\n'
'--------------------------------------------------------\n'
'%s\n'
'--------------------------------------------------------\n'
'\n'
'Note that this hook may change in future versions, and\n'
'this will be harder to maintain. Check the git-j source\n'
'for the latest updates.\n\n'
% (filename, HOOK_SCRIPT))
def setup_hook(filename, upgrade=False):
if upgrade:
print 'Upgrading post-checkout hook for git-j...'
else:
print 'Installing post-checkout hook for git-j...'
print
with open(filename, 'w') as fp:
fp.write(HOOK_SCRIPT)
os.chmod(filename, 0755)
def record_jump(old_ref, *args):
history = History()
history.push(old_ref)
history.save()
def checkout_branch(prev_ref, merge_changes=False):
args = ['git', 'checkout', prev_ref]
if merge_changes:
args.append('-m')
execute(args, to_console=True, show_die_error=False)
def get_ref_name(ref):
return execute(['git', 'name-rev', '--name-only', ref]).strip()
def show_history():
history = History()
for i, ref in enumerate(history):
print '%d - %s' % (i + 1, ref)
def set_alias(args):
if '--global' in args:
is_global = True
args.remove('--global')
elif '-g' in args:
is_global = True
args.remove('-g')
else:
is_global = False
if len(args) != 1:
die('Usage: git j alias [-g|--global] alias_name[=[branch]]\n')
config = Config()
parts = args[0].split('=', 1)
name = parts[0]
if len(parts) == 2:
target = parts[1]
if target:
config.set_alias(name, target, is_global=is_global)
else:
config.remove_alias(name, is_global=is_global)
else:
target_name = config.get_alias(name, is_global=is_global)
if target_name:
print target_name
else:
die('%s is not an alias' % name)
def show_aliases(args):
is_global = ('--global' in args or '-g' in args)
config = Config()
for alias_name, target_name in config.get_aliases(is_global=is_global):
print '%s = %s' % (alias_name, target_name)
def jump(args):
merge_changes = '-m' in args
if merge_changes:
args.remove('-m')
dest = args[0]
if dest == '-':
history = History()
prev_ref = history.pop()
if prev_ref:
history.save()
checkout_branch(prev_ref, merge_changes)
else:
die('There is no previous branch to jump to.\n')
elif dest == '..':
cur_ref = execute(['git', 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
refs = execute(['git', 'rev-list', '--pretty=tformat:%P',
cur_ref, '--not', 'master'],
split_lines=True)
refs = [
ref.strip()
for ref in refs
if not ref.startswith('commit')
]
if len(refs) == 0:
die('There is no parent to jump to.\n')
checkout_branch(get_ref_name(refs[0]), merge_changes)
else:
try:
num = int(dest)
if num > History.MAX_HISTORY_LEN:
num = None
except ValueError:
num = None
if num is not None:
history = History()
try:
ref = history[num - 1]
except IndexError:
die('%d is not a valid index in the history.\n' % num)
checkout_branch(ref, merge_changes)
else:
config = Config()
checkout_branch(config.get_alias(dest, dest), merge_changes)
def main(args):
global git_dir
if len(args) == 0:
die('Usage: git j [branch|-|..]\n')
git_dir = execute(['git', 'rev-parse', '--git-dir']).strip()
hook_file = os.path.join(git_dir, 'hooks', 'post-checkout')
if os.path.exists(hook_file):
check_hook(hook_file)
else:
setup_hook(hook_file)
if args[0] == 'record-jump':
record_jump(*args[1:])
elif args[0] == 'history':
show_history()
elif args[0] == 'alias':
set_alias(args[1:])
elif args[0] == 'aliases':
show_aliases(args[1:])
else:
jump(args)
main(sys.argv[1:])