blob: 3632090b067714130b3d5ccc5a6d8157634c5f62 [file] [log] [blame]
Lann Martin4fbdf202018-08-30 12:02:52 -06001# Copyright 2018 The Chromium OS Authors. All rights reserved.
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
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
Lann Martin4fbdf202018-08-30 12:02:52 -060013import os
14import sys
15
16from chromite.lib import commandline
17from chromite.lib import cros_build_lib
18from chromite.lib import cros_logging as logging
19from chromite.lib import git
20from chromite.lib import parallel
21from chromite.lib import repo_util
22
23
Lann Martin4fbdf202018-08-30 12:02:52 -060024BRANCH_REF_PREFIX = 'refs/heads/'
25
26
27def GetParser():
28 """Creates the argparse parser."""
29 parser = commandline.ArgumentParser(description=__doc__)
30 parser.add_argument('--repo-path', type='path', default='.',
31 help='Path to the repo to snapshot.')
32 parser.add_argument('--snapshot-ref',
33 help='Remote ref to create for projects whose HEAD is '
34 'not reachable from its current upstream branch. '
35 'Projects with multiple checkouts may have a '
36 'unique suffix appended to this ref.')
37 parser.add_argument('--output-file', type='path',
38 help='Path to write the manifest snapshot XML to.')
39 parser.add_argument('--dry-run', action='store_true',
40 help='Do not actually push to remotes.')
41 # This is for limiting network traffic to the git remote(s).
42 parser.add_argument('--jobs', type=int, default=16,
43 help='The number of parallel processes to run for '
44 'git push operations.')
45 return parser
46
47
48def _GetUpstreamBranch(project):
49 """Return a best guess at the project's upstream branch name."""
50 branch = project.upstream
51 if branch and branch.startswith(BRANCH_REF_PREFIX):
52 branch = branch[len(BRANCH_REF_PREFIX):]
53 return branch
54
55
56def _NeedsSnapshot(repo_root, project):
57 """Test if project's revision is reachable from its upstream ref."""
Mike Frysinger6e3f23c2021-02-17 14:06:19 -050058 # Some projects don't have an upstream set. Try 'main' anyway.
59 branch = _GetUpstreamBranch(project) or 'main'
Lann Martin4fbdf202018-08-30 12:02:52 -060060 upstream_ref = 'refs/remotes/%s/%s' % (project.Remote().GitName(), branch)
61 project_path = os.path.join(repo_root, project.Path())
62 try:
63 if git.IsReachable(project_path, project.revision, upstream_ref):
64 return False
65 except cros_build_lib.RunCommandError as e:
66 logging.debug('Reachability check failed: %s', e)
67 logging.info('Project %s revision %s not reachable from upstream %r.',
68 project.name, project.revision, upstream_ref)
69 return True
70
71
72def _MakeUniqueRef(project, base_ref, used_refs):
73 """Return a git ref for project that isn't in used_refs.
74
75 Args:
76 project: The Project object to create a ref for.
77 base_ref: A base ref name; this may be appended to to generate a unique ref.
78 used_refs: A set of ref names to uniquify against. It is updated with the
79 newly generated ref.
80 """
81 ref = base_ref
82
Mike Frysinger6e3f23c2021-02-17 14:06:19 -050083 # If the project upstream is a non-main branch, append it to the ref.
Lann Martin4fbdf202018-08-30 12:02:52 -060084 branch = _GetUpstreamBranch(project)
Mike Frysinger6e3f23c2021-02-17 14:06:19 -050085 if branch and branch != 'main':
Lann Martin4fbdf202018-08-30 12:02:52 -060086 ref = '%s/%s' % (ref, branch)
87
88 if ref in used_refs:
89 # Append incrementing numbers until we find an unused ref.
90 for i in range(1, len(used_refs) + 2):
91 numbered = '%s/%d' % (ref, i)
92 if numbered not in used_refs:
93 ref = numbered
94 break
95 else:
96 raise AssertionError('failed to make unique ref (ref=%s used_refs=%r)' %
97 (ref, used_refs))
98
99 used_refs.add(ref)
100 return ref
101
102
103def _GitPushProjectUpstream(repo_root, project, dry_run):
104 """Push the project revision to its remote upstream."""
105 git.GitPush(
106 os.path.join(repo_root, project.Path()),
107 project.revision,
108 git.RemoteRef(project.Remote().GitName(), project.upstream),
109 dry_run=dry_run)
110
111
112def main(argv):
113 parser = GetParser()
114 options = parser.parse_args(argv)
115 options.Freeze()
116
117 snapshot_ref = options.snapshot_ref
118 if snapshot_ref and not snapshot_ref.startswith('refs/'):
119 snapshot_ref = BRANCH_REF_PREFIX + snapshot_ref
120
121 repo = repo_util.Repository.Find(options.repo_path)
122 if repo is None:
123 cros_build_lib.Die('No repo found in --repo_path %r.', options.repo_path)
124
125 manifest = repo.Manifest(revision_locked=True)
126 projects = list(manifest.Projects())
127
128 # Check if projects need snapshots (in parallel).
129 needs_snapshot_results = parallel.RunTasksInProcessPool(
130 _NeedsSnapshot, [(repo.root, x) for x in projects])
131
132 # Group snapshot-needing projects by project name.
133 snapshot_projects = {}
134 for project, needs_snapshot in zip(projects, needs_snapshot_results):
135 if needs_snapshot:
136 snapshot_projects.setdefault(project.name, []).append(project)
137
138 if snapshot_projects and not snapshot_ref:
139 cros_build_lib.Die('Some project(s) need snapshot refs but no '
140 '--snapshot-ref specified.')
141
142 # Push snapshot refs (in parallel).
143 with parallel.BackgroundTaskRunner(_GitPushProjectUpstream,
144 repo.root, dry_run=options.dry_run,
145 processes=options.jobs) as queue:
146 for projects in snapshot_projects.values():
147 # Since some projects (e.g. chromiumos/third_party/kernel) are checked out
148 # multiple places, we may need to push each checkout to a unique ref.
149 need_unique_refs = len(projects) > 1
150 used_refs = set()
151 for project in projects:
152 if need_unique_refs:
153 ref = _MakeUniqueRef(project, snapshot_ref, used_refs)
154 else:
155 ref = snapshot_ref
156 # Update the upstream ref both for the push and the output XML.
157 project.upstream = ref
158 queue.put([project])
159
160 dest = options.output_file
161 if dest is None or dest == '-':
162 dest = sys.stdout
163
164 manifest.Write(dest)