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