|  | #!/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:]) | 
|  |  |