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