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