summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Wu <peter@lekensteyn.nl>2015-10-16 18:22:05 +0200
committerPeter Wu <peter@lekensteyn.nl>2015-10-16 18:22:05 +0200
commitca75547d3c99b9ede62e000f4b788f04242adb8d (patch)
tree7bbe700193d1901c2f81c4bc5427c8b4039b2d88
parent9fcda6eda1644ce9e1ed12c4434bd29812923aca (diff)
downloadscripts-ca75547d3c99b9ede62e000f4b788f04242adb8d.tar.gz
git-gerrit-diff: find out what changes are merged
Useful to find out if I forgot to backport something and whether some branches can be removed or not.
-rwxr-xr-xgit-gerrit-diff212
1 files changed, 212 insertions, 0 deletions
diff --git a/git-gerrit-diff b/git-gerrit-diff
new file mode 100755
index 0000000..86f765e
--- /dev/null
+++ b/git-gerrit-diff
@@ -0,0 +1,212 @@
+#!/usr/bin/env python3
+"""
+Finds the differences between two given trees based on the Change-Id.
+
+First all commits not merged into upstream are shown, then all changes that
+are not merged into HEAD are displayed. Before each commit, "Not merged in
+(branch)" is displayed. Note that contextual differences or patch revisions are
+ignored, a change is simply identified by its Change-Id. It is tested with the
+Wireshark repository which uses a linear history in a branch without merges.
+
+When standard output is a terminal, a less piper will be opened, searching for
+lines beginning with "commit". Then you can press "n" and "p" to jump between
+commits.
+
+Examples:
+
+ # Assume that this script is located at ~/scripts/, add an alias and mark it
+ # such that it completes like "git log".
+ git config --add alias.gerrit-diff '!: git log; ~/scripts/git-gerrit-diff'
+
+ # Check whether the current branch is merged into upstream master
+ git gerrit-diff
+
+ # Check whether the current branch is merged into upstream master-2.0
+ git gerrit-diff origin/master-2.0
+
+ # Check whether the branch bug/12345 is merged into upstream master
+ git gerrit-diff origin/master-2.0 bug/12345
+
+ # Add options to git-log
+ git gerrit-diff origin/master-2.0 --color --stat
+ git gerrit-diff origin/master-2.0 bug/12345 --color
+"""
+
+import re, os, subprocess, sys
+from collections import OrderedDict
+
+def print_usage():
+ print("""
+Usage: git gerrit-diff [-h] [upstream [head]] [git log options]
+
+ --no-pager Disable the less pager
+ -h, --help show this help
+ -v, --verbose be verbose
+
+ upstream Upstream branch to search for equivalent commits.
+ Defaults to the upstream branch of HEAD.
+ head Working branch; defaults to HEAD
+""")
+ sys.exit(0)
+
+def parse_arguments():
+ no_pager = False
+ upstream = None
+ head = "HEAD"
+ git_options = []
+
+ args = sys.argv[1:]
+ # Parse options of this script
+ count = 0
+ for i, arg in enumerate(args):
+ if arg in ("-h", "--help"):
+ print_usage()
+ elif arg == "--no-pager":
+ no_pager = True
+ else:
+ break
+ count += 1
+ # Ignore processed options
+ del args[0:count]
+
+ # Parse upstream and head
+ count = 0
+ for i, arg in enumerate(args):
+ # Stop as soon as other options are visible.
+ if arg.startswith("-"):
+ break
+ if i == 0:
+ upstream = arg
+ elif i == 1:
+ head = arg
+ else:
+ break
+ count += 1
+ # Ignore upstream and head
+ del args[:count]
+
+ # remaining arguments are for git
+ git_options = args
+
+ return no_pager, upstream, head, git_options
+
+def split_args(args):
+ options, files = args, []
+ for i, arg in enumerate(args):
+ if arg == "--":
+ if i == 0:
+ options, files = [], args[1:]
+ else:
+ options, files = args[:i], args[i+1:]
+ break
+ elif not arg.startswith("-"):
+ options, files = args[:i], args[i:]
+ break
+ if files and files[0] == "--":
+ del a[:1]
+ return options, files
+
+def strip_color(text):
+ return re.sub("\033[^m]*m", "", text)
+
+
+def get_upstream_branch():
+ return subprocess.check_output(["git", "rev-parse", "--abbrev-ref",
+ "@{upstream}"], universal_newlines=True).strip()
+
+def get_merge_base(commit1, commit2):
+ return subprocess.check_output(["git", "merge-base", commit1, commit2],
+ universal_newlines=True).strip()
+
+def get_commits(commit_range, git_options, git_paths):
+ cmd = ["git", "log"] + git_options + [commit_range, "--"] + git_paths
+ git_proc = subprocess.Popen(cmd, universal_newlines=True,
+ stdin=subprocess.DEVNULL, stdout=subprocess.PIPE)
+ commit_hash = ""
+ block = ""
+ for line in git_proc.stdout:
+ if strip_color(line).startswith("commit "):
+ if block:
+ yield commit_hash, block
+ block = ""
+ commit_hash = line.split()[1]
+ block += line
+ if block:
+ if not commit_hash:
+ raise KeyError("Cannot find commit ID")
+ yield commit_hash, block
+
+def get_change_id(commit, identifier):
+ text = strip_color(commit)
+ m = re.search("^ Change-Id: (I[0-9a-f]{40})$", text, re.M | re.I)
+ if not m:
+ raise KeyError("Change-Id not found for %s" % identifier)
+ return m.group(1)
+
+def get_changes(commit_base, commit_head, *args):
+ commit_range = "%s...%s" % (commit_base, commit_head)
+ changes = OrderedDict()
+ for commit_hash, commit in get_commits(commit_range, *args):
+ try:
+ change_id = get_change_id(commit, commit_hash)
+ except KeyError as e:
+ # Note: this can happen when entering the SVN history (Jan 2014).
+ # Ignore older commits for now.
+ print(e, file=sys.stderr)
+ print("Skipping following commits", file=sys.stderr)
+ break
+ changes[change_id] = commit
+ return changes
+
+def main():
+ no_pager, upstream, head, git_log_args = parse_arguments()
+
+ # (options, file_paths) for git-log
+ git_args = split_args(git_log_args)
+
+ if upstream is None:
+ upstream = get_upstream_branch()
+
+ # Find the point before the branches diverged.
+ commit_base = get_merge_base(upstream, head)
+
+ # Find all commits for each branch
+ upstream_changes = get_changes(commit_base, upstream, *git_args)
+ head_changes = get_changes(commit_base, head, *git_args)
+
+ if not no_pager and os.isatty(sys.stdout.fileno()):
+ less_pipe = subprocess.Popen(["less", "-Rj2", "+/^commit"],
+ universal_newlines=True, stdin=subprocess.PIPE)
+ output = less_pipe.stdin
+ else:
+ less_pipe = None
+ output = sys.stdout
+
+ def print_change(change, upstream_missing):
+ where = upstream if upstream_missing else head
+ output.write("Not merged in %s\n" % where)
+ output.write(change)
+
+ # Print changes with are not in upstream...
+ for change_id, change in head_changes.items():
+ if change_id not in upstream_changes:
+ print_change(change, True)
+
+ # ...and upstream changes not in HEAD
+ for change_id, change in upstream_changes.items():
+ if change_id not in head_changes:
+ print_change(change, False)
+
+ if less_pipe is not None:
+ less_pipe.stdin.close()
+ less_pipe.wait()
+
+if __name__ == "__main__":
+ try:
+ main()
+ except BrokenPipeError:
+ # Ignore broken pipe errors, it is not that interesting.
+ pass
+ except subprocess.CalledProcessError as e:
+ # Git errors
+ sys.exit(e.returncode)