xref: /aosp_15_r20/tools/external_updater/git_updater.py (revision 3c875a214f382db1236d28570d1304ce57138f32)
1# Copyright (C) 2018 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Module to check updates from Git upstream."""
15
16import base_updater
17import fileutils
18import git_utils
19import updater_utils
20# pylint: disable=import-error
21from color import Color, color_string
22from manifest import Manifest
23import metadata_pb2  # type: ignore
24
25
26class GitUpdater(base_updater.Updater):
27    """Updater for Git upstream."""
28    UPSTREAM_REMOTE_NAME: str = "update_origin"
29
30    def is_supported_url(self) -> bool:
31        return git_utils.is_valid_url(self._proj_path, self._old_identifier.value)
32
33    def setup_remote(self) -> None:
34        remotes = git_utils.list_remotes(self._proj_path)
35        current_remote_url = None
36        for name, url in remotes.items():
37            if name == self.UPSTREAM_REMOTE_NAME:
38                current_remote_url = url
39
40        if current_remote_url is not None and current_remote_url != self._old_identifier.value:
41            git_utils.remove_remote(self._proj_path, self.UPSTREAM_REMOTE_NAME)
42            current_remote_url = None
43
44        if current_remote_url is None:
45            git_utils.add_remote(self._proj_path, self.UPSTREAM_REMOTE_NAME,
46                                 self._old_identifier.value)
47
48        git_utils.fetch(self._proj_path, self.UPSTREAM_REMOTE_NAME)
49
50    def set_custom_version(self, custom_version: str) -> None:
51        super().set_custom_version(custom_version)
52        if not git_utils.list_branches_with_commit(self._proj_path, custom_version, self.UPSTREAM_REMOTE_NAME):
53            raise RuntimeError(
54                f"Can not upgrade to {custom_version}. This version does not belong to any branches.")
55
56    def set_new_versions_for_commit(self, latest_sha: str, latest_tag: str | None = None) -> None:
57        self._new_identifier.version = latest_sha
58        if latest_tag is not None and git_utils.is_ancestor(
59            self._proj_path, self._old_identifier.version, latest_tag):
60            self._alternative_new_ver = latest_tag
61
62    def set_new_versions_for_tag(self, latest_sha: str, latest_tag: str | None = None) -> None:
63        if latest_tag is None:
64            project = fileutils.canonicalize_project_path(self.project_path)
65            print(color_string(
66                f"{project} is currently tracking upstream tags but either no "
67                "tags were found in the upstream repository or the tag does not "
68                "belong to any branch. No latest tag available", Color.STALE
69            ))
70            self._new_identifier.ClearField("version")
71            self._alternative_new_ver = latest_sha
72            return
73        self._new_identifier.version = latest_tag
74        if git_utils.is_ancestor(
75            self._proj_path, self._old_identifier.version, latest_sha):
76            self._alternative_new_ver = latest_sha
77
78    def check(self) -> None:
79        """Checks upstream and returns whether a new version is available."""
80        self.setup_remote()
81
82        latest_sha = self.current_head_of_upstream_default_branch()
83        latest_tag = self.latest_tag_of_upstream()
84
85        if git_utils.is_commit(self._old_identifier.version):
86            self.set_new_versions_for_commit(latest_sha, latest_tag)
87        else:
88            self.set_new_versions_for_tag(latest_sha, latest_tag)
89
90    def latest_tag_of_upstream(self) -> str | None:
91        tags = git_utils.list_remote_tags(self._proj_path, self.UPSTREAM_REMOTE_NAME)
92        if not tags:
93            return None
94
95        parsed_tags = [updater_utils.parse_remote_tag(tag) for tag in tags]
96        tag = updater_utils.get_latest_stable_release_tag(self._old_identifier.version, parsed_tags)
97        if not git_utils.list_branches_with_commit(self._proj_path, tag, self.UPSTREAM_REMOTE_NAME):
98            return None
99
100        return tag
101
102    def current_head_of_upstream_default_branch(self) -> str:
103        branch = git_utils.detect_default_branch(self._proj_path,
104                                                 self.UPSTREAM_REMOTE_NAME)
105        return git_utils.get_sha_for_branch(
106            self._proj_path, self.UPSTREAM_REMOTE_NAME + '/' + branch)
107
108    def update(self) -> None:
109        """Updates the package.
110        Has to call check() before this function.
111        """
112        print(f"Running `git merge {self._new_identifier.version}`...")
113        git_utils.merge(self._proj_path, self._new_identifier.version)
114
115    def _determine_android_fetch_ref(self) -> str:
116        """Returns the ref that should be fetched from the android remote."""
117        # It isn't particularly efficient to reparse the tree for every
118        # project, but we don't guarantee that all paths passed to updater.sh
119        # are actually in the same tree so it wouldn't necessarily be correct
120        # to do this once at the top level. This isn't the slow part anyway,
121        # so it can be dealt with if that ever changes.
122        root = fileutils.find_tree_containing(self._proj_path)
123        manifest = Manifest.for_tree(root)
124        manifest_path = str(self._proj_path.relative_to(root))
125        try:
126            project = manifest.project_with_path(manifest_path)
127        except KeyError as ex:
128            raise RuntimeError(
129                f"Did not find {manifest_path} in {manifest.path} (tree root is {root})"
130            ) from ex
131        return project.revision
132