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