| Charles Hofer | ac55121 | 2016-10-20 16:33:41 -0500 | [diff] [blame] | 1 | #!/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 Hofer | 046fc91 | 2016-11-10 10:42:13 -0600 | [diff] [blame^] | 26 | import argparse | 
| Charles Hofer | ac55121 | 2016-10-20 16:33:41 -0500 | [diff] [blame] | 27 | import logging | 
 | 28 | import os | 
 | 29 | import re | 
 | 30 | import sys | 
 | 31 | import time | 
 | 32 | import git | 
 | 33 |  | 
 | 34 | ############################################################################### | 
 | 35 | # @brief Main function for the script | 
 | 36 | # | 
 | 37 | # @param i_args : Command line arguments | 
 | 38 | ############################################################################### | 
 | 39 | def main(i_args): | 
| Charles Hofer | 046fc91 | 2016-11-10 10:42:13 -0600 | [diff] [blame^] | 40 |     # Parse the arguments | 
 | 41 |     l_args_obj = parse_arguments(i_args) | 
| Charles Hofer | ac55121 | 2016-10-20 16:33:41 -0500 | [diff] [blame] | 42 |  | 
| Charles Hofer | 046fc91 | 2016-11-10 10:42:13 -0600 | [diff] [blame^] | 43 |     # Print every commit | 
 | 44 |     print 'Getting commits for ' + l_args_obj.repo_dir | 
 | 45 |     print_commits(l_args_obj.repo_dir, l_args_obj.latest_commit, | 
 | 46 |                   l_args_obj.earliest_commit) | 
| Charles Hofer | ac55121 | 2016-10-20 16:33:41 -0500 | [diff] [blame] | 47 |  | 
| Charles Hofer | 046fc91 | 2016-11-10 10:42:13 -0600 | [diff] [blame^] | 48 | ############################################################################### | 
 | 49 | # @brief Parses the arguments from the command line | 
 | 50 | # | 
 | 51 | # @param i_args : The list of arguments from the command line, excluding the | 
 | 52 | #                 name of the script | 
 | 53 | # | 
 | 54 | # @return An object representin the parsed arguments | 
 | 55 | ############################################################################### | 
 | 56 | def parse_arguments(i_args): | 
 | 57 |     l_parser = argparse.ArgumentParser( | 
 | 58 |         description='Prints commit information from the given repo and all ' \ | 
 | 59 |                     +'sub-repos specified with SRC_REV, starting from the ' \ | 
 | 60 |                     +'most recent commit specified going back to the ' \ | 
 | 61 |                     +'earliest commit specified.') | 
 | 62 |     l_parser.add_argument( | 
 | 63 |         'repo_dir', | 
 | 64 |         help='The directory of the repo to get commit information for') | 
 | 65 |     l_parser.add_argument( | 
 | 66 |         'latest_commit', | 
 | 67 |         help='A reference (branch name, HEAD, SHA, etc.) to the most ' \ | 
 | 68 |              +'recent commit to get information for') | 
 | 69 |     l_parser.add_argument( | 
 | 70 |         'earliest_commit', | 
 | 71 |         help='A reference to the earliest commit to get information for') | 
 | 72 |     return l_parser.parse_args(i_args) | 
| Charles Hofer | ac55121 | 2016-10-20 16:33:41 -0500 | [diff] [blame] | 73 |  | 
 | 74 | ############################################################################### | 
 | 75 | # @brief Prints all the commits from this repo and commits from | 
 | 76 | #        subrepos between the given references | 
 | 77 | # | 
 | 78 | # @param i_repo_path    : The path to the repo to print commits for | 
 | 79 | # @param i_begin_commit : A reference to the most recent commit. What | 
 | 80 | #                         commit to start printing at | 
 | 81 | # @param i_end_commit   : A reference to the commit farthest in the | 
 | 82 | #                         past. The commit to stop print at | 
 | 83 | ############################################################################### | 
 | 84 | def print_commits(i_repo_path, i_begin_commit, i_end_commit, i_level=0): | 
 | 85 |     try: | 
 | 86 |         l_repo = git.Repo(i_repo_path) | 
 | 87 |     except git.exc.InvalidGitRepositoryError: | 
 | 88 |         logging.error(str(i_repo_path) + ' is not a valid git repository') | 
 | 89 |         return | 
 | 90 |  | 
 | 91 |     # Get commits between the beginning and end references | 
 | 92 |     try: | 
 | 93 |         l_commits = l_repo.iter_commits(rev=(i_begin_commit + '...' | 
 | 94 |         + i_end_commit)) | 
 | 95 |         # Go through each commit | 
 | 96 |         for l_commit in l_commits: | 
 | 97 |             print_commit_info(i_repo_path, l_commit, i_level) | 
 | 98 |             # Search the diffs for any bumps of submodule versions | 
 | 99 |             l_diffs = l_commit.diff(str(l_commit.hexsha) + '~1') | 
 | 100 |             for l_diff in l_diffs: | 
 | 101 |                 # If we have two files to compare with diff... | 
 | 102 |                 if l_diff.a_path and l_diff.b_path: | 
 | 103 |                     # ... get info about the change, log it... | 
 | 104 |                     l_subrepo_uri, l_subrepo_new_hash, l_subrepo_old_hash \ | 
 | 105 |                         = get_bumped_repo(l_repo, str(l_commit.hexsha),                                                  i_repo_path + '/' + l_diff.b_path) | 
 | 106 |                     logging.debug('Found diff...') | 
 | 107 |                     logging.debug('  Subrepo URI: ' + str(l_subrepo_uri)) | 
 | 108 |                     logging.debug('  Subrepo new hash: ' | 
 | 109 |                                   + str(l_subrepo_new_hash)) | 
 | 110 |                     logging.debug('  Subrepo old hash: ' | 
 | 111 |                                   + str(l_subrepo_old_hash)) | 
 | 112 |                     logging.debug('  Found in: ' + str(l_diff.a_path)) | 
 | 113 |                     # ... and print the commits for the subrepo if this was a | 
 | 114 |                     #     version bump | 
 | 115 |                     if (l_subrepo_new_hash | 
 | 116 |                             and l_subrepo_old_hash | 
 | 117 |                             and l_subrepo_uri | 
 | 118 |                             and l_subrepo_uri.startswith('git')): | 
 | 119 |                         logging.debug('  Bumped') | 
 | 120 |                         l_subrepo_path = l_subrepo_uri.split('/')[-1] | 
 | 121 |                         clone_or_update(l_subrepo_uri, l_subrepo_path) | 
 | 122 |                         print_commits(l_subrepo_path, l_subrepo_new_hash, | 
 | 123 |                                       l_subrepo_old_hash, i_level=i_level+1) | 
 | 124 |     except git.exc.GitCommandError: | 
 | 125 |         logging.error(str(i_begin_commit) + ' and ' + str(i_end_commit) | 
 | 126 |                       + ' are invalid revisions') | 
 | 127 |  | 
 | 128 | ############################################################################### | 
 | 129 | # @brief Gets the repo URI, the updated SHA, and the old SHA from a | 
 | 130 | #        given repo, commit SHA and file | 
 | 131 | # | 
 | 132 | # @param i_repo      : The Repo object to get version bump information | 
 | 133 | #                      from | 
 | 134 | # @param i_file      : The path to the file to search for version | 
 | 135 | #                      bumps | 
 | 136 | # @param i_hexsha    : The hex hash for the commit to search for | 
 | 137 | #                      version bumps | 
 | 138 | # | 
 | 139 | # @return Returns the repo URI, the updated SHA, and the old SHA in | 
 | 140 | #         a tuple in that order | 
 | 141 | ############################################################################### | 
 | 142 | def get_bumped_repo(i_repo, i_hexsha, i_file): | 
 | 143 |     # Checkout the old repo | 
 | 144 |     i_repo.git.checkout(i_hexsha) | 
 | 145 |     # Get the diff text | 
 | 146 |     l_diff_text = i_repo.git.diff(i_hexsha, i_hexsha + '~1', '--', i_file) | 
 | 147 |  | 
 | 148 |     # SRCREV sets the SHA for the version of the other repo to use when | 
 | 149 |     # building openbmc. SHAs should be stored in the file in a format | 
 | 150 |     # like  SRCRV =? "<SHA>". Find both the new '+' and old '-' ones | 
 | 151 |     l_old_hash = None | 
 | 152 |     l_new_hash = None | 
 | 153 |     l_old_hash_match = re.search('-SRCREV[+=? ]+"([a-f0-9]+)"', l_diff_text) | 
 | 154 |     l_new_hash_match = re.search('\+SRCREV[+=? ]+"([a-f0-9]+)"', l_diff_text) | 
 | 155 |     if l_old_hash_match: | 
 | 156 |         l_old_hash = l_old_hash_match.group(1) | 
 | 157 |     if l_new_hash_match: | 
 | 158 |         l_new_hash = l_new_hash_match.group(1) | 
 | 159 |  | 
 | 160 |     # Get the URI of the subrepo | 
 | 161 |     l_uri = None | 
 | 162 |     if os.path.isfile(i_file): | 
 | 163 |         l_changed_file = open(i_file, 'r') | 
 | 164 |         for l_line in l_changed_file: | 
 | 165 |             # URIs should be stored in a format similar to | 
 | 166 |             # SRC_URI ?= "git://github.com/<path to repo>" | 
 | 167 |             l_uri_match = re.search('_URI[+=? ]+"([-a-zA-Z0-9/:\.]+)"', l_line) | 
 | 168 |             if l_uri_match: | 
 | 169 |                 l_uri = l_uri_match.group(1) | 
 | 170 |                 break | 
 | 171 |  | 
 | 172 |     # Go back to master | 
 | 173 |     i_repo.git.checkout('master') | 
 | 174 |     return l_uri, l_new_hash, l_old_hash | 
 | 175 |  | 
 | 176 | ############################################################################### | 
 | 177 | # @brief Updates the repo under the given path or clones it from the | 
 | 178 | #        uri if it doesn't yet exist | 
 | 179 | # | 
 | 180 | # @param i_uri  : The URI to the remote repo to clone | 
 | 181 | # @param i_path : The file path to where the repo currently exists or | 
 | 182 | #                 where it will be created | 
 | 183 | ############################################################################### | 
 | 184 | def clone_or_update(i_uri, i_path): | 
 | 185 |     # If the repo exists, just update it | 
 | 186 |     if os.path.isdir(i_path): | 
 | 187 |         l_repo = git.Repo(i_path) | 
 | 188 |         l_repo.remotes[0].pull() | 
 | 189 |  | 
 | 190 |     # If it doesn't exist, clone it | 
 | 191 |     else: | 
 | 192 |         os.mkdir(i_path) | 
 | 193 |         l_repo = git.Repo.init(i_path) | 
 | 194 |         origin = l_repo.create_remote('origin', i_uri) | 
 | 195 |         origin.fetch() | 
 | 196 |         l_repo.create_head('master', origin.refs.master) \ | 
 | 197 |             .set_tracking_branch(origin.refs.master) | 
 | 198 |         origin.pull() | 
 | 199 |  | 
 | 200 | ############################################################################### | 
 | 201 | # @brief Prints information for a given commit to the command line | 
 | 202 | # | 
 | 203 | # @param i_repo_path    : The file path to the repo | 
 | 204 | # @param i_commit       : The commit object to print infor for | 
 | 205 | # @param i_level        : What subrepo level is this | 
 | 206 | ############################################################################### | 
 | 207 | def print_commit_info(i_repo_path, i_commit, i_level): | 
 | 208 |     # Use these to print console text with colors. RED + <text to print> + ENDC | 
 | 209 |     # will print the text in red | 
 | 210 |     RED = '\033[31m' | 
 | 211 |     BLUE = '\033[94m' | 
 | 212 |     ENDC = '\033[0m' | 
 | 213 |  | 
 | 214 |     # Take just the first seven digits in the commit hash | 
 | 215 |     l_name_rev = i_commit.name_rev | 
 | 216 |     l_hash, l_name = l_name_rev.split() | 
 | 217 |     l_name_rev = l_hash[0:7] + l_name | 
 | 218 |  | 
 | 219 |     # Print out the line describing this commit | 
 | 220 |     print ('  ' * i_level) + RED + i_repo_path + ENDC  + ' ' + BLUE \ | 
 | 221 |         + l_name_rev + ENDC + ' ' + re.sub('\s+', ' ', i_commit.summary) | 
 | 222 |  | 
| Charles Hofer | ac55121 | 2016-10-20 16:33:41 -0500 | [diff] [blame] | 223 | # Only run main if run as a script | 
 | 224 | if __name__ == '__main__': | 
 | 225 |     main(sys.argv[1:]) | 
 | 226 |  |