blob: 3c6d5a925d8ed53b8cfd37884499e49599c4d280 [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."""
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 Martin4fbdf202018-08-30 12:02:52 -060062
63
64def _GetUpstreamBranch(project):
Alex Klein1699fab2022-09-08 08:46:06 -060065 """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 Martin4fbdf202018-08-30 12:02:52 -060070
71
72def _NeedsSnapshot(repo_root, project):
Alex Klein1699fab2022-09-08 08:46:06 -060073 """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 Martin4fbdf202018-08-30 12:02:52 -060090
91
92def _MakeUniqueRef(project, base_ref, used_refs):
Alex Klein1699fab2022-09-08 08:46:06 -060093 """Return a git ref for project that isn't in used_refs.
Lann Martin4fbdf202018-08-30 12:02:52 -060094
Alex Klein1699fab2022-09-08 08:46:06 -060095 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 Martin4fbdf202018-08-30 12:02:52 -0600102
Alex Klein1699fab2022-09-08 08:46:06 -0600103 # 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 Martin4fbdf202018-08-30 12:02:52 -0600107
Alex Klein1699fab2022-09-08 08:46:06 -0600108 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 Martin4fbdf202018-08-30 12:02:52 -0600120
Alex Klein1699fab2022-09-08 08:46:06 -0600121 used_refs.add(ref)
122 return ref
Lann Martin4fbdf202018-08-30 12:02:52 -0600123
124
125def _GitPushProjectUpstream(repo_root, project, dry_run):
Alex Klein1699fab2022-09-08 08:46:06 -0600126 """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 Martin4fbdf202018-08-30 12:02:52 -0600133
134
135def main(argv):
Alex Klein1699fab2022-09-08 08:46:06 -0600136 parser = GetParser()
137 options = parser.parse_args(argv)
138 options.Freeze()
Lann Martin4fbdf202018-08-30 12:02:52 -0600139
Alex Klein1699fab2022-09-08 08:46:06 -0600140 snapshot_ref = options.snapshot_ref
141 if snapshot_ref and not snapshot_ref.startswith("refs/"):
142 snapshot_ref = BRANCH_REF_PREFIX + snapshot_ref
Lann Martin4fbdf202018-08-30 12:02:52 -0600143
Alex Klein1699fab2022-09-08 08:46:06 -0600144 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 Martin4fbdf202018-08-30 12:02:52 -0600149
Alex Klein1699fab2022-09-08 08:46:06 -0600150 manifest = repo.Manifest(revision_locked=True)
151 projects = list(manifest.Projects())
Lann Martin4fbdf202018-08-30 12:02:52 -0600152
Alex Klein1699fab2022-09-08 08:46:06 -0600153 # Check if projects need snapshots (in parallel).
154 needs_snapshot_results = parallel.RunTasksInProcessPool(
155 _NeedsSnapshot, [(repo.root, x) for x in projects]
156 )
Lann Martin4fbdf202018-08-30 12:02:52 -0600157
Alex Klein1699fab2022-09-08 08:46:06 -0600158 # 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 Martin4fbdf202018-08-30 12:02:52 -0600163
Alex Klein1699fab2022-09-08 08:46:06 -0600164 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 Martin4fbdf202018-08-30 12:02:52 -0600169
Alex Klein1699fab2022-09-08 08:46:06 -0600170 # 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 Martin4fbdf202018-08-30 12:02:52 -0600190
Alex Klein1699fab2022-09-08 08:46:06 -0600191 dest = options.output_file
192 if dest is None or dest == "-":
193 dest = sys.stdout
Lann Martin4fbdf202018-08-30 12:02:52 -0600194
Alex Klein1699fab2022-09-08 08:46:06 -0600195 manifest.Write(dest)