Emily Bernier | d0a1eb7 | 2015-03-24 16:35:39 -0400 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # Copyright 2014 the V8 project authors. All rights reserved. |
| 3 | # Use of this source code is governed by a BSD-style license that can be |
| 4 | # found in the LICENSE file. |
| 5 | |
| 6 | import argparse |
| 7 | import subprocess |
| 8 | import sys |
| 9 | |
| 10 | |
| 11 | def GetArgs(): |
| 12 | parser = argparse.ArgumentParser( |
| 13 | description="Finds a commit that a given patch can be applied to. " |
| 14 | "Does not actually apply the patch or modify your checkout " |
| 15 | "in any way.") |
| 16 | parser.add_argument("patch_file", help="Patch file to match") |
| 17 | parser.add_argument( |
| 18 | "--branch", "-b", default="origin/master", type=str, |
| 19 | help="Git tree-ish where to start searching for commits, " |
| 20 | "default: %(default)s") |
| 21 | parser.add_argument( |
| 22 | "--limit", "-l", default=500, type=int, |
| 23 | help="Maximum number of commits to search, default: %(default)s") |
| 24 | parser.add_argument( |
| 25 | "--verbose", "-v", default=False, action="store_true", |
| 26 | help="Print verbose output for your entertainment") |
| 27 | return parser.parse_args() |
| 28 | |
| 29 | |
| 30 | def FindFilesInPatch(patch_file): |
| 31 | files = {} |
| 32 | next_file = "" |
| 33 | with open(patch_file) as patch: |
| 34 | for line in patch: |
| 35 | if line.startswith("diff --git "): |
| 36 | # diff --git a/src/objects.cc b/src/objects.cc |
| 37 | words = line.split() |
| 38 | assert words[2].startswith("a/") and len(words[2]) > 2 |
| 39 | next_file = words[2][2:] |
| 40 | elif line.startswith("index "): |
| 41 | # index add3e61..d1bbf6a 100644 |
| 42 | hashes = line.split()[1] |
| 43 | old_hash = hashes.split("..")[0] |
| 44 | if old_hash.startswith("0000000"): continue # Ignore new files. |
| 45 | files[next_file] = old_hash |
| 46 | return files |
| 47 | |
| 48 | |
| 49 | def GetGitCommitHash(treeish): |
| 50 | cmd = ["git", "log", "-1", "--format=%H", treeish] |
| 51 | return subprocess.check_output(cmd).strip() |
| 52 | |
| 53 | |
| 54 | def CountMatchingFiles(commit, files): |
| 55 | matched_files = 0 |
| 56 | # Calling out to git once and parsing the result Python-side is faster |
| 57 | # than calling 'git ls-tree' for every file. |
| 58 | cmd = ["git", "ls-tree", "-r", commit] + [f for f in files] |
| 59 | output = subprocess.check_output(cmd) |
| 60 | for line in output.splitlines(): |
| 61 | # 100644 blob c6d5daaa7d42e49a653f9861224aad0a0244b944 src/objects.cc |
| 62 | _, _, actual_hash, filename = line.split() |
| 63 | expected_hash = files[filename] |
| 64 | if actual_hash.startswith(expected_hash): matched_files += 1 |
| 65 | return matched_files |
| 66 | |
| 67 | |
| 68 | def FindFirstMatchingCommit(start, files, limit, verbose): |
| 69 | commit = GetGitCommitHash(start) |
| 70 | num_files = len(files) |
| 71 | if verbose: print(">>> Found %d files modified by patch." % num_files) |
| 72 | for _ in range(limit): |
| 73 | matched_files = CountMatchingFiles(commit, files) |
| 74 | if verbose: print("Commit %s matched %d files" % (commit, matched_files)) |
| 75 | if matched_files == num_files: |
| 76 | return commit |
| 77 | commit = GetGitCommitHash("%s^" % commit) |
| 78 | print("Sorry, no matching commit found. " |
| 79 | "Try running 'git fetch', specifying the correct --branch, " |
| 80 | "and/or setting a higher --limit.") |
| 81 | sys.exit(1) |
| 82 | |
| 83 | |
| 84 | if __name__ == "__main__": |
| 85 | args = GetArgs() |
| 86 | files = FindFilesInPatch(args.patch_file) |
| 87 | commit = FindFirstMatchingCommit(args.branch, files, args.limit, args.verbose) |
| 88 | if args.verbose: |
| 89 | print(">>> Matching commit: %s" % commit) |
| 90 | print(subprocess.check_output(["git", "log", "-1", commit])) |
| 91 | print(">>> Kthxbai.") |
| 92 | else: |
| 93 | print(commit) |