blob: 72a02034364603ec037f85ccc2e3bfd497a60a01 [file] [log] [blame]
#!/usr/bin/python
##
# Copyright c 2016 IBM Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##
###############################################################################
# @file commit-tracker
# @brief Prints out all commits on the master branch of the specified
# repository, as well as all commits on linked submodule
# repositories
###############################################################################
import argparse
import logging
import os
import re
import sys
import time
import git
###############################################################################
# @brief Main function for the script
#
# @param i_args : Command line arguments
###############################################################################
def main(i_args):
# Parse the arguments
l_args_obj = parse_arguments(i_args)
# Print every commit
print 'Getting commits for ' + l_args_obj.repo_dir
print_commits(l_args_obj.repo_dir, l_args_obj.latest_commit,
l_args_obj.earliest_commit)
###############################################################################
# @brief Parses the arguments from the command line
#
# @param i_args : The list of arguments from the command line, excluding the
# name of the script
#
# @return An object representin the parsed arguments
###############################################################################
def parse_arguments(i_args):
l_parser = argparse.ArgumentParser(
description='Prints commit information from the given repo and all ' \
+'sub-repos specified with SRC_REV, starting from the ' \
+'most recent commit specified going back to the ' \
+'earliest commit specified.')
l_parser.add_argument(
'repo_dir',
help='The directory of the repo to get commit information for')
l_parser.add_argument(
'latest_commit',
help='A reference (branch name, HEAD, SHA, etc.) to the most ' \
+'recent commit to get information for')
l_parser.add_argument(
'earliest_commit',
help='A reference to the earliest commit to get information for')
return l_parser.parse_args(i_args)
###############################################################################
# @brief Prints all the commits from this repo and commits from
# subrepos between the given references
#
# @param i_repo_path : The path to the repo to print commits for
# @param i_begin_commit : A reference to the most recent commit. What
# commit to start printing at
# @param i_end_commit : A reference to the commit farthest in the
# past. The commit to stop print at
###############################################################################
def print_commits(i_repo_path, i_begin_commit, i_end_commit, i_level=0):
try:
l_repo = git.Repo(i_repo_path)
except git.exc.InvalidGitRepositoryError:
logging.error(str(i_repo_path) + ' is not a valid git repository')
return
# Get commits between the beginning and end references
try:
l_commits = l_repo.iter_commits(rev=(i_begin_commit + '...'
+ i_end_commit))
# Go through each commit
for l_commit in l_commits:
print_commit_info(i_repo_path, l_commit, i_level)
# Search the diffs for any bumps of submodule versions
l_diffs = l_commit.diff(str(l_commit.hexsha) + '~1')
for l_diff in l_diffs:
# If we have two files to compare with diff...
if l_diff.a_path and l_diff.b_path:
# ... get info about the change, log it...
l_subrepo_uri, l_subrepo_new_hash, l_subrepo_old_hash \
= get_bumped_repo(l_repo, str(l_commit.hexsha), i_repo_path + '/' + l_diff.b_path)
logging.debug('Found diff...')
logging.debug(' Subrepo URI: ' + str(l_subrepo_uri))
logging.debug(' Subrepo new hash: '
+ str(l_subrepo_new_hash))
logging.debug(' Subrepo old hash: '
+ str(l_subrepo_old_hash))
logging.debug(' Found in: ' + str(l_diff.a_path))
# ... and print the commits for the subrepo if this was a
# version bump
if (l_subrepo_new_hash
and l_subrepo_old_hash
and l_subrepo_uri
and l_subrepo_uri.startswith('git')):
logging.debug(' Bumped')
l_subrepo_path = l_subrepo_uri.split('/')[-1]
clone_or_update(l_subrepo_uri, l_subrepo_path)
print_commits(l_subrepo_path, l_subrepo_new_hash,
l_subrepo_old_hash, i_level=i_level+1)
except git.exc.GitCommandError:
logging.error(str(i_begin_commit) + ' and ' + str(i_end_commit)
+ ' are invalid revisions')
###############################################################################
# @brief Gets the repo URI, the updated SHA, and the old SHA from a
# given repo, commit SHA and file
#
# @param i_repo : The Repo object to get version bump information
# from
# @param i_file : The path to the file to search for version
# bumps
# @param i_hexsha : The hex hash for the commit to search for
# version bumps
#
# @return Returns the repo URI, the updated SHA, and the old SHA in
# a tuple in that order
###############################################################################
def get_bumped_repo(i_repo, i_hexsha, i_file):
# Checkout the old repo
i_repo.git.checkout(i_hexsha)
# Get the diff text
l_diff_text = i_repo.git.diff(i_hexsha, i_hexsha + '~1', '--', i_file)
# SRCREV sets the SHA for the version of the other repo to use when
# building openbmc. SHAs should be stored in the file in a format
# like SRCRV =? "<SHA>". Find both the new '+' and old '-' ones
l_old_hash = None
l_new_hash = None
l_old_hash_match = re.search('-SRCREV[+=? ]+"([a-f0-9]+)"', l_diff_text)
l_new_hash_match = re.search('\+SRCREV[+=? ]+"([a-f0-9]+)"', l_diff_text)
if l_old_hash_match:
l_old_hash = l_old_hash_match.group(1)
if l_new_hash_match:
l_new_hash = l_new_hash_match.group(1)
# Get the URI of the subrepo
l_uri = None
if os.path.isfile(i_file):
l_changed_file = open(i_file, 'r')
for l_line in l_changed_file:
# URIs should be stored in a format similar to
# SRC_URI ?= "git://github.com/<path to repo>"
l_uri_match = re.search('_URI[+=? ]+"([-a-zA-Z0-9/:\.]+)"', l_line)
if l_uri_match:
l_uri = l_uri_match.group(1)
break
# Go back to master
i_repo.git.checkout('master')
return l_uri, l_new_hash, l_old_hash
###############################################################################
# @brief Updates the repo under the given path or clones it from the
# uri if it doesn't yet exist
#
# @param i_uri : The URI to the remote repo to clone
# @param i_path : The file path to where the repo currently exists or
# where it will be created
###############################################################################
def clone_or_update(i_uri, i_path):
# If the repo exists, just update it
if os.path.isdir(i_path):
l_repo = git.Repo(i_path)
l_repo.remotes[0].pull()
# If it doesn't exist, clone it
else:
os.mkdir(i_path)
l_repo = git.Repo.init(i_path)
origin = l_repo.create_remote('origin', i_uri)
origin.fetch()
l_repo.create_head('master', origin.refs.master) \
.set_tracking_branch(origin.refs.master)
origin.pull()
###############################################################################
# @brief Prints information for a given commit to the command line
#
# @param i_repo_path : The file path to the repo
# @param i_commit : The commit object to print infor for
# @param i_level : What subrepo level is this
###############################################################################
def print_commit_info(i_repo_path, i_commit, i_level):
# Use these to print console text with colors. RED + <text to print> + ENDC
# will print the text in red
RED = '\033[31m'
BLUE = '\033[94m'
ENDC = '\033[0m'
# Take just the first seven digits in the commit hash
l_name_rev = i_commit.name_rev
l_hash, l_name = l_name_rev.split()
l_name_rev = l_hash[0:7] + l_name
# Print out the line describing this commit
print (' ' * i_level) + RED + i_repo_path + ENDC + ' ' + BLUE \
+ l_name_rev + ENDC + ' ' + re.sub('\s+', ' ', i_commit.summary)
# Only run main if run as a script
if __name__ == '__main__':
main(sys.argv[1:])