blob: b268007d75982da145c0ceb6d1c1660b545907e7 [file] [log] [blame]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001# Copyright (C) 2008 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
Shawn O. Pearce438ee1c2008-11-03 09:59:36 -080015import errno
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070016import filecmp
Wink Saville4c426ef2015-06-03 08:05:17 -070017import glob
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070018import os
LaMont Jonesd82be3e2022-04-05 19:30:46 +000019import platform
Shawn O. Pearcec325dc32011-10-03 08:30:24 -070020import random
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070021import re
22import shutil
23import stat
Shawn O. Pearce5e7127d2012-08-02 14:57:37 -070024import subprocess
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070025import sys
Julien Campergue335f5ef2013-10-16 11:02:35 +020026import tarfile
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +080027import tempfile
Shawn O. Pearcec325dc32011-10-03 08:30:24 -070028import time
Jason Chang32b59562023-07-14 16:45:35 -070029from typing import NamedTuple, List
Mike Frysingeracf63b22019-06-13 02:24:21 -040030import urllib.parse
Shawn O. Pearcedf5ee522011-10-11 14:05:21 -070031
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070032from color import Coloring
LaMont Jones0de4fc32022-04-21 17:18:35 +000033import fetch
Dave Borowitzb42b4742012-10-31 12:27:27 -070034from git_command import GitCommand, git_require
Gavin Makea2e3302023-03-11 06:46:20 +000035from git_config import (
36 GitConfig,
37 IsId,
38 GetSchemeFromUrl,
39 GetUrlCookieFile,
40 ID_RE,
41)
LaMont Jonesff6b1da2022-06-01 21:03:34 +000042import git_superproject
LaMont Jones55ee3042022-04-06 17:10:21 +000043from git_trace2_event_log import EventLog
Jason Chang32b59562023-07-14 16:45:35 -070044from error import (
45 GitError,
46 UploadError,
47 DownloadError,
48 RepoError,
49)
Mike Frysingere6a202f2019-08-02 15:57:57 -040050from error import ManifestInvalidRevisionError, ManifestInvalidPathError
LaMont Jones409407a2022-04-05 21:21:56 +000051from error import NoManifestException, ManifestParseError
Renaud Paquayd5cec5e2016-11-01 11:24:03 -070052import platform_utils
Mike Frysinger70d861f2019-08-26 15:22:36 -040053import progress
Joanna Wanga6c52f52022-11-03 16:51:19 -040054from repo_trace import Trace
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070055
Mike Frysinger21b7fbe2020-02-26 23:53:36 -050056from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M, R_WORKTREE_M
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070057
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -070058
LaMont Jones1eddca82022-09-01 15:15:04 +000059class SyncNetworkHalfResult(NamedTuple):
Gavin Makea2e3302023-03-11 06:46:20 +000060 """Sync_NetworkHalf return value."""
61
Gavin Makea2e3302023-03-11 06:46:20 +000062 # Did we query the remote? False when optimized_fetch is True and we have
63 # the commit already present.
64 remote_fetched: bool
Jason Chang32b59562023-07-14 16:45:35 -070065 # Error from SyncNetworkHalf
66 error: Exception = None
67
68 @property
69 def success(self) -> bool:
70 return not self.error
71
72
73class SyncNetworkHalfError(RepoError):
74 """Failure trying to sync."""
75
76
77class DeleteWorktreeError(RepoError):
78 """Failure to delete worktree."""
79
80 def __init__(
81 self, *args, aggregate_errors: List[Exception] = None, **kwargs
82 ) -> None:
83 super().__init__(*args, **kwargs)
84 self.aggregate_errors = aggregate_errors or []
85
86
87class DeleteDirtyWorktreeError(DeleteWorktreeError):
88 """Failure to delete worktree due to uncommitted changes."""
LaMont Jones1eddca82022-09-01 15:15:04 +000089
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +010090
George Engelbrecht9bc283e2020-04-02 12:36:09 -060091# Maximum sleep time allowed during retries.
92MAXIMUM_RETRY_SLEEP_SEC = 3600.0
93# +-10% random jitter is added to each Fetches retry sleep duration.
94RETRY_JITTER_PERCENT = 0.1
95
LaMont Jonesfa8d9392022-11-02 22:01:29 +000096# Whether to use alternates. Switching back and forth is *NOT* supported.
Mike Frysinger1d00a7e2021-12-21 00:40:31 -050097# TODO(vapier): Remove knob once behavior is verified.
Gavin Makea2e3302023-03-11 06:46:20 +000098_ALTERNATES = os.environ.get("REPO_USE_ALTERNATES") == "1"
George Engelbrecht9bc283e2020-04-02 12:36:09 -060099
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +0100100
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700101def _lwrite(path, content):
Gavin Makea2e3302023-03-11 06:46:20 +0000102 lock = "%s.lock" % path
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700103
Gavin Makea2e3302023-03-11 06:46:20 +0000104 # Maintain Unix line endings on all OS's to match git behavior.
105 with open(lock, "w", newline="\n") as fd:
106 fd.write(content)
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700107
Gavin Makea2e3302023-03-11 06:46:20 +0000108 try:
109 platform_utils.rename(lock, path)
110 except OSError:
111 platform_utils.remove(lock)
112 raise
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700113
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700114
Shawn O. Pearce48244782009-04-16 08:25:57 -0700115def _error(fmt, *args):
Gavin Makea2e3302023-03-11 06:46:20 +0000116 msg = fmt % args
117 print("error: %s" % msg, file=sys.stderr)
Shawn O. Pearce48244782009-04-16 08:25:57 -0700118
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700119
David Pursehousef33929d2015-08-24 14:39:14 +0900120def _warn(fmt, *args):
Gavin Makea2e3302023-03-11 06:46:20 +0000121 msg = fmt % args
122 print("warn: %s" % msg, file=sys.stderr)
David Pursehousef33929d2015-08-24 14:39:14 +0900123
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700124
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700125def not_rev(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000126 return "^" + r
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700127
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700128
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800129def sq(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000130 return "'" + r.replace("'", "'''") + "'"
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -0800131
David Pursehouse819827a2020-02-12 15:20:19 +0900132
Jonathan Nieder93719792015-03-17 11:29:58 -0700133_project_hook_list = None
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700134
135
Jonathan Nieder93719792015-03-17 11:29:58 -0700136def _ProjectHooks():
Gavin Makea2e3302023-03-11 06:46:20 +0000137 """List the hooks present in the 'hooks' directory.
Jonathan Nieder93719792015-03-17 11:29:58 -0700138
Gavin Makea2e3302023-03-11 06:46:20 +0000139 These hooks are project hooks and are copied to the '.git/hooks' directory
140 of all subprojects.
Jonathan Nieder93719792015-03-17 11:29:58 -0700141
Gavin Makea2e3302023-03-11 06:46:20 +0000142 This function caches the list of hooks (based on the contents of the
143 'repo/hooks' directory) on the first call.
Jonathan Nieder93719792015-03-17 11:29:58 -0700144
Gavin Makea2e3302023-03-11 06:46:20 +0000145 Returns:
146 A list of absolute paths to all of the files in the hooks directory.
147 """
148 global _project_hook_list
149 if _project_hook_list is None:
150 d = platform_utils.realpath(os.path.abspath(os.path.dirname(__file__)))
151 d = os.path.join(d, "hooks")
152 _project_hook_list = [
153 os.path.join(d, x) for x in platform_utils.listdir(d)
154 ]
155 return _project_hook_list
Jonathan Nieder93719792015-03-17 11:29:58 -0700156
157
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700158class DownloadedChange(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000159 _commit_cache = None
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700160
Gavin Makea2e3302023-03-11 06:46:20 +0000161 def __init__(self, project, base, change_id, ps_id, commit):
162 self.project = project
163 self.base = base
164 self.change_id = change_id
165 self.ps_id = ps_id
166 self.commit = commit
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700167
Gavin Makea2e3302023-03-11 06:46:20 +0000168 @property
169 def commits(self):
170 if self._commit_cache is None:
171 self._commit_cache = self.project.bare_git.rev_list(
172 "--abbrev=8",
173 "--abbrev-commit",
174 "--pretty=oneline",
175 "--reverse",
176 "--date-order",
177 not_rev(self.base),
178 self.commit,
179 "--",
180 )
181 return self._commit_cache
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700182
183
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700184class ReviewableBranch(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000185 _commit_cache = None
186 _base_exists = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700187
Gavin Makea2e3302023-03-11 06:46:20 +0000188 def __init__(self, project, branch, base):
189 self.project = project
190 self.branch = branch
191 self.base = base
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700192
Gavin Makea2e3302023-03-11 06:46:20 +0000193 @property
194 def name(self):
195 return self.branch.name
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700196
Gavin Makea2e3302023-03-11 06:46:20 +0000197 @property
198 def commits(self):
199 if self._commit_cache is None:
200 args = (
201 "--abbrev=8",
202 "--abbrev-commit",
203 "--pretty=oneline",
204 "--reverse",
205 "--date-order",
206 not_rev(self.base),
207 R_HEADS + self.name,
208 "--",
209 )
210 try:
211 self._commit_cache = self.project.bare_git.rev_list(*args)
212 except GitError:
213 # We weren't able to probe the commits for this branch. Was it
214 # tracking a branch that no longer exists? If so, return no
215 # commits. Otherwise, rethrow the error as we don't know what's
216 # going on.
217 if self.base_exists:
218 raise
Mike Frysinger6da17752019-09-11 18:43:17 -0400219
Gavin Makea2e3302023-03-11 06:46:20 +0000220 self._commit_cache = []
Mike Frysinger6da17752019-09-11 18:43:17 -0400221
Gavin Makea2e3302023-03-11 06:46:20 +0000222 return self._commit_cache
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700223
Gavin Makea2e3302023-03-11 06:46:20 +0000224 @property
225 def unabbrev_commits(self):
226 r = dict()
227 for commit in self.project.bare_git.rev_list(
228 not_rev(self.base), R_HEADS + self.name, "--"
229 ):
230 r[commit[0:8]] = commit
231 return r
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800232
Gavin Makea2e3302023-03-11 06:46:20 +0000233 @property
234 def date(self):
235 return self.project.bare_git.log(
236 "--pretty=format:%cd", "-n", "1", R_HEADS + self.name, "--"
237 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700238
Gavin Makea2e3302023-03-11 06:46:20 +0000239 @property
240 def base_exists(self):
241 """Whether the branch we're tracking exists.
Mike Frysinger6da17752019-09-11 18:43:17 -0400242
Gavin Makea2e3302023-03-11 06:46:20 +0000243 Normally it should, but sometimes branches we track can get deleted.
244 """
245 if self._base_exists is None:
246 try:
247 self.project.bare_git.rev_parse("--verify", not_rev(self.base))
248 # If we're still here, the base branch exists.
249 self._base_exists = True
250 except GitError:
251 # If we failed to verify, the base branch doesn't exist.
252 self._base_exists = False
Mike Frysinger6da17752019-09-11 18:43:17 -0400253
Gavin Makea2e3302023-03-11 06:46:20 +0000254 return self._base_exists
Mike Frysinger6da17752019-09-11 18:43:17 -0400255
Gavin Makea2e3302023-03-11 06:46:20 +0000256 def UploadForReview(
257 self,
258 people,
259 dryrun=False,
260 auto_topic=False,
261 hashtags=(),
262 labels=(),
263 private=False,
264 notify=None,
265 wip=False,
266 ready=False,
267 dest_branch=None,
268 validate_certs=True,
269 push_options=None,
270 ):
271 self.project.UploadForReview(
272 branch=self.name,
273 people=people,
274 dryrun=dryrun,
275 auto_topic=auto_topic,
276 hashtags=hashtags,
277 labels=labels,
278 private=private,
279 notify=notify,
280 wip=wip,
281 ready=ready,
282 dest_branch=dest_branch,
283 validate_certs=validate_certs,
284 push_options=push_options,
285 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700286
Gavin Makea2e3302023-03-11 06:46:20 +0000287 def GetPublishedRefs(self):
288 refs = {}
289 output = self.project.bare_git.ls_remote(
290 self.branch.remote.SshReviewUrl(self.project.UserEmail),
291 "refs/changes/*",
292 )
293 for line in output.split("\n"):
294 try:
295 (sha, ref) = line.split()
296 refs[sha] = ref
297 except ValueError:
298 pass
Ficus Kirkpatrickbc7ef672009-05-04 12:45:11 -0700299
Gavin Makea2e3302023-03-11 06:46:20 +0000300 return refs
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700301
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700302
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700303class StatusColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000304 def __init__(self, config):
305 super().__init__(config, "status")
306 self.project = self.printer("header", attr="bold")
307 self.branch = self.printer("header", attr="bold")
308 self.nobranch = self.printer("nobranch", fg="red")
309 self.important = self.printer("important", fg="red")
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700310
Gavin Makea2e3302023-03-11 06:46:20 +0000311 self.added = self.printer("added", fg="green")
312 self.changed = self.printer("changed", fg="red")
313 self.untracked = self.printer("untracked", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700314
315
316class DiffColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000317 def __init__(self, config):
318 super().__init__(config, "diff")
319 self.project = self.printer("header", attr="bold")
320 self.fail = self.printer("fail", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700321
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700322
Jack Neus6ea0cae2021-07-20 20:52:33 +0000323class Annotation(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000324 def __init__(self, name, value, keep):
325 self.name = name
326 self.value = value
327 self.keep = keep
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700328
Gavin Makea2e3302023-03-11 06:46:20 +0000329 def __eq__(self, other):
330 if not isinstance(other, Annotation):
331 return False
332 return self.__dict__ == other.__dict__
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700333
Gavin Makea2e3302023-03-11 06:46:20 +0000334 def __lt__(self, other):
335 # This exists just so that lists of Annotation objects can be sorted,
336 # for use in comparisons.
337 if not isinstance(other, Annotation):
338 raise ValueError("comparison is not between two Annotation objects")
339 if self.name == other.name:
340 if self.value == other.value:
341 return self.keep < other.keep
342 return self.value < other.value
343 return self.name < other.name
Jack Neus6ea0cae2021-07-20 20:52:33 +0000344
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700345
Mike Frysingere6a202f2019-08-02 15:57:57 -0400346def _SafeExpandPath(base, subpath, skipfinal=False):
Gavin Makea2e3302023-03-11 06:46:20 +0000347 """Make sure |subpath| is completely safe under |base|.
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700348
Gavin Makea2e3302023-03-11 06:46:20 +0000349 We make sure no intermediate symlinks are traversed, and that the final path
350 is not a special file (e.g. not a socket or fifo).
Mike Frysingere6a202f2019-08-02 15:57:57 -0400351
Gavin Makea2e3302023-03-11 06:46:20 +0000352 NB: We rely on a number of paths already being filtered out while parsing
353 the manifest. See the validation logic in manifest_xml.py for more details.
354 """
355 # Split up the path by its components. We can't use os.path.sep exclusively
356 # as some platforms (like Windows) will convert / to \ and that bypasses all
357 # our constructed logic here. Especially since manifest authors only use
358 # / in their paths.
359 resep = re.compile(r"[/%s]" % re.escape(os.path.sep))
360 components = resep.split(subpath)
361 if skipfinal:
362 # Whether the caller handles the final component itself.
363 finalpart = components.pop()
Mike Frysingere6a202f2019-08-02 15:57:57 -0400364
Gavin Makea2e3302023-03-11 06:46:20 +0000365 path = base
366 for part in components:
367 if part in {".", ".."}:
368 raise ManifestInvalidPathError(
369 '%s: "%s" not allowed in paths' % (subpath, part)
370 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400371
Gavin Makea2e3302023-03-11 06:46:20 +0000372 path = os.path.join(path, part)
373 if platform_utils.islink(path):
374 raise ManifestInvalidPathError(
375 "%s: traversing symlinks not allow" % (path,)
376 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400377
Gavin Makea2e3302023-03-11 06:46:20 +0000378 if os.path.exists(path):
379 if not os.path.isfile(path) and not platform_utils.isdir(path):
380 raise ManifestInvalidPathError(
381 "%s: only regular files & directories allowed" % (path,)
382 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400383
Gavin Makea2e3302023-03-11 06:46:20 +0000384 if skipfinal:
385 path = os.path.join(path, finalpart)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400386
Gavin Makea2e3302023-03-11 06:46:20 +0000387 return path
Mike Frysingere6a202f2019-08-02 15:57:57 -0400388
389
390class _CopyFile(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000391 """Container for <copyfile> manifest element."""
Mike Frysingere6a202f2019-08-02 15:57:57 -0400392
Gavin Makea2e3302023-03-11 06:46:20 +0000393 def __init__(self, git_worktree, src, topdir, dest):
394 """Register a <copyfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400395
Gavin Makea2e3302023-03-11 06:46:20 +0000396 Args:
397 git_worktree: Absolute path to the git project checkout.
398 src: Relative path under |git_worktree| of file to read.
399 topdir: Absolute path to the top of the repo client checkout.
400 dest: Relative path under |topdir| of file to write.
401 """
402 self.git_worktree = git_worktree
403 self.topdir = topdir
404 self.src = src
405 self.dest = dest
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700406
Gavin Makea2e3302023-03-11 06:46:20 +0000407 def _Copy(self):
408 src = _SafeExpandPath(self.git_worktree, self.src)
409 dest = _SafeExpandPath(self.topdir, self.dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400410
Gavin Makea2e3302023-03-11 06:46:20 +0000411 if platform_utils.isdir(src):
412 raise ManifestInvalidPathError(
413 "%s: copying from directory not supported" % (self.src,)
414 )
415 if platform_utils.isdir(dest):
416 raise ManifestInvalidPathError(
417 "%s: copying to directory not allowed" % (self.dest,)
418 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400419
Gavin Makea2e3302023-03-11 06:46:20 +0000420 # Copy file if it does not exist or is out of date.
421 if not os.path.exists(dest) or not filecmp.cmp(src, dest):
422 try:
423 # Remove existing file first, since it might be read-only.
424 if os.path.exists(dest):
425 platform_utils.remove(dest)
426 else:
427 dest_dir = os.path.dirname(dest)
428 if not platform_utils.isdir(dest_dir):
429 os.makedirs(dest_dir)
430 shutil.copy(src, dest)
431 # Make the file read-only.
432 mode = os.stat(dest)[stat.ST_MODE]
433 mode = mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
434 os.chmod(dest, mode)
435 except IOError:
436 _error("Cannot copy file %s to %s", src, dest)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700437
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700438
Anthony King7bdac712014-07-16 12:56:40 +0100439class _LinkFile(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000440 """Container for <linkfile> manifest element."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700441
Gavin Makea2e3302023-03-11 06:46:20 +0000442 def __init__(self, git_worktree, src, topdir, dest):
443 """Register a <linkfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400444
Gavin Makea2e3302023-03-11 06:46:20 +0000445 Args:
446 git_worktree: Absolute path to the git project checkout.
447 src: Target of symlink relative to path under |git_worktree|.
448 topdir: Absolute path to the top of the repo client checkout.
449 dest: Relative path under |topdir| of symlink to create.
450 """
451 self.git_worktree = git_worktree
452 self.topdir = topdir
453 self.src = src
454 self.dest = dest
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500455
Gavin Makea2e3302023-03-11 06:46:20 +0000456 def __linkIt(self, relSrc, absDest):
457 # Link file if it does not exist or is out of date.
458 if not platform_utils.islink(absDest) or (
459 platform_utils.readlink(absDest) != relSrc
460 ):
461 try:
462 # Remove existing file first, since it might be read-only.
463 if os.path.lexists(absDest):
464 platform_utils.remove(absDest)
465 else:
466 dest_dir = os.path.dirname(absDest)
467 if not platform_utils.isdir(dest_dir):
468 os.makedirs(dest_dir)
469 platform_utils.symlink(relSrc, absDest)
470 except IOError:
471 _error("Cannot link file %s to %s", relSrc, absDest)
472
473 def _Link(self):
474 """Link the self.src & self.dest paths.
475
476 Handles wild cards on the src linking all of the files in the source in
477 to the destination directory.
478 """
479 # Some people use src="." to create stable links to projects. Let's
480 # allow that but reject all other uses of "." to keep things simple.
481 if self.src == ".":
482 src = self.git_worktree
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500483 else:
Gavin Makea2e3302023-03-11 06:46:20 +0000484 src = _SafeExpandPath(self.git_worktree, self.src)
Wink Saville4c426ef2015-06-03 08:05:17 -0700485
Gavin Makea2e3302023-03-11 06:46:20 +0000486 if not glob.has_magic(src):
487 # Entity does not contain a wild card so just a simple one to one
488 # link operation.
489 dest = _SafeExpandPath(self.topdir, self.dest, skipfinal=True)
490 # dest & src are absolute paths at this point. Make sure the target
491 # of the symlink is relative in the context of the repo client
492 # checkout.
493 relpath = os.path.relpath(src, os.path.dirname(dest))
494 self.__linkIt(relpath, dest)
495 else:
496 dest = _SafeExpandPath(self.topdir, self.dest)
497 # Entity contains a wild card.
498 if os.path.exists(dest) and not platform_utils.isdir(dest):
499 _error(
500 "Link error: src with wildcard, %s must be a directory",
501 dest,
502 )
503 else:
504 for absSrcFile in glob.glob(src):
505 # Create a releative path from source dir to destination
506 # dir.
507 absSrcDir = os.path.dirname(absSrcFile)
508 relSrcDir = os.path.relpath(absSrcDir, dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400509
Gavin Makea2e3302023-03-11 06:46:20 +0000510 # Get the source file name.
511 srcFile = os.path.basename(absSrcFile)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400512
Gavin Makea2e3302023-03-11 06:46:20 +0000513 # Now form the final full paths to srcFile. They will be
514 # absolute for the desintaiton and relative for the source.
515 absDest = os.path.join(dest, srcFile)
516 relSrc = os.path.join(relSrcDir, srcFile)
517 self.__linkIt(relSrc, absDest)
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500518
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700519
Shawn O. Pearced1f70d92009-05-19 14:58:02 -0700520class RemoteSpec(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000521 def __init__(
522 self,
523 name,
524 url=None,
525 pushUrl=None,
526 review=None,
527 revision=None,
528 orig_name=None,
529 fetchUrl=None,
530 ):
531 self.name = name
532 self.url = url
533 self.pushUrl = pushUrl
534 self.review = review
535 self.revision = revision
536 self.orig_name = orig_name
537 self.fetchUrl = fetchUrl
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700538
Ian Kasprzak0286e312021-02-05 10:06:18 -0800539
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700540class Project(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000541 # These objects can be shared between several working trees.
542 @property
543 def shareable_dirs(self):
544 """Return the shareable directories"""
545 if self.UseAlternates:
546 return ["hooks", "rr-cache"]
547 else:
548 return ["hooks", "objects", "rr-cache"]
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700549
Gavin Makea2e3302023-03-11 06:46:20 +0000550 def __init__(
551 self,
552 manifest,
553 name,
554 remote,
555 gitdir,
556 objdir,
557 worktree,
558 relpath,
559 revisionExpr,
560 revisionId,
561 rebase=True,
562 groups=None,
563 sync_c=False,
564 sync_s=False,
565 sync_tags=True,
566 clone_depth=None,
567 upstream=None,
568 parent=None,
569 use_git_worktrees=False,
570 is_derived=False,
571 dest_branch=None,
572 optimized_fetch=False,
573 retry_fetches=0,
574 old_revision=None,
575 ):
576 """Init a Project object.
Doug Anderson3ba5f952011-04-07 12:51:04 -0700577
578 Args:
Gavin Makea2e3302023-03-11 06:46:20 +0000579 manifest: The XmlManifest object.
580 name: The `name` attribute of manifest.xml's project element.
581 remote: RemoteSpec object specifying its remote's properties.
582 gitdir: Absolute path of git directory.
583 objdir: Absolute path of directory to store git objects.
584 worktree: Absolute path of git working tree.
585 relpath: Relative path of git working tree to repo's top directory.
586 revisionExpr: The `revision` attribute of manifest.xml's project
587 element.
588 revisionId: git commit id for checking out.
589 rebase: The `rebase` attribute of manifest.xml's project element.
590 groups: The `groups` attribute of manifest.xml's project element.
591 sync_c: The `sync-c` attribute of manifest.xml's project element.
592 sync_s: The `sync-s` attribute of manifest.xml's project element.
593 sync_tags: The `sync-tags` attribute of manifest.xml's project
594 element.
595 upstream: The `upstream` attribute of manifest.xml's project
596 element.
597 parent: The parent Project object.
598 use_git_worktrees: Whether to use `git worktree` for this project.
599 is_derived: False if the project was explicitly defined in the
600 manifest; True if the project is a discovered submodule.
601 dest_branch: The branch to which to push changes for review by
602 default.
603 optimized_fetch: If True, when a project is set to a sha1 revision,
604 only fetch from the remote if the sha1 is not present locally.
605 retry_fetches: Retry remote fetches n times upon receiving transient
606 error with exponential backoff and jitter.
607 old_revision: saved git commit id for open GITC projects.
608 """
609 self.client = self.manifest = manifest
610 self.name = name
611 self.remote = remote
612 self.UpdatePaths(relpath, worktree, gitdir, objdir)
613 self.SetRevision(revisionExpr, revisionId=revisionId)
614
615 self.rebase = rebase
616 self.groups = groups
617 self.sync_c = sync_c
618 self.sync_s = sync_s
619 self.sync_tags = sync_tags
620 self.clone_depth = clone_depth
621 self.upstream = upstream
622 self.parent = parent
623 # NB: Do not use this setting in __init__ to change behavior so that the
624 # manifest.git checkout can inspect & change it after instantiating.
625 # See the XmlManifest init code for more info.
626 self.use_git_worktrees = use_git_worktrees
627 self.is_derived = is_derived
628 self.optimized_fetch = optimized_fetch
629 self.retry_fetches = max(0, retry_fetches)
630 self.subprojects = []
631
632 self.snapshots = {}
633 self.copyfiles = []
634 self.linkfiles = []
635 self.annotations = []
636 self.dest_branch = dest_branch
637 self.old_revision = old_revision
638
639 # This will be filled in if a project is later identified to be the
640 # project containing repo hooks.
641 self.enabled_repo_hooks = []
642
643 def RelPath(self, local=True):
644 """Return the path for the project relative to a manifest.
645
646 Args:
647 local: a boolean, if True, the path is relative to the local
648 (sub)manifest. If false, the path is relative to the outermost
649 manifest.
650 """
651 if local:
652 return self.relpath
653 return os.path.join(self.manifest.path_prefix, self.relpath)
654
655 def SetRevision(self, revisionExpr, revisionId=None):
656 """Set revisionId based on revision expression and id"""
657 self.revisionExpr = revisionExpr
658 if revisionId is None and revisionExpr and IsId(revisionExpr):
659 self.revisionId = self.revisionExpr
660 else:
661 self.revisionId = revisionId
662
663 def UpdatePaths(self, relpath, worktree, gitdir, objdir):
664 """Update paths used by this project"""
665 self.gitdir = gitdir.replace("\\", "/")
666 self.objdir = objdir.replace("\\", "/")
667 if worktree:
668 self.worktree = os.path.normpath(worktree).replace("\\", "/")
669 else:
670 self.worktree = None
671 self.relpath = relpath
672
673 self.config = GitConfig.ForRepository(
674 gitdir=self.gitdir, defaults=self.manifest.globalConfig
675 )
676
677 if self.worktree:
678 self.work_git = self._GitGetByExec(
679 self, bare=False, gitdir=self.gitdir
680 )
681 else:
682 self.work_git = None
683 self.bare_git = self._GitGetByExec(self, bare=True, gitdir=self.gitdir)
684 self.bare_ref = GitRefs(self.gitdir)
685 self.bare_objdir = self._GitGetByExec(
686 self, bare=True, gitdir=self.objdir
687 )
688
689 @property
690 def UseAlternates(self):
691 """Whether git alternates are in use.
692
693 This will be removed once migration to alternates is complete.
694 """
695 return _ALTERNATES or self.manifest.is_multimanifest
696
697 @property
698 def Derived(self):
699 return self.is_derived
700
701 @property
702 def Exists(self):
703 return platform_utils.isdir(self.gitdir) and platform_utils.isdir(
704 self.objdir
705 )
706
707 @property
708 def CurrentBranch(self):
709 """Obtain the name of the currently checked out branch.
710
711 The branch name omits the 'refs/heads/' prefix.
712 None is returned if the project is on a detached HEAD, or if the
713 work_git is otheriwse inaccessible (e.g. an incomplete sync).
714 """
715 try:
716 b = self.work_git.GetHead()
717 except NoManifestException:
718 # If the local checkout is in a bad state, don't barf. Let the
719 # callers process this like the head is unreadable.
720 return None
721 if b.startswith(R_HEADS):
722 return b[len(R_HEADS) :]
723 return None
724
725 def IsRebaseInProgress(self):
726 return (
727 os.path.exists(self.work_git.GetDotgitPath("rebase-apply"))
728 or os.path.exists(self.work_git.GetDotgitPath("rebase-merge"))
729 or os.path.exists(os.path.join(self.worktree, ".dotest"))
730 )
731
732 def IsDirty(self, consider_untracked=True):
733 """Is the working directory modified in some way?"""
734 self.work_git.update_index(
735 "-q", "--unmerged", "--ignore-missing", "--refresh"
736 )
737 if self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD):
738 return True
739 if self.work_git.DiffZ("diff-files"):
740 return True
741 if consider_untracked and self.UntrackedFiles():
742 return True
743 return False
744
745 _userident_name = None
746 _userident_email = None
747
748 @property
749 def UserName(self):
750 """Obtain the user's personal name."""
751 if self._userident_name is None:
752 self._LoadUserIdentity()
753 return self._userident_name
754
755 @property
756 def UserEmail(self):
757 """Obtain the user's email address. This is very likely
758 to be their Gerrit login.
759 """
760 if self._userident_email is None:
761 self._LoadUserIdentity()
762 return self._userident_email
763
764 def _LoadUserIdentity(self):
765 u = self.bare_git.var("GIT_COMMITTER_IDENT")
766 m = re.compile("^(.*) <([^>]*)> ").match(u)
767 if m:
768 self._userident_name = m.group(1)
769 self._userident_email = m.group(2)
770 else:
771 self._userident_name = ""
772 self._userident_email = ""
773
774 def GetRemote(self, name=None):
775 """Get the configuration for a single remote.
776
777 Defaults to the current project's remote.
778 """
779 if name is None:
780 name = self.remote.name
781 return self.config.GetRemote(name)
782
783 def GetBranch(self, name):
784 """Get the configuration for a single branch."""
785 return self.config.GetBranch(name)
786
787 def GetBranches(self):
788 """Get all existing local branches."""
789 current = self.CurrentBranch
790 all_refs = self._allrefs
791 heads = {}
792
793 for name, ref_id in all_refs.items():
794 if name.startswith(R_HEADS):
795 name = name[len(R_HEADS) :]
796 b = self.GetBranch(name)
797 b.current = name == current
798 b.published = None
799 b.revision = ref_id
800 heads[name] = b
801
802 for name, ref_id in all_refs.items():
803 if name.startswith(R_PUB):
804 name = name[len(R_PUB) :]
805 b = heads.get(name)
806 if b:
807 b.published = ref_id
808
809 return heads
810
811 def MatchesGroups(self, manifest_groups):
812 """Returns true if the manifest groups specified at init should cause
813 this project to be synced.
814 Prefixing a manifest group with "-" inverts the meaning of a group.
815 All projects are implicitly labelled with "all".
816
817 labels are resolved in order. In the example case of
818 project_groups: "all,group1,group2"
819 manifest_groups: "-group1,group2"
820 the project will be matched.
821
822 The special manifest group "default" will match any project that
823 does not have the special project group "notdefault"
824 """
825 default_groups = self.manifest.default_groups or ["default"]
826 expanded_manifest_groups = manifest_groups or default_groups
827 expanded_project_groups = ["all"] + (self.groups or [])
828 if "notdefault" not in expanded_project_groups:
829 expanded_project_groups += ["default"]
830
831 matched = False
832 for group in expanded_manifest_groups:
833 if group.startswith("-") and group[1:] in expanded_project_groups:
834 matched = False
835 elif group in expanded_project_groups:
836 matched = True
837
838 return matched
839
840 def UncommitedFiles(self, get_all=True):
841 """Returns a list of strings, uncommitted files in the git tree.
842
843 Args:
844 get_all: a boolean, if True - get information about all different
845 uncommitted files. If False - return as soon as any kind of
846 uncommitted files is detected.
847 """
848 details = []
849 self.work_git.update_index(
850 "-q", "--unmerged", "--ignore-missing", "--refresh"
851 )
852 if self.IsRebaseInProgress():
853 details.append("rebase in progress")
854 if not get_all:
855 return details
856
857 changes = self.work_git.DiffZ("diff-index", "--cached", HEAD).keys()
858 if changes:
859 details.extend(changes)
860 if not get_all:
861 return details
862
863 changes = self.work_git.DiffZ("diff-files").keys()
864 if changes:
865 details.extend(changes)
866 if not get_all:
867 return details
868
869 changes = self.UntrackedFiles()
870 if changes:
871 details.extend(changes)
872
873 return details
874
875 def UntrackedFiles(self):
876 """Returns a list of strings, untracked files in the git tree."""
877 return self.work_git.LsOthers()
878
879 def HasChanges(self):
880 """Returns true if there are uncommitted changes."""
881 return bool(self.UncommitedFiles(get_all=False))
882
883 def PrintWorkTreeStatus(self, output_redir=None, quiet=False, local=False):
884 """Prints the status of the repository to stdout.
885
886 Args:
887 output_redir: If specified, redirect the output to this object.
888 quiet: If True then only print the project name. Do not print
889 the modified files, branch name, etc.
890 local: a boolean, if True, the path is relative to the local
891 (sub)manifest. If false, the path is relative to the outermost
892 manifest.
893 """
894 if not platform_utils.isdir(self.worktree):
895 if output_redir is None:
896 output_redir = sys.stdout
897 print(file=output_redir)
898 print("project %s/" % self.RelPath(local), file=output_redir)
899 print(' missing (run "repo sync")', file=output_redir)
900 return
901
902 self.work_git.update_index(
903 "-q", "--unmerged", "--ignore-missing", "--refresh"
904 )
905 rb = self.IsRebaseInProgress()
906 di = self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD)
907 df = self.work_git.DiffZ("diff-files")
908 do = self.work_git.LsOthers()
909 if not rb and not di and not df and not do and not self.CurrentBranch:
910 return "CLEAN"
911
912 out = StatusColoring(self.config)
913 if output_redir is not None:
914 out.redirect(output_redir)
915 out.project("project %-40s", self.RelPath(local) + "/ ")
916
917 if quiet:
918 out.nl()
919 return "DIRTY"
920
921 branch = self.CurrentBranch
922 if branch is None:
923 out.nobranch("(*** NO BRANCH ***)")
924 else:
925 out.branch("branch %s", branch)
926 out.nl()
927
928 if rb:
929 out.important("prior sync failed; rebase still in progress")
930 out.nl()
931
932 paths = list()
933 paths.extend(di.keys())
934 paths.extend(df.keys())
935 paths.extend(do)
936
937 for p in sorted(set(paths)):
938 try:
939 i = di[p]
940 except KeyError:
941 i = None
942
943 try:
944 f = df[p]
945 except KeyError:
946 f = None
947
948 if i:
949 i_status = i.status.upper()
950 else:
951 i_status = "-"
952
953 if f:
954 f_status = f.status.lower()
955 else:
956 f_status = "-"
957
958 if i and i.src_path:
959 line = " %s%s\t%s => %s (%s%%)" % (
960 i_status,
961 f_status,
962 i.src_path,
963 p,
964 i.level,
965 )
966 else:
967 line = " %s%s\t%s" % (i_status, f_status, p)
968
969 if i and not f:
970 out.added("%s", line)
971 elif (i and f) or (not i and f):
972 out.changed("%s", line)
973 elif not i and not f:
974 out.untracked("%s", line)
975 else:
976 out.write("%s", line)
977 out.nl()
978
979 return "DIRTY"
980
981 def PrintWorkTreeDiff(
982 self, absolute_paths=False, output_redir=None, local=False
983 ):
984 """Prints the status of the repository to stdout."""
985 out = DiffColoring(self.config)
986 if output_redir:
987 out.redirect(output_redir)
988 cmd = ["diff"]
989 if out.is_on:
990 cmd.append("--color")
991 cmd.append(HEAD)
992 if absolute_paths:
993 cmd.append("--src-prefix=a/%s/" % self.RelPath(local))
994 cmd.append("--dst-prefix=b/%s/" % self.RelPath(local))
995 cmd.append("--")
996 try:
997 p = GitCommand(self, cmd, capture_stdout=True, capture_stderr=True)
998 p.Wait()
999 except GitError as e:
1000 out.nl()
1001 out.project("project %s/" % self.RelPath(local))
1002 out.nl()
1003 out.fail("%s", str(e))
1004 out.nl()
1005 return False
1006 if p.stdout:
1007 out.nl()
1008 out.project("project %s/" % self.RelPath(local))
1009 out.nl()
1010 out.write("%s", p.stdout)
1011 return p.Wait() == 0
1012
1013 def WasPublished(self, branch, all_refs=None):
1014 """Was the branch published (uploaded) for code review?
1015 If so, returns the SHA-1 hash of the last published
1016 state for the branch.
1017 """
1018 key = R_PUB + branch
1019 if all_refs is None:
1020 try:
1021 return self.bare_git.rev_parse(key)
1022 except GitError:
1023 return None
1024 else:
1025 try:
1026 return all_refs[key]
1027 except KeyError:
1028 return None
1029
1030 def CleanPublishedCache(self, all_refs=None):
1031 """Prunes any stale published refs."""
1032 if all_refs is None:
1033 all_refs = self._allrefs
1034 heads = set()
1035 canrm = {}
1036 for name, ref_id in all_refs.items():
1037 if name.startswith(R_HEADS):
1038 heads.add(name)
1039 elif name.startswith(R_PUB):
1040 canrm[name] = ref_id
1041
1042 for name, ref_id in canrm.items():
1043 n = name[len(R_PUB) :]
1044 if R_HEADS + n not in heads:
1045 self.bare_git.DeleteRef(name, ref_id)
1046
1047 def GetUploadableBranches(self, selected_branch=None):
1048 """List any branches which can be uploaded for review."""
1049 heads = {}
1050 pubed = {}
1051
1052 for name, ref_id in self._allrefs.items():
1053 if name.startswith(R_HEADS):
1054 heads[name[len(R_HEADS) :]] = ref_id
1055 elif name.startswith(R_PUB):
1056 pubed[name[len(R_PUB) :]] = ref_id
1057
1058 ready = []
1059 for branch, ref_id in heads.items():
1060 if branch in pubed and pubed[branch] == ref_id:
1061 continue
1062 if selected_branch and branch != selected_branch:
1063 continue
1064
1065 rb = self.GetUploadableBranch(branch)
1066 if rb:
1067 ready.append(rb)
1068 return ready
1069
1070 def GetUploadableBranch(self, branch_name):
1071 """Get a single uploadable branch, or None."""
1072 branch = self.GetBranch(branch_name)
1073 base = branch.LocalMerge
1074 if branch.LocalMerge:
1075 rb = ReviewableBranch(self, branch, base)
1076 if rb.commits:
1077 return rb
1078 return None
1079
1080 def UploadForReview(
1081 self,
1082 branch=None,
1083 people=([], []),
1084 dryrun=False,
1085 auto_topic=False,
1086 hashtags=(),
1087 labels=(),
1088 private=False,
1089 notify=None,
1090 wip=False,
1091 ready=False,
1092 dest_branch=None,
1093 validate_certs=True,
1094 push_options=None,
1095 ):
1096 """Uploads the named branch for code review."""
1097 if branch is None:
1098 branch = self.CurrentBranch
1099 if branch is None:
Jason Chang32b59562023-07-14 16:45:35 -07001100 raise GitError("not currently on a branch", project=self.name)
Gavin Makea2e3302023-03-11 06:46:20 +00001101
1102 branch = self.GetBranch(branch)
1103 if not branch.LocalMerge:
Jason Chang32b59562023-07-14 16:45:35 -07001104 raise GitError(
1105 "branch %s does not track a remote" % branch.name,
1106 project=self.name,
1107 )
Gavin Makea2e3302023-03-11 06:46:20 +00001108 if not branch.remote.review:
Jason Chang32b59562023-07-14 16:45:35 -07001109 raise GitError(
1110 "remote %s has no review url" % branch.remote.name,
1111 project=self.name,
1112 )
Gavin Makea2e3302023-03-11 06:46:20 +00001113
1114 # Basic validity check on label syntax.
1115 for label in labels:
1116 if not re.match(r"^.+[+-][0-9]+$", label):
1117 raise UploadError(
1118 f'invalid label syntax "{label}": labels use forms like '
1119 "CodeReview+1 or Verified-1"
1120 )
1121
1122 if dest_branch is None:
1123 dest_branch = self.dest_branch
1124 if dest_branch is None:
1125 dest_branch = branch.merge
1126 if not dest_branch.startswith(R_HEADS):
1127 dest_branch = R_HEADS + dest_branch
1128
1129 if not branch.remote.projectname:
1130 branch.remote.projectname = self.name
1131 branch.remote.Save()
1132
1133 url = branch.remote.ReviewUrl(self.UserEmail, validate_certs)
1134 if url is None:
1135 raise UploadError("review not configured")
1136 cmd = ["push"]
1137 if dryrun:
1138 cmd.append("-n")
1139
1140 if url.startswith("ssh://"):
1141 cmd.append("--receive-pack=gerrit receive-pack")
1142
Aravind Vasudevan99ebf622023-04-04 23:44:37 +00001143 # This stops git from pushing all reachable annotated tags when
1144 # push.followTags is configured. Gerrit does not accept any tags
1145 # pushed to a CL.
1146 if git_require((1, 8, 3)):
1147 cmd.append("--no-follow-tags")
1148
Gavin Makea2e3302023-03-11 06:46:20 +00001149 for push_option in push_options or []:
1150 cmd.append("-o")
1151 cmd.append(push_option)
1152
1153 cmd.append(url)
1154
1155 if dest_branch.startswith(R_HEADS):
1156 dest_branch = dest_branch[len(R_HEADS) :]
1157
1158 ref_spec = "%s:refs/for/%s" % (R_HEADS + branch.name, dest_branch)
1159 opts = []
1160 if auto_topic:
1161 opts += ["topic=" + branch.name]
1162 opts += ["t=%s" % p for p in hashtags]
1163 # NB: No need to encode labels as they've been validated above.
1164 opts += ["l=%s" % p for p in labels]
1165
1166 opts += ["r=%s" % p for p in people[0]]
1167 opts += ["cc=%s" % p for p in people[1]]
1168 if notify:
1169 opts += ["notify=" + notify]
1170 if private:
1171 opts += ["private"]
1172 if wip:
1173 opts += ["wip"]
1174 if ready:
1175 opts += ["ready"]
1176 if opts:
1177 ref_spec = ref_spec + "%" + ",".join(opts)
1178 cmd.append(ref_spec)
1179
1180 if GitCommand(self, cmd, bare=True).Wait() != 0:
1181 raise UploadError("Upload failed")
1182
1183 if not dryrun:
1184 msg = "posted to %s for %s" % (branch.remote.review, dest_branch)
1185 self.bare_git.UpdateRef(
1186 R_PUB + branch.name, R_HEADS + branch.name, message=msg
1187 )
1188
1189 def _ExtractArchive(self, tarpath, path=None):
1190 """Extract the given tar on its current location
1191
1192 Args:
1193 tarpath: The path to the actual tar file
1194
1195 """
1196 try:
1197 with tarfile.open(tarpath, "r") as tar:
1198 tar.extractall(path=path)
1199 return True
1200 except (IOError, tarfile.TarError) as e:
1201 _error("Cannot extract archive %s: %s", tarpath, str(e))
1202 return False
1203
1204 def Sync_NetworkHalf(
1205 self,
1206 quiet=False,
1207 verbose=False,
1208 output_redir=None,
1209 is_new=None,
1210 current_branch_only=None,
1211 force_sync=False,
1212 clone_bundle=True,
1213 tags=None,
1214 archive=False,
1215 optimized_fetch=False,
1216 retry_fetches=0,
1217 prune=False,
1218 submodules=False,
1219 ssh_proxy=None,
1220 clone_filter=None,
1221 partial_clone_exclude=set(),
Jason Chang17833322023-05-23 13:06:55 -07001222 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001223 ):
1224 """Perform only the network IO portion of the sync process.
1225 Local working directory/branch state is not affected.
1226 """
1227 if archive and not isinstance(self, MetaProject):
1228 if self.remote.url.startswith(("http://", "https://")):
Jason Chang32b59562023-07-14 16:45:35 -07001229 msg_template = (
1230 "%s: Cannot fetch archives from http/https remotes."
Gavin Makea2e3302023-03-11 06:46:20 +00001231 )
Jason Chang32b59562023-07-14 16:45:35 -07001232 msg_args = self.name
1233 msg = msg_template % msg_args
1234 _error(
1235 msg_template,
1236 msg_args,
1237 )
1238 return SyncNetworkHalfResult(
1239 False, SyncNetworkHalfError(msg, project=self.name)
1240 )
Gavin Makea2e3302023-03-11 06:46:20 +00001241
1242 name = self.relpath.replace("\\", "/")
1243 name = name.replace("/", "_")
1244 tarpath = "%s.tar" % name
1245 topdir = self.manifest.topdir
1246
1247 try:
1248 self._FetchArchive(tarpath, cwd=topdir)
1249 except GitError as e:
1250 _error("%s", e)
Jason Chang32b59562023-07-14 16:45:35 -07001251 return SyncNetworkHalfResult(False, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001252
1253 # From now on, we only need absolute tarpath.
1254 tarpath = os.path.join(topdir, tarpath)
1255
1256 if not self._ExtractArchive(tarpath, path=topdir):
Jason Chang32b59562023-07-14 16:45:35 -07001257 return SyncNetworkHalfResult(
1258 True,
1259 SyncNetworkHalfError(
1260 f"Unable to Extract Archive {tarpath}",
1261 project=self.name,
1262 ),
1263 )
Gavin Makea2e3302023-03-11 06:46:20 +00001264 try:
1265 platform_utils.remove(tarpath)
1266 except OSError as e:
1267 _warn("Cannot remove archive %s: %s", tarpath, str(e))
1268 self._CopyAndLinkFiles()
Jason Chang32b59562023-07-14 16:45:35 -07001269 return SyncNetworkHalfResult(True)
Gavin Makea2e3302023-03-11 06:46:20 +00001270
1271 # If the shared object dir already exists, don't try to rebootstrap with
1272 # a clone bundle download. We should have the majority of objects
1273 # already.
1274 if clone_bundle and os.path.exists(self.objdir):
1275 clone_bundle = False
1276
1277 if self.name in partial_clone_exclude:
1278 clone_bundle = True
1279 clone_filter = None
1280
1281 if is_new is None:
1282 is_new = not self.Exists
1283 if is_new:
1284 self._InitGitDir(force_sync=force_sync, quiet=quiet)
1285 else:
1286 self._UpdateHooks(quiet=quiet)
1287 self._InitRemote()
1288
1289 if self.UseAlternates:
1290 # If gitdir/objects is a symlink, migrate it from the old layout.
1291 gitdir_objects = os.path.join(self.gitdir, "objects")
1292 if platform_utils.islink(gitdir_objects):
1293 platform_utils.remove(gitdir_objects, missing_ok=True)
1294 gitdir_alt = os.path.join(self.gitdir, "objects/info/alternates")
1295 if not os.path.exists(gitdir_alt):
1296 os.makedirs(os.path.dirname(gitdir_alt), exist_ok=True)
1297 _lwrite(
1298 gitdir_alt,
1299 os.path.join(
1300 os.path.relpath(self.objdir, gitdir_objects), "objects"
1301 )
1302 + "\n",
1303 )
1304
1305 if is_new:
1306 alt = os.path.join(self.objdir, "objects/info/alternates")
1307 try:
1308 with open(alt) as fd:
1309 # This works for both absolute and relative alternate
1310 # directories.
1311 alt_dir = os.path.join(
1312 self.objdir, "objects", fd.readline().rstrip()
1313 )
1314 except IOError:
1315 alt_dir = None
1316 else:
1317 alt_dir = None
1318
1319 if (
1320 clone_bundle
1321 and alt_dir is None
1322 and self._ApplyCloneBundle(
1323 initial=is_new, quiet=quiet, verbose=verbose
1324 )
1325 ):
1326 is_new = False
1327
1328 if current_branch_only is None:
1329 if self.sync_c:
1330 current_branch_only = True
1331 elif not self.manifest._loaded:
1332 # Manifest cannot check defaults until it syncs.
1333 current_branch_only = False
1334 elif self.manifest.default.sync_c:
1335 current_branch_only = True
1336
1337 if tags is None:
1338 tags = self.sync_tags
1339
1340 if self.clone_depth:
1341 depth = self.clone_depth
1342 else:
1343 depth = self.manifest.manifestProject.depth
1344
Jason Chang17833322023-05-23 13:06:55 -07001345 if depth and clone_filter_for_depth:
1346 depth = None
1347 clone_filter = clone_filter_for_depth
1348
Gavin Makea2e3302023-03-11 06:46:20 +00001349 # See if we can skip the network fetch entirely.
1350 remote_fetched = False
1351 if not (
1352 optimized_fetch
1353 and (
1354 ID_RE.match(self.revisionExpr)
1355 and self._CheckForImmutableRevision()
1356 )
1357 ):
1358 remote_fetched = True
Jason Chang32b59562023-07-14 16:45:35 -07001359 try:
1360 if not self._RemoteFetch(
1361 initial=is_new,
1362 quiet=quiet,
1363 verbose=verbose,
1364 output_redir=output_redir,
1365 alt_dir=alt_dir,
1366 current_branch_only=current_branch_only,
1367 tags=tags,
1368 prune=prune,
1369 depth=depth,
1370 submodules=submodules,
1371 force_sync=force_sync,
1372 ssh_proxy=ssh_proxy,
1373 clone_filter=clone_filter,
1374 retry_fetches=retry_fetches,
1375 ):
1376 return SyncNetworkHalfResult(
1377 remote_fetched,
1378 SyncNetworkHalfError(
1379 f"Unable to remote fetch project {self.name}",
1380 project=self.name,
1381 ),
1382 )
1383 except RepoError as e:
1384 return SyncNetworkHalfResult(
1385 remote_fetched,
1386 e,
1387 )
Gavin Makea2e3302023-03-11 06:46:20 +00001388
1389 mp = self.manifest.manifestProject
1390 dissociate = mp.dissociate
1391 if dissociate:
1392 alternates_file = os.path.join(
1393 self.objdir, "objects/info/alternates"
1394 )
1395 if os.path.exists(alternates_file):
1396 cmd = ["repack", "-a", "-d"]
1397 p = GitCommand(
1398 self,
1399 cmd,
1400 bare=True,
1401 capture_stdout=bool(output_redir),
1402 merge_output=bool(output_redir),
1403 )
1404 if p.stdout and output_redir:
1405 output_redir.write(p.stdout)
1406 if p.Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07001407 return SyncNetworkHalfResult(
1408 remote_fetched,
1409 GitError(
1410 "Unable to repack alternates", project=self.name
1411 ),
1412 )
Gavin Makea2e3302023-03-11 06:46:20 +00001413 platform_utils.remove(alternates_file)
1414
1415 if self.worktree:
1416 self._InitMRef()
1417 else:
1418 self._InitMirrorHead()
1419 platform_utils.remove(
1420 os.path.join(self.gitdir, "FETCH_HEAD"), missing_ok=True
1421 )
Jason Chang32b59562023-07-14 16:45:35 -07001422 return SyncNetworkHalfResult(remote_fetched)
Gavin Makea2e3302023-03-11 06:46:20 +00001423
1424 def PostRepoUpgrade(self):
1425 self._InitHooks()
1426
1427 def _CopyAndLinkFiles(self):
1428 if self.client.isGitcClient:
1429 return
1430 for copyfile in self.copyfiles:
1431 copyfile._Copy()
1432 for linkfile in self.linkfiles:
1433 linkfile._Link()
1434
1435 def GetCommitRevisionId(self):
1436 """Get revisionId of a commit.
1437
1438 Use this method instead of GetRevisionId to get the id of the commit
1439 rather than the id of the current git object (for example, a tag)
1440
1441 """
1442 if not self.revisionExpr.startswith(R_TAGS):
1443 return self.GetRevisionId(self._allrefs)
1444
1445 try:
1446 return self.bare_git.rev_list(self.revisionExpr, "-1")[0]
1447 except GitError:
1448 raise ManifestInvalidRevisionError(
1449 "revision %s in %s not found" % (self.revisionExpr, self.name)
1450 )
1451
1452 def GetRevisionId(self, all_refs=None):
1453 if self.revisionId:
1454 return self.revisionId
1455
1456 rem = self.GetRemote()
1457 rev = rem.ToLocal(self.revisionExpr)
1458
1459 if all_refs is not None and rev in all_refs:
1460 return all_refs[rev]
1461
1462 try:
1463 return self.bare_git.rev_parse("--verify", "%s^0" % rev)
1464 except GitError:
1465 raise ManifestInvalidRevisionError(
1466 "revision %s in %s not found" % (self.revisionExpr, self.name)
1467 )
1468
1469 def SetRevisionId(self, revisionId):
1470 if self.revisionExpr:
1471 self.upstream = self.revisionExpr
1472
1473 self.revisionId = revisionId
1474
Jason Chang32b59562023-07-14 16:45:35 -07001475 def Sync_LocalHalf(
1476 self, syncbuf, force_sync=False, submodules=False, errors=None
1477 ):
Gavin Makea2e3302023-03-11 06:46:20 +00001478 """Perform only the local IO portion of the sync process.
1479
1480 Network access is not required.
1481 """
Jason Chang32b59562023-07-14 16:45:35 -07001482 if errors is None:
1483 errors = []
1484
1485 def fail(error: Exception):
1486 errors.append(error)
1487 syncbuf.fail(self, error)
1488
Gavin Makea2e3302023-03-11 06:46:20 +00001489 if not os.path.exists(self.gitdir):
Jason Chang32b59562023-07-14 16:45:35 -07001490 fail(
1491 LocalSyncFail(
1492 "Cannot checkout %s due to missing network sync; Run "
1493 "`repo sync -n %s` first." % (self.name, self.name),
1494 project=self.name,
1495 )
Gavin Makea2e3302023-03-11 06:46:20 +00001496 )
1497 return
1498
1499 self._InitWorkTree(force_sync=force_sync, submodules=submodules)
1500 all_refs = self.bare_ref.all
1501 self.CleanPublishedCache(all_refs)
1502 revid = self.GetRevisionId(all_refs)
1503
1504 # Special case the root of the repo client checkout. Make sure it
1505 # doesn't contain files being checked out to dirs we don't allow.
1506 if self.relpath == ".":
1507 PROTECTED_PATHS = {".repo"}
1508 paths = set(
1509 self.work_git.ls_tree("-z", "--name-only", "--", revid).split(
1510 "\0"
1511 )
1512 )
1513 bad_paths = paths & PROTECTED_PATHS
1514 if bad_paths:
Jason Chang32b59562023-07-14 16:45:35 -07001515 fail(
1516 LocalSyncFail(
1517 "Refusing to checkout project that writes to protected "
1518 "paths: %s" % (", ".join(bad_paths),),
1519 project=self.name,
1520 )
Gavin Makea2e3302023-03-11 06:46:20 +00001521 )
1522 return
1523
1524 def _doff():
1525 self._FastForward(revid)
1526 self._CopyAndLinkFiles()
1527
1528 def _dosubmodules():
1529 self._SyncSubmodules(quiet=True)
1530
1531 head = self.work_git.GetHead()
1532 if head.startswith(R_HEADS):
1533 branch = head[len(R_HEADS) :]
1534 try:
1535 head = all_refs[head]
1536 except KeyError:
1537 head = None
1538 else:
1539 branch = None
1540
1541 if branch is None or syncbuf.detach_head:
1542 # Currently on a detached HEAD. The user is assumed to
1543 # not have any local modifications worth worrying about.
1544 if self.IsRebaseInProgress():
Jason Chang32b59562023-07-14 16:45:35 -07001545 fail(_PriorSyncFailedError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001546 return
1547
1548 if head == revid:
1549 # No changes; don't do anything further.
1550 # Except if the head needs to be detached.
1551 if not syncbuf.detach_head:
1552 # The copy/linkfile config may have changed.
1553 self._CopyAndLinkFiles()
1554 return
1555 else:
1556 lost = self._revlist(not_rev(revid), HEAD)
1557 if lost:
1558 syncbuf.info(self, "discarding %d commits", len(lost))
1559
1560 try:
1561 self._Checkout(revid, quiet=True)
1562 if submodules:
1563 self._SyncSubmodules(quiet=True)
1564 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001565 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001566 return
1567 self._CopyAndLinkFiles()
1568 return
1569
1570 if head == revid:
1571 # No changes; don't do anything further.
1572 #
1573 # The copy/linkfile config may have changed.
1574 self._CopyAndLinkFiles()
1575 return
1576
1577 branch = self.GetBranch(branch)
1578
1579 if not branch.LocalMerge:
1580 # The current branch has no tracking configuration.
1581 # Jump off it to a detached HEAD.
1582 syncbuf.info(
1583 self, "leaving %s; does not track upstream", branch.name
1584 )
1585 try:
1586 self._Checkout(revid, quiet=True)
1587 if submodules:
1588 self._SyncSubmodules(quiet=True)
1589 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001590 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001591 return
1592 self._CopyAndLinkFiles()
1593 return
1594
1595 upstream_gain = self._revlist(not_rev(HEAD), revid)
1596
1597 # See if we can perform a fast forward merge. This can happen if our
1598 # branch isn't in the exact same state as we last published.
1599 try:
1600 self.work_git.merge_base("--is-ancestor", HEAD, revid)
1601 # Skip the published logic.
1602 pub = False
1603 except GitError:
1604 pub = self.WasPublished(branch.name, all_refs)
1605
1606 if pub:
1607 not_merged = self._revlist(not_rev(revid), pub)
1608 if not_merged:
1609 if upstream_gain:
1610 # The user has published this branch and some of those
1611 # commits are not yet merged upstream. We do not want
1612 # to rewrite the published commits so we punt.
Jason Chang32b59562023-07-14 16:45:35 -07001613 fail(
1614 LocalSyncFail(
1615 "branch %s is published (but not merged) and is "
1616 "now %d commits behind"
1617 % (branch.name, len(upstream_gain)),
1618 project=self.name,
1619 )
Gavin Makea2e3302023-03-11 06:46:20 +00001620 )
1621 return
1622 elif pub == head:
1623 # All published commits are merged, and thus we are a
1624 # strict subset. We can fast-forward safely.
1625 syncbuf.later1(self, _doff)
1626 if submodules:
1627 syncbuf.later1(self, _dosubmodules)
1628 return
1629
1630 # Examine the local commits not in the remote. Find the
1631 # last one attributed to this user, if any.
1632 local_changes = self._revlist(not_rev(revid), HEAD, format="%H %ce")
1633 last_mine = None
1634 cnt_mine = 0
1635 for commit in local_changes:
1636 commit_id, committer_email = commit.split(" ", 1)
1637 if committer_email == self.UserEmail:
1638 last_mine = commit_id
1639 cnt_mine += 1
1640
1641 if not upstream_gain and cnt_mine == len(local_changes):
1642 # The copy/linkfile config may have changed.
1643 self._CopyAndLinkFiles()
1644 return
1645
1646 if self.IsDirty(consider_untracked=False):
Jason Chang32b59562023-07-14 16:45:35 -07001647 fail(_DirtyError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001648 return
1649
1650 # If the upstream switched on us, warn the user.
1651 if branch.merge != self.revisionExpr:
1652 if branch.merge and self.revisionExpr:
1653 syncbuf.info(
1654 self,
1655 "manifest switched %s...%s",
1656 branch.merge,
1657 self.revisionExpr,
1658 )
1659 elif branch.merge:
1660 syncbuf.info(self, "manifest no longer tracks %s", branch.merge)
1661
1662 if cnt_mine < len(local_changes):
1663 # Upstream rebased. Not everything in HEAD was created by this user.
1664 syncbuf.info(
1665 self,
1666 "discarding %d commits removed from upstream",
1667 len(local_changes) - cnt_mine,
1668 )
1669
1670 branch.remote = self.GetRemote()
1671 if not ID_RE.match(self.revisionExpr):
1672 # In case of manifest sync the revisionExpr might be a SHA1.
1673 branch.merge = self.revisionExpr
1674 if not branch.merge.startswith("refs/"):
1675 branch.merge = R_HEADS + branch.merge
1676 branch.Save()
1677
1678 if cnt_mine > 0 and self.rebase:
1679
1680 def _docopyandlink():
1681 self._CopyAndLinkFiles()
1682
1683 def _dorebase():
1684 self._Rebase(upstream="%s^1" % last_mine, onto=revid)
1685
1686 syncbuf.later2(self, _dorebase)
1687 if submodules:
1688 syncbuf.later2(self, _dosubmodules)
1689 syncbuf.later2(self, _docopyandlink)
1690 elif local_changes:
1691 try:
1692 self._ResetHard(revid)
1693 if submodules:
1694 self._SyncSubmodules(quiet=True)
1695 self._CopyAndLinkFiles()
1696 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001697 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001698 return
1699 else:
1700 syncbuf.later1(self, _doff)
1701 if submodules:
1702 syncbuf.later1(self, _dosubmodules)
1703
1704 def AddCopyFile(self, src, dest, topdir):
1705 """Mark |src| for copying to |dest| (relative to |topdir|).
1706
1707 No filesystem changes occur here. Actual copying happens later on.
1708
1709 Paths should have basic validation run on them before being queued.
1710 Further checking will be handled when the actual copy happens.
1711 """
1712 self.copyfiles.append(_CopyFile(self.worktree, src, topdir, dest))
1713
1714 def AddLinkFile(self, src, dest, topdir):
1715 """Mark |dest| to create a symlink (relative to |topdir|) pointing to
1716 |src|.
1717
1718 No filesystem changes occur here. Actual linking happens later on.
1719
1720 Paths should have basic validation run on them before being queued.
1721 Further checking will be handled when the actual link happens.
1722 """
1723 self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest))
1724
1725 def AddAnnotation(self, name, value, keep):
1726 self.annotations.append(Annotation(name, value, keep))
1727
1728 def DownloadPatchSet(self, change_id, patch_id):
1729 """Download a single patch set of a single change to FETCH_HEAD."""
1730 remote = self.GetRemote()
1731
1732 cmd = ["fetch", remote.name]
1733 cmd.append(
1734 "refs/changes/%2.2d/%d/%d" % (change_id % 100, change_id, patch_id)
1735 )
1736 if GitCommand(self, cmd, bare=True).Wait() != 0:
1737 return None
1738 return DownloadedChange(
1739 self,
1740 self.GetRevisionId(),
1741 change_id,
1742 patch_id,
1743 self.bare_git.rev_parse("FETCH_HEAD"),
1744 )
1745
1746 def DeleteWorktree(self, quiet=False, force=False):
1747 """Delete the source checkout and any other housekeeping tasks.
1748
1749 This currently leaves behind the internal .repo/ cache state. This
1750 helps when switching branches or manifest changes get reverted as we
1751 don't have to redownload all the git objects. But we should do some GC
1752 at some point.
1753
1754 Args:
1755 quiet: Whether to hide normal messages.
1756 force: Always delete tree even if dirty.
Doug Anderson3ba5f952011-04-07 12:51:04 -07001757
1758 Returns:
Gavin Makea2e3302023-03-11 06:46:20 +00001759 True if the worktree was completely cleaned out.
1760 """
1761 if self.IsDirty():
1762 if force:
1763 print(
1764 "warning: %s: Removing dirty project: uncommitted changes "
1765 "lost." % (self.RelPath(local=False),),
1766 file=sys.stderr,
1767 )
1768 else:
Jason Chang32b59562023-07-14 16:45:35 -07001769 msg = (
1770 "error: %s: Cannot remove project: uncommitted"
1771 "changes are present.\n" % self.RelPath(local=False)
Gavin Makea2e3302023-03-11 06:46:20 +00001772 )
Jason Chang32b59562023-07-14 16:45:35 -07001773 print(msg, file=sys.stderr)
1774 raise DeleteDirtyWorktreeError(msg, project=self)
Wink Saville02d79452009-04-10 13:01:24 -07001775
Gavin Makea2e3302023-03-11 06:46:20 +00001776 if not quiet:
1777 print(
1778 "%s: Deleting obsolete checkout." % (self.RelPath(local=False),)
1779 )
Wink Saville02d79452009-04-10 13:01:24 -07001780
Gavin Makea2e3302023-03-11 06:46:20 +00001781 # Unlock and delink from the main worktree. We don't use git's worktree
1782 # remove because it will recursively delete projects -- we handle that
1783 # ourselves below. https://crbug.com/git/48
1784 if self.use_git_worktrees:
1785 needle = platform_utils.realpath(self.gitdir)
1786 # Find the git worktree commondir under .repo/worktrees/.
1787 output = self.bare_git.worktree("list", "--porcelain").splitlines()[
1788 0
1789 ]
1790 assert output.startswith("worktree "), output
1791 commondir = output[9:]
1792 # Walk each of the git worktrees to see where they point.
1793 configs = os.path.join(commondir, "worktrees")
1794 for name in os.listdir(configs):
1795 gitdir = os.path.join(configs, name, "gitdir")
1796 with open(gitdir) as fp:
1797 relpath = fp.read().strip()
1798 # Resolve the checkout path and see if it matches this project.
1799 fullpath = platform_utils.realpath(
1800 os.path.join(configs, name, relpath)
1801 )
1802 if fullpath == needle:
1803 platform_utils.rmtree(os.path.join(configs, name))
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001804
Gavin Makea2e3302023-03-11 06:46:20 +00001805 # Delete the .git directory first, so we're less likely to have a
1806 # partially working git repository around. There shouldn't be any git
1807 # projects here, so rmtree works.
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001808
Gavin Makea2e3302023-03-11 06:46:20 +00001809 # Try to remove plain files first in case of git worktrees. If this
1810 # fails for any reason, we'll fall back to rmtree, and that'll display
1811 # errors if it can't remove things either.
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001812 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001813 platform_utils.remove(self.gitdir)
1814 except OSError:
1815 pass
1816 try:
1817 platform_utils.rmtree(self.gitdir)
1818 except OSError as e:
1819 if e.errno != errno.ENOENT:
1820 print("error: %s: %s" % (self.gitdir, e), file=sys.stderr)
1821 print(
1822 "error: %s: Failed to delete obsolete checkout; remove "
1823 "manually, then run `repo sync -l`."
1824 % (self.RelPath(local=False),),
1825 file=sys.stderr,
1826 )
Jason Chang32b59562023-07-14 16:45:35 -07001827 raise DeleteWorktreeError(aggregate_errors=[e])
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001828
Gavin Makea2e3302023-03-11 06:46:20 +00001829 # Delete everything under the worktree, except for directories that
1830 # contain another git project.
1831 dirs_to_remove = []
1832 failed = False
Jason Chang32b59562023-07-14 16:45:35 -07001833 errors = []
Gavin Makea2e3302023-03-11 06:46:20 +00001834 for root, dirs, files in platform_utils.walk(self.worktree):
1835 for f in files:
1836 path = os.path.join(root, f)
1837 try:
1838 platform_utils.remove(path)
1839 except OSError as e:
1840 if e.errno != errno.ENOENT:
1841 print(
1842 "error: %s: Failed to remove: %s" % (path, e),
1843 file=sys.stderr,
1844 )
1845 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001846 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001847 dirs[:] = [
1848 d
1849 for d in dirs
1850 if not os.path.lexists(os.path.join(root, d, ".git"))
1851 ]
1852 dirs_to_remove += [
1853 os.path.join(root, d)
1854 for d in dirs
1855 if os.path.join(root, d) not in dirs_to_remove
1856 ]
1857 for d in reversed(dirs_to_remove):
1858 if platform_utils.islink(d):
1859 try:
1860 platform_utils.remove(d)
1861 except OSError as e:
1862 if e.errno != errno.ENOENT:
1863 print(
1864 "error: %s: Failed to remove: %s" % (d, e),
1865 file=sys.stderr,
1866 )
1867 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001868 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001869 elif not platform_utils.listdir(d):
1870 try:
1871 platform_utils.rmdir(d)
1872 except OSError as e:
1873 if e.errno != errno.ENOENT:
1874 print(
1875 "error: %s: Failed to remove: %s" % (d, e),
1876 file=sys.stderr,
1877 )
1878 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001879 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001880 if failed:
1881 print(
1882 "error: %s: Failed to delete obsolete checkout."
1883 % (self.RelPath(local=False),),
1884 file=sys.stderr,
1885 )
1886 print(
1887 " Remove manually, then run `repo sync -l`.",
1888 file=sys.stderr,
1889 )
Jason Chang32b59562023-07-14 16:45:35 -07001890 raise DeleteWorktreeError(aggregate_errors=errors)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07001891
Gavin Makea2e3302023-03-11 06:46:20 +00001892 # Try deleting parent dirs if they are empty.
1893 path = self.worktree
1894 while path != self.manifest.topdir:
1895 try:
1896 platform_utils.rmdir(path)
1897 except OSError as e:
1898 if e.errno != errno.ENOENT:
1899 break
1900 path = os.path.dirname(path)
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001901
Gavin Makea2e3302023-03-11 06:46:20 +00001902 return True
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001903
Gavin Makea2e3302023-03-11 06:46:20 +00001904 def StartBranch(self, name, branch_merge="", revision=None):
1905 """Create a new branch off the manifest's revision."""
1906 if not branch_merge:
1907 branch_merge = self.revisionExpr
1908 head = self.work_git.GetHead()
1909 if head == (R_HEADS + name):
1910 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001911
David Pursehouse8a68ff92012-09-24 12:15:13 +09001912 all_refs = self.bare_ref.all
Gavin Makea2e3302023-03-11 06:46:20 +00001913 if R_HEADS + name in all_refs:
1914 return GitCommand(self, ["checkout", "-q", name, "--"]).Wait() == 0
Shawn O. Pearce88443382010-10-08 10:02:09 +02001915
Gavin Makea2e3302023-03-11 06:46:20 +00001916 branch = self.GetBranch(name)
1917 branch.remote = self.GetRemote()
1918 branch.merge = branch_merge
1919 if not branch.merge.startswith("refs/") and not ID_RE.match(
1920 branch_merge
1921 ):
1922 branch.merge = R_HEADS + branch_merge
Shawn O. Pearce88443382010-10-08 10:02:09 +02001923
Gavin Makea2e3302023-03-11 06:46:20 +00001924 if revision is None:
1925 revid = self.GetRevisionId(all_refs)
Shawn O. Pearce88443382010-10-08 10:02:09 +02001926 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001927 revid = self.work_git.rev_parse(revision)
Brian Harring14a66742012-09-28 20:21:57 -07001928
Gavin Makea2e3302023-03-11 06:46:20 +00001929 if head.startswith(R_HEADS):
Kevin Degiabaa7f32014-11-12 11:27:45 -07001930 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001931 head = all_refs[head]
1932 except KeyError:
1933 head = None
1934 if revid and head and revid == head:
1935 ref = R_HEADS + name
1936 self.work_git.update_ref(ref, revid)
1937 self.work_git.symbolic_ref(HEAD, ref)
1938 branch.Save()
1939 return True
Kevin Degib1a07b82015-07-27 13:33:43 -06001940
Gavin Makea2e3302023-03-11 06:46:20 +00001941 if (
1942 GitCommand(
1943 self, ["checkout", "-q", "-b", branch.name, revid]
1944 ).Wait()
1945 == 0
1946 ):
1947 branch.Save()
1948 return True
1949 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06001950
Gavin Makea2e3302023-03-11 06:46:20 +00001951 def CheckoutBranch(self, name):
1952 """Checkout a local topic branch.
Shawn O. Pearce2816d4f2009-03-03 17:53:18 -08001953
Gavin Makea2e3302023-03-11 06:46:20 +00001954 Args:
1955 name: The name of the branch to checkout.
Shawn O. Pearce88443382010-10-08 10:02:09 +02001956
Gavin Makea2e3302023-03-11 06:46:20 +00001957 Returns:
1958 True if the checkout succeeded; False if it didn't; None if the
1959 branch didn't exist.
1960 """
1961 rev = R_HEADS + name
1962 head = self.work_git.GetHead()
1963 if head == rev:
1964 # Already on the branch.
1965 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001966
Gavin Makea2e3302023-03-11 06:46:20 +00001967 all_refs = self.bare_ref.all
1968 try:
1969 revid = all_refs[rev]
1970 except KeyError:
1971 # Branch does not exist in this project.
1972 return None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001973
Gavin Makea2e3302023-03-11 06:46:20 +00001974 if head.startswith(R_HEADS):
1975 try:
1976 head = all_refs[head]
1977 except KeyError:
1978 head = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001979
Gavin Makea2e3302023-03-11 06:46:20 +00001980 if head == revid:
1981 # Same revision; just update HEAD to point to the new
1982 # target branch, but otherwise take no other action.
1983 _lwrite(
1984 self.work_git.GetDotgitPath(subpath=HEAD),
1985 "ref: %s%s\n" % (R_HEADS, name),
1986 )
1987 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05001988
Gavin Makea2e3302023-03-11 06:46:20 +00001989 return (
1990 GitCommand(
1991 self,
1992 ["checkout", name, "--"],
1993 capture_stdout=True,
1994 capture_stderr=True,
1995 ).Wait()
1996 == 0
1997 )
Mike Frysinger98bb7652021-12-20 21:15:59 -05001998
Gavin Makea2e3302023-03-11 06:46:20 +00001999 def AbandonBranch(self, name):
2000 """Destroy a local topic branch.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002001
Gavin Makea2e3302023-03-11 06:46:20 +00002002 Args:
2003 name: The name of the branch to abandon.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002004
Gavin Makea2e3302023-03-11 06:46:20 +00002005 Returns:
2006 True if the abandon succeeded; False if it didn't; None if the
2007 branch didn't exist.
2008 """
2009 rev = R_HEADS + name
2010 all_refs = self.bare_ref.all
2011 if rev not in all_refs:
2012 # Doesn't exist
2013 return None
2014
2015 head = self.work_git.GetHead()
2016 if head == rev:
2017 # We can't destroy the branch while we are sitting
2018 # on it. Switch to a detached HEAD.
2019 head = all_refs[head]
2020
2021 revid = self.GetRevisionId(all_refs)
2022 if head == revid:
2023 _lwrite(
2024 self.work_git.GetDotgitPath(subpath=HEAD), "%s\n" % revid
2025 )
2026 else:
2027 self._Checkout(revid, quiet=True)
2028
2029 return (
2030 GitCommand(
2031 self,
2032 ["branch", "-D", name],
2033 capture_stdout=True,
2034 capture_stderr=True,
2035 ).Wait()
2036 == 0
2037 )
2038
2039 def PruneHeads(self):
2040 """Prune any topic branches already merged into upstream."""
2041 cb = self.CurrentBranch
2042 kill = []
2043 left = self._allrefs
2044 for name in left.keys():
2045 if name.startswith(R_HEADS):
2046 name = name[len(R_HEADS) :]
2047 if cb is None or name != cb:
2048 kill.append(name)
2049
2050 # Minor optimization: If there's nothing to prune, then don't try to
2051 # read any project state.
2052 if not kill and not cb:
2053 return []
2054
2055 rev = self.GetRevisionId(left)
2056 if (
2057 cb is not None
2058 and not self._revlist(HEAD + "..." + rev)
2059 and not self.IsDirty(consider_untracked=False)
2060 ):
2061 self.work_git.DetachHead(HEAD)
2062 kill.append(cb)
2063
2064 if kill:
2065 old = self.bare_git.GetHead()
2066
2067 try:
2068 self.bare_git.DetachHead(rev)
2069
2070 b = ["branch", "-d"]
2071 b.extend(kill)
2072 b = GitCommand(
2073 self, b, bare=True, capture_stdout=True, capture_stderr=True
2074 )
2075 b.Wait()
2076 finally:
2077 if ID_RE.match(old):
2078 self.bare_git.DetachHead(old)
2079 else:
2080 self.bare_git.SetHead(old)
2081 left = self._allrefs
2082
2083 for branch in kill:
2084 if (R_HEADS + branch) not in left:
2085 self.CleanPublishedCache()
2086 break
2087
2088 if cb and cb not in kill:
2089 kill.append(cb)
2090 kill.sort()
2091
2092 kept = []
2093 for branch in kill:
2094 if R_HEADS + branch in left:
2095 branch = self.GetBranch(branch)
2096 base = branch.LocalMerge
2097 if not base:
2098 base = rev
2099 kept.append(ReviewableBranch(self, branch, base))
2100 return kept
2101
2102 def GetRegisteredSubprojects(self):
2103 result = []
2104
2105 def rec(subprojects):
2106 if not subprojects:
2107 return
2108 result.extend(subprojects)
2109 for p in subprojects:
2110 rec(p.subprojects)
2111
2112 rec(self.subprojects)
2113 return result
2114
2115 def _GetSubmodules(self):
2116 # Unfortunately we cannot call `git submodule status --recursive` here
2117 # because the working tree might not exist yet, and it cannot be used
2118 # without a working tree in its current implementation.
2119
2120 def get_submodules(gitdir, rev):
2121 # Parse .gitmodules for submodule sub_paths and sub_urls.
2122 sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
2123 if not sub_paths:
2124 return []
2125 # Run `git ls-tree` to read SHAs of submodule object, which happen
2126 # to be revision of submodule repository.
2127 sub_revs = git_ls_tree(gitdir, rev, sub_paths)
2128 submodules = []
2129 for sub_path, sub_url in zip(sub_paths, sub_urls):
2130 try:
2131 sub_rev = sub_revs[sub_path]
2132 except KeyError:
2133 # Ignore non-exist submodules.
2134 continue
2135 submodules.append((sub_rev, sub_path, sub_url))
2136 return submodules
2137
2138 re_path = re.compile(r"^submodule\.(.+)\.path=(.*)$")
2139 re_url = re.compile(r"^submodule\.(.+)\.url=(.*)$")
2140
2141 def parse_gitmodules(gitdir, rev):
2142 cmd = ["cat-file", "blob", "%s:.gitmodules" % rev]
2143 try:
2144 p = GitCommand(
2145 None,
2146 cmd,
2147 capture_stdout=True,
2148 capture_stderr=True,
2149 bare=True,
2150 gitdir=gitdir,
2151 )
2152 except GitError:
2153 return [], []
2154 if p.Wait() != 0:
2155 return [], []
2156
2157 gitmodules_lines = []
2158 fd, temp_gitmodules_path = tempfile.mkstemp()
2159 try:
2160 os.write(fd, p.stdout.encode("utf-8"))
2161 os.close(fd)
2162 cmd = ["config", "--file", temp_gitmodules_path, "--list"]
2163 p = GitCommand(
2164 None,
2165 cmd,
2166 capture_stdout=True,
2167 capture_stderr=True,
2168 bare=True,
2169 gitdir=gitdir,
2170 )
2171 if p.Wait() != 0:
2172 return [], []
2173 gitmodules_lines = p.stdout.split("\n")
2174 except GitError:
2175 return [], []
2176 finally:
2177 platform_utils.remove(temp_gitmodules_path)
2178
2179 names = set()
2180 paths = {}
2181 urls = {}
2182 for line in gitmodules_lines:
2183 if not line:
2184 continue
2185 m = re_path.match(line)
2186 if m:
2187 names.add(m.group(1))
2188 paths[m.group(1)] = m.group(2)
2189 continue
2190 m = re_url.match(line)
2191 if m:
2192 names.add(m.group(1))
2193 urls[m.group(1)] = m.group(2)
2194 continue
2195 names = sorted(names)
2196 return (
2197 [paths.get(name, "") for name in names],
2198 [urls.get(name, "") for name in names],
2199 )
2200
2201 def git_ls_tree(gitdir, rev, paths):
2202 cmd = ["ls-tree", rev, "--"]
2203 cmd.extend(paths)
2204 try:
2205 p = GitCommand(
2206 None,
2207 cmd,
2208 capture_stdout=True,
2209 capture_stderr=True,
2210 bare=True,
2211 gitdir=gitdir,
2212 )
2213 except GitError:
2214 return []
2215 if p.Wait() != 0:
2216 return []
2217 objects = {}
2218 for line in p.stdout.split("\n"):
2219 if not line.strip():
2220 continue
2221 object_rev, object_path = line.split()[2:4]
2222 objects[object_path] = object_rev
2223 return objects
2224
2225 try:
2226 rev = self.GetRevisionId()
2227 except GitError:
2228 return []
2229 return get_submodules(self.gitdir, rev)
2230
2231 def GetDerivedSubprojects(self):
2232 result = []
2233 if not self.Exists:
2234 # If git repo does not exist yet, querying its submodules will
2235 # mess up its states; so return here.
2236 return result
2237 for rev, path, url in self._GetSubmodules():
2238 name = self.manifest.GetSubprojectName(self, path)
2239 (
2240 relpath,
2241 worktree,
2242 gitdir,
2243 objdir,
2244 ) = self.manifest.GetSubprojectPaths(self, name, path)
2245 project = self.manifest.paths.get(relpath)
2246 if project:
2247 result.extend(project.GetDerivedSubprojects())
2248 continue
2249
2250 if url.startswith(".."):
2251 url = urllib.parse.urljoin("%s/" % self.remote.url, url)
2252 remote = RemoteSpec(
2253 self.remote.name,
2254 url=url,
2255 pushUrl=self.remote.pushUrl,
2256 review=self.remote.review,
2257 revision=self.remote.revision,
2258 )
2259 subproject = Project(
2260 manifest=self.manifest,
2261 name=name,
2262 remote=remote,
2263 gitdir=gitdir,
2264 objdir=objdir,
2265 worktree=worktree,
2266 relpath=relpath,
2267 revisionExpr=rev,
2268 revisionId=rev,
2269 rebase=self.rebase,
2270 groups=self.groups,
2271 sync_c=self.sync_c,
2272 sync_s=self.sync_s,
2273 sync_tags=self.sync_tags,
2274 parent=self,
2275 is_derived=True,
2276 )
2277 result.append(subproject)
2278 result.extend(subproject.GetDerivedSubprojects())
2279 return result
2280
2281 def EnableRepositoryExtension(self, key, value="true", version=1):
2282 """Enable git repository extension |key| with |value|.
2283
2284 Args:
2285 key: The extension to enabled. Omit the "extensions." prefix.
2286 value: The value to use for the extension.
2287 version: The minimum git repository version needed.
2288 """
2289 # Make sure the git repo version is new enough already.
2290 found_version = self.config.GetInt("core.repositoryFormatVersion")
2291 if found_version is None:
2292 found_version = 0
2293 if found_version < version:
2294 self.config.SetString("core.repositoryFormatVersion", str(version))
2295
2296 # Enable the extension!
2297 self.config.SetString("extensions.%s" % (key,), value)
2298
2299 def ResolveRemoteHead(self, name=None):
2300 """Find out what the default branch (HEAD) points to.
2301
2302 Normally this points to refs/heads/master, but projects are moving to
2303 main. Support whatever the server uses rather than hardcoding "master"
2304 ourselves.
2305 """
2306 if name is None:
2307 name = self.remote.name
2308
2309 # The output will look like (NB: tabs are separators):
2310 # ref: refs/heads/master HEAD
2311 # 5f6803b100bb3cd0f534e96e88c91373e8ed1c44 HEAD
2312 output = self.bare_git.ls_remote(
2313 "-q", "--symref", "--exit-code", name, "HEAD"
2314 )
2315
2316 for line in output.splitlines():
2317 lhs, rhs = line.split("\t", 1)
2318 if rhs == "HEAD" and lhs.startswith("ref:"):
2319 return lhs[4:].strip()
2320
2321 return None
2322
2323 def _CheckForImmutableRevision(self):
2324 try:
2325 # if revision (sha or tag) is not present then following function
2326 # throws an error.
2327 self.bare_git.rev_list(
2328 "-1", "--missing=allow-any", "%s^0" % self.revisionExpr, "--"
2329 )
2330 if self.upstream:
2331 rev = self.GetRemote().ToLocal(self.upstream)
2332 self.bare_git.rev_list(
2333 "-1", "--missing=allow-any", "%s^0" % rev, "--"
2334 )
2335 self.bare_git.merge_base(
2336 "--is-ancestor", self.revisionExpr, rev
2337 )
2338 return True
2339 except GitError:
2340 # There is no such persistent revision. We have to fetch it.
2341 return False
2342
2343 def _FetchArchive(self, tarpath, cwd=None):
2344 cmd = ["archive", "-v", "-o", tarpath]
2345 cmd.append("--remote=%s" % self.remote.url)
2346 cmd.append("--prefix=%s/" % self.RelPath(local=False))
2347 cmd.append(self.revisionExpr)
2348
2349 command = GitCommand(
Jason Chang32b59562023-07-14 16:45:35 -07002350 self,
2351 cmd,
2352 cwd=cwd,
2353 capture_stdout=True,
2354 capture_stderr=True,
2355 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00002356 )
Jason Chang32b59562023-07-14 16:45:35 -07002357 command.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00002358
2359 def _RemoteFetch(
2360 self,
2361 name=None,
2362 current_branch_only=False,
2363 initial=False,
2364 quiet=False,
2365 verbose=False,
2366 output_redir=None,
2367 alt_dir=None,
2368 tags=True,
2369 prune=False,
2370 depth=None,
2371 submodules=False,
2372 ssh_proxy=None,
2373 force_sync=False,
2374 clone_filter=None,
2375 retry_fetches=2,
2376 retry_sleep_initial_sec=4.0,
2377 retry_exp_factor=2.0,
Jason Chang32b59562023-07-14 16:45:35 -07002378 ) -> bool:
Gavin Makea2e3302023-03-11 06:46:20 +00002379 is_sha1 = False
2380 tag_name = None
2381 # The depth should not be used when fetching to a mirror because
2382 # it will result in a shallow repository that cannot be cloned or
2383 # fetched from.
2384 # The repo project should also never be synced with partial depth.
2385 if self.manifest.IsMirror or self.relpath == ".repo/repo":
2386 depth = None
2387
2388 if depth:
2389 current_branch_only = True
2390
2391 if ID_RE.match(self.revisionExpr) is not None:
2392 is_sha1 = True
2393
2394 if current_branch_only:
2395 if self.revisionExpr.startswith(R_TAGS):
2396 # This is a tag and its commit id should never change.
2397 tag_name = self.revisionExpr[len(R_TAGS) :]
2398 elif self.upstream and self.upstream.startswith(R_TAGS):
2399 # This is a tag and its commit id should never change.
2400 tag_name = self.upstream[len(R_TAGS) :]
2401
2402 if is_sha1 or tag_name is not None:
2403 if self._CheckForImmutableRevision():
2404 if verbose:
2405 print(
2406 "Skipped fetching project %s (already have "
2407 "persistent ref)" % self.name
2408 )
2409 return True
2410 if is_sha1 and not depth:
2411 # When syncing a specific commit and --depth is not set:
2412 # * if upstream is explicitly specified and is not a sha1, fetch
2413 # only upstream as users expect only upstream to be fetch.
2414 # Note: The commit might not be in upstream in which case the
2415 # sync will fail.
2416 # * otherwise, fetch all branches to make sure we end up with
2417 # the specific commit.
2418 if self.upstream:
2419 current_branch_only = not ID_RE.match(self.upstream)
2420 else:
2421 current_branch_only = False
2422
2423 if not name:
2424 name = self.remote.name
2425
2426 remote = self.GetRemote(name)
2427 if not remote.PreConnectFetch(ssh_proxy):
2428 ssh_proxy = None
2429
2430 if initial:
2431 if alt_dir and "objects" == os.path.basename(alt_dir):
2432 ref_dir = os.path.dirname(alt_dir)
2433 packed_refs = os.path.join(self.gitdir, "packed-refs")
2434
2435 all_refs = self.bare_ref.all
2436 ids = set(all_refs.values())
2437 tmp = set()
2438
2439 for r, ref_id in GitRefs(ref_dir).all.items():
2440 if r not in all_refs:
2441 if r.startswith(R_TAGS) or remote.WritesTo(r):
2442 all_refs[r] = ref_id
2443 ids.add(ref_id)
2444 continue
2445
2446 if ref_id in ids:
2447 continue
2448
2449 r = "refs/_alt/%s" % ref_id
2450 all_refs[r] = ref_id
2451 ids.add(ref_id)
2452 tmp.add(r)
2453
2454 tmp_packed_lines = []
2455 old_packed_lines = []
2456
2457 for r in sorted(all_refs):
2458 line = "%s %s\n" % (all_refs[r], r)
2459 tmp_packed_lines.append(line)
2460 if r not in tmp:
2461 old_packed_lines.append(line)
2462
2463 tmp_packed = "".join(tmp_packed_lines)
2464 old_packed = "".join(old_packed_lines)
2465 _lwrite(packed_refs, tmp_packed)
2466 else:
2467 alt_dir = None
2468
2469 cmd = ["fetch"]
2470
2471 if clone_filter:
2472 git_require((2, 19, 0), fail=True, msg="partial clones")
2473 cmd.append("--filter=%s" % clone_filter)
2474 self.EnableRepositoryExtension("partialclone", self.remote.name)
2475
2476 if depth:
2477 cmd.append("--depth=%s" % depth)
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002478 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002479 # If this repo has shallow objects, then we don't know which refs
2480 # have shallow objects or not. Tell git to unshallow all fetched
2481 # refs. Don't do this with projects that don't have shallow
2482 # objects, since it is less efficient.
2483 if os.path.exists(os.path.join(self.gitdir, "shallow")):
2484 cmd.append("--depth=2147483647")
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002485
Gavin Makea2e3302023-03-11 06:46:20 +00002486 if not verbose:
2487 cmd.append("--quiet")
2488 if not quiet and sys.stdout.isatty():
2489 cmd.append("--progress")
2490 if not self.worktree:
2491 cmd.append("--update-head-ok")
2492 cmd.append(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002493
Gavin Makea2e3302023-03-11 06:46:20 +00002494 if force_sync:
2495 cmd.append("--force")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002496
Gavin Makea2e3302023-03-11 06:46:20 +00002497 if prune:
2498 cmd.append("--prune")
Mike Frysinger29626b42021-05-01 09:37:13 -04002499
Gavin Makea2e3302023-03-11 06:46:20 +00002500 # Always pass something for --recurse-submodules, git with GIT_DIR
2501 # behaves incorrectly when not given `--recurse-submodules=no`.
2502 # (b/218891912)
2503 cmd.append(
2504 f'--recurse-submodules={"on-demand" if submodules else "no"}'
2505 )
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002506
Gavin Makea2e3302023-03-11 06:46:20 +00002507 spec = []
2508 if not current_branch_only:
2509 # Fetch whole repo.
2510 spec.append(
2511 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2512 )
2513 elif tag_name is not None:
2514 spec.append("tag")
2515 spec.append(tag_name)
Remy Böhmer1469c282020-12-15 18:49:02 +01002516
Gavin Makea2e3302023-03-11 06:46:20 +00002517 if self.manifest.IsMirror and not current_branch_only:
2518 branch = None
Remy Böhmer1469c282020-12-15 18:49:02 +01002519 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002520 branch = self.revisionExpr
2521 if (
2522 not self.manifest.IsMirror
2523 and is_sha1
2524 and depth
2525 and git_require((1, 8, 3))
2526 ):
2527 # Shallow checkout of a specific commit, fetch from that commit and
2528 # not the heads only as the commit might be deeper in the history.
2529 spec.append(branch)
2530 if self.upstream:
2531 spec.append(self.upstream)
David James8d201162013-10-11 17:03:19 -07002532 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002533 if is_sha1:
2534 branch = self.upstream
2535 if branch is not None and branch.strip():
2536 if not branch.startswith("refs/"):
2537 branch = R_HEADS + branch
2538 spec.append(str(("+%s:" % branch) + remote.ToLocal(branch)))
David James8d201162013-10-11 17:03:19 -07002539
Gavin Makea2e3302023-03-11 06:46:20 +00002540 # If mirroring repo and we cannot deduce the tag or branch to fetch,
2541 # fetch whole repo.
2542 if self.manifest.IsMirror and not spec:
2543 spec.append(
2544 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2545 )
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002546
Gavin Makea2e3302023-03-11 06:46:20 +00002547 # If using depth then we should not get all the tags since they may
2548 # be outside of the depth.
2549 if not tags or depth:
2550 cmd.append("--no-tags")
2551 else:
2552 cmd.append("--tags")
2553 spec.append(str(("+refs/tags/*:") + remote.ToLocal("refs/tags/*")))
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002554
Gavin Makea2e3302023-03-11 06:46:20 +00002555 cmd.extend(spec)
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002556
Gavin Makea2e3302023-03-11 06:46:20 +00002557 # At least one retry minimum due to git remote prune.
2558 retry_fetches = max(retry_fetches, 2)
2559 retry_cur_sleep = retry_sleep_initial_sec
2560 ok = prune_tried = False
2561 for try_n in range(retry_fetches):
Jason Chang32b59562023-07-14 16:45:35 -07002562 verify_command = try_n == retry_fetches - 1
Gavin Makea2e3302023-03-11 06:46:20 +00002563 gitcmd = GitCommand(
2564 self,
2565 cmd,
2566 bare=True,
2567 objdir=os.path.join(self.objdir, "objects"),
2568 ssh_proxy=ssh_proxy,
2569 merge_output=True,
2570 capture_stdout=quiet or bool(output_redir),
Jason Chang32b59562023-07-14 16:45:35 -07002571 verify_command=verify_command,
Gavin Makea2e3302023-03-11 06:46:20 +00002572 )
2573 if gitcmd.stdout and not quiet and output_redir:
2574 output_redir.write(gitcmd.stdout)
2575 ret = gitcmd.Wait()
2576 if ret == 0:
2577 ok = True
2578 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002579
Gavin Makea2e3302023-03-11 06:46:20 +00002580 # Retry later due to HTTP 429 Too Many Requests.
2581 elif (
2582 gitcmd.stdout
2583 and "error:" in gitcmd.stdout
2584 and "HTTP 429" in gitcmd.stdout
2585 ):
2586 # Fallthru to sleep+retry logic at the bottom.
2587 pass
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002588
Gavin Makea2e3302023-03-11 06:46:20 +00002589 # Try to prune remote branches once in case there are conflicts.
2590 # For example, if the remote had refs/heads/upstream, but deleted
2591 # that and now has refs/heads/upstream/foo.
2592 elif (
2593 gitcmd.stdout
2594 and "error:" in gitcmd.stdout
2595 and "git remote prune" in gitcmd.stdout
2596 and not prune_tried
2597 ):
2598 prune_tried = True
2599 prunecmd = GitCommand(
2600 self,
2601 ["remote", "prune", name],
2602 bare=True,
2603 ssh_proxy=ssh_proxy,
2604 )
2605 ret = prunecmd.Wait()
2606 if ret:
2607 break
2608 print(
2609 "retrying fetch after pruning remote branches",
2610 file=output_redir,
2611 )
2612 # Continue right away so we don't sleep as we shouldn't need to.
2613 continue
2614 elif current_branch_only and is_sha1 and ret == 128:
2615 # Exit code 128 means "couldn't find the ref you asked for"; if
2616 # we're in sha1 mode, we just tried sync'ing from the upstream
2617 # field; it doesn't exist, thus abort the optimization attempt
2618 # and do a full sync.
2619 break
2620 elif ret < 0:
2621 # Git died with a signal, exit immediately.
2622 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002623
Gavin Makea2e3302023-03-11 06:46:20 +00002624 # Figure out how long to sleep before the next attempt, if there is
2625 # one.
2626 if not verbose and gitcmd.stdout:
2627 print(
2628 "\n%s:\n%s" % (self.name, gitcmd.stdout),
2629 end="",
2630 file=output_redir,
2631 )
2632 if try_n < retry_fetches - 1:
2633 print(
2634 "%s: sleeping %s seconds before retrying"
2635 % (self.name, retry_cur_sleep),
2636 file=output_redir,
2637 )
2638 time.sleep(retry_cur_sleep)
2639 retry_cur_sleep = min(
2640 retry_exp_factor * retry_cur_sleep, MAXIMUM_RETRY_SLEEP_SEC
2641 )
2642 retry_cur_sleep *= 1 - random.uniform(
2643 -RETRY_JITTER_PERCENT, RETRY_JITTER_PERCENT
2644 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002645
Gavin Makea2e3302023-03-11 06:46:20 +00002646 if initial:
2647 if alt_dir:
2648 if old_packed != "":
2649 _lwrite(packed_refs, old_packed)
2650 else:
2651 platform_utils.remove(packed_refs)
2652 self.bare_git.pack_refs("--all", "--prune")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002653
Gavin Makea2e3302023-03-11 06:46:20 +00002654 if is_sha1 and current_branch_only:
2655 # We just synced the upstream given branch; verify we
2656 # got what we wanted, else trigger a second run of all
2657 # refs.
2658 if not self._CheckForImmutableRevision():
2659 # Sync the current branch only with depth set to None.
2660 # We always pass depth=None down to avoid infinite recursion.
2661 return self._RemoteFetch(
2662 name=name,
2663 quiet=quiet,
2664 verbose=verbose,
2665 output_redir=output_redir,
2666 current_branch_only=current_branch_only and depth,
2667 initial=False,
2668 alt_dir=alt_dir,
Nasser Grainawiaafed292023-05-24 12:51:03 -06002669 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00002670 depth=None,
2671 ssh_proxy=ssh_proxy,
2672 clone_filter=clone_filter,
2673 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002674
Gavin Makea2e3302023-03-11 06:46:20 +00002675 return ok
Mike Frysingerf4545122019-11-11 04:34:16 -05002676
Gavin Makea2e3302023-03-11 06:46:20 +00002677 def _ApplyCloneBundle(self, initial=False, quiet=False, verbose=False):
2678 if initial and (
2679 self.manifest.manifestProject.depth or self.clone_depth
2680 ):
2681 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002682
Gavin Makea2e3302023-03-11 06:46:20 +00002683 remote = self.GetRemote()
2684 bundle_url = remote.url + "/clone.bundle"
2685 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
2686 if GetSchemeFromUrl(bundle_url) not in (
2687 "http",
2688 "https",
2689 "persistent-http",
2690 "persistent-https",
2691 ):
2692 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06002693
Gavin Makea2e3302023-03-11 06:46:20 +00002694 bundle_dst = os.path.join(self.gitdir, "clone.bundle")
2695 bundle_tmp = os.path.join(self.gitdir, "clone.bundle.tmp")
2696
2697 exist_dst = os.path.exists(bundle_dst)
2698 exist_tmp = os.path.exists(bundle_tmp)
2699
2700 if not initial and not exist_dst and not exist_tmp:
2701 return False
2702
2703 if not exist_dst:
2704 exist_dst = self._FetchBundle(
2705 bundle_url, bundle_tmp, bundle_dst, quiet, verbose
2706 )
2707 if not exist_dst:
2708 return False
2709
2710 cmd = ["fetch"]
2711 if not verbose:
2712 cmd.append("--quiet")
2713 if not quiet and sys.stdout.isatty():
2714 cmd.append("--progress")
2715 if not self.worktree:
2716 cmd.append("--update-head-ok")
2717 cmd.append(bundle_dst)
2718 for f in remote.fetch:
2719 cmd.append(str(f))
2720 cmd.append("+refs/tags/*:refs/tags/*")
2721
2722 ok = (
2723 GitCommand(
2724 self,
2725 cmd,
2726 bare=True,
2727 objdir=os.path.join(self.objdir, "objects"),
2728 ).Wait()
2729 == 0
2730 )
2731 platform_utils.remove(bundle_dst, missing_ok=True)
2732 platform_utils.remove(bundle_tmp, missing_ok=True)
2733 return ok
2734
2735 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
2736 platform_utils.remove(dstPath, missing_ok=True)
2737
2738 cmd = ["curl", "--fail", "--output", tmpPath, "--netrc", "--location"]
2739 if quiet:
2740 cmd += ["--silent", "--show-error"]
2741 if os.path.exists(tmpPath):
2742 size = os.stat(tmpPath).st_size
2743 if size >= 1024:
2744 cmd += ["--continue-at", "%d" % (size,)]
2745 else:
2746 platform_utils.remove(tmpPath)
2747 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
2748 if cookiefile:
2749 cmd += ["--cookie", cookiefile]
2750 if proxy:
2751 cmd += ["--proxy", proxy]
2752 elif "http_proxy" in os.environ and "darwin" == sys.platform:
2753 cmd += ["--proxy", os.environ["http_proxy"]]
2754 if srcUrl.startswith("persistent-https"):
2755 srcUrl = "http" + srcUrl[len("persistent-https") :]
2756 elif srcUrl.startswith("persistent-http"):
2757 srcUrl = "http" + srcUrl[len("persistent-http") :]
2758 cmd += [srcUrl]
2759
2760 proc = None
2761 with Trace("Fetching bundle: %s", " ".join(cmd)):
2762 if verbose:
2763 print("%s: Downloading bundle: %s" % (self.name, srcUrl))
2764 stdout = None if verbose else subprocess.PIPE
2765 stderr = None if verbose else subprocess.STDOUT
2766 try:
2767 proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
2768 except OSError:
2769 return False
2770
2771 (output, _) = proc.communicate()
2772 curlret = proc.returncode
2773
2774 if curlret == 22:
2775 # From curl man page:
2776 # 22: HTTP page not retrieved. The requested url was not found
2777 # or returned another error with the HTTP error code being 400
2778 # or above. This return code only appears if -f, --fail is used.
2779 if verbose:
2780 print(
2781 "%s: Unable to retrieve clone.bundle; ignoring."
2782 % self.name
2783 )
2784 if output:
2785 print("Curl output:\n%s" % output)
2786 return False
2787 elif curlret and not verbose and output:
2788 print("%s" % output, file=sys.stderr)
2789
2790 if os.path.exists(tmpPath):
2791 if curlret == 0 and self._IsValidBundle(tmpPath, quiet):
2792 platform_utils.rename(tmpPath, dstPath)
2793 return True
2794 else:
2795 platform_utils.remove(tmpPath)
2796 return False
2797 else:
2798 return False
2799
2800 def _IsValidBundle(self, path, quiet):
2801 try:
2802 with open(path, "rb") as f:
2803 if f.read(16) == b"# v2 git bundle\n":
2804 return True
2805 else:
2806 if not quiet:
2807 print(
2808 "Invalid clone.bundle file; ignoring.",
2809 file=sys.stderr,
2810 )
2811 return False
2812 except OSError:
2813 return False
2814
2815 def _Checkout(self, rev, quiet=False):
2816 cmd = ["checkout"]
2817 if quiet:
2818 cmd.append("-q")
2819 cmd.append(rev)
2820 cmd.append("--")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002821 if GitCommand(self, cmd).Wait() != 0:
Gavin Makea2e3302023-03-11 06:46:20 +00002822 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002823 raise GitError(
2824 "%s checkout %s " % (self.name, rev), project=self.name
2825 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002826
Gavin Makea2e3302023-03-11 06:46:20 +00002827 def _CherryPick(self, rev, ffonly=False, record_origin=False):
2828 cmd = ["cherry-pick"]
2829 if ffonly:
2830 cmd.append("--ff")
2831 if record_origin:
2832 cmd.append("-x")
2833 cmd.append(rev)
2834 cmd.append("--")
2835 if GitCommand(self, cmd).Wait() != 0:
2836 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002837 raise GitError(
2838 "%s cherry-pick %s " % (self.name, rev), project=self.name
2839 )
Victor Boivie0960b5b2010-11-26 13:42:13 +01002840
Gavin Makea2e3302023-03-11 06:46:20 +00002841 def _LsRemote(self, refs):
2842 cmd = ["ls-remote", self.remote.name, refs]
2843 p = GitCommand(self, cmd, capture_stdout=True)
2844 if p.Wait() == 0:
2845 return p.stdout
2846 return None
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002847
Gavin Makea2e3302023-03-11 06:46:20 +00002848 def _Revert(self, rev):
2849 cmd = ["revert"]
2850 cmd.append("--no-edit")
2851 cmd.append(rev)
2852 cmd.append("--")
2853 if GitCommand(self, cmd).Wait() != 0:
2854 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002855 raise GitError(
2856 "%s revert %s " % (self.name, rev), project=self.name
2857 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002858
Gavin Makea2e3302023-03-11 06:46:20 +00002859 def _ResetHard(self, rev, quiet=True):
2860 cmd = ["reset", "--hard"]
2861 if quiet:
2862 cmd.append("-q")
2863 cmd.append(rev)
2864 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002865 raise GitError(
2866 "%s reset --hard %s " % (self.name, rev), project=self.name
2867 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002868
Gavin Makea2e3302023-03-11 06:46:20 +00002869 def _SyncSubmodules(self, quiet=True):
2870 cmd = ["submodule", "update", "--init", "--recursive"]
2871 if quiet:
2872 cmd.append("-q")
2873 if GitCommand(self, cmd).Wait() != 0:
2874 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07002875 "%s submodule update --init --recursive " % self.name,
2876 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00002877 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002878
Gavin Makea2e3302023-03-11 06:46:20 +00002879 def _Rebase(self, upstream, onto=None):
2880 cmd = ["rebase"]
2881 if onto is not None:
2882 cmd.extend(["--onto", onto])
2883 cmd.append(upstream)
2884 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002885 raise GitError(
2886 "%s rebase %s " % (self.name, upstream), project=self.name
2887 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002888
Gavin Makea2e3302023-03-11 06:46:20 +00002889 def _FastForward(self, head, ffonly=False):
2890 cmd = ["merge", "--no-stat", head]
2891 if ffonly:
2892 cmd.append("--ff-only")
2893 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002894 raise GitError(
2895 "%s merge %s " % (self.name, head), project=self.name
2896 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002897
Gavin Makea2e3302023-03-11 06:46:20 +00002898 def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False):
2899 init_git_dir = not os.path.exists(self.gitdir)
2900 init_obj_dir = not os.path.exists(self.objdir)
2901 try:
2902 # Initialize the bare repository, which contains all of the objects.
2903 if init_obj_dir:
2904 os.makedirs(self.objdir)
2905 self.bare_objdir.init()
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002906
Gavin Makea2e3302023-03-11 06:46:20 +00002907 self._UpdateHooks(quiet=quiet)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002908
Gavin Makea2e3302023-03-11 06:46:20 +00002909 if self.use_git_worktrees:
2910 # Enable per-worktree config file support if possible. This
2911 # is more a nice-to-have feature for users rather than a
2912 # hard requirement.
2913 if git_require((2, 20, 0)):
2914 self.EnableRepositoryExtension("worktreeConfig")
Renaud Paquay788e9622017-01-27 11:41:12 -08002915
Gavin Makea2e3302023-03-11 06:46:20 +00002916 # If we have a separate directory to hold refs, initialize it as
2917 # well.
2918 if self.objdir != self.gitdir:
2919 if init_git_dir:
2920 os.makedirs(self.gitdir)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002921
Gavin Makea2e3302023-03-11 06:46:20 +00002922 if init_obj_dir or init_git_dir:
2923 self._ReferenceGitDir(
2924 self.objdir, self.gitdir, copy_all=True
2925 )
2926 try:
2927 self._CheckDirReference(self.objdir, self.gitdir)
2928 except GitError as e:
2929 if force_sync:
2930 print(
2931 "Retrying clone after deleting %s" % self.gitdir,
2932 file=sys.stderr,
2933 )
2934 try:
2935 platform_utils.rmtree(
2936 platform_utils.realpath(self.gitdir)
2937 )
2938 if self.worktree and os.path.exists(
2939 platform_utils.realpath(self.worktree)
2940 ):
2941 platform_utils.rmtree(
2942 platform_utils.realpath(self.worktree)
2943 )
2944 return self._InitGitDir(
2945 mirror_git=mirror_git,
2946 force_sync=False,
2947 quiet=quiet,
2948 )
2949 except Exception:
2950 raise e
2951 raise e
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002952
Gavin Makea2e3302023-03-11 06:46:20 +00002953 if init_git_dir:
2954 mp = self.manifest.manifestProject
2955 ref_dir = mp.reference or ""
Julien Camperguedd654222014-01-09 16:21:37 +01002956
Gavin Makea2e3302023-03-11 06:46:20 +00002957 def _expanded_ref_dirs():
2958 """Iterate through possible git reference dir paths."""
2959 name = self.name + ".git"
2960 yield mirror_git or os.path.join(ref_dir, name)
2961 for prefix in "", self.remote.name:
2962 yield os.path.join(
2963 ref_dir, ".repo", "project-objects", prefix, name
2964 )
2965 yield os.path.join(
2966 ref_dir, ".repo", "worktrees", prefix, name
2967 )
2968
2969 if ref_dir or mirror_git:
2970 found_ref_dir = None
2971 for path in _expanded_ref_dirs():
2972 if os.path.exists(path):
2973 found_ref_dir = path
2974 break
2975 ref_dir = found_ref_dir
2976
2977 if ref_dir:
2978 if not os.path.isabs(ref_dir):
2979 # The alternate directory is relative to the object
2980 # database.
2981 ref_dir = os.path.relpath(
2982 ref_dir, os.path.join(self.objdir, "objects")
2983 )
2984 _lwrite(
2985 os.path.join(
2986 self.objdir, "objects/info/alternates"
2987 ),
2988 os.path.join(ref_dir, "objects") + "\n",
2989 )
2990
2991 m = self.manifest.manifestProject.config
2992 for key in ["user.name", "user.email"]:
2993 if m.Has(key, include_defaults=False):
2994 self.config.SetString(key, m.GetString(key))
2995 if not self.manifest.EnableGitLfs:
2996 self.config.SetString(
2997 "filter.lfs.smudge", "git-lfs smudge --skip -- %f"
2998 )
2999 self.config.SetString(
3000 "filter.lfs.process", "git-lfs filter-process --skip"
3001 )
3002 self.config.SetBoolean(
3003 "core.bare", True if self.manifest.IsMirror else None
3004 )
3005 except Exception:
3006 if init_obj_dir and os.path.exists(self.objdir):
3007 platform_utils.rmtree(self.objdir)
3008 if init_git_dir and os.path.exists(self.gitdir):
3009 platform_utils.rmtree(self.gitdir)
3010 raise
3011
3012 def _UpdateHooks(self, quiet=False):
3013 if os.path.exists(self.objdir):
3014 self._InitHooks(quiet=quiet)
3015
3016 def _InitHooks(self, quiet=False):
3017 hooks = platform_utils.realpath(os.path.join(self.objdir, "hooks"))
3018 if not os.path.exists(hooks):
3019 os.makedirs(hooks)
3020
3021 # Delete sample hooks. They're noise.
3022 for hook in glob.glob(os.path.join(hooks, "*.sample")):
3023 try:
3024 platform_utils.remove(hook, missing_ok=True)
3025 except PermissionError:
3026 pass
3027
3028 for stock_hook in _ProjectHooks():
3029 name = os.path.basename(stock_hook)
3030
3031 if (
3032 name in ("commit-msg",)
3033 and not self.remote.review
3034 and self is not self.manifest.manifestProject
3035 ):
3036 # Don't install a Gerrit Code Review hook if this
3037 # project does not appear to use it for reviews.
3038 #
3039 # Since the manifest project is one of those, but also
3040 # managed through gerrit, it's excluded.
3041 continue
3042
3043 dst = os.path.join(hooks, name)
3044 if platform_utils.islink(dst):
3045 continue
3046 if os.path.exists(dst):
3047 # If the files are the same, we'll leave it alone. We create
3048 # symlinks below by default but fallback to hardlinks if the OS
3049 # blocks them. So if we're here, it's probably because we made a
3050 # hardlink below.
3051 if not filecmp.cmp(stock_hook, dst, shallow=False):
3052 if not quiet:
3053 _warn(
3054 "%s: Not replacing locally modified %s hook",
3055 self.RelPath(local=False),
3056 name,
3057 )
3058 continue
3059 try:
3060 platform_utils.symlink(
3061 os.path.relpath(stock_hook, os.path.dirname(dst)), dst
3062 )
3063 except OSError as e:
3064 if e.errno == errno.EPERM:
3065 try:
3066 os.link(stock_hook, dst)
3067 except OSError:
Jason Chang32b59562023-07-14 16:45:35 -07003068 raise GitError(
3069 self._get_symlink_error_message(), project=self.name
3070 )
Gavin Makea2e3302023-03-11 06:46:20 +00003071 else:
3072 raise
3073
3074 def _InitRemote(self):
3075 if self.remote.url:
3076 remote = self.GetRemote()
3077 remote.url = self.remote.url
3078 remote.pushUrl = self.remote.pushUrl
3079 remote.review = self.remote.review
3080 remote.projectname = self.name
3081
3082 if self.worktree:
3083 remote.ResetFetch(mirror=False)
3084 else:
3085 remote.ResetFetch(mirror=True)
3086 remote.Save()
3087
3088 def _InitMRef(self):
3089 """Initialize the pseudo m/<manifest branch> ref."""
3090 if self.manifest.branch:
3091 if self.use_git_worktrees:
3092 # Set up the m/ space to point to the worktree-specific ref
3093 # space. We'll update the worktree-specific ref space on each
3094 # checkout.
3095 ref = R_M + self.manifest.branch
3096 if not self.bare_ref.symref(ref):
3097 self.bare_git.symbolic_ref(
3098 "-m",
3099 "redirecting to worktree scope",
3100 ref,
3101 R_WORKTREE_M + self.manifest.branch,
3102 )
3103
3104 # We can't update this ref with git worktrees until it exists.
3105 # We'll wait until the initial checkout to set it.
3106 if not os.path.exists(self.worktree):
3107 return
3108
3109 base = R_WORKTREE_M
3110 active_git = self.work_git
3111
3112 self._InitAnyMRef(HEAD, self.bare_git, detach=True)
3113 else:
3114 base = R_M
3115 active_git = self.bare_git
3116
3117 self._InitAnyMRef(base + self.manifest.branch, active_git)
3118
3119 def _InitMirrorHead(self):
3120 self._InitAnyMRef(HEAD, self.bare_git)
3121
3122 def _InitAnyMRef(self, ref, active_git, detach=False):
3123 """Initialize |ref| in |active_git| to the value in the manifest.
3124
3125 This points |ref| to the <project> setting in the manifest.
3126
3127 Args:
3128 ref: The branch to update.
3129 active_git: The git repository to make updates in.
3130 detach: Whether to update target of symbolic refs, or overwrite the
3131 ref directly (and thus make it non-symbolic).
3132 """
3133 cur = self.bare_ref.symref(ref)
3134
3135 if self.revisionId:
3136 if cur != "" or self.bare_ref.get(ref) != self.revisionId:
3137 msg = "manifest set to %s" % self.revisionId
3138 dst = self.revisionId + "^0"
3139 active_git.UpdateRef(ref, dst, message=msg, detach=True)
Julien Camperguedd654222014-01-09 16:21:37 +01003140 else:
Gavin Makea2e3302023-03-11 06:46:20 +00003141 remote = self.GetRemote()
3142 dst = remote.ToLocal(self.revisionExpr)
3143 if cur != dst:
3144 msg = "manifest set to %s" % self.revisionExpr
3145 if detach:
3146 active_git.UpdateRef(ref, dst, message=msg, detach=True)
3147 else:
3148 active_git.symbolic_ref("-m", msg, ref, dst)
Julien Camperguedd654222014-01-09 16:21:37 +01003149
Gavin Makea2e3302023-03-11 06:46:20 +00003150 def _CheckDirReference(self, srcdir, destdir):
3151 # Git worktrees don't use symlinks to share at all.
3152 if self.use_git_worktrees:
3153 return
Julien Camperguedd654222014-01-09 16:21:37 +01003154
Gavin Makea2e3302023-03-11 06:46:20 +00003155 for name in self.shareable_dirs:
3156 # Try to self-heal a bit in simple cases.
3157 dst_path = os.path.join(destdir, name)
3158 src_path = os.path.join(srcdir, name)
Julien Camperguedd654222014-01-09 16:21:37 +01003159
Gavin Makea2e3302023-03-11 06:46:20 +00003160 dst = platform_utils.realpath(dst_path)
3161 if os.path.lexists(dst):
3162 src = platform_utils.realpath(src_path)
3163 # Fail if the links are pointing to the wrong place.
3164 if src != dst:
3165 _error("%s is different in %s vs %s", name, destdir, srcdir)
3166 raise GitError(
3167 "--force-sync not enabled; cannot overwrite a local "
3168 "work tree. If you're comfortable with the "
3169 "possibility of losing the work tree's git metadata,"
3170 " use `repo sync --force-sync {0}` to "
Jason Chang32b59562023-07-14 16:45:35 -07003171 "proceed.".format(self.RelPath(local=False)),
3172 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003173 )
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003174
Gavin Makea2e3302023-03-11 06:46:20 +00003175 def _ReferenceGitDir(self, gitdir, dotgit, copy_all):
3176 """Update |dotgit| to reference |gitdir|, using symlinks where possible.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003177
Gavin Makea2e3302023-03-11 06:46:20 +00003178 Args:
3179 gitdir: The bare git repository. Must already be initialized.
3180 dotgit: The repository you would like to initialize.
3181 copy_all: If true, copy all remaining files from |gitdir| ->
3182 |dotgit|. This saves you the effort of initializing |dotgit|
3183 yourself.
3184 """
3185 symlink_dirs = self.shareable_dirs[:]
3186 to_symlink = symlink_dirs
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003187
Gavin Makea2e3302023-03-11 06:46:20 +00003188 to_copy = []
3189 if copy_all:
3190 to_copy = platform_utils.listdir(gitdir)
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003191
Gavin Makea2e3302023-03-11 06:46:20 +00003192 dotgit = platform_utils.realpath(dotgit)
3193 for name in set(to_copy).union(to_symlink):
3194 try:
3195 src = platform_utils.realpath(os.path.join(gitdir, name))
3196 dst = os.path.join(dotgit, name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003197
Gavin Makea2e3302023-03-11 06:46:20 +00003198 if os.path.lexists(dst):
3199 continue
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003200
Gavin Makea2e3302023-03-11 06:46:20 +00003201 # If the source dir doesn't exist, create an empty dir.
3202 if name in symlink_dirs and not os.path.lexists(src):
3203 os.makedirs(src)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003204
Gavin Makea2e3302023-03-11 06:46:20 +00003205 if name in to_symlink:
3206 platform_utils.symlink(
3207 os.path.relpath(src, os.path.dirname(dst)), dst
3208 )
3209 elif copy_all and not platform_utils.islink(dst):
3210 if platform_utils.isdir(src):
3211 shutil.copytree(src, dst)
3212 elif os.path.isfile(src):
3213 shutil.copy(src, dst)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003214
Gavin Makea2e3302023-03-11 06:46:20 +00003215 except OSError as e:
3216 if e.errno == errno.EPERM:
3217 raise DownloadError(self._get_symlink_error_message())
3218 else:
3219 raise
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003220
Gavin Makea2e3302023-03-11 06:46:20 +00003221 def _InitGitWorktree(self):
3222 """Init the project using git worktrees."""
3223 self.bare_git.worktree("prune")
3224 self.bare_git.worktree(
3225 "add",
3226 "-ff",
3227 "--checkout",
3228 "--detach",
3229 "--lock",
3230 self.worktree,
3231 self.GetRevisionId(),
3232 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003233
Gavin Makea2e3302023-03-11 06:46:20 +00003234 # Rewrite the internal state files to use relative paths between the
3235 # checkouts & worktrees.
3236 dotgit = os.path.join(self.worktree, ".git")
3237 with open(dotgit, "r") as fp:
3238 # Figure out the checkout->worktree path.
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003239 setting = fp.read()
Gavin Makea2e3302023-03-11 06:46:20 +00003240 assert setting.startswith("gitdir:")
3241 git_worktree_path = setting.split(":", 1)[1].strip()
3242 # Some platforms (e.g. Windows) won't let us update dotgit in situ
3243 # because of file permissions. Delete it and recreate it from scratch
3244 # to avoid.
3245 platform_utils.remove(dotgit)
3246 # Use relative path from checkout->worktree & maintain Unix line endings
3247 # on all OS's to match git behavior.
3248 with open(dotgit, "w", newline="\n") as fp:
3249 print(
3250 "gitdir:",
3251 os.path.relpath(git_worktree_path, self.worktree),
3252 file=fp,
3253 )
3254 # Use relative path from worktree->checkout & maintain Unix line endings
3255 # on all OS's to match git behavior.
3256 with open(
3257 os.path.join(git_worktree_path, "gitdir"), "w", newline="\n"
3258 ) as fp:
3259 print(os.path.relpath(dotgit, git_worktree_path), file=fp)
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003260
Gavin Makea2e3302023-03-11 06:46:20 +00003261 self._InitMRef()
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003262
Gavin Makea2e3302023-03-11 06:46:20 +00003263 def _InitWorkTree(self, force_sync=False, submodules=False):
3264 """Setup the worktree .git path.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003265
Gavin Makea2e3302023-03-11 06:46:20 +00003266 This is the user-visible path like src/foo/.git/.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003267
Gavin Makea2e3302023-03-11 06:46:20 +00003268 With non-git-worktrees, this will be a symlink to the .repo/projects/
3269 path. With git-worktrees, this will be a .git file using "gitdir: ..."
3270 syntax.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003271
Gavin Makea2e3302023-03-11 06:46:20 +00003272 Older checkouts had .git/ directories. If we see that, migrate it.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003273
Gavin Makea2e3302023-03-11 06:46:20 +00003274 This also handles changes in the manifest. Maybe this project was
3275 backed by "foo/bar" on the server, but now it's "new/foo/bar". We have
3276 to update the path we point to under .repo/projects/ to match.
3277 """
3278 dotgit = os.path.join(self.worktree, ".git")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003279
Gavin Makea2e3302023-03-11 06:46:20 +00003280 # If using an old layout style (a directory), migrate it.
3281 if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
Jason Chang32b59562023-07-14 16:45:35 -07003282 self._MigrateOldWorkTreeGitDir(dotgit, project=self.name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003283
Gavin Makea2e3302023-03-11 06:46:20 +00003284 init_dotgit = not os.path.exists(dotgit)
3285 if self.use_git_worktrees:
3286 if init_dotgit:
3287 self._InitGitWorktree()
3288 self._CopyAndLinkFiles()
3289 else:
3290 if not init_dotgit:
3291 # See if the project has changed.
3292 if platform_utils.realpath(
3293 self.gitdir
3294 ) != platform_utils.realpath(dotgit):
3295 platform_utils.remove(dotgit)
Doug Anderson37282b42011-03-04 11:54:18 -08003296
Gavin Makea2e3302023-03-11 06:46:20 +00003297 if init_dotgit or not os.path.exists(dotgit):
3298 os.makedirs(self.worktree, exist_ok=True)
3299 platform_utils.symlink(
3300 os.path.relpath(self.gitdir, self.worktree), dotgit
3301 )
Doug Anderson37282b42011-03-04 11:54:18 -08003302
Gavin Makea2e3302023-03-11 06:46:20 +00003303 if init_dotgit:
3304 _lwrite(
3305 os.path.join(dotgit, HEAD), "%s\n" % self.GetRevisionId()
3306 )
Doug Anderson37282b42011-03-04 11:54:18 -08003307
Gavin Makea2e3302023-03-11 06:46:20 +00003308 # Finish checking out the worktree.
3309 cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
3310 if GitCommand(self, cmd).Wait() != 0:
3311 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07003312 "Cannot initialize work tree for " + self.name,
3313 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003314 )
Doug Anderson37282b42011-03-04 11:54:18 -08003315
Gavin Makea2e3302023-03-11 06:46:20 +00003316 if submodules:
3317 self._SyncSubmodules(quiet=True)
3318 self._CopyAndLinkFiles()
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003319
Gavin Makea2e3302023-03-11 06:46:20 +00003320 @classmethod
Jason Chang32b59562023-07-14 16:45:35 -07003321 def _MigrateOldWorkTreeGitDir(cls, dotgit, project=None):
Gavin Makea2e3302023-03-11 06:46:20 +00003322 """Migrate the old worktree .git/ dir style to a symlink.
3323
3324 This logic specifically only uses state from |dotgit| to figure out
3325 where to move content and not |self|. This way if the backing project
3326 also changed places, we only do the .git/ dir to .git symlink migration
3327 here. The path updates will happen independently.
3328 """
3329 # Figure out where in .repo/projects/ it's pointing to.
3330 if not os.path.islink(os.path.join(dotgit, "refs")):
Jason Chang32b59562023-07-14 16:45:35 -07003331 raise GitError(
3332 f"{dotgit}: unsupported checkout state", project=project
3333 )
Gavin Makea2e3302023-03-11 06:46:20 +00003334 gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, "refs")))
3335
3336 # Remove known symlink paths that exist in .repo/projects/.
3337 KNOWN_LINKS = {
3338 "config",
3339 "description",
3340 "hooks",
3341 "info",
3342 "logs",
3343 "objects",
3344 "packed-refs",
3345 "refs",
3346 "rr-cache",
3347 "shallow",
3348 "svn",
3349 }
3350 # Paths that we know will be in both, but are safe to clobber in
3351 # .repo/projects/.
3352 SAFE_TO_CLOBBER = {
3353 "COMMIT_EDITMSG",
3354 "FETCH_HEAD",
3355 "HEAD",
3356 "gc.log",
3357 "gitk.cache",
3358 "index",
3359 "ORIG_HEAD",
3360 }
3361
3362 # First see if we'd succeed before starting the migration.
3363 unknown_paths = []
3364 for name in platform_utils.listdir(dotgit):
3365 # Ignore all temporary/backup names. These are common with vim &
3366 # emacs.
3367 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3368 continue
3369
3370 dotgit_path = os.path.join(dotgit, name)
3371 if name in KNOWN_LINKS:
3372 if not platform_utils.islink(dotgit_path):
3373 unknown_paths.append(f"{dotgit_path}: should be a symlink")
3374 else:
3375 gitdir_path = os.path.join(gitdir, name)
3376 if name not in SAFE_TO_CLOBBER and os.path.exists(gitdir_path):
3377 unknown_paths.append(
3378 f"{dotgit_path}: unknown file; please file a bug"
3379 )
3380 if unknown_paths:
Jason Chang32b59562023-07-14 16:45:35 -07003381 raise GitError(
3382 "Aborting migration: " + "\n".join(unknown_paths),
3383 project=project,
3384 )
Gavin Makea2e3302023-03-11 06:46:20 +00003385
3386 # Now walk the paths and sync the .git/ to .repo/projects/.
3387 for name in platform_utils.listdir(dotgit):
3388 dotgit_path = os.path.join(dotgit, name)
3389
3390 # Ignore all temporary/backup names. These are common with vim &
3391 # emacs.
3392 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3393 platform_utils.remove(dotgit_path)
3394 elif name in KNOWN_LINKS:
3395 platform_utils.remove(dotgit_path)
3396 else:
3397 gitdir_path = os.path.join(gitdir, name)
3398 platform_utils.remove(gitdir_path, missing_ok=True)
3399 platform_utils.rename(dotgit_path, gitdir_path)
3400
3401 # Now that the dir should be empty, clear it out, and symlink it over.
3402 platform_utils.rmdir(dotgit)
3403 platform_utils.symlink(
3404 os.path.relpath(gitdir, os.path.dirname(dotgit)), dotgit
3405 )
3406
3407 def _get_symlink_error_message(self):
3408 if platform_utils.isWindows():
3409 return (
3410 "Unable to create symbolic link. Please re-run the command as "
3411 "Administrator, or see "
3412 "https://github.com/git-for-windows/git/wiki/Symbolic-Links "
3413 "for other options."
3414 )
3415 return "filesystem must support symlinks"
3416
3417 def _revlist(self, *args, **kw):
3418 a = []
3419 a.extend(args)
3420 a.append("--")
3421 return self.work_git.rev_list(*a, **kw)
3422
3423 @property
3424 def _allrefs(self):
3425 return self.bare_ref.all
3426
3427 def _getLogs(
3428 self, rev1, rev2, oneline=False, color=True, pretty_format=None
3429 ):
3430 """Get logs between two revisions of this project."""
3431 comp = ".."
3432 if rev1:
3433 revs = [rev1]
3434 if rev2:
3435 revs.extend([comp, rev2])
3436 cmd = ["log", "".join(revs)]
3437 out = DiffColoring(self.config)
3438 if out.is_on and color:
3439 cmd.append("--color")
3440 if pretty_format is not None:
3441 cmd.append("--pretty=format:%s" % pretty_format)
3442 if oneline:
3443 cmd.append("--oneline")
3444
3445 try:
3446 log = GitCommand(
3447 self, cmd, capture_stdout=True, capture_stderr=True
3448 )
3449 if log.Wait() == 0:
3450 return log.stdout
3451 except GitError:
3452 # worktree may not exist if groups changed for example. In that
3453 # case, try in gitdir instead.
3454 if not os.path.exists(self.worktree):
3455 return self.bare_git.log(*cmd[1:])
3456 else:
3457 raise
3458 return None
3459
3460 def getAddedAndRemovedLogs(
3461 self, toProject, oneline=False, color=True, pretty_format=None
3462 ):
3463 """Get the list of logs from this revision to given revisionId"""
3464 logs = {}
3465 selfId = self.GetRevisionId(self._allrefs)
3466 toId = toProject.GetRevisionId(toProject._allrefs)
3467
3468 logs["added"] = self._getLogs(
3469 selfId,
3470 toId,
3471 oneline=oneline,
3472 color=color,
3473 pretty_format=pretty_format,
3474 )
3475 logs["removed"] = self._getLogs(
3476 toId,
3477 selfId,
3478 oneline=oneline,
3479 color=color,
3480 pretty_format=pretty_format,
3481 )
3482 return logs
3483
3484 class _GitGetByExec(object):
3485 def __init__(self, project, bare, gitdir):
3486 self._project = project
3487 self._bare = bare
3488 self._gitdir = gitdir
3489
3490 # __getstate__ and __setstate__ are required for pickling because
3491 # __getattr__ exists.
3492 def __getstate__(self):
3493 return (self._project, self._bare, self._gitdir)
3494
3495 def __setstate__(self, state):
3496 self._project, self._bare, self._gitdir = state
3497
3498 def LsOthers(self):
3499 p = GitCommand(
3500 self._project,
3501 ["ls-files", "-z", "--others", "--exclude-standard"],
3502 bare=False,
3503 gitdir=self._gitdir,
3504 capture_stdout=True,
3505 capture_stderr=True,
3506 )
3507 if p.Wait() == 0:
3508 out = p.stdout
3509 if out:
3510 # Backslash is not anomalous.
3511 return out[:-1].split("\0")
3512 return []
3513
3514 def DiffZ(self, name, *args):
3515 cmd = [name]
3516 cmd.append("-z")
3517 cmd.append("--ignore-submodules")
3518 cmd.extend(args)
3519 p = GitCommand(
3520 self._project,
3521 cmd,
3522 gitdir=self._gitdir,
3523 bare=False,
3524 capture_stdout=True,
3525 capture_stderr=True,
3526 )
3527 p.Wait()
3528 r = {}
3529 out = p.stdout
3530 if out:
3531 out = iter(out[:-1].split("\0"))
3532 while out:
3533 try:
3534 info = next(out)
3535 path = next(out)
3536 except StopIteration:
3537 break
3538
3539 class _Info(object):
3540 def __init__(self, path, omode, nmode, oid, nid, state):
3541 self.path = path
3542 self.src_path = None
3543 self.old_mode = omode
3544 self.new_mode = nmode
3545 self.old_id = oid
3546 self.new_id = nid
3547
3548 if len(state) == 1:
3549 self.status = state
3550 self.level = None
3551 else:
3552 self.status = state[:1]
3553 self.level = state[1:]
3554 while self.level.startswith("0"):
3555 self.level = self.level[1:]
3556
3557 info = info[1:].split(" ")
3558 info = _Info(path, *info)
3559 if info.status in ("R", "C"):
3560 info.src_path = info.path
3561 info.path = next(out)
3562 r[info.path] = info
3563 return r
3564
3565 def GetDotgitPath(self, subpath=None):
3566 """Return the full path to the .git dir.
3567
3568 As a convenience, append |subpath| if provided.
3569 """
3570 if self._bare:
3571 dotgit = self._gitdir
3572 else:
3573 dotgit = os.path.join(self._project.worktree, ".git")
3574 if os.path.isfile(dotgit):
3575 # Git worktrees use a "gitdir:" syntax to point to the
3576 # scratch space.
3577 with open(dotgit) as fp:
3578 setting = fp.read()
3579 assert setting.startswith("gitdir:")
3580 gitdir = setting.split(":", 1)[1].strip()
3581 dotgit = os.path.normpath(
3582 os.path.join(self._project.worktree, gitdir)
3583 )
3584
3585 return dotgit if subpath is None else os.path.join(dotgit, subpath)
3586
3587 def GetHead(self):
3588 """Return the ref that HEAD points to."""
3589 path = self.GetDotgitPath(subpath=HEAD)
3590 try:
3591 with open(path) as fd:
3592 line = fd.readline()
3593 except IOError as e:
3594 raise NoManifestException(path, str(e))
3595 try:
3596 line = line.decode()
3597 except AttributeError:
3598 pass
3599 if line.startswith("ref: "):
3600 return line[5:-1]
3601 return line[:-1]
3602
3603 def SetHead(self, ref, message=None):
3604 cmdv = []
3605 if message is not None:
3606 cmdv.extend(["-m", message])
3607 cmdv.append(HEAD)
3608 cmdv.append(ref)
3609 self.symbolic_ref(*cmdv)
3610
3611 def DetachHead(self, new, message=None):
3612 cmdv = ["--no-deref"]
3613 if message is not None:
3614 cmdv.extend(["-m", message])
3615 cmdv.append(HEAD)
3616 cmdv.append(new)
3617 self.update_ref(*cmdv)
3618
3619 def UpdateRef(self, name, new, old=None, message=None, detach=False):
3620 cmdv = []
3621 if message is not None:
3622 cmdv.extend(["-m", message])
3623 if detach:
3624 cmdv.append("--no-deref")
3625 cmdv.append(name)
3626 cmdv.append(new)
3627 if old is not None:
3628 cmdv.append(old)
3629 self.update_ref(*cmdv)
3630
3631 def DeleteRef(self, name, old=None):
3632 if not old:
3633 old = self.rev_parse(name)
3634 self.update_ref("-d", name, old)
3635 self._project.bare_ref.deleted(name)
3636
3637 def rev_list(self, *args, **kw):
3638 if "format" in kw:
3639 cmdv = ["log", "--pretty=format:%s" % kw["format"]]
3640 else:
3641 cmdv = ["rev-list"]
3642 cmdv.extend(args)
3643 p = GitCommand(
3644 self._project,
3645 cmdv,
3646 bare=self._bare,
3647 gitdir=self._gitdir,
3648 capture_stdout=True,
3649 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003650 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00003651 )
Jason Chang32b59562023-07-14 16:45:35 -07003652 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003653 return p.stdout.splitlines()
3654
3655 def __getattr__(self, name):
3656 """Allow arbitrary git commands using pythonic syntax.
3657
3658 This allows you to do things like:
3659 git_obj.rev_parse('HEAD')
3660
3661 Since we don't have a 'rev_parse' method defined, the __getattr__
3662 will run. We'll replace the '_' with a '-' and try to run a git
3663 command. Any other positional arguments will be passed to the git
3664 command, and the following keyword arguments are supported:
3665 config: An optional dict of git config options to be passed with
3666 '-c'.
3667
3668 Args:
3669 name: The name of the git command to call. Any '_' characters
3670 will be replaced with '-'.
3671
3672 Returns:
3673 A callable object that will try to call git with the named
3674 command.
3675 """
3676 name = name.replace("_", "-")
3677
3678 def runner(*args, **kwargs):
3679 cmdv = []
3680 config = kwargs.pop("config", None)
3681 for k in kwargs:
3682 raise TypeError(
3683 "%s() got an unexpected keyword argument %r" % (name, k)
3684 )
3685 if config is not None:
3686 for k, v in config.items():
3687 cmdv.append("-c")
3688 cmdv.append("%s=%s" % (k, v))
3689 cmdv.append(name)
3690 cmdv.extend(args)
3691 p = GitCommand(
3692 self._project,
3693 cmdv,
3694 bare=self._bare,
3695 gitdir=self._gitdir,
3696 capture_stdout=True,
3697 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003698 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00003699 )
Jason Chang32b59562023-07-14 16:45:35 -07003700 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003701 r = p.stdout
3702 if r.endswith("\n") and r.index("\n") == len(r) - 1:
3703 return r[:-1]
3704 return r
3705
3706 return runner
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003707
3708
Jason Chang32b59562023-07-14 16:45:35 -07003709class LocalSyncFail(RepoError):
3710 """Default error when there is an Sync_LocalHalf error."""
3711
3712
3713class _PriorSyncFailedError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003714 def __str__(self):
3715 return "prior sync failed; rebase still in progress"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003716
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003717
Jason Chang32b59562023-07-14 16:45:35 -07003718class _DirtyError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003719 def __str__(self):
3720 return "contains uncommitted changes"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003721
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003722
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003723class _InfoMessage(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003724 def __init__(self, project, text):
3725 self.project = project
3726 self.text = text
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003727
Gavin Makea2e3302023-03-11 06:46:20 +00003728 def Print(self, syncbuf):
3729 syncbuf.out.info(
3730 "%s/: %s", self.project.RelPath(local=False), self.text
3731 )
3732 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003733
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003734
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003735class _Failure(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003736 def __init__(self, project, why):
3737 self.project = project
3738 self.why = why
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003739
Gavin Makea2e3302023-03-11 06:46:20 +00003740 def Print(self, syncbuf):
3741 syncbuf.out.fail(
3742 "error: %s/: %s", self.project.RelPath(local=False), str(self.why)
3743 )
3744 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003745
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003746
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003747class _Later(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003748 def __init__(self, project, action):
3749 self.project = project
3750 self.action = action
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003751
Gavin Makea2e3302023-03-11 06:46:20 +00003752 def Run(self, syncbuf):
3753 out = syncbuf.out
3754 out.project("project %s/", self.project.RelPath(local=False))
3755 out.nl()
3756 try:
3757 self.action()
3758 out.nl()
3759 return True
3760 except GitError:
3761 out.nl()
3762 return False
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003763
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003764
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003765class _SyncColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +00003766 def __init__(self, config):
3767 super().__init__(config, "reposync")
3768 self.project = self.printer("header", attr="bold")
3769 self.info = self.printer("info")
3770 self.fail = self.printer("fail", fg="red")
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003771
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003772
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003773class SyncBuffer(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003774 def __init__(self, config, detach_head=False):
3775 self._messages = []
3776 self._failures = []
3777 self._later_queue1 = []
3778 self._later_queue2 = []
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003779
Gavin Makea2e3302023-03-11 06:46:20 +00003780 self.out = _SyncColoring(config)
3781 self.out.redirect(sys.stderr)
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003782
Gavin Makea2e3302023-03-11 06:46:20 +00003783 self.detach_head = detach_head
3784 self.clean = True
3785 self.recent_clean = True
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003786
Gavin Makea2e3302023-03-11 06:46:20 +00003787 def info(self, project, fmt, *args):
3788 self._messages.append(_InfoMessage(project, fmt % args))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003789
Gavin Makea2e3302023-03-11 06:46:20 +00003790 def fail(self, project, err=None):
3791 self._failures.append(_Failure(project, err))
David Rileye0684ad2017-04-05 00:02:59 -07003792 self._MarkUnclean()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003793
Gavin Makea2e3302023-03-11 06:46:20 +00003794 def later1(self, project, what):
3795 self._later_queue1.append(_Later(project, what))
Mike Frysinger70d861f2019-08-26 15:22:36 -04003796
Gavin Makea2e3302023-03-11 06:46:20 +00003797 def later2(self, project, what):
3798 self._later_queue2.append(_Later(project, what))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003799
Gavin Makea2e3302023-03-11 06:46:20 +00003800 def Finish(self):
3801 self._PrintMessages()
3802 self._RunLater()
3803 self._PrintMessages()
3804 return self.clean
3805
3806 def Recently(self):
3807 recent_clean = self.recent_clean
3808 self.recent_clean = True
3809 return recent_clean
3810
3811 def _MarkUnclean(self):
3812 self.clean = False
3813 self.recent_clean = False
3814
3815 def _RunLater(self):
3816 for q in ["_later_queue1", "_later_queue2"]:
3817 if not self._RunQueue(q):
3818 return
3819
3820 def _RunQueue(self, queue):
3821 for m in getattr(self, queue):
3822 if not m.Run(self):
3823 self._MarkUnclean()
3824 return False
3825 setattr(self, queue, [])
3826 return True
3827
3828 def _PrintMessages(self):
3829 if self._messages or self._failures:
3830 if os.isatty(2):
3831 self.out.write(progress.CSI_ERASE_LINE)
3832 self.out.write("\r")
3833
3834 for m in self._messages:
3835 m.Print(self)
3836 for m in self._failures:
3837 m.Print(self)
3838
3839 self._messages = []
3840 self._failures = []
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003841
3842
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003843class MetaProject(Project):
Gavin Makea2e3302023-03-11 06:46:20 +00003844 """A special project housed under .repo."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003845
Gavin Makea2e3302023-03-11 06:46:20 +00003846 def __init__(self, manifest, name, gitdir, worktree):
3847 Project.__init__(
3848 self,
3849 manifest=manifest,
3850 name=name,
3851 gitdir=gitdir,
3852 objdir=gitdir,
3853 worktree=worktree,
3854 remote=RemoteSpec("origin"),
3855 relpath=".repo/%s" % name,
3856 revisionExpr="refs/heads/master",
3857 revisionId=None,
3858 groups=None,
3859 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003860
Gavin Makea2e3302023-03-11 06:46:20 +00003861 def PreSync(self):
3862 if self.Exists:
3863 cb = self.CurrentBranch
3864 if cb:
3865 base = self.GetBranch(cb).merge
3866 if base:
3867 self.revisionExpr = base
3868 self.revisionId = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003869
Gavin Makea2e3302023-03-11 06:46:20 +00003870 @property
3871 def HasChanges(self):
3872 """Has the remote received new commits not yet checked out?"""
3873 if not self.remote or not self.revisionExpr:
3874 return False
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003875
Gavin Makea2e3302023-03-11 06:46:20 +00003876 all_refs = self.bare_ref.all
3877 revid = self.GetRevisionId(all_refs)
3878 head = self.work_git.GetHead()
3879 if head.startswith(R_HEADS):
3880 try:
3881 head = all_refs[head]
3882 except KeyError:
3883 head = None
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003884
Gavin Makea2e3302023-03-11 06:46:20 +00003885 if revid == head:
3886 return False
3887 elif self._revlist(not_rev(HEAD), revid):
3888 return True
3889 return False
LaMont Jones9b72cf22022-03-29 21:54:22 +00003890
3891
3892class RepoProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003893 """The MetaProject for repo itself."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003894
Gavin Makea2e3302023-03-11 06:46:20 +00003895 @property
3896 def LastFetch(self):
3897 try:
3898 fh = os.path.join(self.gitdir, "FETCH_HEAD")
3899 return os.path.getmtime(fh)
3900 except OSError:
3901 return 0
LaMont Jones9b72cf22022-03-29 21:54:22 +00003902
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +01003903
LaMont Jones9b72cf22022-03-29 21:54:22 +00003904class ManifestProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003905 """The MetaProject for manifests."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003906
Gavin Makea2e3302023-03-11 06:46:20 +00003907 def MetaBranchSwitch(self, submodules=False):
3908 """Prepare for manifest branch switch."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003909
Gavin Makea2e3302023-03-11 06:46:20 +00003910 # detach and delete manifest branch, allowing a new
3911 # branch to take over
3912 syncbuf = SyncBuffer(self.config, detach_head=True)
3913 self.Sync_LocalHalf(syncbuf, submodules=submodules)
3914 syncbuf.Finish()
LaMont Jones9b72cf22022-03-29 21:54:22 +00003915
Gavin Makea2e3302023-03-11 06:46:20 +00003916 return (
3917 GitCommand(
3918 self,
3919 ["update-ref", "-d", "refs/heads/default"],
3920 capture_stdout=True,
3921 capture_stderr=True,
3922 ).Wait()
3923 == 0
3924 )
LaMont Jones9b03f152022-03-29 23:01:18 +00003925
Gavin Makea2e3302023-03-11 06:46:20 +00003926 @property
3927 def standalone_manifest_url(self):
3928 """The URL of the standalone manifest, or None."""
3929 return self.config.GetString("manifest.standalone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003930
Gavin Makea2e3302023-03-11 06:46:20 +00003931 @property
3932 def manifest_groups(self):
3933 """The manifest groups string."""
3934 return self.config.GetString("manifest.groups")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003935
Gavin Makea2e3302023-03-11 06:46:20 +00003936 @property
3937 def reference(self):
3938 """The --reference for this manifest."""
3939 return self.config.GetString("repo.reference")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003940
Gavin Makea2e3302023-03-11 06:46:20 +00003941 @property
3942 def dissociate(self):
3943 """Whether to dissociate."""
3944 return self.config.GetBoolean("repo.dissociate")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003945
Gavin Makea2e3302023-03-11 06:46:20 +00003946 @property
3947 def archive(self):
3948 """Whether we use archive."""
3949 return self.config.GetBoolean("repo.archive")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003950
Gavin Makea2e3302023-03-11 06:46:20 +00003951 @property
3952 def mirror(self):
3953 """Whether we use mirror."""
3954 return self.config.GetBoolean("repo.mirror")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003955
Gavin Makea2e3302023-03-11 06:46:20 +00003956 @property
3957 def use_worktree(self):
3958 """Whether we use worktree."""
3959 return self.config.GetBoolean("repo.worktree")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003960
Gavin Makea2e3302023-03-11 06:46:20 +00003961 @property
3962 def clone_bundle(self):
3963 """Whether we use clone_bundle."""
3964 return self.config.GetBoolean("repo.clonebundle")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003965
Gavin Makea2e3302023-03-11 06:46:20 +00003966 @property
3967 def submodules(self):
3968 """Whether we use submodules."""
3969 return self.config.GetBoolean("repo.submodules")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003970
Gavin Makea2e3302023-03-11 06:46:20 +00003971 @property
3972 def git_lfs(self):
3973 """Whether we use git_lfs."""
3974 return self.config.GetBoolean("repo.git-lfs")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003975
Gavin Makea2e3302023-03-11 06:46:20 +00003976 @property
3977 def use_superproject(self):
3978 """Whether we use superproject."""
3979 return self.config.GetBoolean("repo.superproject")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003980
Gavin Makea2e3302023-03-11 06:46:20 +00003981 @property
3982 def partial_clone(self):
3983 """Whether this is a partial clone."""
3984 return self.config.GetBoolean("repo.partialclone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003985
Gavin Makea2e3302023-03-11 06:46:20 +00003986 @property
3987 def depth(self):
3988 """Partial clone depth."""
3989 return self.config.GetString("repo.depth")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003990
Gavin Makea2e3302023-03-11 06:46:20 +00003991 @property
3992 def clone_filter(self):
3993 """The clone filter."""
3994 return self.config.GetString("repo.clonefilter")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003995
Gavin Makea2e3302023-03-11 06:46:20 +00003996 @property
3997 def partial_clone_exclude(self):
3998 """Partial clone exclude string"""
3999 return self.config.GetString("repo.partialcloneexclude")
LaMont Jones4ada0432022-04-14 15:10:43 +00004000
Gavin Makea2e3302023-03-11 06:46:20 +00004001 @property
Jason Chang17833322023-05-23 13:06:55 -07004002 def clone_filter_for_depth(self):
4003 """Replace shallow clone with partial clone."""
4004 return self.config.GetString("repo.clonefilterfordepth")
4005
4006 @property
Gavin Makea2e3302023-03-11 06:46:20 +00004007 def manifest_platform(self):
4008 """The --platform argument from `repo init`."""
4009 return self.config.GetString("manifest.platform")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004010
Gavin Makea2e3302023-03-11 06:46:20 +00004011 @property
4012 def _platform_name(self):
4013 """Return the name of the platform."""
4014 return platform.system().lower()
LaMont Jones9b03f152022-03-29 23:01:18 +00004015
Gavin Makea2e3302023-03-11 06:46:20 +00004016 def SyncWithPossibleInit(
4017 self,
4018 submanifest,
4019 verbose=False,
4020 current_branch_only=False,
4021 tags="",
4022 git_event_log=None,
4023 ):
4024 """Sync a manifestProject, possibly for the first time.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004025
Gavin Makea2e3302023-03-11 06:46:20 +00004026 Call Sync() with arguments from the most recent `repo init`. If this is
4027 a new sub manifest, then inherit options from the parent's
4028 manifestProject.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004029
Gavin Makea2e3302023-03-11 06:46:20 +00004030 This is used by subcmds.Sync() to do an initial download of new sub
4031 manifests.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004032
Gavin Makea2e3302023-03-11 06:46:20 +00004033 Args:
4034 submanifest: an XmlSubmanifest, the submanifest to re-sync.
4035 verbose: a boolean, whether to show all output, rather than only
4036 errors.
4037 current_branch_only: a boolean, whether to only fetch the current
4038 manifest branch from the server.
4039 tags: a boolean, whether to fetch tags.
4040 git_event_log: an EventLog, for git tracing.
4041 """
4042 # TODO(lamontjones): when refactoring sync (and init?) consider how to
4043 # better get the init options that we should use for new submanifests
4044 # that are added when syncing an existing workspace.
4045 git_event_log = git_event_log or EventLog()
LaMont Jonesb90a4222022-04-14 15:00:09 +00004046 spec = submanifest.ToSubmanifestSpec()
Gavin Makea2e3302023-03-11 06:46:20 +00004047 # Use the init options from the existing manifestProject, or the parent
4048 # if it doesn't exist.
4049 #
4050 # Today, we only support changing manifest_groups on the sub-manifest,
4051 # with no supported-for-the-user way to change the other arguments from
4052 # those specified by the outermost manifest.
4053 #
4054 # TODO(lamontjones): determine which of these should come from the
4055 # outermost manifest and which should come from the parent manifest.
4056 mp = self if self.Exists else submanifest.parent.manifestProject
4057 return self.Sync(
LaMont Jones55ee3042022-04-06 17:10:21 +00004058 manifest_url=spec.manifestUrl,
4059 manifest_branch=spec.revision,
Gavin Makea2e3302023-03-11 06:46:20 +00004060 standalone_manifest=mp.standalone_manifest_url,
4061 groups=mp.manifest_groups,
4062 platform=mp.manifest_platform,
4063 mirror=mp.mirror,
4064 dissociate=mp.dissociate,
4065 reference=mp.reference,
4066 worktree=mp.use_worktree,
4067 submodules=mp.submodules,
4068 archive=mp.archive,
4069 partial_clone=mp.partial_clone,
4070 clone_filter=mp.clone_filter,
4071 partial_clone_exclude=mp.partial_clone_exclude,
4072 clone_bundle=mp.clone_bundle,
4073 git_lfs=mp.git_lfs,
4074 use_superproject=mp.use_superproject,
LaMont Jones55ee3042022-04-06 17:10:21 +00004075 verbose=verbose,
4076 current_branch_only=current_branch_only,
4077 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00004078 depth=mp.depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004079 git_event_log=git_event_log,
4080 manifest_name=spec.manifestName,
Gavin Makea2e3302023-03-11 06:46:20 +00004081 this_manifest_only=True,
LaMont Jones55ee3042022-04-06 17:10:21 +00004082 outer_manifest=False,
Jason Chang17833322023-05-23 13:06:55 -07004083 clone_filter_for_depth=mp.clone_filter_for_depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004084 )
LaMont Jones409407a2022-04-05 21:21:56 +00004085
Gavin Makea2e3302023-03-11 06:46:20 +00004086 def Sync(
4087 self,
4088 _kwargs_only=(),
4089 manifest_url="",
4090 manifest_branch=None,
4091 standalone_manifest=False,
4092 groups="",
4093 mirror=False,
4094 reference="",
4095 dissociate=False,
4096 worktree=False,
4097 submodules=False,
4098 archive=False,
4099 partial_clone=None,
4100 depth=None,
4101 clone_filter="blob:none",
4102 partial_clone_exclude=None,
4103 clone_bundle=None,
4104 git_lfs=None,
4105 use_superproject=None,
4106 verbose=False,
4107 current_branch_only=False,
4108 git_event_log=None,
4109 platform="",
4110 manifest_name="default.xml",
4111 tags="",
4112 this_manifest_only=False,
4113 outer_manifest=True,
Jason Chang17833322023-05-23 13:06:55 -07004114 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00004115 ):
4116 """Sync the manifest and all submanifests.
LaMont Jones409407a2022-04-05 21:21:56 +00004117
Gavin Makea2e3302023-03-11 06:46:20 +00004118 Args:
4119 manifest_url: a string, the URL of the manifest project.
4120 manifest_branch: a string, the manifest branch to use.
4121 standalone_manifest: a boolean, whether to store the manifest as a
4122 static file.
4123 groups: a string, restricts the checkout to projects with the
4124 specified groups.
4125 mirror: a boolean, whether to create a mirror of the remote
4126 repository.
4127 reference: a string, location of a repo instance to use as a
4128 reference.
4129 dissociate: a boolean, whether to dissociate from reference mirrors
4130 after clone.
4131 worktree: a boolean, whether to use git-worktree to manage projects.
4132 submodules: a boolean, whether sync submodules associated with the
4133 manifest project.
4134 archive: a boolean, whether to checkout each project as an archive.
4135 See git-archive.
4136 partial_clone: a boolean, whether to perform a partial clone.
4137 depth: an int, how deep of a shallow clone to create.
4138 clone_filter: a string, filter to use with partial_clone.
4139 partial_clone_exclude : a string, comma-delimeted list of project
4140 names to exclude from partial clone.
4141 clone_bundle: a boolean, whether to enable /clone.bundle on
4142 HTTP/HTTPS.
4143 git_lfs: a boolean, whether to enable git LFS support.
4144 use_superproject: a boolean, whether to use the manifest
4145 superproject to sync projects.
4146 verbose: a boolean, whether to show all output, rather than only
4147 errors.
4148 current_branch_only: a boolean, whether to only fetch the current
4149 manifest branch from the server.
4150 platform: a string, restrict the checkout to projects with the
4151 specified platform group.
4152 git_event_log: an EventLog, for git tracing.
4153 tags: a boolean, whether to fetch tags.
4154 manifest_name: a string, the name of the manifest file to use.
4155 this_manifest_only: a boolean, whether to only operate on the
4156 current sub manifest.
4157 outer_manifest: a boolean, whether to start at the outermost
4158 manifest.
Jason Chang17833322023-05-23 13:06:55 -07004159 clone_filter_for_depth: a string, when specified replaces shallow
4160 clones with partial.
LaMont Jones9b03f152022-03-29 23:01:18 +00004161
Gavin Makea2e3302023-03-11 06:46:20 +00004162 Returns:
4163 a boolean, whether the sync was successful.
4164 """
4165 assert _kwargs_only == (), "Sync only accepts keyword arguments."
LaMont Jones9b03f152022-03-29 23:01:18 +00004166
Gavin Makea2e3302023-03-11 06:46:20 +00004167 groups = groups or self.manifest.GetDefaultGroupsStr(
4168 with_platform=False
4169 )
4170 platform = platform or "auto"
4171 git_event_log = git_event_log or EventLog()
4172 if outer_manifest and self.manifest.is_submanifest:
4173 # In a multi-manifest checkout, use the outer manifest unless we are
4174 # told not to.
4175 return self.client.outer_manifest.manifestProject.Sync(
4176 manifest_url=manifest_url,
4177 manifest_branch=manifest_branch,
4178 standalone_manifest=standalone_manifest,
4179 groups=groups,
4180 platform=platform,
4181 mirror=mirror,
4182 dissociate=dissociate,
4183 reference=reference,
4184 worktree=worktree,
4185 submodules=submodules,
4186 archive=archive,
4187 partial_clone=partial_clone,
4188 clone_filter=clone_filter,
4189 partial_clone_exclude=partial_clone_exclude,
4190 clone_bundle=clone_bundle,
4191 git_lfs=git_lfs,
4192 use_superproject=use_superproject,
4193 verbose=verbose,
4194 current_branch_only=current_branch_only,
4195 tags=tags,
4196 depth=depth,
4197 git_event_log=git_event_log,
4198 manifest_name=manifest_name,
4199 this_manifest_only=this_manifest_only,
4200 outer_manifest=False,
4201 )
LaMont Jones9b03f152022-03-29 23:01:18 +00004202
Gavin Makea2e3302023-03-11 06:46:20 +00004203 # If repo has already been initialized, we take -u with the absence of
4204 # --standalone-manifest to mean "transition to a standard repo set up",
4205 # which necessitates starting fresh.
4206 # If --standalone-manifest is set, we always tear everything down and
4207 # start anew.
4208 if self.Exists:
4209 was_standalone_manifest = self.config.GetString(
4210 "manifest.standalone"
4211 )
4212 if was_standalone_manifest and not manifest_url:
4213 print(
4214 "fatal: repo was initialized with a standlone manifest, "
4215 "cannot be re-initialized without --manifest-url/-u"
4216 )
4217 return False
4218
4219 if standalone_manifest or (
4220 was_standalone_manifest and manifest_url
4221 ):
4222 self.config.ClearCache()
4223 if self.gitdir and os.path.exists(self.gitdir):
4224 platform_utils.rmtree(self.gitdir)
4225 if self.worktree and os.path.exists(self.worktree):
4226 platform_utils.rmtree(self.worktree)
4227
4228 is_new = not self.Exists
4229 if is_new:
4230 if not manifest_url:
4231 print("fatal: manifest url is required.", file=sys.stderr)
4232 return False
4233
4234 if verbose:
4235 print(
4236 "Downloading manifest from %s"
4237 % (GitConfig.ForUser().UrlInsteadOf(manifest_url),),
4238 file=sys.stderr,
4239 )
4240
4241 # The manifest project object doesn't keep track of the path on the
4242 # server where this git is located, so let's save that here.
4243 mirrored_manifest_git = None
4244 if reference:
4245 manifest_git_path = urllib.parse.urlparse(manifest_url).path[1:]
4246 mirrored_manifest_git = os.path.join(
4247 reference, manifest_git_path
4248 )
4249 if not mirrored_manifest_git.endswith(".git"):
4250 mirrored_manifest_git += ".git"
4251 if not os.path.exists(mirrored_manifest_git):
4252 mirrored_manifest_git = os.path.join(
4253 reference, ".repo/manifests.git"
4254 )
4255
4256 self._InitGitDir(mirror_git=mirrored_manifest_git)
4257
4258 # If standalone_manifest is set, mark the project as "standalone" --
4259 # we'll still do much of the manifests.git set up, but will avoid actual
4260 # syncs to a remote.
4261 if standalone_manifest:
4262 self.config.SetString("manifest.standalone", manifest_url)
4263 elif not manifest_url and not manifest_branch:
4264 # If -u is set and --standalone-manifest is not, then we're not in
4265 # standalone mode. Otherwise, use config to infer what we were in
4266 # the last init.
4267 standalone_manifest = bool(
4268 self.config.GetString("manifest.standalone")
4269 )
4270 if not standalone_manifest:
4271 self.config.SetString("manifest.standalone", None)
4272
4273 self._ConfigureDepth(depth)
4274
4275 # Set the remote URL before the remote branch as we might need it below.
4276 if manifest_url:
4277 r = self.GetRemote()
4278 r.url = manifest_url
4279 r.ResetFetch()
4280 r.Save()
4281
4282 if not standalone_manifest:
4283 if manifest_branch:
4284 if manifest_branch == "HEAD":
4285 manifest_branch = self.ResolveRemoteHead()
4286 if manifest_branch is None:
4287 print("fatal: unable to resolve HEAD", file=sys.stderr)
4288 return False
4289 self.revisionExpr = manifest_branch
4290 else:
4291 if is_new:
4292 default_branch = self.ResolveRemoteHead()
4293 if default_branch is None:
4294 # If the remote doesn't have HEAD configured, default to
4295 # master.
4296 default_branch = "refs/heads/master"
4297 self.revisionExpr = default_branch
4298 else:
4299 self.PreSync()
4300
4301 groups = re.split(r"[,\s]+", groups or "")
4302 all_platforms = ["linux", "darwin", "windows"]
4303 platformize = lambda x: "platform-" + x
4304 if platform == "auto":
4305 if not mirror and not self.mirror:
4306 groups.append(platformize(self._platform_name))
4307 elif platform == "all":
4308 groups.extend(map(platformize, all_platforms))
4309 elif platform in all_platforms:
4310 groups.append(platformize(platform))
4311 elif platform != "none":
4312 print("fatal: invalid platform flag", file=sys.stderr)
4313 return False
4314 self.config.SetString("manifest.platform", platform)
4315
4316 groups = [x for x in groups if x]
4317 groupstr = ",".join(groups)
4318 if (
4319 platform == "auto"
4320 and groupstr == self.manifest.GetDefaultGroupsStr()
4321 ):
4322 groupstr = None
4323 self.config.SetString("manifest.groups", groupstr)
4324
4325 if reference:
4326 self.config.SetString("repo.reference", reference)
4327
4328 if dissociate:
4329 self.config.SetBoolean("repo.dissociate", dissociate)
4330
4331 if worktree:
4332 if mirror:
4333 print(
4334 "fatal: --mirror and --worktree are incompatible",
4335 file=sys.stderr,
4336 )
4337 return False
4338 if submodules:
4339 print(
4340 "fatal: --submodules and --worktree are incompatible",
4341 file=sys.stderr,
4342 )
4343 return False
4344 self.config.SetBoolean("repo.worktree", worktree)
4345 if is_new:
4346 self.use_git_worktrees = True
4347 print("warning: --worktree is experimental!", file=sys.stderr)
4348
4349 if archive:
4350 if is_new:
4351 self.config.SetBoolean("repo.archive", archive)
4352 else:
4353 print(
4354 "fatal: --archive is only supported when initializing a "
4355 "new workspace.",
4356 file=sys.stderr,
4357 )
4358 print(
4359 "Either delete the .repo folder in this workspace, or "
4360 "initialize in another location.",
4361 file=sys.stderr,
4362 )
4363 return False
4364
4365 if mirror:
4366 if is_new:
4367 self.config.SetBoolean("repo.mirror", mirror)
4368 else:
4369 print(
4370 "fatal: --mirror is only supported when initializing a new "
4371 "workspace.",
4372 file=sys.stderr,
4373 )
4374 print(
4375 "Either delete the .repo folder in this workspace, or "
4376 "initialize in another location.",
4377 file=sys.stderr,
4378 )
4379 return False
4380
4381 if partial_clone is not None:
4382 if mirror:
4383 print(
4384 "fatal: --mirror and --partial-clone are mutually "
4385 "exclusive",
4386 file=sys.stderr,
4387 )
4388 return False
4389 self.config.SetBoolean("repo.partialclone", partial_clone)
4390 if clone_filter:
4391 self.config.SetString("repo.clonefilter", clone_filter)
4392 elif self.partial_clone:
4393 clone_filter = self.clone_filter
4394 else:
4395 clone_filter = None
4396
4397 if partial_clone_exclude is not None:
4398 self.config.SetString(
4399 "repo.partialcloneexclude", partial_clone_exclude
4400 )
4401
4402 if clone_bundle is None:
4403 clone_bundle = False if partial_clone else True
4404 else:
4405 self.config.SetBoolean("repo.clonebundle", clone_bundle)
4406
4407 if submodules:
4408 self.config.SetBoolean("repo.submodules", submodules)
4409
4410 if git_lfs is not None:
4411 if git_lfs:
4412 git_require((2, 17, 0), fail=True, msg="Git LFS support")
4413
4414 self.config.SetBoolean("repo.git-lfs", git_lfs)
4415 if not is_new:
4416 print(
4417 "warning: Changing --git-lfs settings will only affect new "
4418 "project checkouts.\n"
4419 " Existing projects will require manual updates.\n",
4420 file=sys.stderr,
4421 )
4422
Jason Chang17833322023-05-23 13:06:55 -07004423 if clone_filter_for_depth is not None:
4424 self.ConfigureCloneFilterForDepth(clone_filter_for_depth)
4425
Gavin Makea2e3302023-03-11 06:46:20 +00004426 if use_superproject is not None:
4427 self.config.SetBoolean("repo.superproject", use_superproject)
4428
4429 if not standalone_manifest:
4430 success = self.Sync_NetworkHalf(
4431 is_new=is_new,
4432 quiet=not verbose,
4433 verbose=verbose,
4434 clone_bundle=clone_bundle,
4435 current_branch_only=current_branch_only,
4436 tags=tags,
4437 submodules=submodules,
4438 clone_filter=clone_filter,
4439 partial_clone_exclude=self.manifest.PartialCloneExclude,
Jason Chang17833322023-05-23 13:06:55 -07004440 clone_filter_for_depth=self.manifest.CloneFilterForDepth,
Gavin Makea2e3302023-03-11 06:46:20 +00004441 ).success
4442 if not success:
4443 r = self.GetRemote()
4444 print(
4445 "fatal: cannot obtain manifest %s" % r.url, file=sys.stderr
4446 )
4447
4448 # Better delete the manifest git dir if we created it; otherwise
4449 # next time (when user fixes problems) we won't go through the
4450 # "is_new" logic.
4451 if is_new:
4452 platform_utils.rmtree(self.gitdir)
4453 return False
4454
4455 if manifest_branch:
4456 self.MetaBranchSwitch(submodules=submodules)
4457
4458 syncbuf = SyncBuffer(self.config)
4459 self.Sync_LocalHalf(syncbuf, submodules=submodules)
4460 syncbuf.Finish()
4461
4462 if is_new or self.CurrentBranch is None:
4463 if not self.StartBranch("default"):
4464 print(
4465 "fatal: cannot create default in manifest",
4466 file=sys.stderr,
4467 )
4468 return False
4469
4470 if not manifest_name:
4471 print("fatal: manifest name (-m) is required.", file=sys.stderr)
4472 return False
4473
4474 elif is_new:
4475 # This is a new standalone manifest.
4476 manifest_name = "default.xml"
4477 manifest_data = fetch.fetch_file(manifest_url, verbose=verbose)
4478 dest = os.path.join(self.worktree, manifest_name)
4479 os.makedirs(os.path.dirname(dest), exist_ok=True)
4480 with open(dest, "wb") as f:
4481 f.write(manifest_data)
4482
4483 try:
4484 self.manifest.Link(manifest_name)
4485 except ManifestParseError as e:
4486 print(
4487 "fatal: manifest '%s' not available" % manifest_name,
4488 file=sys.stderr,
4489 )
4490 print("fatal: %s" % str(e), file=sys.stderr)
4491 return False
4492
4493 if not this_manifest_only:
4494 for submanifest in self.manifest.submanifests.values():
4495 spec = submanifest.ToSubmanifestSpec()
4496 submanifest.repo_client.manifestProject.Sync(
4497 manifest_url=spec.manifestUrl,
4498 manifest_branch=spec.revision,
4499 standalone_manifest=standalone_manifest,
4500 groups=self.manifest_groups,
4501 platform=platform,
4502 mirror=mirror,
4503 dissociate=dissociate,
4504 reference=reference,
4505 worktree=worktree,
4506 submodules=submodules,
4507 archive=archive,
4508 partial_clone=partial_clone,
4509 clone_filter=clone_filter,
4510 partial_clone_exclude=partial_clone_exclude,
4511 clone_bundle=clone_bundle,
4512 git_lfs=git_lfs,
4513 use_superproject=use_superproject,
4514 verbose=verbose,
4515 current_branch_only=current_branch_only,
4516 tags=tags,
4517 depth=depth,
4518 git_event_log=git_event_log,
4519 manifest_name=spec.manifestName,
4520 this_manifest_only=False,
4521 outer_manifest=False,
4522 )
4523
4524 # Lastly, if the manifest has a <superproject> then have the
4525 # superproject sync it (if it will be used).
4526 if git_superproject.UseSuperproject(use_superproject, self.manifest):
4527 sync_result = self.manifest.superproject.Sync(git_event_log)
4528 if not sync_result.success:
4529 submanifest = ""
4530 if self.manifest.path_prefix:
4531 submanifest = f"for {self.manifest.path_prefix} "
4532 print(
4533 f"warning: git update of superproject {submanifest}failed, "
4534 "repo sync will not use superproject to fetch source; "
4535 "while this error is not fatal, and you can continue to "
4536 "run repo sync, please run repo init with the "
4537 "--no-use-superproject option to stop seeing this warning",
4538 file=sys.stderr,
4539 )
4540 if sync_result.fatal and use_superproject is not None:
4541 return False
4542
4543 return True
4544
Jason Chang17833322023-05-23 13:06:55 -07004545 def ConfigureCloneFilterForDepth(self, clone_filter_for_depth):
4546 """Configure clone filter to replace shallow clones.
4547
4548 Args:
4549 clone_filter_for_depth: a string or None, e.g. 'blob:none' will
4550 disable shallow clones and replace with partial clone. None will
4551 enable shallow clones.
4552 """
4553 self.config.SetString(
4554 "repo.clonefilterfordepth", clone_filter_for_depth
4555 )
4556
Gavin Makea2e3302023-03-11 06:46:20 +00004557 def _ConfigureDepth(self, depth):
4558 """Configure the depth we'll sync down.
4559
4560 Args:
4561 depth: an int, how deep of a partial clone to create.
4562 """
4563 # Opt.depth will be non-None if user actually passed --depth to repo
4564 # init.
4565 if depth is not None:
4566 if depth > 0:
4567 # Positive values will set the depth.
4568 depth = str(depth)
4569 else:
4570 # Negative numbers will clear the depth; passing None to
4571 # SetString will do that.
4572 depth = None
4573
4574 # We store the depth in the main manifest project.
4575 self.config.SetString("repo.depth", depth)