blob: 599a9f24bc409f1c7c2f025b5c90379d97325ff9 [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2018 The ChromiumOS Authors
Lann Martin4fbdf202018-08-30 12:02:52 -06002# 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
7This starts with the output of `repo manifest -r` and updates the manifest to
8account for local changes that may not already be available remotely; for any
9commits that aren't already reachable from the upstream tracking branch, push
10refs to the remotes so that this snapshot can be reproduced remotely.
11"""
12
Chris McDonald59650c32021-07-20 15:29:28 -060013import logging
Lann Martin4fbdf202018-08-30 12:02:52 -060014import os
15import sys
16
17from chromite.lib import commandline
18from chromite.lib import cros_build_lib
Lann Martin4fbdf202018-08-30 12:02:52 -060019from chromite.lib import git
20from chromite.lib import parallel
21from chromite.lib import repo_util
22
23
Alex Klein1699fab2022-09-08 08:46:06 -060024BRANCH_REF_PREFIX = "refs/heads/"
Lann Martin4fbdf202018-08-30 12:02:52 -060025
26
27def GetParser():
Alex Klein1699fab2022-09-08 08:46:06 -060028 """Creates the argparse parser."""
Mike Frysinger66ac6e32023-02-02 09:03:31 -050029 parser = commandline.ArgumentParser(description=__doc__, dryrun=True)
Alex Klein1699fab2022-09-08 08:46:06 -060030 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 )
Alex Klein1699fab2022-09-08 08:46:06 -060048 # This is for limiting network traffic to the git remote(s).
49 parser.add_argument(
50 "--jobs",
51 type=int,
52 default=16,
53 help="The number of parallel processes to run for "
54 "git push operations.",
55 )
56 return parser
Lann Martin4fbdf202018-08-30 12:02:52 -060057
58
59def _GetUpstreamBranch(project):
Alex Klein9e7b29e2023-04-11 16:10:31 -060060 """Return the best guess at the project's upstream branch name."""
Alex Klein1699fab2022-09-08 08:46:06 -060061 branch = project.upstream
62 if branch and branch.startswith(BRANCH_REF_PREFIX):
63 branch = branch[len(BRANCH_REF_PREFIX) :]
64 return branch
Lann Martin4fbdf202018-08-30 12:02:52 -060065
66
67def _NeedsSnapshot(repo_root, project):
Alex Klein1699fab2022-09-08 08:46:06 -060068 """Test if project's revision is reachable from its upstream ref."""
69 # Some projects don't have an upstream set. Try 'main' anyway.
70 branch = _GetUpstreamBranch(project) or "main"
71 upstream_ref = "refs/remotes/%s/%s" % (project.Remote().GitName(), branch)
72 project_path = os.path.join(repo_root, project.Path())
73 try:
74 if git.IsReachable(project_path, project.revision, upstream_ref):
75 return False
76 except cros_build_lib.RunCommandError as e:
77 logging.debug("Reachability check failed: %s", e)
78 logging.info(
79 "Project %s revision %s not reachable from upstream %r.",
80 project.name,
81 project.revision,
82 upstream_ref,
83 )
84 return True
Lann Martin4fbdf202018-08-30 12:02:52 -060085
86
87def _MakeUniqueRef(project, base_ref, used_refs):
Alex Klein1699fab2022-09-08 08:46:06 -060088 """Return a git ref for project that isn't in used_refs.
Lann Martin4fbdf202018-08-30 12:02:52 -060089
Alex Klein1699fab2022-09-08 08:46:06 -060090 Args:
Alex Klein9e7b29e2023-04-11 16:10:31 -060091 project: The Project object to create a ref for.
92 base_ref: A base ref name; this may be appended to, to generate a unique
93 ref.
94 used_refs: A set of ref names to uniquify against. It is updated with
95 the newly generated ref.
Alex Klein1699fab2022-09-08 08:46:06 -060096 """
97 ref = base_ref
Lann Martin4fbdf202018-08-30 12:02:52 -060098
Alex Klein1699fab2022-09-08 08:46:06 -060099 # If the project upstream is a non-main branch, append it to the ref.
100 branch = _GetUpstreamBranch(project)
101 if branch and branch != "main":
102 ref = "%s/%s" % (ref, branch)
Lann Martin4fbdf202018-08-30 12:02:52 -0600103
Alex Klein1699fab2022-09-08 08:46:06 -0600104 if ref in used_refs:
105 # Append incrementing numbers until we find an unused ref.
106 for i in range(1, len(used_refs) + 2):
107 numbered = "%s/%d" % (ref, i)
108 if numbered not in used_refs:
109 ref = numbered
110 break
111 else:
112 raise AssertionError(
113 "failed to make unique ref (ref=%s used_refs=%r)"
114 % (ref, used_refs)
115 )
Lann Martin4fbdf202018-08-30 12:02:52 -0600116
Alex Klein1699fab2022-09-08 08:46:06 -0600117 used_refs.add(ref)
118 return ref
Lann Martin4fbdf202018-08-30 12:02:52 -0600119
120
Mike Frysinger66ac6e32023-02-02 09:03:31 -0500121def _GitPushProjectUpstream(repo_root, project, dryrun):
Alex Klein1699fab2022-09-08 08:46:06 -0600122 """Push the project revision to its remote upstream."""
123 git.GitPush(
124 os.path.join(repo_root, project.Path()),
125 project.revision,
126 git.RemoteRef(project.Remote().GitName(), project.upstream),
Mike Frysinger66ac6e32023-02-02 09:03:31 -0500127 dry_run=dryrun,
Alex Klein1699fab2022-09-08 08:46:06 -0600128 )
Lann Martin4fbdf202018-08-30 12:02:52 -0600129
130
131def main(argv):
Alex Klein1699fab2022-09-08 08:46:06 -0600132 parser = GetParser()
133 options = parser.parse_args(argv)
134 options.Freeze()
Lann Martin4fbdf202018-08-30 12:02:52 -0600135
Alex Klein1699fab2022-09-08 08:46:06 -0600136 snapshot_ref = options.snapshot_ref
137 if snapshot_ref and not snapshot_ref.startswith("refs/"):
138 snapshot_ref = BRANCH_REF_PREFIX + snapshot_ref
Lann Martin4fbdf202018-08-30 12:02:52 -0600139
Alex Klein1699fab2022-09-08 08:46:06 -0600140 repo = repo_util.Repository.Find(options.repo_path)
141 if repo is None:
142 cros_build_lib.Die(
143 "No repo found in --repo_path %r.", options.repo_path
144 )
Lann Martin4fbdf202018-08-30 12:02:52 -0600145
Alex Klein1699fab2022-09-08 08:46:06 -0600146 manifest = repo.Manifest(revision_locked=True)
147 projects = list(manifest.Projects())
Lann Martin4fbdf202018-08-30 12:02:52 -0600148
Alex Klein1699fab2022-09-08 08:46:06 -0600149 # Check if projects need snapshots (in parallel).
150 needs_snapshot_results = parallel.RunTasksInProcessPool(
151 _NeedsSnapshot, [(repo.root, x) for x in projects]
152 )
Lann Martin4fbdf202018-08-30 12:02:52 -0600153
Alex Klein1699fab2022-09-08 08:46:06 -0600154 # Group snapshot-needing projects by project name.
155 snapshot_projects = {}
156 for project, needs_snapshot in zip(projects, needs_snapshot_results):
157 if needs_snapshot:
158 snapshot_projects.setdefault(project.name, []).append(project)
Lann Martin4fbdf202018-08-30 12:02:52 -0600159
Alex Klein1699fab2022-09-08 08:46:06 -0600160 if snapshot_projects and not snapshot_ref:
161 cros_build_lib.Die(
162 "Some project(s) need snapshot refs but no "
163 "--snapshot-ref specified."
164 )
Lann Martin4fbdf202018-08-30 12:02:52 -0600165
Alex Klein1699fab2022-09-08 08:46:06 -0600166 # Push snapshot refs (in parallel).
167 with parallel.BackgroundTaskRunner(
168 _GitPushProjectUpstream,
169 repo.root,
Mike Frysinger66ac6e32023-02-02 09:03:31 -0500170 dryrun=options.dryrun,
Alex Klein1699fab2022-09-08 08:46:06 -0600171 processes=options.jobs,
172 ) as queue:
173 for projects in snapshot_projects.values():
Alex Klein9e7b29e2023-04-11 16:10:31 -0600174 # Since some projects (e.g. chromiumos/third_party/kernel) are
175 # checked out multiple places, we may need to push each checkout to
176 # a unique ref.
Alex Klein1699fab2022-09-08 08:46:06 -0600177 need_unique_refs = len(projects) > 1
178 used_refs = set()
179 for project in projects:
180 if need_unique_refs:
181 ref = _MakeUniqueRef(project, snapshot_ref, used_refs)
182 else:
183 ref = snapshot_ref
184 # Update the upstream ref both for the push and the output XML.
185 project.upstream = ref
186 queue.put([project])
Lann Martin4fbdf202018-08-30 12:02:52 -0600187
Alex Klein1699fab2022-09-08 08:46:06 -0600188 dest = options.output_file
189 if dest is None or dest == "-":
190 dest = sys.stdout
Lann Martin4fbdf202018-08-30 12:02:52 -0600191
Alex Klein1699fab2022-09-08 08:46:06 -0600192 manifest.Write(dest)