1 #!/usr/bin/env python 2 # Copyright 2015 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 operator 8 import os 9 import re 10 from sets import Set 11 from subprocess import Popen, PIPE 12 import sys 13 14 def search_all_related_commits( 15 git_working_dir, start_hash, until, separator, verbose=False): 16 17 all_commits_raw = _find_commits_inbetween( 18 start_hash, until, git_working_dir, verbose) 19 if verbose: 20 print "All commits between <of> and <until>: " + all_commits_raw 21 22 # Adding start hash too 23 all_commits = [start_hash] 24 all_commits.extend(all_commits_raw.splitlines()) 25 all_related_commits = {} 26 already_treated_commits = Set([]) 27 for commit in all_commits: 28 if commit in already_treated_commits: 29 continue 30 31 related_commits = _search_related_commits( 32 git_working_dir, commit, until, separator, verbose) 33 if len(related_commits) > 0: 34 all_related_commits[commit] = related_commits 35 already_treated_commits.update(related_commits) 36 37 already_treated_commits.update(commit) 38 39 return all_related_commits 40 41 def _search_related_commits( 42 git_working_dir, start_hash, until, separator, verbose=False): 43 44 if separator: 45 commits_between = _find_commits_inbetween( 46 start_hash, separator, git_working_dir, verbose) 47 if commits_between == "": 48 return [] 49 50 # Extract commit position 51 original_message = git_execute( 52 git_working_dir, 53 ["show", "-s", "--format=%B", start_hash], 54 verbose) 55 title = original_message.splitlines()[0] 56 57 matches = re.search("(\{#)([0-9]*)(\})", original_message) 58 59 if not matches: 60 return [] 61 62 commit_position = matches.group(2) 63 if verbose: 64 print "1.) Commit position to look for: " + commit_position 65 66 search_range = start_hash + ".." + until 67 68 def git_args(grep_pattern): 69 return [ 70 "log", 71 "--reverse", 72 "--grep=" + grep_pattern, 73 "--format=%H", 74 search_range, 75 ] 76 77 found_by_hash = git_execute( 78 git_working_dir, git_args(start_hash), verbose).strip() 79 80 if verbose: 81 print "2.) Found by hash: " + found_by_hash 82 83 found_by_commit_pos = git_execute( 84 git_working_dir, git_args(commit_position), verbose).strip() 85 86 if verbose: 87 print "3.) Found by commit position: " + found_by_commit_pos 88 89 # Replace brackets or else they are wrongly interpreted by --grep 90 title = title.replace("[", "\\[") 91 title = title.replace("]", "\\]") 92 93 found_by_title = git_execute( 94 git_working_dir, git_args(title), verbose).strip() 95 96 if verbose: 97 print "4.) Found by title: " + found_by_title 98 99 hits = ( 100 _convert_to_array(found_by_hash) + 101 _convert_to_array(found_by_commit_pos) + 102 _convert_to_array(found_by_title)) 103 hits = _remove_duplicates(hits) 104 105 if separator: 106 for current_hit in hits: 107 commits_between = _find_commits_inbetween( 108 separator, current_hit, git_working_dir, verbose) 109 if commits_between != "": 110 return hits 111 return [] 112 113 return hits 114 115 def _find_commits_inbetween(start_hash, end_hash, git_working_dir, verbose): 116 commits_between = git_execute( 117 git_working_dir, 118 ["rev-list", "--reverse", start_hash + ".." + end_hash], 119 verbose) 120 return commits_between.strip() 121 122 def _convert_to_array(string_of_hashes): 123 return string_of_hashes.splitlines() 124 125 def _remove_duplicates(array): 126 no_duplicates = [] 127 for current in array: 128 if not current in no_duplicates: 129 no_duplicates.append(current) 130 return no_duplicates 131 132 def git_execute(working_dir, args, verbose=False): 133 command = ["git", "-C", working_dir] + args 134 if verbose: 135 print "Git working dir: " + working_dir 136 print "Executing git command:" + str(command) 137 p = Popen(args=command, stdin=PIPE, 138 stdout=PIPE, stderr=PIPE) 139 output, err = p.communicate() 140 rc = p.returncode 141 if rc != 0: 142 raise Exception(err) 143 if verbose: 144 print "Git return value: " + output 145 return output 146 147 def _pretty_print_entry(hash, git_dir, pre_text, verbose): 148 text_to_print = git_execute( 149 git_dir, 150 ["show", 151 "--quiet", 152 "--date=iso", 153 hash, 154 "--format=%ad # %H # %s"], 155 verbose) 156 return pre_text + text_to_print.strip() 157 158 def main(options): 159 all_related_commits = search_all_related_commits( 160 options.git_dir, 161 options.of[0], 162 options.until[0], 163 options.separator, 164 options.verbose) 165 166 sort_key = lambda x: ( 167 git_execute( 168 options.git_dir, 169 ["show", "--quiet", "--date=iso", x, "--format=%ad"], 170 options.verbose)).strip() 171 172 high_level_commits = sorted(all_related_commits.keys(), key=sort_key) 173 174 for current_key in high_level_commits: 175 if options.prettyprint: 176 yield _pretty_print_entry( 177 current_key, 178 options.git_dir, 179 "+", 180 options.verbose) 181 else: 182 yield "+" + current_key 183 184 found_commits = all_related_commits[current_key] 185 for current_commit in found_commits: 186 if options.prettyprint: 187 yield _pretty_print_entry( 188 current_commit, 189 options.git_dir, 190 "| ", 191 options.verbose) 192 else: 193 yield "| " + current_commit 194 195 if __name__ == "__main__": # pragma: no cover 196 parser = argparse.ArgumentParser( 197 "This tool analyzes the commit range between <of> and <until>. " 198 "It finds commits which belong together e.g. Implement/Revert pairs and " 199 "Implement/Port/Revert triples. All supplied hashes need to be " 200 "from the same branch e.g. master.") 201 parser.add_argument("-g", "--git-dir", required=False, default=".", 202 help="The path to your git working directory.") 203 parser.add_argument("--verbose", action="store_true", 204 help="Enables a very verbose output") 205 parser.add_argument("of", nargs=1, 206 help="Hash of the commit to be searched.") 207 parser.add_argument("until", nargs=1, 208 help="Commit when searching should stop") 209 parser.add_argument("--separator", required=False, 210 help="The script will only list related commits " 211 "which are separated by hash <--separator>.") 212 parser.add_argument("--prettyprint", action="store_true", 213 help="Pretty prints the output") 214 215 args = sys.argv[1:] 216 options = parser.parse_args(args) 217 for current_line in main(options): 218 print current_line 219