Mike Frysinger | f1ba7ad | 2022-09-12 05:42:57 -0400 | [diff] [blame] | 1 | # Copyright 2018 The ChromiumOS Authors |
Lann Martin | 4fbdf20 | 2018-08-30 12:02:52 -0600 | [diff] [blame] | 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | |
| 5 | """Create a manifest snapshot of a repo checkout. |
| 6 | |
| 7 | This starts with the output of `repo manifest -r` and updates the manifest to |
| 8 | account for local changes that may not already be available remotely; for any |
| 9 | commits that aren't already reachable from the upstream tracking branch, push |
| 10 | refs to the remotes so that this snapshot can be reproduced remotely. |
| 11 | """ |
| 12 | |
Chris McDonald | 59650c3 | 2021-07-20 15:29:28 -0600 | [diff] [blame] | 13 | import logging |
Lann Martin | 4fbdf20 | 2018-08-30 12:02:52 -0600 | [diff] [blame] | 14 | import os |
| 15 | import sys |
| 16 | |
| 17 | from chromite.lib import commandline |
| 18 | from chromite.lib import cros_build_lib |
Lann Martin | 4fbdf20 | 2018-08-30 12:02:52 -0600 | [diff] [blame] | 19 | from chromite.lib import git |
| 20 | from chromite.lib import parallel |
| 21 | from chromite.lib import repo_util |
| 22 | |
| 23 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 24 | BRANCH_REF_PREFIX = "refs/heads/" |
Lann Martin | 4fbdf20 | 2018-08-30 12:02:52 -0600 | [diff] [blame] | 25 | |
| 26 | |
| 27 | def GetParser(): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 28 | """Creates the argparse parser.""" |
| 29 | parser = commandline.ArgumentParser(description=__doc__) |
| 30 | parser.add_argument( |
| 31 | "--repo-path", |
| 32 | type="path", |
| 33 | default=".", |
| 34 | help="Path to the repo to snapshot.", |
| 35 | ) |
| 36 | parser.add_argument( |
| 37 | "--snapshot-ref", |
| 38 | help="Remote ref to create for projects whose HEAD is " |
| 39 | "not reachable from its current upstream branch. " |
| 40 | "Projects with multiple checkouts may have a " |
| 41 | "unique suffix appended to this ref.", |
| 42 | ) |
| 43 | parser.add_argument( |
| 44 | "--output-file", |
| 45 | type="path", |
| 46 | help="Path to write the manifest snapshot XML to.", |
| 47 | ) |
| 48 | parser.add_argument( |
| 49 | "--dry-run", |
| 50 | action="store_true", |
| 51 | help="Do not actually push to remotes.", |
| 52 | ) |
| 53 | # This is for limiting network traffic to the git remote(s). |
| 54 | parser.add_argument( |
| 55 | "--jobs", |
| 56 | type=int, |
| 57 | default=16, |
| 58 | help="The number of parallel processes to run for " |
| 59 | "git push operations.", |
| 60 | ) |
| 61 | return parser |
Lann Martin | 4fbdf20 | 2018-08-30 12:02:52 -0600 | [diff] [blame] | 62 | |
| 63 | |
| 64 | def _GetUpstreamBranch(project): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 65 | """Return a best guess at the project's upstream branch name.""" |
| 66 | branch = project.upstream |
| 67 | if branch and branch.startswith(BRANCH_REF_PREFIX): |
| 68 | branch = branch[len(BRANCH_REF_PREFIX) :] |
| 69 | return branch |
Lann Martin | 4fbdf20 | 2018-08-30 12:02:52 -0600 | [diff] [blame] | 70 | |
| 71 | |
| 72 | def _NeedsSnapshot(repo_root, project): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 73 | """Test if project's revision is reachable from its upstream ref.""" |
| 74 | # Some projects don't have an upstream set. Try 'main' anyway. |
| 75 | branch = _GetUpstreamBranch(project) or "main" |
| 76 | upstream_ref = "refs/remotes/%s/%s" % (project.Remote().GitName(), branch) |
| 77 | project_path = os.path.join(repo_root, project.Path()) |
| 78 | try: |
| 79 | if git.IsReachable(project_path, project.revision, upstream_ref): |
| 80 | return False |
| 81 | except cros_build_lib.RunCommandError as e: |
| 82 | logging.debug("Reachability check failed: %s", e) |
| 83 | logging.info( |
| 84 | "Project %s revision %s not reachable from upstream %r.", |
| 85 | project.name, |
| 86 | project.revision, |
| 87 | upstream_ref, |
| 88 | ) |
| 89 | return True |
Lann Martin | 4fbdf20 | 2018-08-30 12:02:52 -0600 | [diff] [blame] | 90 | |
| 91 | |
| 92 | def _MakeUniqueRef(project, base_ref, used_refs): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 93 | """Return a git ref for project that isn't in used_refs. |
Lann Martin | 4fbdf20 | 2018-08-30 12:02:52 -0600 | [diff] [blame] | 94 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 95 | Args: |
| 96 | project: The Project object to create a ref for. |
| 97 | base_ref: A base ref name; this may be appended to to generate a unique ref. |
| 98 | used_refs: A set of ref names to uniquify against. It is updated with the |
| 99 | newly generated ref. |
| 100 | """ |
| 101 | ref = base_ref |
Lann Martin | 4fbdf20 | 2018-08-30 12:02:52 -0600 | [diff] [blame] | 102 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 103 | # If the project upstream is a non-main branch, append it to the ref. |
| 104 | branch = _GetUpstreamBranch(project) |
| 105 | if branch and branch != "main": |
| 106 | ref = "%s/%s" % (ref, branch) |
Lann Martin | 4fbdf20 | 2018-08-30 12:02:52 -0600 | [diff] [blame] | 107 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 108 | if ref in used_refs: |
| 109 | # Append incrementing numbers until we find an unused ref. |
| 110 | for i in range(1, len(used_refs) + 2): |
| 111 | numbered = "%s/%d" % (ref, i) |
| 112 | if numbered not in used_refs: |
| 113 | ref = numbered |
| 114 | break |
| 115 | else: |
| 116 | raise AssertionError( |
| 117 | "failed to make unique ref (ref=%s used_refs=%r)" |
| 118 | % (ref, used_refs) |
| 119 | ) |
Lann Martin | 4fbdf20 | 2018-08-30 12:02:52 -0600 | [diff] [blame] | 120 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 121 | used_refs.add(ref) |
| 122 | return ref |
Lann Martin | 4fbdf20 | 2018-08-30 12:02:52 -0600 | [diff] [blame] | 123 | |
| 124 | |
| 125 | def _GitPushProjectUpstream(repo_root, project, dry_run): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 126 | """Push the project revision to its remote upstream.""" |
| 127 | git.GitPush( |
| 128 | os.path.join(repo_root, project.Path()), |
| 129 | project.revision, |
| 130 | git.RemoteRef(project.Remote().GitName(), project.upstream), |
| 131 | dry_run=dry_run, |
| 132 | ) |
Lann Martin | 4fbdf20 | 2018-08-30 12:02:52 -0600 | [diff] [blame] | 133 | |
| 134 | |
| 135 | def main(argv): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 136 | parser = GetParser() |
| 137 | options = parser.parse_args(argv) |
| 138 | options.Freeze() |
Lann Martin | 4fbdf20 | 2018-08-30 12:02:52 -0600 | [diff] [blame] | 139 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 140 | snapshot_ref = options.snapshot_ref |
| 141 | if snapshot_ref and not snapshot_ref.startswith("refs/"): |
| 142 | snapshot_ref = BRANCH_REF_PREFIX + snapshot_ref |
Lann Martin | 4fbdf20 | 2018-08-30 12:02:52 -0600 | [diff] [blame] | 143 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 144 | repo = repo_util.Repository.Find(options.repo_path) |
| 145 | if repo is None: |
| 146 | cros_build_lib.Die( |
| 147 | "No repo found in --repo_path %r.", options.repo_path |
| 148 | ) |
Lann Martin | 4fbdf20 | 2018-08-30 12:02:52 -0600 | [diff] [blame] | 149 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 150 | manifest = repo.Manifest(revision_locked=True) |
| 151 | projects = list(manifest.Projects()) |
Lann Martin | 4fbdf20 | 2018-08-30 12:02:52 -0600 | [diff] [blame] | 152 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 153 | # Check if projects need snapshots (in parallel). |
| 154 | needs_snapshot_results = parallel.RunTasksInProcessPool( |
| 155 | _NeedsSnapshot, [(repo.root, x) for x in projects] |
| 156 | ) |
Lann Martin | 4fbdf20 | 2018-08-30 12:02:52 -0600 | [diff] [blame] | 157 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 158 | # Group snapshot-needing projects by project name. |
| 159 | snapshot_projects = {} |
| 160 | for project, needs_snapshot in zip(projects, needs_snapshot_results): |
| 161 | if needs_snapshot: |
| 162 | snapshot_projects.setdefault(project.name, []).append(project) |
Lann Martin | 4fbdf20 | 2018-08-30 12:02:52 -0600 | [diff] [blame] | 163 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 164 | if snapshot_projects and not snapshot_ref: |
| 165 | cros_build_lib.Die( |
| 166 | "Some project(s) need snapshot refs but no " |
| 167 | "--snapshot-ref specified." |
| 168 | ) |
Lann Martin | 4fbdf20 | 2018-08-30 12:02:52 -0600 | [diff] [blame] | 169 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 170 | # Push snapshot refs (in parallel). |
| 171 | with parallel.BackgroundTaskRunner( |
| 172 | _GitPushProjectUpstream, |
| 173 | repo.root, |
| 174 | dry_run=options.dry_run, |
| 175 | processes=options.jobs, |
| 176 | ) as queue: |
| 177 | for projects in snapshot_projects.values(): |
| 178 | # Since some projects (e.g. chromiumos/third_party/kernel) are checked out |
| 179 | # multiple places, we may need to push each checkout to a unique ref. |
| 180 | need_unique_refs = len(projects) > 1 |
| 181 | used_refs = set() |
| 182 | for project in projects: |
| 183 | if need_unique_refs: |
| 184 | ref = _MakeUniqueRef(project, snapshot_ref, used_refs) |
| 185 | else: |
| 186 | ref = snapshot_ref |
| 187 | # Update the upstream ref both for the push and the output XML. |
| 188 | project.upstream = ref |
| 189 | queue.put([project]) |
Lann Martin | 4fbdf20 | 2018-08-30 12:02:52 -0600 | [diff] [blame] | 190 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 191 | dest = options.output_file |
| 192 | if dest is None or dest == "-": |
| 193 | dest = sys.stdout |
Lann Martin | 4fbdf20 | 2018-08-30 12:02:52 -0600 | [diff] [blame] | 194 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 195 | manifest.Write(dest) |