Skip to content
Find file
Fetching contributors…
Cannot retrieve contributors at this time
executable file 298 lines (252 sloc) 9.24 KB
#!/usr/bin/env python
"""
git-cherry-tree [OPTIONS] SRCBRANCH [LIMIT] [-- PATH...]
Find commits in SRCBRANCH not merged in the current branch.
Same as `git-cherry`, but consider only patches touching given subdirs, and try
hard to check if the patch was actually applied. Has also extra features for
interactive picking of a patch sequence.
Examples:
Which patches are in master branch, but not in the current one
$ git cherry-tree master
Which patches are in master branch, in directory `spam`, but not in current
$ git cherry-tree master -- spam
Interactively cherry-pick missing patches in `spam` directory
$ git cherry-tree -p master -- spam
"""
# Author: Pauli Virtanen <pav@iki.fi>. This script is in the public domain.
from subprocess import Popen, PIPE, call
import tempfile
import optparse
import re
import os
import subprocess
def main():
p = optparse.OptionParser(__doc__.strip())
p.add_option("-m", "--no-missing-only", action="store_false",
dest="missing_only", default=True,
help="don't show only missing patches")
p.add_option("-e", "--no-pretty", action="store_false",
dest="pretty", default=True,
help="don't show pretty log for each patch")
p.add_option("-o", "--no-orly", action="store_false",
dest="orly", default=True,
help="don't check if patch has already been applied in working tree")
p.add_option("-l", "--no-log-messages", action="store_false",
dest="messages", default=True,
help="don't trust cherry pick hashes mentioned in commit messages")
p.add_option("--stat", action="store_true",
help="show patch statistics")
p.add_option("-p", "--pick", action="store_true",
help="interactively cherry-pick patches")
p.add_option("-x", "--no-mark-picked", action="store_false",
dest="mark_picked", default=True,
help="if pick: don't mark changeset as picked (see cherry-pick)")
p.add_option("-s", "--signoff", action="store_true",
help="if pick: signoff picked changesets (see cherry-pick)")
p.add_option("-n", "--no-commit", action="store_true",
help="if pick: don't commit (see cherry-pick)")
p.add_option("-d", "--edit", action="store_true",
help="if pick: edit commit message before committing (see cherry-pick)")
options, args = p.parse_args()
paths = p.rargs
cherry_args = ['HEAD'] + p.largs
if len(cherry_args) != 2:
p.error('wrong number of arguments')
if not paths:
# default to root directory
paths = [git('rev-parse', '--show-cdup').strip()]
if paths == ['']:
paths = ['.']
pick_args = []
if options.mark_picked:
pick_args.append('-x')
if options.signoff:
pick_args.append('-s')
if options.no_commit:
pick_args.append('-n')
if options.edit:
pick_args.append('-e')
if options.pick:
print "git-cherry-tree: preparing..."
out = git('cherry', *cherry_args)
commits = [x.split(None, 1) for x in out.split("\n") if x.strip()]
prefix = git('rev-parse', '--show-prefix').strip()
def path_to_re(pth):
pth = os.path.normpath(os.path.join(prefix, pth))
if pth == '.':
pth = ''
pth = pth.replace('.', r'\.')
return pth + '.*'
paths_re = re.compile('|'.join(map(path_to_re, paths)))
to_pick = []
def format_patch(patch, max_len=80):
if options.pretty:
out = git('show', '--abbrev-commit', '--quiet', '--pretty=oneline',
patch).strip()[:max_len]
else:
out = patch
if options.stat:
stat = git('show', '--pretty=oneline', '--stat', patch).strip()
stat = "\n".join(stat.split("\n")[1:-1]).rstrip()
stat = "\n " + stat.replace("\n", "\n ")
out += stat
return out
cherry_hashes = {}
if options.messages:
our_commits = git('rev-list', '%s..' % p.largs[0])
for patch in our_commits.split("\n"):
patch = patch.strip()
if not patch:
continue
out = git('cat-file', 'commit', patch)
m = re.search('cherry picked from commit ([a-f0-9]+)', out,
re.S)
if m:
cherry_hashes[m.group(1)] = patch
for act, patch in commits:
if patch in cherry_hashes:
act = '-'
if options.missing_only and act == '-':
continue
out = git('diff-tree', '--name-only', '-r', patch)
paths = [x for x in out.split("\n") if x.strip()][1:]
ok = False
for p in paths:
if paths_re.match(p):
ok = True
break
if not ok:
continue
if options.orly and act == '+':
if is_patch_already_applied(patch):
act = '-'
if not options.pick:
if options.missing_only:
if act == '+':
print format_patch(patch)
else:
print "%s %s" % (act, format_patch(patch, 78))
if act == '+':
to_pick.append(patch)
if options.pick:
do_cherry_pick(to_pick, pick_args)
def is_patch_already_applied(patch):
patch = git('format-patch', '--stdout', patch + '^!')
cwd = os.getcwd()
env = dict(os.environ)
env['LC_ALL'] = 'C'
try:
dirname = git('rev-parse', '--show-cdup').strip()
if dirname:
os.chdir(dirname)
p = Popen(['patch', '-p1', '-N', '--dry-run'], stdout=PIPE, stderr=PIPE,
stdin=PIPE, env=env)
out, err = p.communicate(patch)
out += err
if 'Reversed (or previously applied) patch detected' in out \
and not 'Hunk' in out:
return True
finally:
os.chdir(cwd)
return False
def do_cherry_pick(to_pick, pick_args):
fd, tmp_fn = tempfile.mkstemp()
os.close(fd)
try:
_do_cherry_pick(tmp_fn, to_pick, pick_args)
finally:
os.unlink(tmp_fn)
def _do_cherry_pick(filename, to_pick, pick_args):
#
# Dump to file for interaction
#
f = open(filename, 'w')
f.write("flags %s\n" % " ".join(pick_args))
infos = []
for patch in to_pick:
pretty = git('show', '--quiet', '--pretty=oneline', '--abbrev-commit', patch).strip()
f.write("skip " + pretty[:70] + "\n")
info = git('show', '--abbrev-commit', '--stat', patch)
lines = ["# " + pretty[:70], "# "]
parts = info.strip().split("\n")[1:]
while parts:
part = parts.pop(0)
lines.append("# " + part[:70])
while len(part) > 70:
part = part[70:]
lines.append("# " + part[:70])
infos.append("\n".join(lines) + "\n#")
f.write("""\
# Select patches to cherry-pick.
#
# Commands:
# s, skip = do not cherry-pick the commit (you can also remove the line)
# p, pick = cherry-pick commit
# r, reword = cherry-pick commit, but edit the commit message
# f, flags = set default flags used for git-cherry-pick for all commits
#
#------------------------------------------------------------------------------
#
""")
f.write("\n".join(infos) + "\n")
f.close()
editor = os.environ.get('EDITOR', 'vi')
subprocess.call([editor, filename])
#
# Read instructions from file
#
to_pick = []
pick_args = []
f = open(filename, 'r')
for line in f:
line = line.strip()
if line.startswith('#') or not line:
continue
parts = line.split()
if len(line) < 2:
continue
cmd = parts.pop(0)
if cmd[0] == 'p':
to_pick.append((parts[0], list(pick_args)))
elif cmd[0] == 'r':
to_pick.append((parts[0], ['-e'] + list(pick_args)))
elif cmd[0] == 'f':
pick_args = parts
else:
# unknown stuff: just skip it
pass
#
# Proceed
#
for patch, flags in to_pick:
print "git-cherry-tree: picking %s" % patch
ret = subprocess.call(['git', 'cherry-pick'] + flags + [patch])
if ret != 0:
shell_seen = False
while True:
if not shell_seen:
ip = raw_input("[s]kip patch, [d]rop to shell, [q]uit: ").strip()
else:
ip = raw_input("[c]ontinue, [d]rop to shell, [q]uit: ").strip()
if (ip == 's' and not shell_seen) or (ip == 'c' and shell_seen):
break
elif ip == 'd':
call(os.environ.get('SHELL', 'bash'))
shell_seen = True
elif ip == 'q':
git('co', '-f')
return
git('co', '-f')
def git(*args, **kw):
input = kw.pop('input', None)
returncode = kw.pop('returncode', False)
if kw:
raise ValueError('Unknown keyword arguments %s' % kw.keys())
p = Popen(['git'] + list(args), stdout=PIPE)
out, err = p.communicate(input=input)
if not returncode:
return out
else:
return out, p.returncode
if __name__ == "__main__":
main()
Something went wrong with that request. Please try again.