From ca75547d3c99b9ede62e000f4b788f04242adb8d Mon Sep 17 00:00:00 2001 From: Peter Wu Date: Fri, 16 Oct 2015 18:22:05 +0200 Subject: 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. --- git-gerrit-diff | 212 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100755 git-gerrit-diff 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) -- cgit v1.2.1