1#!/usr/bin/python
2#
3# Copyright 2015 The Chromium Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Gets list of revisions between two commits and their commit positions.
8
9Example usage:
10  ./fetchInterveningRevisions.py 343b531d31 7b43807df3 chromium
11  ./fetchInterveningRevisions.py 235eff9574 1e4681c33f v8
12
13Note: Another implementation of this functionality can be found in
14findit/common/gitRepository.py (https://goo.gl/Rr8j9O).
15"""
16
17import argparse
18import json
19import urllib2
20
21from bisect_lib import depot_map
22
23_GITILES_PADDING = ')]}\'\n'
24_URL_TEMPLATE = ('https://chromium.googlesource.com/%s/+log/%s..%s'
25                 '?format=json&n=%d')
26
27# Gitiles paginates the list of commits; since we want to get all of the
28# commits at once, the page size should be larger than the largest revision
29# range that we expect to get.
30_PAGE_SIZE = 512
31
32def FetchInterveningRevisions(start, end, depot_name):
33  """Fetches a list of revision in between two commits.
34
35  Args:
36    start (str): A git commit hash in the Chromium src repository.
37    end (str): Another git commit hash, after start.
38    depot_name (str): A respository name.
39
40  Returns:
41    A list of pairs (commit hash, commit position), from earliest to latest,
42    for all commits in between the two given commits, not including either
43    of the given commits.
44
45  Raises:
46    urllib2.URLError: The request to gitiles failed.
47    ValueError: The response wasn't valid JSON.
48    KeyError: The JSON didn't contain the expected data.
49  """
50  revisions = _FetchRangeFromGitiles(start, end, depot_name)
51  # The response from gitiles includes the end revision and is ordered
52  # from latest to earliest.
53  return [_CommitPair(r) for r in reversed(revisions[1:])]
54
55
56def _FetchRangeFromGitiles(start, end, depot_name):
57  """Fetches a list of revision dicts from gitiles.
58
59  Make multiple requests to get multiple pages, if necessary.
60  """
61  revisions = []
62  url = _URL_TEMPLATE % (
63      depot_map.DEPOT_PATH_MAP[depot_name], start, end, _PAGE_SIZE)
64  current_page_url = url
65  while True:
66    response = urllib2.urlopen(current_page_url).read()
67    response_json = response[len(_GITILES_PADDING):]  # Remove padding.
68    response_dict = json.loads(response_json)
69    revisions.extend(response_dict['log'])
70    if 'next' not in response_dict:
71      break
72    current_page_url = url + '&s=' + response_dict['next']
73  return revisions
74
75
76def _CommitPair(commit_dict):
77  return (commit_dict['commit'],
78          _CommitPositionFromMessage(commit_dict['message']))
79
80
81def _CommitPositionFromMessage(message):
82  for line in reversed(message.splitlines()):
83    if line.startswith('Cr-Commit-Position:'):
84      return line.split('#')[1].split('}')[0]
85  return None
86
87
88def main():
89  parser = argparse.ArgumentParser()
90  parser.add_argument('start')
91  parser.add_argument('end')
92  parser.add_argument('depot', choices=list(depot_map.DEPOT_PATH_MAP))
93  args = parser.parse_args()
94  revision_pairs = FetchInterveningRevisions(args.start, args.end, args.depot)
95  print json.dumps(revision_pairs)
96
97
98if __name__ == '__main__':
99  main()
100