1*3c875a21SAndroid Build Coastguard Worker# 2*3c875a21SAndroid Build Coastguard Worker# Copyright (C) 2023 The Android Open Source Project 3*3c875a21SAndroid Build Coastguard Worker# 4*3c875a21SAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License"); 5*3c875a21SAndroid Build Coastguard Worker# you may not use this file except in compliance with the License. 6*3c875a21SAndroid Build Coastguard Worker# You may obtain a copy of the License at 7*3c875a21SAndroid Build Coastguard Worker# 8*3c875a21SAndroid Build Coastguard Worker# http://www.apache.org/licenses/LICENSE-2.0 9*3c875a21SAndroid Build Coastguard Worker# 10*3c875a21SAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software 11*3c875a21SAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS, 12*3c875a21SAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13*3c875a21SAndroid Build Coastguard Worker# See the License for the specific language governing permissions and 14*3c875a21SAndroid Build Coastguard Worker# limitations under the License. 15*3c875a21SAndroid Build Coastguard Worker# 16*3c875a21SAndroid Build Coastguard Worker"""APIs for interacting with git repositories.""" 17*3c875a21SAndroid Build Coastguard Worker# TODO: This should be partially merged with the git_utils APIs. 18*3c875a21SAndroid Build Coastguard Worker# The bulk of this should be lifted out of the tests and used by the rest of 19*3c875a21SAndroid Build Coastguard Worker# external_updater, but we'll want to keep a few of the APIs just in the tests because 20*3c875a21SAndroid Build Coastguard Worker# they're not particularly sensible elsewhere (specifically the shorthand for commit 21*3c875a21SAndroid Build Coastguard Worker# with the update_files and delete_files arguments). It's probably easiest to do that by 22*3c875a21SAndroid Build Coastguard Worker# reworking the git_utils APIs into a class like this and then deriving this one from 23*3c875a21SAndroid Build Coastguard Worker# that. 24*3c875a21SAndroid Build Coastguard Workerfrom __future__ import annotations 25*3c875a21SAndroid Build Coastguard Worker 26*3c875a21SAndroid Build Coastguard Workerimport subprocess 27*3c875a21SAndroid Build Coastguard Workerfrom pathlib import Path 28*3c875a21SAndroid Build Coastguard Worker 29*3c875a21SAndroid Build Coastguard Worker 30*3c875a21SAndroid Build Coastguard Workerclass GitRepo: 31*3c875a21SAndroid Build Coastguard Worker """A git repository for use in tests.""" 32*3c875a21SAndroid Build Coastguard Worker 33*3c875a21SAndroid Build Coastguard Worker def __init__(self, path: Path) -> None: 34*3c875a21SAndroid Build Coastguard Worker self.path = path 35*3c875a21SAndroid Build Coastguard Worker 36*3c875a21SAndroid Build Coastguard Worker def run(self, command: list[str]) -> str: 37*3c875a21SAndroid Build Coastguard Worker """Runs the given git command in the repository, returning the output.""" 38*3c875a21SAndroid Build Coastguard Worker return subprocess.run( 39*3c875a21SAndroid Build Coastguard Worker ["git", "-C", str(self.path)] + command, 40*3c875a21SAndroid Build Coastguard Worker check=True, 41*3c875a21SAndroid Build Coastguard Worker capture_output=True, 42*3c875a21SAndroid Build Coastguard Worker text=True, 43*3c875a21SAndroid Build Coastguard Worker ).stdout 44*3c875a21SAndroid Build Coastguard Worker 45*3c875a21SAndroid Build Coastguard Worker def init(self, branch_name: str | None = None) -> None: 46*3c875a21SAndroid Build Coastguard Worker """Initializes a new git repository.""" 47*3c875a21SAndroid Build Coastguard Worker self.path.mkdir(parents=True) 48*3c875a21SAndroid Build Coastguard Worker cmd = ["init"] 49*3c875a21SAndroid Build Coastguard Worker if branch_name is not None: 50*3c875a21SAndroid Build Coastguard Worker cmd.extend(["-b", branch_name]) 51*3c875a21SAndroid Build Coastguard Worker self.run(cmd) 52*3c875a21SAndroid Build Coastguard Worker 53*3c875a21SAndroid Build Coastguard Worker def head(self) -> str: 54*3c875a21SAndroid Build Coastguard Worker """Returns the SHA of the current HEAD.""" 55*3c875a21SAndroid Build Coastguard Worker return self.run(["rev-parse", "HEAD"]).strip() 56*3c875a21SAndroid Build Coastguard Worker 57*3c875a21SAndroid Build Coastguard Worker def sha_of_ref(self, ref: str) -> str: 58*3c875a21SAndroid Build Coastguard Worker """Returns the sha of the given ref.""" 59*3c875a21SAndroid Build Coastguard Worker return self.run(["rev-list", "-n", "1", ref]).strip() 60*3c875a21SAndroid Build Coastguard Worker 61*3c875a21SAndroid Build Coastguard Worker def current_branch(self) -> str: 62*3c875a21SAndroid Build Coastguard Worker """Returns the name of the current branch.""" 63*3c875a21SAndroid Build Coastguard Worker return self.run(["branch", "--show-current"]).strip() 64*3c875a21SAndroid Build Coastguard Worker 65*3c875a21SAndroid Build Coastguard Worker def fetch(self, ref_or_repo: str | GitRepo) -> None: 66*3c875a21SAndroid Build Coastguard Worker """Fetches the given ref or repo.""" 67*3c875a21SAndroid Build Coastguard Worker if isinstance(ref_or_repo, GitRepo): 68*3c875a21SAndroid Build Coastguard Worker ref_or_repo = str(ref_or_repo.path) 69*3c875a21SAndroid Build Coastguard Worker self.run(["fetch", ref_or_repo]) 70*3c875a21SAndroid Build Coastguard Worker 71*3c875a21SAndroid Build Coastguard Worker def commit( 72*3c875a21SAndroid Build Coastguard Worker self, 73*3c875a21SAndroid Build Coastguard Worker message: str, 74*3c875a21SAndroid Build Coastguard Worker allow_empty: bool = False, 75*3c875a21SAndroid Build Coastguard Worker update_files: dict[str, str] | None = None, 76*3c875a21SAndroid Build Coastguard Worker delete_files: set[str] | None = None, 77*3c875a21SAndroid Build Coastguard Worker ) -> None: 78*3c875a21SAndroid Build Coastguard Worker """Create a commit in the repository.""" 79*3c875a21SAndroid Build Coastguard Worker if update_files is None: 80*3c875a21SAndroid Build Coastguard Worker update_files = {} 81*3c875a21SAndroid Build Coastguard Worker if delete_files is None: 82*3c875a21SAndroid Build Coastguard Worker delete_files = set() 83*3c875a21SAndroid Build Coastguard Worker 84*3c875a21SAndroid Build Coastguard Worker for delete_file in delete_files: 85*3c875a21SAndroid Build Coastguard Worker self.run(["rm", delete_file]) 86*3c875a21SAndroid Build Coastguard Worker 87*3c875a21SAndroid Build Coastguard Worker for update_file, contents in update_files.items(): 88*3c875a21SAndroid Build Coastguard Worker (self.path / update_file).write_text(contents, encoding="utf-8") 89*3c875a21SAndroid Build Coastguard Worker self.run(["add", update_file]) 90*3c875a21SAndroid Build Coastguard Worker 91*3c875a21SAndroid Build Coastguard Worker commit_cmd = ["commit", "-m", message] 92*3c875a21SAndroid Build Coastguard Worker if allow_empty: 93*3c875a21SAndroid Build Coastguard Worker commit_cmd.append("--allow-empty") 94*3c875a21SAndroid Build Coastguard Worker self.run(commit_cmd) 95*3c875a21SAndroid Build Coastguard Worker 96*3c875a21SAndroid Build Coastguard Worker def merge( 97*3c875a21SAndroid Build Coastguard Worker self, 98*3c875a21SAndroid Build Coastguard Worker ref: str, 99*3c875a21SAndroid Build Coastguard Worker allow_fast_forward: bool = True, 100*3c875a21SAndroid Build Coastguard Worker allow_unrelated_histories: bool = False, 101*3c875a21SAndroid Build Coastguard Worker ) -> None: 102*3c875a21SAndroid Build Coastguard Worker """Merges the upstream ref into the repo.""" 103*3c875a21SAndroid Build Coastguard Worker cmd = ["merge"] 104*3c875a21SAndroid Build Coastguard Worker if not allow_fast_forward: 105*3c875a21SAndroid Build Coastguard Worker cmd.append("--no-ff") 106*3c875a21SAndroid Build Coastguard Worker if allow_unrelated_histories: 107*3c875a21SAndroid Build Coastguard Worker cmd.append("--allow-unrelated-histories") 108*3c875a21SAndroid Build Coastguard Worker self.run(cmd + [ref]) 109*3c875a21SAndroid Build Coastguard Worker 110*3c875a21SAndroid Build Coastguard Worker def switch_to_new_branch(self, name: str, start_point: str | None = None) -> None: 111*3c875a21SAndroid Build Coastguard Worker """Creates and switches to a new branch.""" 112*3c875a21SAndroid Build Coastguard Worker args = ["switch", "--create", name] 113*3c875a21SAndroid Build Coastguard Worker if start_point is not None: 114*3c875a21SAndroid Build Coastguard Worker args.append(start_point) 115*3c875a21SAndroid Build Coastguard Worker self.run(args) 116*3c875a21SAndroid Build Coastguard Worker 117*3c875a21SAndroid Build Coastguard Worker def checkout(self, branch: str) -> None: 118*3c875a21SAndroid Build Coastguard Worker """Checks out a branch.""" 119*3c875a21SAndroid Build Coastguard Worker args = ["checkout", branch] 120*3c875a21SAndroid Build Coastguard Worker self.run(args) 121*3c875a21SAndroid Build Coastguard Worker 122*3c875a21SAndroid Build Coastguard Worker def delete_branch(self, name: str) -> None: 123*3c875a21SAndroid Build Coastguard Worker """Deletes a branch""" 124*3c875a21SAndroid Build Coastguard Worker args = ["branch", "-D", name] 125*3c875a21SAndroid Build Coastguard Worker self.run(args) 126*3c875a21SAndroid Build Coastguard Worker 127*3c875a21SAndroid Build Coastguard Worker def tag(self, name: str, ref: str | None = None) -> None: 128*3c875a21SAndroid Build Coastguard Worker """Creates a tag at the given ref, or HEAD if not provided.""" 129*3c875a21SAndroid Build Coastguard Worker args = ["tag", name] 130*3c875a21SAndroid Build Coastguard Worker if ref is not None: 131*3c875a21SAndroid Build Coastguard Worker args.append(ref) 132*3c875a21SAndroid Build Coastguard Worker self.run(args) 133*3c875a21SAndroid Build Coastguard Worker 134*3c875a21SAndroid Build Coastguard Worker def commit_message_at_revision(self, revision: str) -> str: 135*3c875a21SAndroid Build Coastguard Worker """Returns the commit message of the given revision.""" 136*3c875a21SAndroid Build Coastguard Worker # %B is the raw commit body 137*3c875a21SAndroid Build Coastguard Worker # %- eats the separator newline 138*3c875a21SAndroid Build Coastguard Worker # Note that commit messages created with `git commit` will always end with a 139*3c875a21SAndroid Build Coastguard Worker # trailing newline. 140*3c875a21SAndroid Build Coastguard Worker return self.run(["log", "--format=%B%-", "-n1", revision]) 141*3c875a21SAndroid Build Coastguard Worker 142*3c875a21SAndroid Build Coastguard Worker def file_contents_at_revision(self, revision: str, path: str) -> str: 143*3c875a21SAndroid Build Coastguard Worker """Returns the commit message of the given revision.""" 144*3c875a21SAndroid Build Coastguard Worker # %B is the raw commit body 145*3c875a21SAndroid Build Coastguard Worker # %- eats the separator newline 146*3c875a21SAndroid Build Coastguard Worker return self.run(["show", "--format=%B%-", f"{revision}:{path}"]) 147*3c875a21SAndroid Build Coastguard Worker 148*3c875a21SAndroid Build Coastguard Worker def describe(self, sha: str) -> str: 149*3c875a21SAndroid Build Coastguard Worker """Returns the nearest tag to a given commit.""" 150*3c875a21SAndroid Build Coastguard Worker cmd = ["describe", "--contains", sha] 151*3c875a21SAndroid Build Coastguard Worker return self.run(cmd).strip() 152