xref: /aosp_15_r20/tools/repohooks/rh/git.py (revision d68f33bc6fb0cc2476107c2af0573a2f5a63dfc1)
1*d68f33bcSAndroid Build Coastguard Worker# Copyright 2016 The Android Open Source Project
2*d68f33bcSAndroid Build Coastguard Worker#
3*d68f33bcSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
4*d68f33bcSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
5*d68f33bcSAndroid Build Coastguard Worker# You may obtain a copy of the License at
6*d68f33bcSAndroid Build Coastguard Worker#
7*d68f33bcSAndroid Build Coastguard Worker#      http://www.apache.org/licenses/LICENSE-2.0
8*d68f33bcSAndroid Build Coastguard Worker#
9*d68f33bcSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
10*d68f33bcSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
11*d68f33bcSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*d68f33bcSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
13*d68f33bcSAndroid Build Coastguard Worker# limitations under the License.
14*d68f33bcSAndroid Build Coastguard Worker
15*d68f33bcSAndroid Build Coastguard Worker"""Git helper functions."""
16*d68f33bcSAndroid Build Coastguard Worker
17*d68f33bcSAndroid Build Coastguard Workerimport os
18*d68f33bcSAndroid Build Coastguard Workerimport re
19*d68f33bcSAndroid Build Coastguard Workerimport sys
20*d68f33bcSAndroid Build Coastguard Worker
21*d68f33bcSAndroid Build Coastguard Worker_path = os.path.realpath(__file__ + '/../..')
22*d68f33bcSAndroid Build Coastguard Workerif sys.path[0] != _path:
23*d68f33bcSAndroid Build Coastguard Worker    sys.path.insert(0, _path)
24*d68f33bcSAndroid Build Coastguard Workerdel _path
25*d68f33bcSAndroid Build Coastguard Worker
26*d68f33bcSAndroid Build Coastguard Worker# pylint: disable=wrong-import-position
27*d68f33bcSAndroid Build Coastguard Workerimport rh.utils
28*d68f33bcSAndroid Build Coastguard Worker
29*d68f33bcSAndroid Build Coastguard Worker
30*d68f33bcSAndroid Build Coastguard Workerdef get_upstream_remote():
31*d68f33bcSAndroid Build Coastguard Worker    """Returns the current upstream remote name."""
32*d68f33bcSAndroid Build Coastguard Worker    # First get the current branch name.
33*d68f33bcSAndroid Build Coastguard Worker    cmd = ['git', 'rev-parse', '--abbrev-ref', 'HEAD']
34*d68f33bcSAndroid Build Coastguard Worker    result = rh.utils.run(cmd, capture_output=True)
35*d68f33bcSAndroid Build Coastguard Worker    branch = result.stdout.strip()
36*d68f33bcSAndroid Build Coastguard Worker
37*d68f33bcSAndroid Build Coastguard Worker    # Then get the remote associated with this branch.
38*d68f33bcSAndroid Build Coastguard Worker    cmd = ['git', 'config', f'branch.{branch}.remote']
39*d68f33bcSAndroid Build Coastguard Worker    result = rh.utils.run(cmd, capture_output=True)
40*d68f33bcSAndroid Build Coastguard Worker    return result.stdout.strip()
41*d68f33bcSAndroid Build Coastguard Worker
42*d68f33bcSAndroid Build Coastguard Worker
43*d68f33bcSAndroid Build Coastguard Workerdef get_upstream_branch():
44*d68f33bcSAndroid Build Coastguard Worker    """Returns the upstream tracking branch of the current branch.
45*d68f33bcSAndroid Build Coastguard Worker
46*d68f33bcSAndroid Build Coastguard Worker    Raises:
47*d68f33bcSAndroid Build Coastguard Worker      Error if there is no tracking branch
48*d68f33bcSAndroid Build Coastguard Worker    """
49*d68f33bcSAndroid Build Coastguard Worker    cmd = ['git', 'symbolic-ref', 'HEAD']
50*d68f33bcSAndroid Build Coastguard Worker    result = rh.utils.run(cmd, capture_output=True)
51*d68f33bcSAndroid Build Coastguard Worker    current_branch = result.stdout.strip().replace('refs/heads/', '')
52*d68f33bcSAndroid Build Coastguard Worker    if not current_branch:
53*d68f33bcSAndroid Build Coastguard Worker        raise ValueError('Need to be on a tracking branch')
54*d68f33bcSAndroid Build Coastguard Worker
55*d68f33bcSAndroid Build Coastguard Worker    cfg_option = 'branch.' + current_branch + '.'
56*d68f33bcSAndroid Build Coastguard Worker    cmd = ['git', 'config', cfg_option + 'merge']
57*d68f33bcSAndroid Build Coastguard Worker    result = rh.utils.run(cmd, capture_output=True)
58*d68f33bcSAndroid Build Coastguard Worker    full_upstream = result.stdout.strip()
59*d68f33bcSAndroid Build Coastguard Worker    # If remote is not fully qualified, add an implicit namespace.
60*d68f33bcSAndroid Build Coastguard Worker    if '/' not in full_upstream:
61*d68f33bcSAndroid Build Coastguard Worker        full_upstream = f'refs/heads/{full_upstream}'
62*d68f33bcSAndroid Build Coastguard Worker    cmd = ['git', 'config', cfg_option + 'remote']
63*d68f33bcSAndroid Build Coastguard Worker    result = rh.utils.run(cmd, capture_output=True)
64*d68f33bcSAndroid Build Coastguard Worker    remote = result.stdout.strip()
65*d68f33bcSAndroid Build Coastguard Worker    if not remote or not full_upstream:
66*d68f33bcSAndroid Build Coastguard Worker        raise ValueError('Need to be on a tracking branch')
67*d68f33bcSAndroid Build Coastguard Worker
68*d68f33bcSAndroid Build Coastguard Worker    return full_upstream.replace('heads', 'remotes/' + remote)
69*d68f33bcSAndroid Build Coastguard Worker
70*d68f33bcSAndroid Build Coastguard Worker
71*d68f33bcSAndroid Build Coastguard Workerdef get_commit_for_ref(ref):
72*d68f33bcSAndroid Build Coastguard Worker    """Returns the latest commit for this ref."""
73*d68f33bcSAndroid Build Coastguard Worker    cmd = ['git', 'rev-parse', ref]
74*d68f33bcSAndroid Build Coastguard Worker    result = rh.utils.run(cmd, capture_output=True)
75*d68f33bcSAndroid Build Coastguard Worker    return result.stdout.strip()
76*d68f33bcSAndroid Build Coastguard Worker
77*d68f33bcSAndroid Build Coastguard Worker
78*d68f33bcSAndroid Build Coastguard Workerdef get_remote_revision(ref, remote):
79*d68f33bcSAndroid Build Coastguard Worker    """Returns the remote revision for this ref."""
80*d68f33bcSAndroid Build Coastguard Worker    prefix = f'refs/remotes/{remote}/'
81*d68f33bcSAndroid Build Coastguard Worker    if ref.startswith(prefix):
82*d68f33bcSAndroid Build Coastguard Worker        return ref[len(prefix):]
83*d68f33bcSAndroid Build Coastguard Worker    return ref
84*d68f33bcSAndroid Build Coastguard Worker
85*d68f33bcSAndroid Build Coastguard Worker
86*d68f33bcSAndroid Build Coastguard Workerdef get_patch(commit):
87*d68f33bcSAndroid Build Coastguard Worker    """Returns the patch for this commit."""
88*d68f33bcSAndroid Build Coastguard Worker    cmd = ['git', 'format-patch', '--stdout', '-1', commit]
89*d68f33bcSAndroid Build Coastguard Worker    return rh.utils.run(cmd, capture_output=True).stdout
90*d68f33bcSAndroid Build Coastguard Worker
91*d68f33bcSAndroid Build Coastguard Worker
92*d68f33bcSAndroid Build Coastguard Workerdef get_file_content(commit, path):
93*d68f33bcSAndroid Build Coastguard Worker    """Returns the content of a file at a specific commit.
94*d68f33bcSAndroid Build Coastguard Worker
95*d68f33bcSAndroid Build Coastguard Worker    We can't rely on the file as it exists in the filesystem as people might be
96*d68f33bcSAndroid Build Coastguard Worker    uploading a series of changes which modifies the file multiple times.
97*d68f33bcSAndroid Build Coastguard Worker
98*d68f33bcSAndroid Build Coastguard Worker    Note: The "content" of a symlink is just the target.  So if you're expecting
99*d68f33bcSAndroid Build Coastguard Worker    a full file, you should check that first.  One way to detect is that the
100*d68f33bcSAndroid Build Coastguard Worker    content will not have any newlines.
101*d68f33bcSAndroid Build Coastguard Worker    """
102*d68f33bcSAndroid Build Coastguard Worker    cmd = ['git', 'show', f'{commit}:{path}']
103*d68f33bcSAndroid Build Coastguard Worker    return rh.utils.run(cmd, capture_output=True).stdout
104*d68f33bcSAndroid Build Coastguard Worker
105*d68f33bcSAndroid Build Coastguard Worker
106*d68f33bcSAndroid Build Coastguard Workerclass RawDiffEntry(object):
107*d68f33bcSAndroid Build Coastguard Worker    """Representation of a line from raw formatted git diff output."""
108*d68f33bcSAndroid Build Coastguard Worker
109*d68f33bcSAndroid Build Coastguard Worker    # pylint: disable=redefined-builtin
110*d68f33bcSAndroid Build Coastguard Worker    def __init__(self, src_mode=0, dst_mode=0, src_sha=None, dst_sha=None,
111*d68f33bcSAndroid Build Coastguard Worker                 status=None, score=None, src_file=None, dst_file=None,
112*d68f33bcSAndroid Build Coastguard Worker                 file=None):
113*d68f33bcSAndroid Build Coastguard Worker        self.src_mode = src_mode
114*d68f33bcSAndroid Build Coastguard Worker        self.dst_mode = dst_mode
115*d68f33bcSAndroid Build Coastguard Worker        self.src_sha = src_sha
116*d68f33bcSAndroid Build Coastguard Worker        self.dst_sha = dst_sha
117*d68f33bcSAndroid Build Coastguard Worker        self.status = status
118*d68f33bcSAndroid Build Coastguard Worker        self.score = score
119*d68f33bcSAndroid Build Coastguard Worker        self.src_file = src_file
120*d68f33bcSAndroid Build Coastguard Worker        self.dst_file = dst_file
121*d68f33bcSAndroid Build Coastguard Worker        self.file = file
122*d68f33bcSAndroid Build Coastguard Worker
123*d68f33bcSAndroid Build Coastguard Worker
124*d68f33bcSAndroid Build Coastguard Worker# This regular expression pulls apart a line of raw formatted git diff output.
125*d68f33bcSAndroid Build Coastguard WorkerDIFF_RE = re.compile(
126*d68f33bcSAndroid Build Coastguard Worker    r':(?P<src_mode>[0-7]*) (?P<dst_mode>[0-7]*) '
127*d68f33bcSAndroid Build Coastguard Worker    r'(?P<src_sha>[0-9a-f]*)(\.)* (?P<dst_sha>[0-9a-f]*)(\.)* '
128*d68f33bcSAndroid Build Coastguard Worker    r'(?P<status>[ACDMRTUX])(?P<score>[0-9]+)?\t'
129*d68f33bcSAndroid Build Coastguard Worker    r'(?P<src_file>[^\t]+)\t?(?P<dst_file>[^\t]+)?')
130*d68f33bcSAndroid Build Coastguard Worker
131*d68f33bcSAndroid Build Coastguard Worker
132*d68f33bcSAndroid Build Coastguard Workerdef raw_diff(path, target):
133*d68f33bcSAndroid Build Coastguard Worker    """Return the parsed raw format diff of target
134*d68f33bcSAndroid Build Coastguard Worker
135*d68f33bcSAndroid Build Coastguard Worker    Args:
136*d68f33bcSAndroid Build Coastguard Worker      path: Path to the git repository to diff in.
137*d68f33bcSAndroid Build Coastguard Worker      target: The target to diff.
138*d68f33bcSAndroid Build Coastguard Worker
139*d68f33bcSAndroid Build Coastguard Worker    Returns:
140*d68f33bcSAndroid Build Coastguard Worker      A list of RawDiffEntry's.
141*d68f33bcSAndroid Build Coastguard Worker    """
142*d68f33bcSAndroid Build Coastguard Worker    entries = []
143*d68f33bcSAndroid Build Coastguard Worker
144*d68f33bcSAndroid Build Coastguard Worker    cmd = ['git', 'diff', '--no-ext-diff', '-M', '--raw', target]
145*d68f33bcSAndroid Build Coastguard Worker    diff = rh.utils.run(cmd, cwd=path, capture_output=True).stdout
146*d68f33bcSAndroid Build Coastguard Worker    diff_lines = diff.strip().splitlines()
147*d68f33bcSAndroid Build Coastguard Worker    for line in diff_lines:
148*d68f33bcSAndroid Build Coastguard Worker        match = DIFF_RE.match(line)
149*d68f33bcSAndroid Build Coastguard Worker        if not match:
150*d68f33bcSAndroid Build Coastguard Worker            raise ValueError(f'Failed to parse diff output: {line}')
151*d68f33bcSAndroid Build Coastguard Worker        rawdiff = RawDiffEntry(**match.groupdict())
152*d68f33bcSAndroid Build Coastguard Worker        rawdiff.src_mode = int(rawdiff.src_mode)
153*d68f33bcSAndroid Build Coastguard Worker        rawdiff.dst_mode = int(rawdiff.dst_mode)
154*d68f33bcSAndroid Build Coastguard Worker        rawdiff.file = (rawdiff.dst_file
155*d68f33bcSAndroid Build Coastguard Worker                        if rawdiff.dst_file else rawdiff.src_file)
156*d68f33bcSAndroid Build Coastguard Worker        entries.append(rawdiff)
157*d68f33bcSAndroid Build Coastguard Worker
158*d68f33bcSAndroid Build Coastguard Worker    return entries
159*d68f33bcSAndroid Build Coastguard Worker
160*d68f33bcSAndroid Build Coastguard Worker
161*d68f33bcSAndroid Build Coastguard Workerdef get_affected_files(commit):
162*d68f33bcSAndroid Build Coastguard Worker    """Returns list of file paths that were modified/added.
163*d68f33bcSAndroid Build Coastguard Worker
164*d68f33bcSAndroid Build Coastguard Worker    Returns:
165*d68f33bcSAndroid Build Coastguard Worker      A list of modified/added (and perhaps deleted) files
166*d68f33bcSAndroid Build Coastguard Worker    """
167*d68f33bcSAndroid Build Coastguard Worker    return raw_diff(os.getcwd(), f'{commit}^-')
168*d68f33bcSAndroid Build Coastguard Worker
169*d68f33bcSAndroid Build Coastguard Worker
170*d68f33bcSAndroid Build Coastguard Workerdef get_commits(ignore_merged_commits=False):
171*d68f33bcSAndroid Build Coastguard Worker    """Returns a list of commits for this review."""
172*d68f33bcSAndroid Build Coastguard Worker    cmd = ['git', 'rev-list', f'{get_upstream_branch()}..']
173*d68f33bcSAndroid Build Coastguard Worker    if ignore_merged_commits:
174*d68f33bcSAndroid Build Coastguard Worker        cmd.append('--first-parent')
175*d68f33bcSAndroid Build Coastguard Worker    return rh.utils.run(cmd, capture_output=True).stdout.split()
176*d68f33bcSAndroid Build Coastguard Worker
177*d68f33bcSAndroid Build Coastguard Worker
178*d68f33bcSAndroid Build Coastguard Workerdef get_commit_desc(commit):
179*d68f33bcSAndroid Build Coastguard Worker    """Returns the full commit message of a commit."""
180*d68f33bcSAndroid Build Coastguard Worker    cmd = ['git', 'diff-tree', '-s', '--always', '--format=%B', commit]
181*d68f33bcSAndroid Build Coastguard Worker    return rh.utils.run(cmd, capture_output=True).stdout
182*d68f33bcSAndroid Build Coastguard Worker
183*d68f33bcSAndroid Build Coastguard Worker
184*d68f33bcSAndroid Build Coastguard Workerdef find_repo_root(path=None, outer=False):
185*d68f33bcSAndroid Build Coastguard Worker    """Locate the top level of this repo checkout starting at |path|.
186*d68f33bcSAndroid Build Coastguard Worker
187*d68f33bcSAndroid Build Coastguard Worker    Args:
188*d68f33bcSAndroid Build Coastguard Worker      outer: Whether to find the outermost manifest, or the sub-manifest.
189*d68f33bcSAndroid Build Coastguard Worker    """
190*d68f33bcSAndroid Build Coastguard Worker    if path is None:
191*d68f33bcSAndroid Build Coastguard Worker        path = os.getcwd()
192*d68f33bcSAndroid Build Coastguard Worker    orig_path = path
193*d68f33bcSAndroid Build Coastguard Worker
194*d68f33bcSAndroid Build Coastguard Worker    path = os.path.abspath(path)
195*d68f33bcSAndroid Build Coastguard Worker
196*d68f33bcSAndroid Build Coastguard Worker    # If we are working on a superproject instead of a repo client, use the
197*d68f33bcSAndroid Build Coastguard Worker    # result from git directly.  For regular repo client, this would return
198*d68f33bcSAndroid Build Coastguard Worker    # empty string.
199*d68f33bcSAndroid Build Coastguard Worker    cmd = ['git', 'rev-parse', '--show-superproject-working-tree']
200*d68f33bcSAndroid Build Coastguard Worker    git_worktree_path = rh.utils.run(cmd, cwd=path, capture_output=True).stdout.strip()
201*d68f33bcSAndroid Build Coastguard Worker    if git_worktree_path:
202*d68f33bcSAndroid Build Coastguard Worker        return git_worktree_path
203*d68f33bcSAndroid Build Coastguard Worker
204*d68f33bcSAndroid Build Coastguard Worker    while not os.path.exists(os.path.join(path, '.repo')):
205*d68f33bcSAndroid Build Coastguard Worker        path = os.path.dirname(path)
206*d68f33bcSAndroid Build Coastguard Worker        if path == '/':
207*d68f33bcSAndroid Build Coastguard Worker            raise ValueError(f'Could not locate .repo in {orig_path}')
208*d68f33bcSAndroid Build Coastguard Worker
209*d68f33bcSAndroid Build Coastguard Worker    root = path
210*d68f33bcSAndroid Build Coastguard Worker    if not outer and os.path.isdir(os.path.join(root, '.repo', 'submanifests')):
211*d68f33bcSAndroid Build Coastguard Worker        # If there are submanifests, walk backward from path until we find the
212*d68f33bcSAndroid Build Coastguard Worker        # corresponding submanifest root.
213*d68f33bcSAndroid Build Coastguard Worker        abs_orig_path = os.path.abspath(orig_path)
214*d68f33bcSAndroid Build Coastguard Worker        parts = os.path.relpath(abs_orig_path, root).split(os.path.sep)
215*d68f33bcSAndroid Build Coastguard Worker        while parts and not os.path.isdir(
216*d68f33bcSAndroid Build Coastguard Worker            os.path.join(root, '.repo', 'submanifests', *parts, 'manifests')):
217*d68f33bcSAndroid Build Coastguard Worker            parts.pop()
218*d68f33bcSAndroid Build Coastguard Worker        path = os.path.join(root, *parts)
219*d68f33bcSAndroid Build Coastguard Worker
220*d68f33bcSAndroid Build Coastguard Worker    return path
221*d68f33bcSAndroid Build Coastguard Worker
222*d68f33bcSAndroid Build Coastguard Worker
223*d68f33bcSAndroid Build Coastguard Workerdef is_git_repository(path):
224*d68f33bcSAndroid Build Coastguard Worker    """Returns True if the path is a valid git repository."""
225*d68f33bcSAndroid Build Coastguard Worker    cmd = ['git', 'rev-parse', '--resolve-git-dir', os.path.join(path, '.git')]
226*d68f33bcSAndroid Build Coastguard Worker    result = rh.utils.run(cmd, capture_output=True, check=False)
227*d68f33bcSAndroid Build Coastguard Worker    return result.returncode == 0
228