find-commit-for-patch.py 3.3 KB

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