1from __future__ import unicode_literals 2 3from collections import defaultdict 4from copy import deepcopy 5 6import six 7 8from rbtools.hooks.common import execute, get_review_request_id 9 10 11def get_branch_name(ref_name): 12 """Returns the branch name corresponding to the specified ref name.""" 13 branch_ref_prefix = 'refs/heads/' 14 15 if ref_name.startswith(branch_ref_prefix): 16 return ref_name[len(branch_ref_prefix):] 17 18 19def get_commit_hashes(old_rev, new_rev): 20 """Returns a list of abbreviated commit hashes from old_rev to new_rev.""" 21 git_command = ['git', 'rev-list', '--abbrev-commit', '--reverse', '%s..%s' 22 % (old_rev, new_rev)] 23 return execute(git_command).split('\n') 24 25 26def get_unique_commit_hashes(ref_name, new_rev): 27 """Returns a list of abbreviated commit hashes unique to ref_name.""" 28 git_command = ['git', 'rev-list', new_rev, '--abbrev-commit', '--reverse', 29 '--not'] 30 git_command.extend(get_excluded_branches(ref_name)) 31 return execute(git_command).strip().split('\n') 32 33 34def get_excluded_branches(ref_name): 35 """Returns a list of all branches, excluding the specified branch.""" 36 git_command = ['git', 'for-each-ref', 'refs/heads/', '--format=%(refname)'] 37 all_branches = execute(git_command).strip().split('\n') 38 return [branch.strip() for branch in all_branches if branch != ref_name] 39 40 41def get_branches_containing_commit(commit_hash): 42 """Returns a list of all branches containing the specified commit.""" 43 git_command = ['git', 'branch', '--contains', commit_hash] 44 branches = execute(git_command).replace('*', '').split('\n') 45 return [branch.strip() for branch in branches] 46 47 48def get_commit_message(commit): 49 """Returns the specified commit's commit message.""" 50 git_command = ['git', 'show', '-s', '--pretty=format:%B', commit] 51 return execute(git_command).strip() 52 53 54def get_review_id_to_commits_map(lines, regex): 55 """Returns a dictionary, mapping a review request ID to a list of commits. 56 57 The commits must be in the form: oldrev newrev refname (separated by 58 newlines), as given by a Git pre-receive or post-receive hook. 59 60 If a commit's commit message does not contain a review request ID, we 61 append the commit to the key 0. 62 """ 63 review_id_to_commits_map = defaultdict(list) 64 65 # Store a list of new branches (which have an all-zero old_rev value) 66 # created in this push to handle them specially. 67 new_branches = [] 68 null_sha1 = '0' * 40 69 70 for line in lines: 71 old_rev, new_rev, ref_name = line.split() 72 branch_name = get_branch_name(ref_name) 73 74 if not branch_name or new_rev == null_sha1: 75 continue 76 77 if old_rev == null_sha1: 78 new_branches.append(branch_name) 79 commit_hashes = get_unique_commit_hashes(ref_name, new_rev) 80 else: 81 commit_hashes = get_commit_hashes(old_rev, new_rev) 82 83 for commit_hash in commit_hashes: 84 if commit_hash: 85 commit_message = get_commit_message(commit_hash) 86 review_request_id = get_review_request_id(regex, 87 commit_message) 88 89 commit = '%s (%s)' % (branch_name, commit_hash) 90 review_id_to_commits_map[review_request_id].append(commit) 91 92 # If there are new branches, check every commit in the dictionary 93 # (corresponding to only old branches) to see if the new branches also 94 # contain that commit. 95 if new_branches: 96 review_id_to_commits_map_copy = deepcopy(review_id_to_commits_map) 97 98 for review_id, commit_list in six.iteritems( 99 review_id_to_commits_map_copy): 100 for commit in commit_list: 101 commit_branch = commit[:commit.find('(') - 1] 102 103 if commit_branch in new_branches: 104 continue 105 106 commit_hash = commit[commit.find('(') + 1:-1] 107 commit_branches = get_branches_containing_commit(commit_hash) 108 109 for branch in set(new_branches).intersection(commit_branches): 110 new_commit = '%s (%s)' % (branch, commit_hash) 111 review_id_to_commits_map[review_id].append(new_commit) 112 113 return review_id_to_commits_map 114