blob: 3c7e7a2d626e0048ab1b07a33938c750003e0784 [file] [log] [blame]
Charles Hoferac551212016-10-20 16:33:41 -05001#!/usr/bin/python
2
3##
4# Copyright c 2016 IBM Corporation
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17##
18
19###############################################################################
20# @file commit-tracker
21# @brief Prints out all commits on the master branch of the specified
22# repository, as well as all commits on linked submodule
23# repositories
24###############################################################################
25
Charles Hofer046fc912016-11-10 10:42:13 -060026import argparse
Charles Hoferac551212016-10-20 16:33:41 -050027import logging
28import os
29import re
30import sys
31import time
32import git
33
34###############################################################################
Charles Hofer493cd6f2016-11-29 15:54:17 -060035# @class CommitReport
36# @brief A class representing information about a commit and all commits in
37# relevant subrepos
38###############################################################################
39class CommitReport:
40 def __init__(self, i_repo_uri, i_repo_name, i_sha, i_nice_name,
41 i_summary, i_insertions, i_deletions):
42 self.repo_uri = i_repo_uri
43 self.repo_name = i_repo_name
44 self.sha = i_sha
45 self.nice_name = i_nice_name
46 self.summary = i_summary
47 self.insertions = i_insertions
48 self.deletions = i_deletions
49 self.subreports = []
50
51 def to_cl_string(self, i_level=0):
52 # Define colors for the console
53 RED = '\033[31m'
54 BLUE = '\033[94m'
55 ENDC = '\033[0m'
56 # Put the string together
57 l_cl_string = (' ' * i_level) + RED + self.repo_name + ENDC + ' ' \
58 + BLUE + self.nice_name + ENDC + ' ' \
59 + re.sub('\s+', ' ', self.summary)
60 # Do the same for every subreport
61 for l_report in self.subreports:
62 l_cl_string += '\n' + l_report.to_cl_string(i_level + 1)
63 return l_cl_string
64
65 def get_total_insertions(self):
66 l_insertions = self.insertions
67 for l_commit in self.subreports:
68 l_insertions += l_commit.get_total_insertions()
69 return l_insertions
70
71 def get_total_deletions(self):
72 l_deletions = self.deletions
73 for l_commit in self.subreports:
74 l_deletions += l_commit.get_total_deletions()
75 return l_deletions
76
77###############################################################################
Charles Hoferac551212016-10-20 16:33:41 -050078# @brief Main function for the script
79#
80# @param i_args : Command line arguments
81###############################################################################
82def main(i_args):
Charles Hofer046fc912016-11-10 10:42:13 -060083 # Parse the arguments
84 l_args_obj = parse_arguments(i_args)
Charles Hoferac551212016-10-20 16:33:41 -050085
Charles Hofer046fc912016-11-10 10:42:13 -060086 # Print every commit
Charles Hofer493cd6f2016-11-29 15:54:17 -060087 print 'Getting report for ' + l_args_obj.repo_dir
88 l_reports = generate_commit_reports(
89 l_args_obj.repo_uri,
90 l_args_obj.repo_dir,
91 l_args_obj.latest_commit,
92 l_args_obj.earliest_commit)
93
94 # Compile issues, insertions, and deletions
95 l_total_deletions = 0
96 l_total_insertions = 0
97 for l_report in l_reports:
98 l_total_deletions += l_report.get_total_deletions()
99 l_total_insertions += l_report.get_total_insertions()
100
101 # Print commit information to the console
102 print 'Commits'
103 for l_report in l_reports:
104 print l_report.to_cl_string()
105 print 'Insertions and Deletions'
106 print str(l_total_insertions) + ' insertions'
107 print str(l_total_deletions) + ' deletions'
Charles Hoferac551212016-10-20 16:33:41 -0500108
Charles Hofer046fc912016-11-10 10:42:13 -0600109###############################################################################
110# @brief Parses the arguments from the command line
111#
112# @param i_args : The list of arguments from the command line, excluding the
113# name of the script
114#
115# @return An object representin the parsed arguments
116###############################################################################
117def parse_arguments(i_args):
118 l_parser = argparse.ArgumentParser(
119 description='Prints commit information from the given repo and all ' \
120 +'sub-repos specified with SRC_REV, starting from the ' \
121 +'most recent commit specified going back to the ' \
122 +'earliest commit specified.')
123 l_parser.add_argument(
Charles Hofer493cd6f2016-11-29 15:54:17 -0600124 'repo_uri',
125 help='The URI of the repo to get commit information for')
126 l_parser.add_argument(
Charles Hofer046fc912016-11-10 10:42:13 -0600127 'repo_dir',
128 help='The directory of the repo to get commit information for')
129 l_parser.add_argument(
130 'latest_commit',
131 help='A reference (branch name, HEAD, SHA, etc.) to the most ' \
132 +'recent commit to get information for')
133 l_parser.add_argument(
134 'earliest_commit',
135 help='A reference to the earliest commit to get information for')
136 return l_parser.parse_args(i_args)
Charles Hoferac551212016-10-20 16:33:41 -0500137
138###############################################################################
Charles Hofer493cd6f2016-11-29 15:54:17 -0600139# @brief Generates a list of CommitReport objects, each one
140# representing a commit in the given repo URI and path,
141# starting at the beginning commit inclusive, ending at the
142# end commit exclusive
Charles Hoferac551212016-10-20 16:33:41 -0500143#
Charles Hofer493cd6f2016-11-29 15:54:17 -0600144# @param i_repo_uri : The URI to the repo to get reports for
145# @param i_repo_path : The path to the repo to get reports for
146# @param i_begin_commit : A reference to the most recent commit. The
147# most recent commit to get a report for
Charles Hoferac551212016-10-20 16:33:41 -0500148# @param i_end_commit : A reference to the commit farthest in the
Charles Hofer493cd6f2016-11-29 15:54:17 -0600149# past. The next youngest commit will be
150# the last one to get a report for
151#
152# @return A list of CommitReport objects in order from newest to
153# oldest commit
Charles Hoferac551212016-10-20 16:33:41 -0500154###############################################################################
Charles Hofer493cd6f2016-11-29 15:54:17 -0600155def generate_commit_reports(i_repo_uri, i_repo_path, i_begin_commit,
156 i_end_commit):
157 # Get the repo that the user requested
158 clone_or_update(i_repo_uri, i_repo_path)
Charles Hoferac551212016-10-20 16:33:41 -0500159 try:
160 l_repo = git.Repo(i_repo_path)
161 except git.exc.InvalidGitRepositoryError:
162 logging.error(str(i_repo_path) + ' is not a valid git repository')
163 return
164
165 # Get commits between the beginning and end references
166 try:
167 l_commits = l_repo.iter_commits(rev=(i_begin_commit + '...'
Charles Hofer493cd6f2016-11-29 15:54:17 -0600168 + i_end_commit))
169 # Go through each commit, generating a report
170 l_reports = []
Charles Hoferac551212016-10-20 16:33:41 -0500171 for l_commit in l_commits:
Charles Hofer493cd6f2016-11-29 15:54:17 -0600172 # Get the insertion and deletion line counts
173 l_insertions, l_deletions = get_line_count(
174 l_repo,str(l_commit.hexsha),
175 str(l_commit.hexsha) + '~1')
176 # Construct a new commit report
177 l_report = CommitReport(
178 i_repo_uri,
179 i_repo_path.split('/')[-1].replace('.git', ''),
180 str(l_commit.hexsha),
181 to_prefix_name_rev(l_commit.name_rev),
182 l_commit.summary,
183 l_insertions,
184 l_deletions)
185
Charles Hoferac551212016-10-20 16:33:41 -0500186 # Search the diffs for any bumps of submodule versions
187 l_diffs = l_commit.diff(str(l_commit.hexsha) + '~1')
188 for l_diff in l_diffs:
189 # If we have two files to compare with diff...
190 if l_diff.a_path and l_diff.b_path:
191 # ... get info about the change, log it...
192 l_subrepo_uri, l_subrepo_new_hash, l_subrepo_old_hash \
Charles Hofer493cd6f2016-11-29 15:54:17 -0600193 = get_bump_info(l_repo, str(l_commit.hexsha),
194 i_repo_path, l_diff.b_path)
Charles Hoferac551212016-10-20 16:33:41 -0500195 logging.debug('Found diff...')
196 logging.debug(' Subrepo URI: ' + str(l_subrepo_uri))
197 logging.debug(' Subrepo new hash: '
198 + str(l_subrepo_new_hash))
199 logging.debug(' Subrepo old hash: '
200 + str(l_subrepo_old_hash))
Charles Hofer493cd6f2016-11-29 15:54:17 -0600201 logging.debug(' Found in: ' + str(l_diff.b_path))
Charles Hoferac551212016-10-20 16:33:41 -0500202 # ... and print the commits for the subrepo if this was a
203 # version bump
204 if (l_subrepo_new_hash
205 and l_subrepo_old_hash
206 and l_subrepo_uri
207 and l_subrepo_uri.startswith('git')):
208 logging.debug(' Bumped')
209 l_subrepo_path = l_subrepo_uri.split('/')[-1]
Charles Hofer493cd6f2016-11-29 15:54:17 -0600210 l_subreports = generate_commit_reports(
211 l_subrepo_uri,
212 l_subrepo_path,
213 l_subrepo_new_hash,
214 l_subrepo_old_hash)
215 l_report.subreports.extend(l_subreports)
216
217 # Put the report on the end of the list
218 l_reports.append(l_report)
219
220 except git.exc.GitCommandError as e:
221 logging.error(e)
Charles Hoferac551212016-10-20 16:33:41 -0500222 logging.error(str(i_begin_commit) + ' and ' + str(i_end_commit)
223 + ' are invalid revisions')
Charles Hofer493cd6f2016-11-29 15:54:17 -0600224 return l_reports
Charles Hoferac551212016-10-20 16:33:41 -0500225
226###############################################################################
227# @brief Gets the repo URI, the updated SHA, and the old SHA from a
228# given repo, commit SHA and file
229#
230# @param i_repo : The Repo object to get version bump information
231# from
Charles Hoferac551212016-10-20 16:33:41 -0500232# @param i_hexsha : The hex hash for the commit to search for
233# version bumps
Charles Hofer493cd6f2016-11-29 15:54:17 -0600234# @param i_repo_path : The path to the repo containing the file to
235# get bump information from
236# @param i_file : The path, starting at the base of the repo,
237# to the file to get bump information from
Charles Hoferac551212016-10-20 16:33:41 -0500238#
239# @return Returns the repo URI, the updated SHA, and the old SHA in
240# a tuple in that order
241###############################################################################
Charles Hofer493cd6f2016-11-29 15:54:17 -0600242def get_bump_info(i_repo, i_hexsha, i_repo_path, i_file):
Charles Hoferac551212016-10-20 16:33:41 -0500243 # Checkout the old repo
244 i_repo.git.checkout(i_hexsha)
245 # Get the diff text
246 l_diff_text = i_repo.git.diff(i_hexsha, i_hexsha + '~1', '--', i_file)
Charles Hofer493cd6f2016-11-29 15:54:17 -0600247 logging.debug('Hash: ' + i_hexsha)
248 logging.debug('File: ' + i_repo_path + '/' + i_file)
249 logging.debug('Diff Text: ' + l_diff_text)
Charles Hoferac551212016-10-20 16:33:41 -0500250
251 # SRCREV sets the SHA for the version of the other repo to use when
252 # building openbmc. SHAs should be stored in the file in a format
253 # like SRCRV =? "<SHA>". Find both the new '+' and old '-' ones
254 l_old_hash = None
255 l_new_hash = None
Charles Hofer493cd6f2016-11-29 15:54:17 -0600256 l_old_hash_match = re.search('-[A-Z_]*SRCREV[+=? ]+"([a-f0-9]+)"',
257 l_diff_text)
258 l_new_hash_match = re.search('\+[A-Z_]*SRCREV[+=? ]+"([a-f0-9]+)"',
259 l_diff_text)
Charles Hoferac551212016-10-20 16:33:41 -0500260 if l_old_hash_match:
261 l_old_hash = l_old_hash_match.group(1)
262 if l_new_hash_match:
263 l_new_hash = l_new_hash_match.group(1)
264
265 # Get the URI of the subrepo
266 l_uri = None
Charles Hofer493cd6f2016-11-29 15:54:17 -0600267 if os.path.isfile(i_repo_path + '/' + i_file):
268 l_changed_file = open(i_repo_path + '/' + i_file, 'r')
Charles Hoferac551212016-10-20 16:33:41 -0500269 for l_line in l_changed_file:
270 # URIs should be stored in a format similar to
271 # SRC_URI ?= "git://github.com/<path to repo>"
272 l_uri_match = re.search('_URI[+=? ]+"([-a-zA-Z0-9/:\.]+)"', l_line)
273 if l_uri_match:
274 l_uri = l_uri_match.group(1)
275 break
Charles Hofer493cd6f2016-11-29 15:54:17 -0600276 else:
277 logging.debug(i_repo_path + '/' + i_file)
Charles Hoferac551212016-10-20 16:33:41 -0500278
279 # Go back to master
280 i_repo.git.checkout('master')
281 return l_uri, l_new_hash, l_old_hash
282
283###############################################################################
284# @brief Updates the repo under the given path or clones it from the
285# uri if it doesn't yet exist
286#
287# @param i_uri : The URI to the remote repo to clone
288# @param i_path : The file path to where the repo currently exists or
289# where it will be created
290###############################################################################
291def clone_or_update(i_uri, i_path):
292 # If the repo exists, just update it
293 if os.path.isdir(i_path):
294 l_repo = git.Repo(i_path)
295 l_repo.remotes[0].pull()
296
297 # If it doesn't exist, clone it
298 else:
299 os.mkdir(i_path)
300 l_repo = git.Repo.init(i_path)
301 origin = l_repo.create_remote('origin', i_uri)
302 origin.fetch()
303 l_repo.create_head('master', origin.refs.master) \
304 .set_tracking_branch(origin.refs.master)
305 origin.pull()
306
307###############################################################################
Charles Hofer493cd6f2016-11-29 15:54:17 -0600308# @brief Gets the number of changed lines between two commits
Charles Hoferac551212016-10-20 16:33:41 -0500309#
Charles Hofer493cd6f2016-11-29 15:54:17 -0600310# @param i_repo : The Repo object these commits are in
311# @param i_begin_commit : A git reference to the beginning commit
312# @param i_end_commit : A git reference to the end commit
313#
314# @return A two-tuple containing the number of insertions and the number of
315# deletions between the begin and end commit
Charles Hoferac551212016-10-20 16:33:41 -0500316###############################################################################
Charles Hofer493cd6f2016-11-29 15:54:17 -0600317def get_line_count(i_repo, i_begin_commit, i_end_commit):
318 diff_output = i_repo.git.diff(i_end_commit, i_begin_commit, shortstat=True)
319 insertions = 0
320 deletions = 0
321 insertion_match = re.search('([0-9]+) insertion', diff_output)
322 deletion_match = re.search('([0-9]+) deletion', diff_output)
323 if insertion_match:
324 insertions = int(insertion_match.group(1))
325 if deletion_match:
326 deletions = int(deletion_match.group(1))
327 return insertions, deletions
Charles Hoferac551212016-10-20 16:33:41 -0500328
Charles Hofer493cd6f2016-11-29 15:54:17 -0600329##############################################################################
330# @brief Cuts the hash in commit revision names down to its 7 digit prefix
331#
332# @param i_name_rev : The name of the revision to change
333#
334# @return The same revision name but with the hash its 7 digit prefix instead
335###############################################################################
336def to_prefix_name_rev(i_name_rev):
337 l_name_rev = i_name_rev
Charles Hoferac551212016-10-20 16:33:41 -0500338 l_hash, l_name = l_name_rev.split()
Charles Hofer493cd6f2016-11-29 15:54:17 -0600339 l_name_rev = l_hash[0:7] + ' ' + l_name
340 return l_name_rev
Charles Hoferac551212016-10-20 16:33:41 -0500341
Charles Hoferac551212016-10-20 16:33:41 -0500342# Only run main if run as a script
343if __name__ == '__main__':
344 main(sys.argv[1:])
345