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