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