# 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
from pathlib
import Path
import re
import sys
THIS_FILE = Path(__file__).resolve()
THIS_DIR = THIS_FILE.parent
sys.path.insert(
0, str(THIS_DIR.parent))
# 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