xref: /aosp_15_r20/tools/external_updater/tests/gitrepo.py (revision 3c875a214f382db1236d28570d1304ce57138f32)
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