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
6import argparse
7import operator
8import os
9import re
10from sets import Set
11from subprocess import Popen, PIPE
12import sys
13
14def 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
41def _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
115def _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
122def _convert_to_array(string_of_hashes):
123  return string_of_hashes.splitlines()
124
125def _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
132def 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
147def _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
158def 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
195if __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