You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
228 lines
7.6 KiB
228 lines
7.6 KiB
# Copyright 2016 The Android Open Source Project
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Git helper functions."""
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
_path = os.path.realpath(__file__ + '/../..')
|
|
if sys.path[0] != _path:
|
|
sys.path.insert(0, _path)
|
|
del _path
|
|
|
|
# pylint: disable=wrong-import-position
|
|
import rh.utils
|
|
|
|
|
|
def get_upstream_remote():
|
|
"""Returns the current upstream remote name."""
|
|
# First get the current branch name.
|
|
cmd = ['git', 'rev-parse', '--abbrev-ref', 'HEAD']
|
|
result = rh.utils.run(cmd, capture_output=True)
|
|
branch = result.stdout.strip()
|
|
|
|
# Then get the remote associated with this branch.
|
|
cmd = ['git', 'config', f'branch.{branch}.remote']
|
|
result = rh.utils.run(cmd, capture_output=True)
|
|
return result.stdout.strip()
|
|
|
|
|
|
def get_upstream_branch():
|
|
"""Returns the upstream tracking branch of the current branch.
|
|
|
|
Raises:
|
|
Error if there is no tracking branch
|
|
"""
|
|
cmd = ['git', 'symbolic-ref', 'HEAD']
|
|
result = rh.utils.run(cmd, capture_output=True)
|
|
current_branch = result.stdout.strip().replace('refs/heads/', '')
|
|
if not current_branch:
|
|
raise ValueError('Need to be on a tracking branch')
|
|
|
|
cfg_option = 'branch.' + current_branch + '.'
|
|
cmd = ['git', 'config', cfg_option + 'merge']
|
|
result = rh.utils.run(cmd, capture_output=True)
|
|
full_upstream = result.stdout.strip()
|
|
# If remote is not fully qualified, add an implicit namespace.
|
|
if '/' not in full_upstream:
|
|
full_upstream = f'refs/heads/{full_upstream}'
|
|
cmd = ['git', 'config', cfg_option + 'remote']
|
|
result = rh.utils.run(cmd, capture_output=True)
|
|
remote = result.stdout.strip()
|
|
if not remote or not full_upstream:
|
|
raise ValueError('Need to be on a tracking branch')
|
|
|
|
return full_upstream.replace('heads', 'remotes/' + remote)
|
|
|
|
|
|
def get_commit_for_ref(ref):
|
|
"""Returns the latest commit for this ref."""
|
|
cmd = ['git', 'rev-parse', ref]
|
|
result = rh.utils.run(cmd, capture_output=True)
|
|
return result.stdout.strip()
|
|
|
|
|
|
def get_remote_revision(ref, remote):
|
|
"""Returns the remote revision for this ref."""
|
|
prefix = f'refs/remotes/{remote}/'
|
|
if ref.startswith(prefix):
|
|
return ref[len(prefix):]
|
|
return ref
|
|
|
|
|
|
def get_patch(commit):
|
|
"""Returns the patch for this commit."""
|
|
cmd = ['git', 'format-patch', '--stdout', '-1', commit]
|
|
return rh.utils.run(cmd, capture_output=True).stdout
|
|
|
|
|
|
def get_file_content(commit, path):
|
|
"""Returns the content of a file at a specific commit.
|
|
|
|
We can't rely on the file as it exists in the filesystem as people might be
|
|
uploading a series of changes which modifies the file multiple times.
|
|
|
|
Note: The "content" of a symlink is just the target. So if you're expecting
|
|
a full file, you should check that first. One way to detect is that the
|
|
content will not have any newlines.
|
|
"""
|
|
cmd = ['git', 'show', f'{commit}:{path}']
|
|
return rh.utils.run(cmd, capture_output=True).stdout
|
|
|
|
|
|
class RawDiffEntry(object):
|
|
"""Representation of a line from raw formatted git diff output."""
|
|
|
|
# pylint: disable=redefined-builtin
|
|
def __init__(self, src_mode=0, dst_mode=0, src_sha=None, dst_sha=None,
|
|
status=None, score=None, src_file=None, dst_file=None,
|
|
file=None):
|
|
self.src_mode = src_mode
|
|
self.dst_mode = dst_mode
|
|
self.src_sha = src_sha
|
|
self.dst_sha = dst_sha
|
|
self.status = status
|
|
self.score = score
|
|
self.src_file = src_file
|
|
self.dst_file = dst_file
|
|
self.file = file
|
|
|
|
|
|
# This regular expression pulls apart a line of raw formatted git diff output.
|
|
DIFF_RE = re.compile(
|
|
r':(?P<src_mode>[0-7]*) (?P<dst_mode>[0-7]*) '
|
|
r'(?P<src_sha>[0-9a-f]*)(\.)* (?P<dst_sha>[0-9a-f]*)(\.)* '
|
|
r'(?P<status>[ACDMRTUX])(?P<score>[0-9]+)?\t'
|
|
r'(?P<src_file>[^\t]+)\t?(?P<dst_file>[^\t]+)?')
|
|
|
|
|
|
def raw_diff(path, target):
|
|
"""Return the parsed raw format diff of target
|
|
|
|
Args:
|
|
path: Path to the git repository to diff in.
|
|
target: The target to diff.
|
|
|
|
Returns:
|
|
A list of RawDiffEntry's.
|
|
"""
|
|
entries = []
|
|
|
|
cmd = ['git', 'diff', '--no-ext-diff', '-M', '--raw', target]
|
|
diff = rh.utils.run(cmd, cwd=path, capture_output=True).stdout
|
|
diff_lines = diff.strip().splitlines()
|
|
for line in diff_lines:
|
|
match = DIFF_RE.match(line)
|
|
if not match:
|
|
raise ValueError(f'Failed to parse diff output: {line}')
|
|
rawdiff = RawDiffEntry(**match.groupdict())
|
|
rawdiff.src_mode = int(rawdiff.src_mode)
|
|
rawdiff.dst_mode = int(rawdiff.dst_mode)
|
|
rawdiff.file = (rawdiff.dst_file
|
|
if rawdiff.dst_file else rawdiff.src_file)
|
|
entries.append(rawdiff)
|
|
|
|
return entries
|
|
|
|
|
|
def get_affected_files(commit):
|
|
"""Returns list of file paths that were modified/added.
|
|
|
|
Returns:
|
|
A list of modified/added (and perhaps deleted) files
|
|
"""
|
|
return raw_diff(os.getcwd(), f'{commit}^-')
|
|
|
|
|
|
def get_commits(ignore_merged_commits=False):
|
|
"""Returns a list of commits for this review."""
|
|
cmd = ['git', 'rev-list', f'{get_upstream_branch()}..']
|
|
if ignore_merged_commits:
|
|
cmd.append('--first-parent')
|
|
return rh.utils.run(cmd, capture_output=True).stdout.split()
|
|
|
|
|
|
def get_commit_desc(commit):
|
|
"""Returns the full commit message of a commit."""
|
|
cmd = ['git', 'diff-tree', '-s', '--always', '--format=%B', commit]
|
|
return rh.utils.run(cmd, capture_output=True).stdout
|
|
|
|
|
|
def find_repo_root(path=None, outer=False):
|
|
"""Locate the top level of this repo checkout starting at |path|.
|
|
|
|
Args:
|
|
outer: Whether to find the outermost manifest, or the sub-manifest.
|
|
"""
|
|
if path is None:
|
|
path = os.getcwd()
|
|
orig_path = path
|
|
|
|
path = os.path.abspath(path)
|
|
|
|
# If we are working on a superproject instead of a repo client, use the
|
|
# result from git directly. For regular repo client, this would return
|
|
# empty string.
|
|
cmd = ['git', 'rev-parse', '--show-superproject-working-tree']
|
|
git_worktree_path = rh.utils.run(cmd, cwd=path, capture_output=True).stdout.strip()
|
|
if git_worktree_path:
|
|
return git_worktree_path
|
|
|
|
while not os.path.exists(os.path.join(path, '.repo')):
|
|
path = os.path.dirname(path)
|
|
if path == '/':
|
|
raise ValueError(f'Could not locate .repo in {orig_path}')
|
|
|
|
root = path
|
|
if not outer and os.path.isdir(os.path.join(root, '.repo', 'submanifests')):
|
|
# If there are submanifests, walk backward from path until we find the
|
|
# corresponding submanifest root.
|
|
abs_orig_path = os.path.abspath(orig_path)
|
|
parts = os.path.relpath(abs_orig_path, root).split(os.path.sep)
|
|
while parts and not os.path.isdir(
|
|
os.path.join(root, '.repo', 'submanifests', *parts, 'manifests')):
|
|
parts.pop()
|
|
path = os.path.join(root, *parts)
|
|
|
|
return path
|
|
|
|
|
|
def is_git_repository(path):
|
|
"""Returns True if the path is a valid git repository."""
|
|
cmd = ['git', 'rev-parse', '--resolve-git-dir', os.path.join(path, '.git')]
|
|
result = rh.utils.run(cmd, capture_output=True, check=False)
|
|
return result.returncode == 0
|