Skip to content

Instantly share code, notes, and snippets.

@ojacobson
Created October 26, 2012 18:02
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ojacobson/3960339 to your computer and use it in GitHub Desktop.
Save ojacobson/3960339 to your computer and use it in GitHub Desktop.
git-issue -- a tool for correlating Redmine tickets to branches
#!/usr/bin/env python
import sys
import subprocess as s
import optparse as o
import urllib2 as u
import json as j
import re
import os
import errno
REDMINE = 'https://redmine.example.com%s'
REMOTE = 'origin'
UPSTREAM = '%s/master' % (REMOTE,)
SUBJECT_MANGLES = [
(re.compile(r"'"), ''),
(re.compile(r'[^a-zA-Z0-9]+'), '-'),
]
PREPARE_COMMITMSG_HOOK=[
'#!/bin/bash -e\n',
'### git-issue hook\n',
'{self} --prepare-commit-msg "$@"\n',
'{chain}\n',
]
def parse_args():
p = o.OptionParser(
usage="%prog [options] NUMBER [DESCRIPTION]",
description="streamline management of local issue branches"
)
p.add_option(
'--install',
action='store_true',
default=False,
help='Install hooks.',
)
p.add_option(
'--prepare-commit-msg',
action='store_true',
default=False,
help='Act as a prepare-commit-msg hook and insert "See #1234: " at the start of the commit message.',
)
options, args = p.parse_args()
if options.install:
return options, None, None
elif len(args) < 1:
p.print_help()
p.exit(1)
elif len(args) == 1:
return options, args[0], None
return options, args[0], ' '.join(args[1:])
def git(*args):
return s.check_output(('git',) + args)
def current_branch():
try:
return git('symbolic-ref', 'HEAD')
except s.CalledProcessError, e:
# "Not on a branch" (and also possibly "not in a git repo" :-\ )
if e.returncode == 128:
return None
def fetch_issue(issue):
issue_path = '/issues/%s.json' % (issue,)
response = u.urlopen(REDMINE % issue_path)
return j.load(response)
def name_issue_branch(issue, description):
summary = description
for pattern, replacement in SUBJECT_MANGLES:
summary = re.sub(pattern, replacement, summary)
assert ' ' not in summary
summary = summary.strip('-')
summary = summary.lower()
return 'issue-{issue}-{summary}'.format(summary=summary, issue=issue)
def is_issue_branch(branch, issue):
return branch_issue(branch) == issue
def branch_issue(branch):
if branch is None:
return None
if branch.startswith('refs/heads/'):
branch = branch[len('refs/heads/'):]
if branch.startswith('issue-'):
issue = ''
for char in branch[len('issue-'):]:
if char == '-':
break
issue += char
return issue
else:
return None
def branches():
branch_list = git('branch', '--list')
assert branch_list[-1] == "\n"
lines = branch_list[:-1].split("\n")
return [line[2:] for line in lines] # strip leading ' ' or '* '
def summarize(**issue_json):
return "Working on [{issue[project][name]}] #{issue[id]}: {issue[subject]}".format(**issue_json)
def install_hooks():
git_dir = git('rev-parse', '--git-dir')[:-1]
hooks = os.path.join(git_dir, 'hooks')
prepare_commit_msg = os.path.join(hooks, 'prepare-commit-msg')
try:
with open(prepare_commit_msg, 'r') as input:
lines = input.readlines()
if lines[:2] == PREPARE_COMMITMSG_HOOK[:2]:
return # hook already installed
os.rename(prepare_commit_msg, prepare_commit_msg + ".git-issue-chain")
chain = prepare_commit_msg + '.git-issue-chain "$@"'
except IOError, e:
# Not found is fine, we're about to create it.
if e.errno != errno.ENOENT:
raise
else:
chain = ""
with open(prepare_commit_msg, 'w') as output:
for line in PREPARE_COMMITMSG_HOOK:
self = os.path.abspath(sys.argv[0])
output.write(line.format(self=self, chain=chain))
os.chmod(prepare_commit_msg, 0755)
def become_issue_branch(issue, description):
if git('status', '--porcelain') != '':
print >>sys.stderr, "Uncommitted changes found in working tree"
return 2
branch = current_branch()
if is_issue_branch(branch, issue):
# Already on the correct issue branch. Do nothing; don't even validate
# that the issue exists in Redmine.
return 0
# Retrieve issue metadata from Redmine
issue_json = fetch_issue(issue)
for branch in branches():
if is_issue_branch(branch, issue):
git('checkout', branch)
print summarize(**issue_json)
return 0
if description is None:
name = name_issue_branch(issue_json['issue']['id'], issue_json['issue']['subject'])
else:
name = name_issue_branch(issue, description)
git('fetch', REMOTE)
git('checkout', UPSTREAM, '-b', name)
print summarize(**issue_json)
return 0
def prepare_commit_msg_hook(filename):
branch = current_branch()
issue = branch_issue(branch)
if issue is None:
return 0
temp = filename + '.git-issue-tmp'
with open(filename, 'r') as input:
with open(temp, 'w') as output:
print >>output, "See #%s: " % (issue,)
for line in input:
output.write(line)
os.rename(temp, filename)
return 0
def main():
options, issue, description = parse_args()
if options.install:
return install_hooks()
if options.prepare_commit_msg:
return prepare_commit_msg_hook(issue)
return become_issue_branch(issue, description)
if __name__ == '__main__':
sys.exit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment