blob: 7b78427dde696c11f894d02a25a14e4d88777a0e [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
Mike Frysinger64477332023-08-21 21:20:32 -040029from typing import List, NamedTuple
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
Mike Frysinger64477332023-08-21 21:20:32 -040033from error import DownloadError
34from error import GitError
35from error import ManifestInvalidPathError
36from error import ManifestInvalidRevisionError
37from error import ManifestParseError
38from error import NoManifestException
39from error import RepoError
40from error import UploadError
LaMont Jones0de4fc32022-04-21 17:18:35 +000041import fetch
Mike Frysinger64477332023-08-21 21:20:32 -040042from git_command import git_require
43from git_command import GitCommand
44from git_config import GetSchemeFromUrl
45from git_config import GetUrlCookieFile
46from git_config import GitConfig
Mike Frysinger64477332023-08-21 21:20:32 -040047from git_config import IsId
48from git_refs import GitRefs
49from git_refs import HEAD
50from git_refs import R_HEADS
51from git_refs import R_M
52from git_refs import R_PUB
53from git_refs import R_TAGS
54from git_refs import R_WORKTREE_M
LaMont Jonesff6b1da2022-06-01 21:03:34 +000055import git_superproject
LaMont Jones55ee3042022-04-06 17:10:21 +000056from git_trace2_event_log import EventLog
Renaud Paquayd5cec5e2016-11-01 11:24:03 -070057import platform_utils
Mike Frysinger70d861f2019-08-26 15:22:36 -040058import progress
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +000059from repo_logging import RepoLogger
Joanna Wanga6c52f52022-11-03 16:51:19 -040060from repo_trace import Trace
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070061
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -070062
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +000063logger = RepoLogger(__file__)
64
65
LaMont Jones1eddca82022-09-01 15:15:04 +000066class SyncNetworkHalfResult(NamedTuple):
Gavin Makea2e3302023-03-11 06:46:20 +000067 """Sync_NetworkHalf return value."""
68
Gavin Makea2e3302023-03-11 06:46:20 +000069 # Did we query the remote? False when optimized_fetch is True and we have
70 # the commit already present.
71 remote_fetched: bool
Jason Chang32b59562023-07-14 16:45:35 -070072 # Error from SyncNetworkHalf
73 error: Exception = None
74
75 @property
76 def success(self) -> bool:
77 return not self.error
78
79
80class SyncNetworkHalfError(RepoError):
81 """Failure trying to sync."""
82
83
84class DeleteWorktreeError(RepoError):
85 """Failure to delete worktree."""
86
87 def __init__(
88 self, *args, aggregate_errors: List[Exception] = None, **kwargs
89 ) -> None:
90 super().__init__(*args, **kwargs)
91 self.aggregate_errors = aggregate_errors or []
92
93
94class DeleteDirtyWorktreeError(DeleteWorktreeError):
95 """Failure to delete worktree due to uncommitted changes."""
LaMont Jones1eddca82022-09-01 15:15:04 +000096
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +010097
George Engelbrecht9bc283e2020-04-02 12:36:09 -060098# Maximum sleep time allowed during retries.
99MAXIMUM_RETRY_SLEEP_SEC = 3600.0
100# +-10% random jitter is added to each Fetches retry sleep duration.
101RETRY_JITTER_PERCENT = 0.1
102
LaMont Jonesfa8d9392022-11-02 22:01:29 +0000103# Whether to use alternates. Switching back and forth is *NOT* supported.
Mike Frysinger1d00a7e2021-12-21 00:40:31 -0500104# TODO(vapier): Remove knob once behavior is verified.
Gavin Makea2e3302023-03-11 06:46:20 +0000105_ALTERNATES = os.environ.get("REPO_USE_ALTERNATES") == "1"
George Engelbrecht9bc283e2020-04-02 12:36:09 -0600106
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +0100107
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700108def _lwrite(path, content):
Gavin Makea2e3302023-03-11 06:46:20 +0000109 lock = "%s.lock" % path
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700110
Gavin Makea2e3302023-03-11 06:46:20 +0000111 # Maintain Unix line endings on all OS's to match git behavior.
112 with open(lock, "w", newline="\n") as fd:
113 fd.write(content)
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700114
Gavin Makea2e3302023-03-11 06:46:20 +0000115 try:
116 platform_utils.rename(lock, path)
117 except OSError:
118 platform_utils.remove(lock)
119 raise
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700120
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700121
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700122def not_rev(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000123 return "^" + r
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700124
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700125
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800126def sq(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000127 return "'" + r.replace("'", "'''") + "'"
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -0800128
David Pursehouse819827a2020-02-12 15:20:19 +0900129
Jonathan Nieder93719792015-03-17 11:29:58 -0700130_project_hook_list = None
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700131
132
Jonathan Nieder93719792015-03-17 11:29:58 -0700133def _ProjectHooks():
Gavin Makea2e3302023-03-11 06:46:20 +0000134 """List the hooks present in the 'hooks' directory.
Jonathan Nieder93719792015-03-17 11:29:58 -0700135
Gavin Makea2e3302023-03-11 06:46:20 +0000136 These hooks are project hooks and are copied to the '.git/hooks' directory
137 of all subprojects.
Jonathan Nieder93719792015-03-17 11:29:58 -0700138
Gavin Makea2e3302023-03-11 06:46:20 +0000139 This function caches the list of hooks (based on the contents of the
140 'repo/hooks' directory) on the first call.
Jonathan Nieder93719792015-03-17 11:29:58 -0700141
Gavin Makea2e3302023-03-11 06:46:20 +0000142 Returns:
143 A list of absolute paths to all of the files in the hooks directory.
144 """
145 global _project_hook_list
146 if _project_hook_list is None:
147 d = platform_utils.realpath(os.path.abspath(os.path.dirname(__file__)))
148 d = os.path.join(d, "hooks")
149 _project_hook_list = [
150 os.path.join(d, x) for x in platform_utils.listdir(d)
151 ]
152 return _project_hook_list
Jonathan Nieder93719792015-03-17 11:29:58 -0700153
154
Mike Frysingerd4aee652023-10-19 05:13:32 -0400155class DownloadedChange:
Gavin Makea2e3302023-03-11 06:46:20 +0000156 _commit_cache = None
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700157
Gavin Makea2e3302023-03-11 06:46:20 +0000158 def __init__(self, project, base, change_id, ps_id, commit):
159 self.project = project
160 self.base = base
161 self.change_id = change_id
162 self.ps_id = ps_id
163 self.commit = commit
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700164
Gavin Makea2e3302023-03-11 06:46:20 +0000165 @property
166 def commits(self):
167 if self._commit_cache is None:
168 self._commit_cache = self.project.bare_git.rev_list(
169 "--abbrev=8",
170 "--abbrev-commit",
171 "--pretty=oneline",
172 "--reverse",
173 "--date-order",
174 not_rev(self.base),
175 self.commit,
176 "--",
177 )
178 return self._commit_cache
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700179
180
Mike Frysingerd4aee652023-10-19 05:13:32 -0400181class ReviewableBranch:
Gavin Makea2e3302023-03-11 06:46:20 +0000182 _commit_cache = None
183 _base_exists = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700184
Gavin Makea2e3302023-03-11 06:46:20 +0000185 def __init__(self, project, branch, base):
186 self.project = project
187 self.branch = branch
188 self.base = base
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700189
Gavin Makea2e3302023-03-11 06:46:20 +0000190 @property
191 def name(self):
192 return self.branch.name
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700193
Gavin Makea2e3302023-03-11 06:46:20 +0000194 @property
195 def commits(self):
196 if self._commit_cache is None:
197 args = (
198 "--abbrev=8",
199 "--abbrev-commit",
200 "--pretty=oneline",
201 "--reverse",
202 "--date-order",
203 not_rev(self.base),
204 R_HEADS + self.name,
205 "--",
206 )
207 try:
Jason Chang87058c62023-09-27 11:34:43 -0700208 self._commit_cache = self.project.bare_git.rev_list(
209 *args, log_as_error=self.base_exists
210 )
Gavin Makea2e3302023-03-11 06:46:20 +0000211 except GitError:
212 # We weren't able to probe the commits for this branch. Was it
213 # tracking a branch that no longer exists? If so, return no
214 # commits. Otherwise, rethrow the error as we don't know what's
215 # going on.
216 if self.base_exists:
217 raise
Mike Frysinger6da17752019-09-11 18:43:17 -0400218
Gavin Makea2e3302023-03-11 06:46:20 +0000219 self._commit_cache = []
Mike Frysinger6da17752019-09-11 18:43:17 -0400220
Gavin Makea2e3302023-03-11 06:46:20 +0000221 return self._commit_cache
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700222
Gavin Makea2e3302023-03-11 06:46:20 +0000223 @property
224 def unabbrev_commits(self):
225 r = dict()
226 for commit in self.project.bare_git.rev_list(
227 not_rev(self.base), R_HEADS + self.name, "--"
228 ):
229 r[commit[0:8]] = commit
230 return r
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800231
Gavin Makea2e3302023-03-11 06:46:20 +0000232 @property
233 def date(self):
234 return self.project.bare_git.log(
235 "--pretty=format:%cd", "-n", "1", R_HEADS + self.name, "--"
236 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700237
Gavin Makea2e3302023-03-11 06:46:20 +0000238 @property
239 def base_exists(self):
240 """Whether the branch we're tracking exists.
Mike Frysinger6da17752019-09-11 18:43:17 -0400241
Gavin Makea2e3302023-03-11 06:46:20 +0000242 Normally it should, but sometimes branches we track can get deleted.
243 """
244 if self._base_exists is None:
245 try:
246 self.project.bare_git.rev_parse("--verify", not_rev(self.base))
247 # If we're still here, the base branch exists.
248 self._base_exists = True
249 except GitError:
250 # If we failed to verify, the base branch doesn't exist.
251 self._base_exists = False
Mike Frysinger6da17752019-09-11 18:43:17 -0400252
Gavin Makea2e3302023-03-11 06:46:20 +0000253 return self._base_exists
Mike Frysinger6da17752019-09-11 18:43:17 -0400254
Gavin Makea2e3302023-03-11 06:46:20 +0000255 def UploadForReview(
256 self,
257 people,
258 dryrun=False,
259 auto_topic=False,
260 hashtags=(),
261 labels=(),
262 private=False,
263 notify=None,
264 wip=False,
265 ready=False,
266 dest_branch=None,
267 validate_certs=True,
268 push_options=None,
269 ):
270 self.project.UploadForReview(
271 branch=self.name,
272 people=people,
273 dryrun=dryrun,
274 auto_topic=auto_topic,
275 hashtags=hashtags,
276 labels=labels,
277 private=private,
278 notify=notify,
279 wip=wip,
280 ready=ready,
281 dest_branch=dest_branch,
282 validate_certs=validate_certs,
283 push_options=push_options,
284 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700285
Gavin Makea2e3302023-03-11 06:46:20 +0000286 def GetPublishedRefs(self):
287 refs = {}
288 output = self.project.bare_git.ls_remote(
289 self.branch.remote.SshReviewUrl(self.project.UserEmail),
290 "refs/changes/*",
291 )
292 for line in output.split("\n"):
293 try:
294 (sha, ref) = line.split()
295 refs[sha] = ref
296 except ValueError:
297 pass
Ficus Kirkpatrickbc7ef672009-05-04 12:45:11 -0700298
Gavin Makea2e3302023-03-11 06:46:20 +0000299 return refs
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700300
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700301
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700302class StatusColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000303 def __init__(self, config):
304 super().__init__(config, "status")
305 self.project = self.printer("header", attr="bold")
306 self.branch = self.printer("header", attr="bold")
307 self.nobranch = self.printer("nobranch", fg="red")
308 self.important = self.printer("important", fg="red")
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700309
Gavin Makea2e3302023-03-11 06:46:20 +0000310 self.added = self.printer("added", fg="green")
311 self.changed = self.printer("changed", fg="red")
312 self.untracked = self.printer("untracked", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700313
314
315class DiffColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000316 def __init__(self, config):
317 super().__init__(config, "diff")
318 self.project = self.printer("header", attr="bold")
319 self.fail = self.printer("fail", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700320
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700321
Mike Frysingerd4aee652023-10-19 05:13:32 -0400322class Annotation:
Gavin Makea2e3302023-03-11 06:46:20 +0000323 def __init__(self, name, value, keep):
324 self.name = name
325 self.value = value
326 self.keep = keep
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700327
Gavin Makea2e3302023-03-11 06:46:20 +0000328 def __eq__(self, other):
329 if not isinstance(other, Annotation):
330 return False
331 return self.__dict__ == other.__dict__
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700332
Gavin Makea2e3302023-03-11 06:46:20 +0000333 def __lt__(self, other):
334 # This exists just so that lists of Annotation objects can be sorted,
335 # for use in comparisons.
336 if not isinstance(other, Annotation):
337 raise ValueError("comparison is not between two Annotation objects")
338 if self.name == other.name:
339 if self.value == other.value:
340 return self.keep < other.keep
341 return self.value < other.value
342 return self.name < other.name
Jack Neus6ea0cae2021-07-20 20:52:33 +0000343
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700344
Mike Frysingere6a202f2019-08-02 15:57:57 -0400345def _SafeExpandPath(base, subpath, skipfinal=False):
Gavin Makea2e3302023-03-11 06:46:20 +0000346 """Make sure |subpath| is completely safe under |base|.
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700347
Gavin Makea2e3302023-03-11 06:46:20 +0000348 We make sure no intermediate symlinks are traversed, and that the final path
349 is not a special file (e.g. not a socket or fifo).
Mike Frysingere6a202f2019-08-02 15:57:57 -0400350
Gavin Makea2e3302023-03-11 06:46:20 +0000351 NB: We rely on a number of paths already being filtered out while parsing
352 the manifest. See the validation logic in manifest_xml.py for more details.
353 """
354 # Split up the path by its components. We can't use os.path.sep exclusively
355 # as some platforms (like Windows) will convert / to \ and that bypasses all
356 # our constructed logic here. Especially since manifest authors only use
357 # / in their paths.
358 resep = re.compile(r"[/%s]" % re.escape(os.path.sep))
359 components = resep.split(subpath)
360 if skipfinal:
361 # Whether the caller handles the final component itself.
362 finalpart = components.pop()
Mike Frysingere6a202f2019-08-02 15:57:57 -0400363
Gavin Makea2e3302023-03-11 06:46:20 +0000364 path = base
365 for part in components:
366 if part in {".", ".."}:
367 raise ManifestInvalidPathError(
368 '%s: "%s" not allowed in paths' % (subpath, part)
369 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400370
Gavin Makea2e3302023-03-11 06:46:20 +0000371 path = os.path.join(path, part)
372 if platform_utils.islink(path):
373 raise ManifestInvalidPathError(
374 "%s: traversing symlinks not allow" % (path,)
375 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400376
Gavin Makea2e3302023-03-11 06:46:20 +0000377 if os.path.exists(path):
378 if not os.path.isfile(path) and not platform_utils.isdir(path):
379 raise ManifestInvalidPathError(
380 "%s: only regular files & directories allowed" % (path,)
381 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400382
Gavin Makea2e3302023-03-11 06:46:20 +0000383 if skipfinal:
384 path = os.path.join(path, finalpart)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400385
Gavin Makea2e3302023-03-11 06:46:20 +0000386 return path
Mike Frysingere6a202f2019-08-02 15:57:57 -0400387
388
Mike Frysingerd4aee652023-10-19 05:13:32 -0400389class _CopyFile:
Gavin Makea2e3302023-03-11 06:46:20 +0000390 """Container for <copyfile> manifest element."""
Mike Frysingere6a202f2019-08-02 15:57:57 -0400391
Gavin Makea2e3302023-03-11 06:46:20 +0000392 def __init__(self, git_worktree, src, topdir, dest):
393 """Register a <copyfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400394
Gavin Makea2e3302023-03-11 06:46:20 +0000395 Args:
396 git_worktree: Absolute path to the git project checkout.
397 src: Relative path under |git_worktree| of file to read.
398 topdir: Absolute path to the top of the repo client checkout.
399 dest: Relative path under |topdir| of file to write.
400 """
401 self.git_worktree = git_worktree
402 self.topdir = topdir
403 self.src = src
404 self.dest = dest
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700405
Gavin Makea2e3302023-03-11 06:46:20 +0000406 def _Copy(self):
407 src = _SafeExpandPath(self.git_worktree, self.src)
408 dest = _SafeExpandPath(self.topdir, self.dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400409
Gavin Makea2e3302023-03-11 06:46:20 +0000410 if platform_utils.isdir(src):
411 raise ManifestInvalidPathError(
412 "%s: copying from directory not supported" % (self.src,)
413 )
414 if platform_utils.isdir(dest):
415 raise ManifestInvalidPathError(
416 "%s: copying to directory not allowed" % (self.dest,)
417 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400418
Gavin Makea2e3302023-03-11 06:46:20 +0000419 # Copy file if it does not exist or is out of date.
420 if not os.path.exists(dest) or not filecmp.cmp(src, dest):
421 try:
422 # Remove existing file first, since it might be read-only.
423 if os.path.exists(dest):
424 platform_utils.remove(dest)
425 else:
426 dest_dir = os.path.dirname(dest)
427 if not platform_utils.isdir(dest_dir):
428 os.makedirs(dest_dir)
429 shutil.copy(src, dest)
430 # Make the file read-only.
431 mode = os.stat(dest)[stat.ST_MODE]
432 mode = mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
433 os.chmod(dest, mode)
Jason R. Coombsae824fb2023-10-20 23:32:40 +0545434 except OSError:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +0000435 logger.error("error: Cannot copy file %s to %s", src, dest)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700436
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700437
Mike Frysingerd4aee652023-10-19 05:13:32 -0400438class _LinkFile:
Gavin Makea2e3302023-03-11 06:46:20 +0000439 """Container for <linkfile> manifest element."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700440
Gavin Makea2e3302023-03-11 06:46:20 +0000441 def __init__(self, git_worktree, src, topdir, dest):
442 """Register a <linkfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400443
Gavin Makea2e3302023-03-11 06:46:20 +0000444 Args:
445 git_worktree: Absolute path to the git project checkout.
446 src: Target of symlink relative to path under |git_worktree|.
447 topdir: Absolute path to the top of the repo client checkout.
448 dest: Relative path under |topdir| of symlink to create.
449 """
450 self.git_worktree = git_worktree
451 self.topdir = topdir
452 self.src = src
453 self.dest = dest
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500454
Gavin Makea2e3302023-03-11 06:46:20 +0000455 def __linkIt(self, relSrc, absDest):
456 # Link file if it does not exist or is out of date.
457 if not platform_utils.islink(absDest) or (
458 platform_utils.readlink(absDest) != relSrc
459 ):
460 try:
461 # Remove existing file first, since it might be read-only.
462 if os.path.lexists(absDest):
463 platform_utils.remove(absDest)
464 else:
465 dest_dir = os.path.dirname(absDest)
466 if not platform_utils.isdir(dest_dir):
467 os.makedirs(dest_dir)
468 platform_utils.symlink(relSrc, absDest)
Jason R. Coombsae824fb2023-10-20 23:32:40 +0545469 except OSError:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +0000470 logger.error(
471 "error: Cannot link file %s to %s", relSrc, absDest
472 )
Gavin Makea2e3302023-03-11 06:46:20 +0000473
474 def _Link(self):
475 """Link the self.src & self.dest paths.
476
477 Handles wild cards on the src linking all of the files in the source in
478 to the destination directory.
479 """
480 # Some people use src="." to create stable links to projects. Let's
481 # allow that but reject all other uses of "." to keep things simple.
482 if self.src == ".":
483 src = self.git_worktree
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500484 else:
Gavin Makea2e3302023-03-11 06:46:20 +0000485 src = _SafeExpandPath(self.git_worktree, self.src)
Wink Saville4c426ef2015-06-03 08:05:17 -0700486
Gavin Makea2e3302023-03-11 06:46:20 +0000487 if not glob.has_magic(src):
488 # Entity does not contain a wild card so just a simple one to one
489 # link operation.
490 dest = _SafeExpandPath(self.topdir, self.dest, skipfinal=True)
491 # dest & src are absolute paths at this point. Make sure the target
492 # of the symlink is relative in the context of the repo client
493 # checkout.
494 relpath = os.path.relpath(src, os.path.dirname(dest))
495 self.__linkIt(relpath, dest)
496 else:
497 dest = _SafeExpandPath(self.topdir, self.dest)
498 # Entity contains a wild card.
499 if os.path.exists(dest) and not platform_utils.isdir(dest):
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +0000500 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +0000501 "Link error: src with wildcard, %s must be a directory",
502 dest,
503 )
504 else:
505 for absSrcFile in glob.glob(src):
506 # Create a releative path from source dir to destination
507 # dir.
508 absSrcDir = os.path.dirname(absSrcFile)
509 relSrcDir = os.path.relpath(absSrcDir, dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400510
Gavin Makea2e3302023-03-11 06:46:20 +0000511 # Get the source file name.
512 srcFile = os.path.basename(absSrcFile)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400513
Gavin Makea2e3302023-03-11 06:46:20 +0000514 # Now form the final full paths to srcFile. They will be
515 # absolute for the desintaiton and relative for the source.
516 absDest = os.path.join(dest, srcFile)
517 relSrc = os.path.join(relSrcDir, srcFile)
518 self.__linkIt(relSrc, absDest)
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500519
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700520
Mike Frysingerd4aee652023-10-19 05:13:32 -0400521class RemoteSpec:
Gavin Makea2e3302023-03-11 06:46:20 +0000522 def __init__(
523 self,
524 name,
525 url=None,
526 pushUrl=None,
527 review=None,
528 revision=None,
529 orig_name=None,
530 fetchUrl=None,
531 ):
532 self.name = name
533 self.url = url
534 self.pushUrl = pushUrl
535 self.review = review
536 self.revision = revision
537 self.orig_name = orig_name
538 self.fetchUrl = fetchUrl
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700539
Ian Kasprzak0286e312021-02-05 10:06:18 -0800540
Mike Frysingerd4aee652023-10-19 05:13:32 -0400541class Project:
Gavin Makea2e3302023-03-11 06:46:20 +0000542 # These objects can be shared between several working trees.
543 @property
544 def shareable_dirs(self):
545 """Return the shareable directories"""
546 if self.UseAlternates:
547 return ["hooks", "rr-cache"]
548 else:
549 return ["hooks", "objects", "rr-cache"]
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700550
Gavin Makea2e3302023-03-11 06:46:20 +0000551 def __init__(
552 self,
553 manifest,
554 name,
555 remote,
556 gitdir,
557 objdir,
558 worktree,
559 relpath,
560 revisionExpr,
561 revisionId,
562 rebase=True,
563 groups=None,
564 sync_c=False,
565 sync_s=False,
566 sync_tags=True,
567 clone_depth=None,
568 upstream=None,
569 parent=None,
570 use_git_worktrees=False,
571 is_derived=False,
572 dest_branch=None,
573 optimized_fetch=False,
574 retry_fetches=0,
575 old_revision=None,
576 ):
577 """Init a Project object.
Doug Anderson3ba5f952011-04-07 12:51:04 -0700578
579 Args:
Gavin Makea2e3302023-03-11 06:46:20 +0000580 manifest: The XmlManifest object.
581 name: The `name` attribute of manifest.xml's project element.
582 remote: RemoteSpec object specifying its remote's properties.
583 gitdir: Absolute path of git directory.
584 objdir: Absolute path of directory to store git objects.
585 worktree: Absolute path of git working tree.
586 relpath: Relative path of git working tree to repo's top directory.
587 revisionExpr: The `revision` attribute of manifest.xml's project
588 element.
589 revisionId: git commit id for checking out.
590 rebase: The `rebase` attribute of manifest.xml's project element.
591 groups: The `groups` attribute of manifest.xml's project element.
592 sync_c: The `sync-c` attribute of manifest.xml's project element.
593 sync_s: The `sync-s` attribute of manifest.xml's project element.
594 sync_tags: The `sync-tags` attribute of manifest.xml's project
595 element.
596 upstream: The `upstream` attribute of manifest.xml's project
597 element.
598 parent: The parent Project object.
599 use_git_worktrees: Whether to use `git worktree` for this project.
600 is_derived: False if the project was explicitly defined in the
601 manifest; True if the project is a discovered submodule.
602 dest_branch: The branch to which to push changes for review by
603 default.
604 optimized_fetch: If True, when a project is set to a sha1 revision,
605 only fetch from the remote if the sha1 is not present locally.
606 retry_fetches: Retry remote fetches n times upon receiving transient
607 error with exponential backoff and jitter.
608 old_revision: saved git commit id for open GITC projects.
609 """
610 self.client = self.manifest = manifest
611 self.name = name
612 self.remote = remote
613 self.UpdatePaths(relpath, worktree, gitdir, objdir)
614 self.SetRevision(revisionExpr, revisionId=revisionId)
615
616 self.rebase = rebase
617 self.groups = groups
618 self.sync_c = sync_c
619 self.sync_s = sync_s
620 self.sync_tags = sync_tags
621 self.clone_depth = clone_depth
622 self.upstream = upstream
623 self.parent = parent
624 # NB: Do not use this setting in __init__ to change behavior so that the
625 # manifest.git checkout can inspect & change it after instantiating.
626 # See the XmlManifest init code for more info.
627 self.use_git_worktrees = use_git_worktrees
628 self.is_derived = is_derived
629 self.optimized_fetch = optimized_fetch
630 self.retry_fetches = max(0, retry_fetches)
631 self.subprojects = []
632
633 self.snapshots = {}
634 self.copyfiles = []
635 self.linkfiles = []
636 self.annotations = []
637 self.dest_branch = dest_branch
638 self.old_revision = old_revision
639
640 # This will be filled in if a project is later identified to be the
641 # project containing repo hooks.
642 self.enabled_repo_hooks = []
643
644 def RelPath(self, local=True):
645 """Return the path for the project relative to a manifest.
646
647 Args:
648 local: a boolean, if True, the path is relative to the local
649 (sub)manifest. If false, the path is relative to the outermost
650 manifest.
651 """
652 if local:
653 return self.relpath
654 return os.path.join(self.manifest.path_prefix, self.relpath)
655
656 def SetRevision(self, revisionExpr, revisionId=None):
657 """Set revisionId based on revision expression and id"""
658 self.revisionExpr = revisionExpr
659 if revisionId is None and revisionExpr and IsId(revisionExpr):
660 self.revisionId = self.revisionExpr
661 else:
662 self.revisionId = revisionId
663
664 def UpdatePaths(self, relpath, worktree, gitdir, objdir):
665 """Update paths used by this project"""
666 self.gitdir = gitdir.replace("\\", "/")
667 self.objdir = objdir.replace("\\", "/")
668 if worktree:
669 self.worktree = os.path.normpath(worktree).replace("\\", "/")
670 else:
671 self.worktree = None
672 self.relpath = relpath
673
674 self.config = GitConfig.ForRepository(
675 gitdir=self.gitdir, defaults=self.manifest.globalConfig
676 )
677
678 if self.worktree:
679 self.work_git = self._GitGetByExec(
680 self, bare=False, gitdir=self.gitdir
681 )
682 else:
683 self.work_git = None
684 self.bare_git = self._GitGetByExec(self, bare=True, gitdir=self.gitdir)
685 self.bare_ref = GitRefs(self.gitdir)
686 self.bare_objdir = self._GitGetByExec(
687 self, bare=True, gitdir=self.objdir
688 )
689
690 @property
691 def UseAlternates(self):
692 """Whether git alternates are in use.
693
694 This will be removed once migration to alternates is complete.
695 """
696 return _ALTERNATES or self.manifest.is_multimanifest
697
698 @property
699 def Derived(self):
700 return self.is_derived
701
702 @property
703 def Exists(self):
704 return platform_utils.isdir(self.gitdir) and platform_utils.isdir(
705 self.objdir
706 )
707
708 @property
709 def CurrentBranch(self):
710 """Obtain the name of the currently checked out branch.
711
712 The branch name omits the 'refs/heads/' prefix.
713 None is returned if the project is on a detached HEAD, or if the
714 work_git is otheriwse inaccessible (e.g. an incomplete sync).
715 """
716 try:
717 b = self.work_git.GetHead()
718 except NoManifestException:
719 # If the local checkout is in a bad state, don't barf. Let the
720 # callers process this like the head is unreadable.
721 return None
722 if b.startswith(R_HEADS):
723 return b[len(R_HEADS) :]
724 return None
725
726 def IsRebaseInProgress(self):
727 return (
728 os.path.exists(self.work_git.GetDotgitPath("rebase-apply"))
729 or os.path.exists(self.work_git.GetDotgitPath("rebase-merge"))
730 or os.path.exists(os.path.join(self.worktree, ".dotest"))
731 )
732
733 def IsDirty(self, consider_untracked=True):
734 """Is the working directory modified in some way?"""
735 self.work_git.update_index(
736 "-q", "--unmerged", "--ignore-missing", "--refresh"
737 )
738 if self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD):
739 return True
740 if self.work_git.DiffZ("diff-files"):
741 return True
742 if consider_untracked and self.UntrackedFiles():
743 return True
744 return False
745
746 _userident_name = None
747 _userident_email = None
748
749 @property
750 def UserName(self):
751 """Obtain the user's personal name."""
752 if self._userident_name is None:
753 self._LoadUserIdentity()
754 return self._userident_name
755
756 @property
757 def UserEmail(self):
758 """Obtain the user's email address. This is very likely
759 to be their Gerrit login.
760 """
761 if self._userident_email is None:
762 self._LoadUserIdentity()
763 return self._userident_email
764
765 def _LoadUserIdentity(self):
766 u = self.bare_git.var("GIT_COMMITTER_IDENT")
767 m = re.compile("^(.*) <([^>]*)> ").match(u)
768 if m:
769 self._userident_name = m.group(1)
770 self._userident_email = m.group(2)
771 else:
772 self._userident_name = ""
773 self._userident_email = ""
774
775 def GetRemote(self, name=None):
776 """Get the configuration for a single remote.
777
778 Defaults to the current project's remote.
779 """
780 if name is None:
781 name = self.remote.name
782 return self.config.GetRemote(name)
783
784 def GetBranch(self, name):
785 """Get the configuration for a single branch."""
786 return self.config.GetBranch(name)
787
788 def GetBranches(self):
789 """Get all existing local branches."""
790 current = self.CurrentBranch
791 all_refs = self._allrefs
792 heads = {}
793
794 for name, ref_id in all_refs.items():
795 if name.startswith(R_HEADS):
796 name = name[len(R_HEADS) :]
797 b = self.GetBranch(name)
798 b.current = name == current
799 b.published = None
800 b.revision = ref_id
801 heads[name] = b
802
803 for name, ref_id in all_refs.items():
804 if name.startswith(R_PUB):
805 name = name[len(R_PUB) :]
806 b = heads.get(name)
807 if b:
808 b.published = ref_id
809
810 return heads
811
812 def MatchesGroups(self, manifest_groups):
813 """Returns true if the manifest groups specified at init should cause
814 this project to be synced.
815 Prefixing a manifest group with "-" inverts the meaning of a group.
816 All projects are implicitly labelled with "all".
817
818 labels are resolved in order. In the example case of
819 project_groups: "all,group1,group2"
820 manifest_groups: "-group1,group2"
821 the project will be matched.
822
823 The special manifest group "default" will match any project that
824 does not have the special project group "notdefault"
825 """
826 default_groups = self.manifest.default_groups or ["default"]
827 expanded_manifest_groups = manifest_groups or default_groups
828 expanded_project_groups = ["all"] + (self.groups or [])
829 if "notdefault" not in expanded_project_groups:
830 expanded_project_groups += ["default"]
831
832 matched = False
833 for group in expanded_manifest_groups:
834 if group.startswith("-") and group[1:] in expanded_project_groups:
835 matched = False
836 elif group in expanded_project_groups:
837 matched = True
838
839 return matched
840
841 def UncommitedFiles(self, get_all=True):
842 """Returns a list of strings, uncommitted files in the git tree.
843
844 Args:
845 get_all: a boolean, if True - get information about all different
846 uncommitted files. If False - return as soon as any kind of
847 uncommitted files is detected.
848 """
849 details = []
850 self.work_git.update_index(
851 "-q", "--unmerged", "--ignore-missing", "--refresh"
852 )
853 if self.IsRebaseInProgress():
854 details.append("rebase in progress")
855 if not get_all:
856 return details
857
858 changes = self.work_git.DiffZ("diff-index", "--cached", HEAD).keys()
859 if changes:
860 details.extend(changes)
861 if not get_all:
862 return details
863
864 changes = self.work_git.DiffZ("diff-files").keys()
865 if changes:
866 details.extend(changes)
867 if not get_all:
868 return details
869
870 changes = self.UntrackedFiles()
871 if changes:
872 details.extend(changes)
873
874 return details
875
876 def UntrackedFiles(self):
877 """Returns a list of strings, untracked files in the git tree."""
878 return self.work_git.LsOthers()
879
880 def HasChanges(self):
881 """Returns true if there are uncommitted changes."""
882 return bool(self.UncommitedFiles(get_all=False))
883
884 def PrintWorkTreeStatus(self, output_redir=None, quiet=False, local=False):
885 """Prints the status of the repository to stdout.
886
887 Args:
888 output_redir: If specified, redirect the output to this object.
889 quiet: If True then only print the project name. Do not print
890 the modified files, branch name, etc.
891 local: a boolean, if True, the path is relative to the local
892 (sub)manifest. If false, the path is relative to the outermost
893 manifest.
894 """
895 if not platform_utils.isdir(self.worktree):
896 if output_redir is None:
897 output_redir = sys.stdout
898 print(file=output_redir)
899 print("project %s/" % self.RelPath(local), file=output_redir)
900 print(' missing (run "repo sync")', file=output_redir)
901 return
902
903 self.work_git.update_index(
904 "-q", "--unmerged", "--ignore-missing", "--refresh"
905 )
906 rb = self.IsRebaseInProgress()
907 di = self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD)
908 df = self.work_git.DiffZ("diff-files")
909 do = self.work_git.LsOthers()
910 if not rb and not di and not df and not do and not self.CurrentBranch:
911 return "CLEAN"
912
913 out = StatusColoring(self.config)
914 if output_redir is not None:
915 out.redirect(output_redir)
916 out.project("project %-40s", self.RelPath(local) + "/ ")
917
918 if quiet:
919 out.nl()
920 return "DIRTY"
921
922 branch = self.CurrentBranch
923 if branch is None:
924 out.nobranch("(*** NO BRANCH ***)")
925 else:
926 out.branch("branch %s", branch)
927 out.nl()
928
929 if rb:
930 out.important("prior sync failed; rebase still in progress")
931 out.nl()
932
933 paths = list()
934 paths.extend(di.keys())
935 paths.extend(df.keys())
936 paths.extend(do)
937
938 for p in sorted(set(paths)):
939 try:
940 i = di[p]
941 except KeyError:
942 i = None
943
944 try:
945 f = df[p]
946 except KeyError:
947 f = None
948
949 if i:
950 i_status = i.status.upper()
951 else:
952 i_status = "-"
953
954 if f:
955 f_status = f.status.lower()
956 else:
957 f_status = "-"
958
959 if i and i.src_path:
960 line = " %s%s\t%s => %s (%s%%)" % (
961 i_status,
962 f_status,
963 i.src_path,
964 p,
965 i.level,
966 )
967 else:
968 line = " %s%s\t%s" % (i_status, f_status, p)
969
970 if i and not f:
971 out.added("%s", line)
972 elif (i and f) or (not i and f):
973 out.changed("%s", line)
974 elif not i and not f:
975 out.untracked("%s", line)
976 else:
977 out.write("%s", line)
978 out.nl()
979
980 return "DIRTY"
981
982 def PrintWorkTreeDiff(
983 self, absolute_paths=False, output_redir=None, local=False
984 ):
985 """Prints the status of the repository to stdout."""
986 out = DiffColoring(self.config)
987 if output_redir:
988 out.redirect(output_redir)
989 cmd = ["diff"]
990 if out.is_on:
991 cmd.append("--color")
992 cmd.append(HEAD)
993 if absolute_paths:
994 cmd.append("--src-prefix=a/%s/" % self.RelPath(local))
995 cmd.append("--dst-prefix=b/%s/" % self.RelPath(local))
996 cmd.append("--")
997 try:
998 p = GitCommand(self, cmd, capture_stdout=True, capture_stderr=True)
999 p.Wait()
1000 except GitError as e:
1001 out.nl()
1002 out.project("project %s/" % self.RelPath(local))
1003 out.nl()
1004 out.fail("%s", str(e))
1005 out.nl()
1006 return False
1007 if p.stdout:
1008 out.nl()
1009 out.project("project %s/" % self.RelPath(local))
1010 out.nl()
1011 out.write("%s", p.stdout)
1012 return p.Wait() == 0
1013
1014 def WasPublished(self, branch, all_refs=None):
1015 """Was the branch published (uploaded) for code review?
1016 If so, returns the SHA-1 hash of the last published
1017 state for the branch.
1018 """
1019 key = R_PUB + branch
1020 if all_refs is None:
1021 try:
1022 return self.bare_git.rev_parse(key)
1023 except GitError:
1024 return None
1025 else:
1026 try:
1027 return all_refs[key]
1028 except KeyError:
1029 return None
1030
1031 def CleanPublishedCache(self, all_refs=None):
1032 """Prunes any stale published refs."""
1033 if all_refs is None:
1034 all_refs = self._allrefs
1035 heads = set()
1036 canrm = {}
1037 for name, ref_id in all_refs.items():
1038 if name.startswith(R_HEADS):
1039 heads.add(name)
1040 elif name.startswith(R_PUB):
1041 canrm[name] = ref_id
1042
1043 for name, ref_id in canrm.items():
1044 n = name[len(R_PUB) :]
1045 if R_HEADS + n not in heads:
1046 self.bare_git.DeleteRef(name, ref_id)
1047
1048 def GetUploadableBranches(self, selected_branch=None):
1049 """List any branches which can be uploaded for review."""
1050 heads = {}
1051 pubed = {}
1052
1053 for name, ref_id in self._allrefs.items():
1054 if name.startswith(R_HEADS):
1055 heads[name[len(R_HEADS) :]] = ref_id
1056 elif name.startswith(R_PUB):
1057 pubed[name[len(R_PUB) :]] = ref_id
1058
1059 ready = []
1060 for branch, ref_id in heads.items():
1061 if branch in pubed and pubed[branch] == ref_id:
1062 continue
1063 if selected_branch and branch != selected_branch:
1064 continue
1065
1066 rb = self.GetUploadableBranch(branch)
1067 if rb:
1068 ready.append(rb)
1069 return ready
1070
1071 def GetUploadableBranch(self, branch_name):
1072 """Get a single uploadable branch, or None."""
1073 branch = self.GetBranch(branch_name)
1074 base = branch.LocalMerge
1075 if branch.LocalMerge:
1076 rb = ReviewableBranch(self, branch, base)
1077 if rb.commits:
1078 return rb
1079 return None
1080
1081 def UploadForReview(
1082 self,
1083 branch=None,
1084 people=([], []),
1085 dryrun=False,
1086 auto_topic=False,
1087 hashtags=(),
1088 labels=(),
1089 private=False,
1090 notify=None,
1091 wip=False,
1092 ready=False,
1093 dest_branch=None,
1094 validate_certs=True,
1095 push_options=None,
1096 ):
1097 """Uploads the named branch for code review."""
1098 if branch is None:
1099 branch = self.CurrentBranch
1100 if branch is None:
Jason Chang32b59562023-07-14 16:45:35 -07001101 raise GitError("not currently on a branch", project=self.name)
Gavin Makea2e3302023-03-11 06:46:20 +00001102
1103 branch = self.GetBranch(branch)
1104 if not branch.LocalMerge:
Jason Chang32b59562023-07-14 16:45:35 -07001105 raise GitError(
1106 "branch %s does not track a remote" % branch.name,
1107 project=self.name,
1108 )
Gavin Makea2e3302023-03-11 06:46:20 +00001109 if not branch.remote.review:
Jason Chang32b59562023-07-14 16:45:35 -07001110 raise GitError(
1111 "remote %s has no review url" % branch.remote.name,
1112 project=self.name,
1113 )
Gavin Makea2e3302023-03-11 06:46:20 +00001114
1115 # Basic validity check on label syntax.
1116 for label in labels:
1117 if not re.match(r"^.+[+-][0-9]+$", label):
1118 raise UploadError(
1119 f'invalid label syntax "{label}": labels use forms like '
Jason Chang5a3a5f72023-08-17 11:36:41 -07001120 "CodeReview+1 or Verified-1",
1121 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00001122 )
1123
1124 if dest_branch is None:
1125 dest_branch = self.dest_branch
1126 if dest_branch is None:
1127 dest_branch = branch.merge
1128 if not dest_branch.startswith(R_HEADS):
1129 dest_branch = R_HEADS + dest_branch
1130
1131 if not branch.remote.projectname:
1132 branch.remote.projectname = self.name
1133 branch.remote.Save()
1134
1135 url = branch.remote.ReviewUrl(self.UserEmail, validate_certs)
1136 if url is None:
Jason Chang5a3a5f72023-08-17 11:36:41 -07001137 raise UploadError("review not configured", project=self.name)
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +00001138 cmd = ["push", "--progress"]
Gavin Makea2e3302023-03-11 06:46:20 +00001139 if dryrun:
1140 cmd.append("-n")
1141
1142 if url.startswith("ssh://"):
1143 cmd.append("--receive-pack=gerrit receive-pack")
1144
Aravind Vasudevan99ebf622023-04-04 23:44:37 +00001145 # This stops git from pushing all reachable annotated tags when
1146 # push.followTags is configured. Gerrit does not accept any tags
1147 # pushed to a CL.
1148 if git_require((1, 8, 3)):
1149 cmd.append("--no-follow-tags")
1150
Gavin Makea2e3302023-03-11 06:46:20 +00001151 for push_option in push_options or []:
1152 cmd.append("-o")
1153 cmd.append(push_option)
1154
1155 cmd.append(url)
1156
1157 if dest_branch.startswith(R_HEADS):
1158 dest_branch = dest_branch[len(R_HEADS) :]
1159
1160 ref_spec = "%s:refs/for/%s" % (R_HEADS + branch.name, dest_branch)
1161 opts = []
1162 if auto_topic:
1163 opts += ["topic=" + branch.name]
1164 opts += ["t=%s" % p for p in hashtags]
1165 # NB: No need to encode labels as they've been validated above.
1166 opts += ["l=%s" % p for p in labels]
1167
1168 opts += ["r=%s" % p for p in people[0]]
1169 opts += ["cc=%s" % p for p in people[1]]
1170 if notify:
1171 opts += ["notify=" + notify]
1172 if private:
1173 opts += ["private"]
1174 if wip:
1175 opts += ["wip"]
1176 if ready:
1177 opts += ["ready"]
1178 if opts:
1179 ref_spec = ref_spec + "%" + ",".join(opts)
1180 cmd.append(ref_spec)
1181
Jason Chang1e9f7b92023-08-25 10:31:04 -07001182 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001183
1184 if not dryrun:
1185 msg = "posted to %s for %s" % (branch.remote.review, dest_branch)
1186 self.bare_git.UpdateRef(
1187 R_PUB + branch.name, R_HEADS + branch.name, message=msg
1188 )
1189
1190 def _ExtractArchive(self, tarpath, path=None):
1191 """Extract the given tar on its current location
1192
1193 Args:
1194 tarpath: The path to the actual tar file
1195
1196 """
1197 try:
1198 with tarfile.open(tarpath, "r") as tar:
1199 tar.extractall(path=path)
1200 return True
Jason R. Coombsae824fb2023-10-20 23:32:40 +05451201 except (OSError, tarfile.TarError) as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001202 logger.error("error: Cannot extract archive %s: %s", tarpath, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001203 return False
1204
1205 def Sync_NetworkHalf(
1206 self,
1207 quiet=False,
1208 verbose=False,
1209 output_redir=None,
1210 is_new=None,
1211 current_branch_only=None,
1212 force_sync=False,
1213 clone_bundle=True,
1214 tags=None,
1215 archive=False,
1216 optimized_fetch=False,
1217 retry_fetches=0,
1218 prune=False,
1219 submodules=False,
1220 ssh_proxy=None,
1221 clone_filter=None,
1222 partial_clone_exclude=set(),
Jason Chang17833322023-05-23 13:06:55 -07001223 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001224 ):
1225 """Perform only the network IO portion of the sync process.
1226 Local working directory/branch state is not affected.
1227 """
1228 if archive and not isinstance(self, MetaProject):
1229 if self.remote.url.startswith(("http://", "https://")):
Jason Chang32b59562023-07-14 16:45:35 -07001230 msg_template = (
1231 "%s: Cannot fetch archives from http/https remotes."
Gavin Makea2e3302023-03-11 06:46:20 +00001232 )
Jason Chang32b59562023-07-14 16:45:35 -07001233 msg_args = self.name
1234 msg = msg_template % msg_args
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001235 logger.error(msg_template, msg_args)
Jason Chang32b59562023-07-14 16:45:35 -07001236 return SyncNetworkHalfResult(
1237 False, SyncNetworkHalfError(msg, project=self.name)
1238 )
Gavin Makea2e3302023-03-11 06:46:20 +00001239
1240 name = self.relpath.replace("\\", "/")
1241 name = name.replace("/", "_")
1242 tarpath = "%s.tar" % name
1243 topdir = self.manifest.topdir
1244
1245 try:
1246 self._FetchArchive(tarpath, cwd=topdir)
1247 except GitError as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001248 logger.error("error: %s", e)
Jason Chang32b59562023-07-14 16:45:35 -07001249 return SyncNetworkHalfResult(False, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001250
1251 # From now on, we only need absolute tarpath.
1252 tarpath = os.path.join(topdir, tarpath)
1253
1254 if not self._ExtractArchive(tarpath, path=topdir):
Jason Chang32b59562023-07-14 16:45:35 -07001255 return SyncNetworkHalfResult(
1256 True,
1257 SyncNetworkHalfError(
1258 f"Unable to Extract Archive {tarpath}",
1259 project=self.name,
1260 ),
1261 )
Gavin Makea2e3302023-03-11 06:46:20 +00001262 try:
1263 platform_utils.remove(tarpath)
1264 except OSError as e:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00001265 logger.warning("warn: Cannot remove archive %s: %s", tarpath, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001266 self._CopyAndLinkFiles()
Jason Chang32b59562023-07-14 16:45:35 -07001267 return SyncNetworkHalfResult(True)
Gavin Makea2e3302023-03-11 06:46:20 +00001268
1269 # If the shared object dir already exists, don't try to rebootstrap with
1270 # a clone bundle download. We should have the majority of objects
1271 # already.
1272 if clone_bundle and os.path.exists(self.objdir):
1273 clone_bundle = False
1274
1275 if self.name in partial_clone_exclude:
1276 clone_bundle = True
1277 clone_filter = None
1278
1279 if is_new is None:
1280 is_new = not self.Exists
1281 if is_new:
1282 self._InitGitDir(force_sync=force_sync, quiet=quiet)
1283 else:
1284 self._UpdateHooks(quiet=quiet)
1285 self._InitRemote()
1286
1287 if self.UseAlternates:
1288 # If gitdir/objects is a symlink, migrate it from the old layout.
1289 gitdir_objects = os.path.join(self.gitdir, "objects")
1290 if platform_utils.islink(gitdir_objects):
1291 platform_utils.remove(gitdir_objects, missing_ok=True)
1292 gitdir_alt = os.path.join(self.gitdir, "objects/info/alternates")
1293 if not os.path.exists(gitdir_alt):
1294 os.makedirs(os.path.dirname(gitdir_alt), exist_ok=True)
1295 _lwrite(
1296 gitdir_alt,
1297 os.path.join(
1298 os.path.relpath(self.objdir, gitdir_objects), "objects"
1299 )
1300 + "\n",
1301 )
1302
1303 if is_new:
1304 alt = os.path.join(self.objdir, "objects/info/alternates")
1305 try:
1306 with open(alt) as fd:
1307 # This works for both absolute and relative alternate
1308 # directories.
1309 alt_dir = os.path.join(
1310 self.objdir, "objects", fd.readline().rstrip()
1311 )
Jason R. Coombsae824fb2023-10-20 23:32:40 +05451312 except OSError:
Gavin Makea2e3302023-03-11 06:46:20 +00001313 alt_dir = None
1314 else:
1315 alt_dir = None
1316
1317 if (
1318 clone_bundle
1319 and alt_dir is None
1320 and self._ApplyCloneBundle(
1321 initial=is_new, quiet=quiet, verbose=verbose
1322 )
1323 ):
1324 is_new = False
1325
1326 if current_branch_only is None:
1327 if self.sync_c:
1328 current_branch_only = True
1329 elif not self.manifest._loaded:
1330 # Manifest cannot check defaults until it syncs.
1331 current_branch_only = False
1332 elif self.manifest.default.sync_c:
1333 current_branch_only = True
1334
1335 if tags is None:
1336 tags = self.sync_tags
1337
1338 if self.clone_depth:
1339 depth = self.clone_depth
1340 else:
1341 depth = self.manifest.manifestProject.depth
1342
Jason Chang17833322023-05-23 13:06:55 -07001343 if depth and clone_filter_for_depth:
1344 depth = None
1345 clone_filter = clone_filter_for_depth
1346
Gavin Makea2e3302023-03-11 06:46:20 +00001347 # See if we can skip the network fetch entirely.
1348 remote_fetched = False
1349 if not (
1350 optimized_fetch
Sylvain56a5a012023-09-11 13:38:00 +02001351 and IsId(self.revisionExpr)
1352 and self._CheckForImmutableRevision()
Gavin Makea2e3302023-03-11 06:46:20 +00001353 ):
1354 remote_fetched = True
Jason Chang32b59562023-07-14 16:45:35 -07001355 try:
1356 if not self._RemoteFetch(
1357 initial=is_new,
1358 quiet=quiet,
1359 verbose=verbose,
1360 output_redir=output_redir,
1361 alt_dir=alt_dir,
1362 current_branch_only=current_branch_only,
1363 tags=tags,
1364 prune=prune,
1365 depth=depth,
1366 submodules=submodules,
1367 force_sync=force_sync,
1368 ssh_proxy=ssh_proxy,
1369 clone_filter=clone_filter,
1370 retry_fetches=retry_fetches,
1371 ):
1372 return SyncNetworkHalfResult(
1373 remote_fetched,
1374 SyncNetworkHalfError(
1375 f"Unable to remote fetch project {self.name}",
1376 project=self.name,
1377 ),
1378 )
1379 except RepoError as e:
1380 return SyncNetworkHalfResult(
1381 remote_fetched,
1382 e,
1383 )
Gavin Makea2e3302023-03-11 06:46:20 +00001384
1385 mp = self.manifest.manifestProject
1386 dissociate = mp.dissociate
1387 if dissociate:
1388 alternates_file = os.path.join(
1389 self.objdir, "objects/info/alternates"
1390 )
1391 if os.path.exists(alternates_file):
1392 cmd = ["repack", "-a", "-d"]
1393 p = GitCommand(
1394 self,
1395 cmd,
1396 bare=True,
1397 capture_stdout=bool(output_redir),
1398 merge_output=bool(output_redir),
1399 )
1400 if p.stdout and output_redir:
1401 output_redir.write(p.stdout)
1402 if p.Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07001403 return SyncNetworkHalfResult(
1404 remote_fetched,
1405 GitError(
1406 "Unable to repack alternates", project=self.name
1407 ),
1408 )
Gavin Makea2e3302023-03-11 06:46:20 +00001409 platform_utils.remove(alternates_file)
1410
1411 if self.worktree:
1412 self._InitMRef()
1413 else:
1414 self._InitMirrorHead()
1415 platform_utils.remove(
1416 os.path.join(self.gitdir, "FETCH_HEAD"), missing_ok=True
1417 )
Jason Chang32b59562023-07-14 16:45:35 -07001418 return SyncNetworkHalfResult(remote_fetched)
Gavin Makea2e3302023-03-11 06:46:20 +00001419
1420 def PostRepoUpgrade(self):
1421 self._InitHooks()
1422
1423 def _CopyAndLinkFiles(self):
1424 if self.client.isGitcClient:
1425 return
1426 for copyfile in self.copyfiles:
1427 copyfile._Copy()
1428 for linkfile in self.linkfiles:
1429 linkfile._Link()
1430
1431 def GetCommitRevisionId(self):
1432 """Get revisionId of a commit.
1433
1434 Use this method instead of GetRevisionId to get the id of the commit
1435 rather than the id of the current git object (for example, a tag)
1436
1437 """
Sylvaine9cb3912023-09-10 23:35:01 +02001438 if self.revisionId:
1439 return self.revisionId
Gavin Makea2e3302023-03-11 06:46:20 +00001440 if not self.revisionExpr.startswith(R_TAGS):
1441 return self.GetRevisionId(self._allrefs)
1442
1443 try:
1444 return self.bare_git.rev_list(self.revisionExpr, "-1")[0]
1445 except GitError:
1446 raise ManifestInvalidRevisionError(
1447 "revision %s in %s not found" % (self.revisionExpr, self.name)
1448 )
1449
1450 def GetRevisionId(self, all_refs=None):
1451 if self.revisionId:
1452 return self.revisionId
1453
1454 rem = self.GetRemote()
1455 rev = rem.ToLocal(self.revisionExpr)
1456
1457 if all_refs is not None and rev in all_refs:
1458 return all_refs[rev]
1459
1460 try:
1461 return self.bare_git.rev_parse("--verify", "%s^0" % rev)
1462 except GitError:
1463 raise ManifestInvalidRevisionError(
1464 "revision %s in %s not found" % (self.revisionExpr, self.name)
1465 )
1466
1467 def SetRevisionId(self, revisionId):
1468 if self.revisionExpr:
1469 self.upstream = self.revisionExpr
1470
1471 self.revisionId = revisionId
1472
Jason Chang32b59562023-07-14 16:45:35 -07001473 def Sync_LocalHalf(
1474 self, syncbuf, force_sync=False, submodules=False, errors=None
1475 ):
Gavin Makea2e3302023-03-11 06:46:20 +00001476 """Perform only the local IO portion of the sync process.
1477
1478 Network access is not required.
1479 """
Jason Chang32b59562023-07-14 16:45:35 -07001480 if errors is None:
1481 errors = []
1482
1483 def fail(error: Exception):
1484 errors.append(error)
1485 syncbuf.fail(self, error)
1486
Gavin Makea2e3302023-03-11 06:46:20 +00001487 if not os.path.exists(self.gitdir):
Jason Chang32b59562023-07-14 16:45:35 -07001488 fail(
1489 LocalSyncFail(
1490 "Cannot checkout %s due to missing network sync; Run "
1491 "`repo sync -n %s` first." % (self.name, self.name),
1492 project=self.name,
1493 )
Gavin Makea2e3302023-03-11 06:46:20 +00001494 )
1495 return
1496
1497 self._InitWorkTree(force_sync=force_sync, submodules=submodules)
1498 all_refs = self.bare_ref.all
1499 self.CleanPublishedCache(all_refs)
1500 revid = self.GetRevisionId(all_refs)
1501
1502 # Special case the root of the repo client checkout. Make sure it
1503 # doesn't contain files being checked out to dirs we don't allow.
1504 if self.relpath == ".":
1505 PROTECTED_PATHS = {".repo"}
1506 paths = set(
1507 self.work_git.ls_tree("-z", "--name-only", "--", revid).split(
1508 "\0"
1509 )
1510 )
1511 bad_paths = paths & PROTECTED_PATHS
1512 if bad_paths:
Jason Chang32b59562023-07-14 16:45:35 -07001513 fail(
1514 LocalSyncFail(
1515 "Refusing to checkout project that writes to protected "
1516 "paths: %s" % (", ".join(bad_paths),),
1517 project=self.name,
1518 )
Gavin Makea2e3302023-03-11 06:46:20 +00001519 )
1520 return
1521
1522 def _doff():
1523 self._FastForward(revid)
1524 self._CopyAndLinkFiles()
1525
1526 def _dosubmodules():
1527 self._SyncSubmodules(quiet=True)
1528
1529 head = self.work_git.GetHead()
1530 if head.startswith(R_HEADS):
1531 branch = head[len(R_HEADS) :]
1532 try:
1533 head = all_refs[head]
1534 except KeyError:
1535 head = None
1536 else:
1537 branch = None
1538
1539 if branch is None or syncbuf.detach_head:
1540 # Currently on a detached HEAD. The user is assumed to
1541 # not have any local modifications worth worrying about.
1542 if self.IsRebaseInProgress():
Jason Chang32b59562023-07-14 16:45:35 -07001543 fail(_PriorSyncFailedError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001544 return
1545
1546 if head == revid:
1547 # No changes; don't do anything further.
1548 # Except if the head needs to be detached.
1549 if not syncbuf.detach_head:
1550 # The copy/linkfile config may have changed.
1551 self._CopyAndLinkFiles()
1552 return
1553 else:
1554 lost = self._revlist(not_rev(revid), HEAD)
1555 if lost:
1556 syncbuf.info(self, "discarding %d commits", len(lost))
1557
1558 try:
1559 self._Checkout(revid, quiet=True)
1560 if submodules:
1561 self._SyncSubmodules(quiet=True)
1562 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001563 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001564 return
1565 self._CopyAndLinkFiles()
1566 return
1567
1568 if head == revid:
1569 # No changes; don't do anything further.
1570 #
1571 # The copy/linkfile config may have changed.
1572 self._CopyAndLinkFiles()
1573 return
1574
1575 branch = self.GetBranch(branch)
1576
1577 if not branch.LocalMerge:
1578 # The current branch has no tracking configuration.
1579 # Jump off it to a detached HEAD.
1580 syncbuf.info(
1581 self, "leaving %s; does not track upstream", branch.name
1582 )
1583 try:
1584 self._Checkout(revid, quiet=True)
1585 if submodules:
1586 self._SyncSubmodules(quiet=True)
1587 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001588 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001589 return
1590 self._CopyAndLinkFiles()
1591 return
1592
1593 upstream_gain = self._revlist(not_rev(HEAD), revid)
1594
1595 # See if we can perform a fast forward merge. This can happen if our
1596 # branch isn't in the exact same state as we last published.
1597 try:
Jason Chang87058c62023-09-27 11:34:43 -07001598 self.work_git.merge_base(
1599 "--is-ancestor", HEAD, revid, log_as_error=False
1600 )
Gavin Makea2e3302023-03-11 06:46:20 +00001601 # 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()
Sylvain56a5a012023-09-11 13:38:00 +02001671 if not IsId(self.revisionExpr):
Gavin Makea2e3302023-03-11 06:46:20 +00001672 # 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 )
Jason Chang1a3612f2023-08-08 14:12:53 -07001736 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001737 return DownloadedChange(
1738 self,
1739 self.GetRevisionId(),
1740 change_id,
1741 patch_id,
1742 self.bare_git.rev_parse("FETCH_HEAD"),
1743 )
1744
1745 def DeleteWorktree(self, quiet=False, force=False):
1746 """Delete the source checkout and any other housekeeping tasks.
1747
1748 This currently leaves behind the internal .repo/ cache state. This
1749 helps when switching branches or manifest changes get reverted as we
1750 don't have to redownload all the git objects. But we should do some GC
1751 at some point.
1752
1753 Args:
1754 quiet: Whether to hide normal messages.
1755 force: Always delete tree even if dirty.
Doug Anderson3ba5f952011-04-07 12:51:04 -07001756
1757 Returns:
Gavin Makea2e3302023-03-11 06:46:20 +00001758 True if the worktree was completely cleaned out.
1759 """
1760 if self.IsDirty():
1761 if force:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00001762 logger.warning(
Gavin Makea2e3302023-03-11 06:46:20 +00001763 "warning: %s: Removing dirty project: uncommitted changes "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001764 "lost.",
1765 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001766 )
1767 else:
Jason Chang32b59562023-07-14 16:45:35 -07001768 msg = (
1769 "error: %s: Cannot remove project: uncommitted"
1770 "changes are present.\n" % self.RelPath(local=False)
Gavin Makea2e3302023-03-11 06:46:20 +00001771 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001772 logger.error(msg)
Jason Chang32b59562023-07-14 16:45:35 -07001773 raise DeleteDirtyWorktreeError(msg, project=self)
Wink Saville02d79452009-04-10 13:01:24 -07001774
Gavin Makea2e3302023-03-11 06:46:20 +00001775 if not quiet:
1776 print(
1777 "%s: Deleting obsolete checkout." % (self.RelPath(local=False),)
1778 )
Wink Saville02d79452009-04-10 13:01:24 -07001779
Gavin Makea2e3302023-03-11 06:46:20 +00001780 # Unlock and delink from the main worktree. We don't use git's worktree
1781 # remove because it will recursively delete projects -- we handle that
1782 # ourselves below. https://crbug.com/git/48
1783 if self.use_git_worktrees:
1784 needle = platform_utils.realpath(self.gitdir)
1785 # Find the git worktree commondir under .repo/worktrees/.
1786 output = self.bare_git.worktree("list", "--porcelain").splitlines()[
1787 0
1788 ]
1789 assert output.startswith("worktree "), output
1790 commondir = output[9:]
1791 # Walk each of the git worktrees to see where they point.
1792 configs = os.path.join(commondir, "worktrees")
1793 for name in os.listdir(configs):
1794 gitdir = os.path.join(configs, name, "gitdir")
1795 with open(gitdir) as fp:
1796 relpath = fp.read().strip()
1797 # Resolve the checkout path and see if it matches this project.
1798 fullpath = platform_utils.realpath(
1799 os.path.join(configs, name, relpath)
1800 )
1801 if fullpath == needle:
1802 platform_utils.rmtree(os.path.join(configs, name))
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001803
Gavin Makea2e3302023-03-11 06:46:20 +00001804 # Delete the .git directory first, so we're less likely to have a
1805 # partially working git repository around. There shouldn't be any git
1806 # projects here, so rmtree works.
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001807
Gavin Makea2e3302023-03-11 06:46:20 +00001808 # Try to remove plain files first in case of git worktrees. If this
1809 # fails for any reason, we'll fall back to rmtree, and that'll display
1810 # errors if it can't remove things either.
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001811 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001812 platform_utils.remove(self.gitdir)
1813 except OSError:
1814 pass
1815 try:
1816 platform_utils.rmtree(self.gitdir)
1817 except OSError as e:
1818 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001819 logger.error("error: %s: %s", self.gitdir, e)
1820 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00001821 "error: %s: Failed to delete obsolete checkout; remove "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001822 "manually, then run `repo sync -l`.",
1823 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001824 )
Jason Chang32b59562023-07-14 16:45:35 -07001825 raise DeleteWorktreeError(aggregate_errors=[e])
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001826
Gavin Makea2e3302023-03-11 06:46:20 +00001827 # Delete everything under the worktree, except for directories that
1828 # contain another git project.
1829 dirs_to_remove = []
1830 failed = False
Jason Chang32b59562023-07-14 16:45:35 -07001831 errors = []
Gavin Makea2e3302023-03-11 06:46:20 +00001832 for root, dirs, files in platform_utils.walk(self.worktree):
1833 for f in files:
1834 path = os.path.join(root, f)
1835 try:
1836 platform_utils.remove(path)
1837 except OSError as e:
1838 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001839 logger.error("error: %s: Failed to remove: %s", path, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001840 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001841 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001842 dirs[:] = [
1843 d
1844 for d in dirs
1845 if not os.path.lexists(os.path.join(root, d, ".git"))
1846 ]
1847 dirs_to_remove += [
1848 os.path.join(root, d)
1849 for d in dirs
1850 if os.path.join(root, d) not in dirs_to_remove
1851 ]
1852 for d in reversed(dirs_to_remove):
1853 if platform_utils.islink(d):
1854 try:
1855 platform_utils.remove(d)
1856 except OSError as e:
1857 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001858 logger.error("error: %s: Failed to remove: %s", d, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001859 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001860 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001861 elif not platform_utils.listdir(d):
1862 try:
1863 platform_utils.rmdir(d)
1864 except OSError as e:
1865 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001866 logger.error("error: %s: Failed to remove: %s", d, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001867 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001868 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001869 if failed:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001870 logger.error(
1871 "error: %s: Failed to delete obsolete checkout.",
1872 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001873 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001874 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00001875 " Remove manually, then run `repo sync -l`.",
Gavin Makea2e3302023-03-11 06:46:20 +00001876 )
Jason Chang32b59562023-07-14 16:45:35 -07001877 raise DeleteWorktreeError(aggregate_errors=errors)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07001878
Gavin Makea2e3302023-03-11 06:46:20 +00001879 # Try deleting parent dirs if they are empty.
1880 path = self.worktree
1881 while path != self.manifest.topdir:
1882 try:
1883 platform_utils.rmdir(path)
1884 except OSError as e:
1885 if e.errno != errno.ENOENT:
1886 break
1887 path = os.path.dirname(path)
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001888
Gavin Makea2e3302023-03-11 06:46:20 +00001889 return True
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001890
Gavin Makea2e3302023-03-11 06:46:20 +00001891 def StartBranch(self, name, branch_merge="", revision=None):
1892 """Create a new branch off the manifest's revision."""
1893 if not branch_merge:
1894 branch_merge = self.revisionExpr
1895 head = self.work_git.GetHead()
1896 if head == (R_HEADS + name):
1897 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001898
David Pursehouse8a68ff92012-09-24 12:15:13 +09001899 all_refs = self.bare_ref.all
Gavin Makea2e3302023-03-11 06:46:20 +00001900 if R_HEADS + name in all_refs:
Jason Chang1a3612f2023-08-08 14:12:53 -07001901 GitCommand(
1902 self, ["checkout", "-q", name, "--"], verify_command=True
1903 ).Wait()
1904 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001905
Gavin Makea2e3302023-03-11 06:46:20 +00001906 branch = self.GetBranch(name)
1907 branch.remote = self.GetRemote()
1908 branch.merge = branch_merge
Sylvain56a5a012023-09-11 13:38:00 +02001909 if not branch.merge.startswith("refs/") and not IsId(branch_merge):
Gavin Makea2e3302023-03-11 06:46:20 +00001910 branch.merge = R_HEADS + branch_merge
Shawn O. Pearce88443382010-10-08 10:02:09 +02001911
Gavin Makea2e3302023-03-11 06:46:20 +00001912 if revision is None:
1913 revid = self.GetRevisionId(all_refs)
Shawn O. Pearce88443382010-10-08 10:02:09 +02001914 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001915 revid = self.work_git.rev_parse(revision)
Brian Harring14a66742012-09-28 20:21:57 -07001916
Gavin Makea2e3302023-03-11 06:46:20 +00001917 if head.startswith(R_HEADS):
Kevin Degiabaa7f32014-11-12 11:27:45 -07001918 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001919 head = all_refs[head]
1920 except KeyError:
1921 head = None
1922 if revid and head and revid == head:
1923 ref = R_HEADS + name
1924 self.work_git.update_ref(ref, revid)
1925 self.work_git.symbolic_ref(HEAD, ref)
1926 branch.Save()
1927 return True
Kevin Degib1a07b82015-07-27 13:33:43 -06001928
Jason Chang1a3612f2023-08-08 14:12:53 -07001929 GitCommand(
1930 self,
1931 ["checkout", "-q", "-b", branch.name, revid],
1932 verify_command=True,
1933 ).Wait()
1934 branch.Save()
1935 return True
Kevin Degi384b3c52014-10-16 16:02:58 -06001936
Gavin Makea2e3302023-03-11 06:46:20 +00001937 def CheckoutBranch(self, name):
1938 """Checkout a local topic branch.
Shawn O. Pearce2816d4f2009-03-03 17:53:18 -08001939
Gavin Makea2e3302023-03-11 06:46:20 +00001940 Args:
1941 name: The name of the branch to checkout.
Shawn O. Pearce88443382010-10-08 10:02:09 +02001942
Gavin Makea2e3302023-03-11 06:46:20 +00001943 Returns:
Jason Chang1a3612f2023-08-08 14:12:53 -07001944 True if the checkout succeeded; False if the
1945 branch doesn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00001946 """
1947 rev = R_HEADS + name
1948 head = self.work_git.GetHead()
1949 if head == rev:
1950 # Already on the branch.
1951 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001952
Gavin Makea2e3302023-03-11 06:46:20 +00001953 all_refs = self.bare_ref.all
1954 try:
1955 revid = all_refs[rev]
1956 except KeyError:
1957 # Branch does not exist in this project.
Jason Chang1a3612f2023-08-08 14:12:53 -07001958 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001959
Gavin Makea2e3302023-03-11 06:46:20 +00001960 if head.startswith(R_HEADS):
1961 try:
1962 head = all_refs[head]
1963 except KeyError:
1964 head = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001965
Gavin Makea2e3302023-03-11 06:46:20 +00001966 if head == revid:
1967 # Same revision; just update HEAD to point to the new
1968 # target branch, but otherwise take no other action.
1969 _lwrite(
1970 self.work_git.GetDotgitPath(subpath=HEAD),
1971 "ref: %s%s\n" % (R_HEADS, name),
1972 )
1973 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05001974
Jason Chang1a3612f2023-08-08 14:12:53 -07001975 GitCommand(
1976 self,
1977 ["checkout", name, "--"],
1978 capture_stdout=True,
1979 capture_stderr=True,
1980 verify_command=True,
1981 ).Wait()
1982 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05001983
Gavin Makea2e3302023-03-11 06:46:20 +00001984 def AbandonBranch(self, name):
1985 """Destroy a local topic branch.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07001986
Gavin Makea2e3302023-03-11 06:46:20 +00001987 Args:
1988 name: The name of the branch to abandon.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07001989
Gavin Makea2e3302023-03-11 06:46:20 +00001990 Returns:
Jason Changf9aacd42023-08-03 14:38:00 -07001991 True if the abandon succeeded; Raises GitCommandError if it didn't;
1992 None if the branch didn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00001993 """
1994 rev = R_HEADS + name
1995 all_refs = self.bare_ref.all
1996 if rev not in all_refs:
1997 # Doesn't exist
1998 return None
1999
2000 head = self.work_git.GetHead()
2001 if head == rev:
2002 # We can't destroy the branch while we are sitting
2003 # on it. Switch to a detached HEAD.
2004 head = all_refs[head]
2005
2006 revid = self.GetRevisionId(all_refs)
2007 if head == revid:
2008 _lwrite(
2009 self.work_git.GetDotgitPath(subpath=HEAD), "%s\n" % revid
2010 )
2011 else:
2012 self._Checkout(revid, quiet=True)
Jason Changf9aacd42023-08-03 14:38:00 -07002013 GitCommand(
2014 self,
2015 ["branch", "-D", name],
2016 capture_stdout=True,
2017 capture_stderr=True,
2018 verify_command=True,
2019 ).Wait()
2020 return True
Gavin Makea2e3302023-03-11 06:46:20 +00002021
2022 def PruneHeads(self):
2023 """Prune any topic branches already merged into upstream."""
2024 cb = self.CurrentBranch
2025 kill = []
2026 left = self._allrefs
2027 for name in left.keys():
2028 if name.startswith(R_HEADS):
2029 name = name[len(R_HEADS) :]
2030 if cb is None or name != cb:
2031 kill.append(name)
2032
2033 # Minor optimization: If there's nothing to prune, then don't try to
2034 # read any project state.
2035 if not kill and not cb:
2036 return []
2037
2038 rev = self.GetRevisionId(left)
2039 if (
2040 cb is not None
2041 and not self._revlist(HEAD + "..." + rev)
2042 and not self.IsDirty(consider_untracked=False)
2043 ):
2044 self.work_git.DetachHead(HEAD)
2045 kill.append(cb)
2046
2047 if kill:
2048 old = self.bare_git.GetHead()
2049
2050 try:
2051 self.bare_git.DetachHead(rev)
2052
2053 b = ["branch", "-d"]
2054 b.extend(kill)
2055 b = GitCommand(
2056 self, b, bare=True, capture_stdout=True, capture_stderr=True
2057 )
2058 b.Wait()
2059 finally:
Sylvain56a5a012023-09-11 13:38:00 +02002060 if IsId(old):
Gavin Makea2e3302023-03-11 06:46:20 +00002061 self.bare_git.DetachHead(old)
2062 else:
2063 self.bare_git.SetHead(old)
2064 left = self._allrefs
2065
2066 for branch in kill:
2067 if (R_HEADS + branch) not in left:
2068 self.CleanPublishedCache()
2069 break
2070
2071 if cb and cb not in kill:
2072 kill.append(cb)
2073 kill.sort()
2074
2075 kept = []
2076 for branch in kill:
2077 if R_HEADS + branch in left:
2078 branch = self.GetBranch(branch)
2079 base = branch.LocalMerge
2080 if not base:
2081 base = rev
2082 kept.append(ReviewableBranch(self, branch, base))
2083 return kept
2084
2085 def GetRegisteredSubprojects(self):
2086 result = []
2087
2088 def rec(subprojects):
2089 if not subprojects:
2090 return
2091 result.extend(subprojects)
2092 for p in subprojects:
2093 rec(p.subprojects)
2094
2095 rec(self.subprojects)
2096 return result
2097
2098 def _GetSubmodules(self):
2099 # Unfortunately we cannot call `git submodule status --recursive` here
2100 # because the working tree might not exist yet, and it cannot be used
2101 # without a working tree in its current implementation.
2102
2103 def get_submodules(gitdir, rev):
2104 # Parse .gitmodules for submodule sub_paths and sub_urls.
2105 sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
2106 if not sub_paths:
2107 return []
2108 # Run `git ls-tree` to read SHAs of submodule object, which happen
2109 # to be revision of submodule repository.
2110 sub_revs = git_ls_tree(gitdir, rev, sub_paths)
2111 submodules = []
2112 for sub_path, sub_url in zip(sub_paths, sub_urls):
2113 try:
2114 sub_rev = sub_revs[sub_path]
2115 except KeyError:
2116 # Ignore non-exist submodules.
2117 continue
2118 submodules.append((sub_rev, sub_path, sub_url))
2119 return submodules
2120
2121 re_path = re.compile(r"^submodule\.(.+)\.path=(.*)$")
2122 re_url = re.compile(r"^submodule\.(.+)\.url=(.*)$")
2123
2124 def parse_gitmodules(gitdir, rev):
2125 cmd = ["cat-file", "blob", "%s:.gitmodules" % rev]
2126 try:
2127 p = GitCommand(
2128 None,
2129 cmd,
2130 capture_stdout=True,
2131 capture_stderr=True,
2132 bare=True,
2133 gitdir=gitdir,
2134 )
2135 except GitError:
2136 return [], []
2137 if p.Wait() != 0:
2138 return [], []
2139
2140 gitmodules_lines = []
2141 fd, temp_gitmodules_path = tempfile.mkstemp()
2142 try:
2143 os.write(fd, p.stdout.encode("utf-8"))
2144 os.close(fd)
2145 cmd = ["config", "--file", temp_gitmodules_path, "--list"]
2146 p = GitCommand(
2147 None,
2148 cmd,
2149 capture_stdout=True,
2150 capture_stderr=True,
2151 bare=True,
2152 gitdir=gitdir,
2153 )
2154 if p.Wait() != 0:
2155 return [], []
2156 gitmodules_lines = p.stdout.split("\n")
2157 except GitError:
2158 return [], []
2159 finally:
2160 platform_utils.remove(temp_gitmodules_path)
2161
2162 names = set()
2163 paths = {}
2164 urls = {}
2165 for line in gitmodules_lines:
2166 if not line:
2167 continue
2168 m = re_path.match(line)
2169 if m:
2170 names.add(m.group(1))
2171 paths[m.group(1)] = m.group(2)
2172 continue
2173 m = re_url.match(line)
2174 if m:
2175 names.add(m.group(1))
2176 urls[m.group(1)] = m.group(2)
2177 continue
2178 names = sorted(names)
2179 return (
2180 [paths.get(name, "") for name in names],
2181 [urls.get(name, "") for name in names],
2182 )
2183
2184 def git_ls_tree(gitdir, rev, paths):
2185 cmd = ["ls-tree", rev, "--"]
2186 cmd.extend(paths)
2187 try:
2188 p = GitCommand(
2189 None,
2190 cmd,
2191 capture_stdout=True,
2192 capture_stderr=True,
2193 bare=True,
2194 gitdir=gitdir,
2195 )
2196 except GitError:
2197 return []
2198 if p.Wait() != 0:
2199 return []
2200 objects = {}
2201 for line in p.stdout.split("\n"):
2202 if not line.strip():
2203 continue
2204 object_rev, object_path = line.split()[2:4]
2205 objects[object_path] = object_rev
2206 return objects
2207
2208 try:
2209 rev = self.GetRevisionId()
2210 except GitError:
2211 return []
2212 return get_submodules(self.gitdir, rev)
2213
2214 def GetDerivedSubprojects(self):
2215 result = []
2216 if not self.Exists:
2217 # If git repo does not exist yet, querying its submodules will
2218 # mess up its states; so return here.
2219 return result
2220 for rev, path, url in self._GetSubmodules():
2221 name = self.manifest.GetSubprojectName(self, path)
2222 (
2223 relpath,
2224 worktree,
2225 gitdir,
2226 objdir,
2227 ) = self.manifest.GetSubprojectPaths(self, name, path)
2228 project = self.manifest.paths.get(relpath)
2229 if project:
2230 result.extend(project.GetDerivedSubprojects())
2231 continue
2232
2233 if url.startswith(".."):
2234 url = urllib.parse.urljoin("%s/" % self.remote.url, url)
2235 remote = RemoteSpec(
2236 self.remote.name,
2237 url=url,
2238 pushUrl=self.remote.pushUrl,
2239 review=self.remote.review,
2240 revision=self.remote.revision,
2241 )
2242 subproject = Project(
2243 manifest=self.manifest,
2244 name=name,
2245 remote=remote,
2246 gitdir=gitdir,
2247 objdir=objdir,
2248 worktree=worktree,
2249 relpath=relpath,
2250 revisionExpr=rev,
2251 revisionId=rev,
2252 rebase=self.rebase,
2253 groups=self.groups,
2254 sync_c=self.sync_c,
2255 sync_s=self.sync_s,
2256 sync_tags=self.sync_tags,
2257 parent=self,
2258 is_derived=True,
2259 )
2260 result.append(subproject)
2261 result.extend(subproject.GetDerivedSubprojects())
2262 return result
2263
2264 def EnableRepositoryExtension(self, key, value="true", version=1):
2265 """Enable git repository extension |key| with |value|.
2266
2267 Args:
2268 key: The extension to enabled. Omit the "extensions." prefix.
2269 value: The value to use for the extension.
2270 version: The minimum git repository version needed.
2271 """
2272 # Make sure the git repo version is new enough already.
2273 found_version = self.config.GetInt("core.repositoryFormatVersion")
2274 if found_version is None:
2275 found_version = 0
2276 if found_version < version:
2277 self.config.SetString("core.repositoryFormatVersion", str(version))
2278
2279 # Enable the extension!
2280 self.config.SetString("extensions.%s" % (key,), value)
2281
2282 def ResolveRemoteHead(self, name=None):
2283 """Find out what the default branch (HEAD) points to.
2284
2285 Normally this points to refs/heads/master, but projects are moving to
2286 main. Support whatever the server uses rather than hardcoding "master"
2287 ourselves.
2288 """
2289 if name is None:
2290 name = self.remote.name
2291
2292 # The output will look like (NB: tabs are separators):
2293 # ref: refs/heads/master HEAD
2294 # 5f6803b100bb3cd0f534e96e88c91373e8ed1c44 HEAD
2295 output = self.bare_git.ls_remote(
2296 "-q", "--symref", "--exit-code", name, "HEAD"
2297 )
2298
2299 for line in output.splitlines():
2300 lhs, rhs = line.split("\t", 1)
2301 if rhs == "HEAD" and lhs.startswith("ref:"):
2302 return lhs[4:].strip()
2303
2304 return None
2305
2306 def _CheckForImmutableRevision(self):
2307 try:
2308 # if revision (sha or tag) is not present then following function
2309 # throws an error.
2310 self.bare_git.rev_list(
Jason Chang87058c62023-09-27 11:34:43 -07002311 "-1",
2312 "--missing=allow-any",
2313 "%s^0" % self.revisionExpr,
2314 "--",
2315 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002316 )
2317 if self.upstream:
2318 rev = self.GetRemote().ToLocal(self.upstream)
2319 self.bare_git.rev_list(
Jason Chang87058c62023-09-27 11:34:43 -07002320 "-1",
2321 "--missing=allow-any",
2322 "%s^0" % rev,
2323 "--",
2324 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002325 )
2326 self.bare_git.merge_base(
Jason Chang87058c62023-09-27 11:34:43 -07002327 "--is-ancestor",
2328 self.revisionExpr,
2329 rev,
2330 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002331 )
2332 return True
2333 except GitError:
2334 # There is no such persistent revision. We have to fetch it.
2335 return False
2336
2337 def _FetchArchive(self, tarpath, cwd=None):
2338 cmd = ["archive", "-v", "-o", tarpath]
2339 cmd.append("--remote=%s" % self.remote.url)
2340 cmd.append("--prefix=%s/" % self.RelPath(local=False))
2341 cmd.append(self.revisionExpr)
2342
2343 command = GitCommand(
Jason Chang32b59562023-07-14 16:45:35 -07002344 self,
2345 cmd,
2346 cwd=cwd,
2347 capture_stdout=True,
2348 capture_stderr=True,
2349 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00002350 )
Jason Chang32b59562023-07-14 16:45:35 -07002351 command.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00002352
2353 def _RemoteFetch(
2354 self,
2355 name=None,
2356 current_branch_only=False,
2357 initial=False,
2358 quiet=False,
2359 verbose=False,
2360 output_redir=None,
2361 alt_dir=None,
2362 tags=True,
2363 prune=False,
2364 depth=None,
2365 submodules=False,
2366 ssh_proxy=None,
2367 force_sync=False,
2368 clone_filter=None,
2369 retry_fetches=2,
2370 retry_sleep_initial_sec=4.0,
2371 retry_exp_factor=2.0,
Jason Chang32b59562023-07-14 16:45:35 -07002372 ) -> bool:
Gavin Makea2e3302023-03-11 06:46:20 +00002373 tag_name = None
2374 # The depth should not be used when fetching to a mirror because
2375 # it will result in a shallow repository that cannot be cloned or
2376 # fetched from.
2377 # The repo project should also never be synced with partial depth.
2378 if self.manifest.IsMirror or self.relpath == ".repo/repo":
2379 depth = None
2380
2381 if depth:
2382 current_branch_only = True
2383
Sylvain56a5a012023-09-11 13:38:00 +02002384 is_sha1 = bool(IsId(self.revisionExpr))
Gavin Makea2e3302023-03-11 06:46:20 +00002385
2386 if current_branch_only:
2387 if self.revisionExpr.startswith(R_TAGS):
2388 # This is a tag and its commit id should never change.
2389 tag_name = self.revisionExpr[len(R_TAGS) :]
2390 elif self.upstream and self.upstream.startswith(R_TAGS):
2391 # This is a tag and its commit id should never change.
2392 tag_name = self.upstream[len(R_TAGS) :]
2393
2394 if is_sha1 or tag_name is not None:
2395 if self._CheckForImmutableRevision():
2396 if verbose:
2397 print(
2398 "Skipped fetching project %s (already have "
2399 "persistent ref)" % self.name
2400 )
2401 return True
2402 if is_sha1 and not depth:
2403 # When syncing a specific commit and --depth is not set:
2404 # * if upstream is explicitly specified and is not a sha1, fetch
2405 # only upstream as users expect only upstream to be fetch.
2406 # Note: The commit might not be in upstream in which case the
2407 # sync will fail.
2408 # * otherwise, fetch all branches to make sure we end up with
2409 # the specific commit.
2410 if self.upstream:
Sylvain56a5a012023-09-11 13:38:00 +02002411 current_branch_only = not IsId(self.upstream)
Gavin Makea2e3302023-03-11 06:46:20 +00002412 else:
2413 current_branch_only = False
2414
2415 if not name:
2416 name = self.remote.name
2417
2418 remote = self.GetRemote(name)
2419 if not remote.PreConnectFetch(ssh_proxy):
2420 ssh_proxy = None
2421
2422 if initial:
2423 if alt_dir and "objects" == os.path.basename(alt_dir):
2424 ref_dir = os.path.dirname(alt_dir)
2425 packed_refs = os.path.join(self.gitdir, "packed-refs")
2426
2427 all_refs = self.bare_ref.all
2428 ids = set(all_refs.values())
2429 tmp = set()
2430
2431 for r, ref_id in GitRefs(ref_dir).all.items():
2432 if r not in all_refs:
2433 if r.startswith(R_TAGS) or remote.WritesTo(r):
2434 all_refs[r] = ref_id
2435 ids.add(ref_id)
2436 continue
2437
2438 if ref_id in ids:
2439 continue
2440
2441 r = "refs/_alt/%s" % ref_id
2442 all_refs[r] = ref_id
2443 ids.add(ref_id)
2444 tmp.add(r)
2445
2446 tmp_packed_lines = []
2447 old_packed_lines = []
2448
2449 for r in sorted(all_refs):
2450 line = "%s %s\n" % (all_refs[r], r)
2451 tmp_packed_lines.append(line)
2452 if r not in tmp:
2453 old_packed_lines.append(line)
2454
2455 tmp_packed = "".join(tmp_packed_lines)
2456 old_packed = "".join(old_packed_lines)
2457 _lwrite(packed_refs, tmp_packed)
2458 else:
2459 alt_dir = None
2460
2461 cmd = ["fetch"]
2462
2463 if clone_filter:
2464 git_require((2, 19, 0), fail=True, msg="partial clones")
2465 cmd.append("--filter=%s" % clone_filter)
2466 self.EnableRepositoryExtension("partialclone", self.remote.name)
2467
2468 if depth:
2469 cmd.append("--depth=%s" % depth)
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002470 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002471 # If this repo has shallow objects, then we don't know which refs
2472 # have shallow objects or not. Tell git to unshallow all fetched
2473 # refs. Don't do this with projects that don't have shallow
2474 # objects, since it is less efficient.
2475 if os.path.exists(os.path.join(self.gitdir, "shallow")):
2476 cmd.append("--depth=2147483647")
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002477
Gavin Makea2e3302023-03-11 06:46:20 +00002478 if not verbose:
2479 cmd.append("--quiet")
2480 if not quiet and sys.stdout.isatty():
2481 cmd.append("--progress")
2482 if not self.worktree:
2483 cmd.append("--update-head-ok")
2484 cmd.append(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002485
Gavin Makea2e3302023-03-11 06:46:20 +00002486 if force_sync:
2487 cmd.append("--force")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002488
Gavin Makea2e3302023-03-11 06:46:20 +00002489 if prune:
2490 cmd.append("--prune")
Mike Frysinger29626b42021-05-01 09:37:13 -04002491
Gavin Makea2e3302023-03-11 06:46:20 +00002492 # Always pass something for --recurse-submodules, git with GIT_DIR
2493 # behaves incorrectly when not given `--recurse-submodules=no`.
2494 # (b/218891912)
2495 cmd.append(
2496 f'--recurse-submodules={"on-demand" if submodules else "no"}'
2497 )
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002498
Gavin Makea2e3302023-03-11 06:46:20 +00002499 spec = []
2500 if not current_branch_only:
2501 # Fetch whole repo.
2502 spec.append(
2503 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2504 )
2505 elif tag_name is not None:
2506 spec.append("tag")
2507 spec.append(tag_name)
Remy Böhmer1469c282020-12-15 18:49:02 +01002508
Gavin Makea2e3302023-03-11 06:46:20 +00002509 if self.manifest.IsMirror and not current_branch_only:
2510 branch = None
Remy Böhmer1469c282020-12-15 18:49:02 +01002511 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002512 branch = self.revisionExpr
2513 if (
2514 not self.manifest.IsMirror
2515 and is_sha1
2516 and depth
2517 and git_require((1, 8, 3))
2518 ):
2519 # Shallow checkout of a specific commit, fetch from that commit and
2520 # not the heads only as the commit might be deeper in the history.
2521 spec.append(branch)
2522 if self.upstream:
2523 spec.append(self.upstream)
David James8d201162013-10-11 17:03:19 -07002524 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002525 if is_sha1:
2526 branch = self.upstream
2527 if branch is not None and branch.strip():
2528 if not branch.startswith("refs/"):
2529 branch = R_HEADS + branch
2530 spec.append(str(("+%s:" % branch) + remote.ToLocal(branch)))
David James8d201162013-10-11 17:03:19 -07002531
Gavin Makea2e3302023-03-11 06:46:20 +00002532 # If mirroring repo and we cannot deduce the tag or branch to fetch,
2533 # fetch whole repo.
2534 if self.manifest.IsMirror and not spec:
2535 spec.append(
2536 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2537 )
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002538
Gavin Makea2e3302023-03-11 06:46:20 +00002539 # If using depth then we should not get all the tags since they may
2540 # be outside of the depth.
2541 if not tags or depth:
2542 cmd.append("--no-tags")
2543 else:
2544 cmd.append("--tags")
2545 spec.append(str(("+refs/tags/*:") + remote.ToLocal("refs/tags/*")))
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002546
Gavin Makea2e3302023-03-11 06:46:20 +00002547 cmd.extend(spec)
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002548
Gavin Makea2e3302023-03-11 06:46:20 +00002549 # At least one retry minimum due to git remote prune.
2550 retry_fetches = max(retry_fetches, 2)
2551 retry_cur_sleep = retry_sleep_initial_sec
2552 ok = prune_tried = False
2553 for try_n in range(retry_fetches):
Jason Chang32b59562023-07-14 16:45:35 -07002554 verify_command = try_n == retry_fetches - 1
Gavin Makea2e3302023-03-11 06:46:20 +00002555 gitcmd = GitCommand(
2556 self,
2557 cmd,
2558 bare=True,
2559 objdir=os.path.join(self.objdir, "objects"),
2560 ssh_proxy=ssh_proxy,
2561 merge_output=True,
2562 capture_stdout=quiet or bool(output_redir),
Jason Chang32b59562023-07-14 16:45:35 -07002563 verify_command=verify_command,
Gavin Makea2e3302023-03-11 06:46:20 +00002564 )
2565 if gitcmd.stdout and not quiet and output_redir:
2566 output_redir.write(gitcmd.stdout)
2567 ret = gitcmd.Wait()
2568 if ret == 0:
2569 ok = True
2570 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002571
Gavin Makea2e3302023-03-11 06:46:20 +00002572 # Retry later due to HTTP 429 Too Many Requests.
2573 elif (
2574 gitcmd.stdout
2575 and "error:" in gitcmd.stdout
2576 and "HTTP 429" in gitcmd.stdout
2577 ):
2578 # Fallthru to sleep+retry logic at the bottom.
2579 pass
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002580
Gavin Makea2e3302023-03-11 06:46:20 +00002581 # Try to prune remote branches once in case there are conflicts.
2582 # For example, if the remote had refs/heads/upstream, but deleted
2583 # that and now has refs/heads/upstream/foo.
2584 elif (
2585 gitcmd.stdout
2586 and "error:" in gitcmd.stdout
2587 and "git remote prune" in gitcmd.stdout
2588 and not prune_tried
2589 ):
2590 prune_tried = True
2591 prunecmd = GitCommand(
2592 self,
2593 ["remote", "prune", name],
2594 bare=True,
2595 ssh_proxy=ssh_proxy,
2596 )
2597 ret = prunecmd.Wait()
2598 if ret:
2599 break
2600 print(
2601 "retrying fetch after pruning remote branches",
2602 file=output_redir,
2603 )
2604 # Continue right away so we don't sleep as we shouldn't need to.
2605 continue
2606 elif current_branch_only and is_sha1 and ret == 128:
2607 # Exit code 128 means "couldn't find the ref you asked for"; if
2608 # we're in sha1 mode, we just tried sync'ing from the upstream
2609 # field; it doesn't exist, thus abort the optimization attempt
2610 # and do a full sync.
2611 break
2612 elif ret < 0:
2613 # Git died with a signal, exit immediately.
2614 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002615
Gavin Makea2e3302023-03-11 06:46:20 +00002616 # Figure out how long to sleep before the next attempt, if there is
2617 # one.
2618 if not verbose and gitcmd.stdout:
2619 print(
2620 "\n%s:\n%s" % (self.name, gitcmd.stdout),
2621 end="",
2622 file=output_redir,
2623 )
2624 if try_n < retry_fetches - 1:
2625 print(
2626 "%s: sleeping %s seconds before retrying"
2627 % (self.name, retry_cur_sleep),
2628 file=output_redir,
2629 )
2630 time.sleep(retry_cur_sleep)
2631 retry_cur_sleep = min(
2632 retry_exp_factor * retry_cur_sleep, MAXIMUM_RETRY_SLEEP_SEC
2633 )
2634 retry_cur_sleep *= 1 - random.uniform(
2635 -RETRY_JITTER_PERCENT, RETRY_JITTER_PERCENT
2636 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002637
Gavin Makea2e3302023-03-11 06:46:20 +00002638 if initial:
2639 if alt_dir:
2640 if old_packed != "":
2641 _lwrite(packed_refs, old_packed)
2642 else:
2643 platform_utils.remove(packed_refs)
2644 self.bare_git.pack_refs("--all", "--prune")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002645
Gavin Makea2e3302023-03-11 06:46:20 +00002646 if is_sha1 and current_branch_only:
2647 # We just synced the upstream given branch; verify we
2648 # got what we wanted, else trigger a second run of all
2649 # refs.
2650 if not self._CheckForImmutableRevision():
2651 # Sync the current branch only with depth set to None.
2652 # We always pass depth=None down to avoid infinite recursion.
2653 return self._RemoteFetch(
2654 name=name,
2655 quiet=quiet,
2656 verbose=verbose,
2657 output_redir=output_redir,
2658 current_branch_only=current_branch_only and depth,
2659 initial=False,
2660 alt_dir=alt_dir,
Nasser Grainawiaafed292023-05-24 12:51:03 -06002661 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00002662 depth=None,
2663 ssh_proxy=ssh_proxy,
2664 clone_filter=clone_filter,
2665 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002666
Gavin Makea2e3302023-03-11 06:46:20 +00002667 return ok
Mike Frysingerf4545122019-11-11 04:34:16 -05002668
Gavin Makea2e3302023-03-11 06:46:20 +00002669 def _ApplyCloneBundle(self, initial=False, quiet=False, verbose=False):
2670 if initial and (
2671 self.manifest.manifestProject.depth or self.clone_depth
2672 ):
2673 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002674
Gavin Makea2e3302023-03-11 06:46:20 +00002675 remote = self.GetRemote()
2676 bundle_url = remote.url + "/clone.bundle"
2677 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
2678 if GetSchemeFromUrl(bundle_url) not in (
2679 "http",
2680 "https",
2681 "persistent-http",
2682 "persistent-https",
2683 ):
2684 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06002685
Gavin Makea2e3302023-03-11 06:46:20 +00002686 bundle_dst = os.path.join(self.gitdir, "clone.bundle")
2687 bundle_tmp = os.path.join(self.gitdir, "clone.bundle.tmp")
2688
2689 exist_dst = os.path.exists(bundle_dst)
2690 exist_tmp = os.path.exists(bundle_tmp)
2691
2692 if not initial and not exist_dst and not exist_tmp:
2693 return False
2694
2695 if not exist_dst:
2696 exist_dst = self._FetchBundle(
2697 bundle_url, bundle_tmp, bundle_dst, quiet, verbose
2698 )
2699 if not exist_dst:
2700 return False
2701
2702 cmd = ["fetch"]
2703 if not verbose:
2704 cmd.append("--quiet")
2705 if not quiet and sys.stdout.isatty():
2706 cmd.append("--progress")
2707 if not self.worktree:
2708 cmd.append("--update-head-ok")
2709 cmd.append(bundle_dst)
2710 for f in remote.fetch:
2711 cmd.append(str(f))
2712 cmd.append("+refs/tags/*:refs/tags/*")
2713
2714 ok = (
2715 GitCommand(
2716 self,
2717 cmd,
2718 bare=True,
2719 objdir=os.path.join(self.objdir, "objects"),
2720 ).Wait()
2721 == 0
2722 )
2723 platform_utils.remove(bundle_dst, missing_ok=True)
2724 platform_utils.remove(bundle_tmp, missing_ok=True)
2725 return ok
2726
2727 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
2728 platform_utils.remove(dstPath, missing_ok=True)
2729
2730 cmd = ["curl", "--fail", "--output", tmpPath, "--netrc", "--location"]
2731 if quiet:
2732 cmd += ["--silent", "--show-error"]
2733 if os.path.exists(tmpPath):
2734 size = os.stat(tmpPath).st_size
2735 if size >= 1024:
2736 cmd += ["--continue-at", "%d" % (size,)]
2737 else:
2738 platform_utils.remove(tmpPath)
2739 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
2740 if cookiefile:
2741 cmd += ["--cookie", cookiefile]
2742 if proxy:
2743 cmd += ["--proxy", proxy]
2744 elif "http_proxy" in os.environ and "darwin" == sys.platform:
2745 cmd += ["--proxy", os.environ["http_proxy"]]
2746 if srcUrl.startswith("persistent-https"):
2747 srcUrl = "http" + srcUrl[len("persistent-https") :]
2748 elif srcUrl.startswith("persistent-http"):
2749 srcUrl = "http" + srcUrl[len("persistent-http") :]
2750 cmd += [srcUrl]
2751
2752 proc = None
2753 with Trace("Fetching bundle: %s", " ".join(cmd)):
2754 if verbose:
2755 print("%s: Downloading bundle: %s" % (self.name, srcUrl))
2756 stdout = None if verbose else subprocess.PIPE
2757 stderr = None if verbose else subprocess.STDOUT
2758 try:
2759 proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
2760 except OSError:
2761 return False
2762
2763 (output, _) = proc.communicate()
2764 curlret = proc.returncode
2765
2766 if curlret == 22:
2767 # From curl man page:
2768 # 22: HTTP page not retrieved. The requested url was not found
2769 # or returned another error with the HTTP error code being 400
2770 # or above. This return code only appears if -f, --fail is used.
2771 if verbose:
2772 print(
2773 "%s: Unable to retrieve clone.bundle; ignoring."
2774 % self.name
2775 )
2776 if output:
2777 print("Curl output:\n%s" % output)
2778 return False
2779 elif curlret and not verbose and output:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002780 logger.error("%s", output)
Gavin Makea2e3302023-03-11 06:46:20 +00002781
2782 if os.path.exists(tmpPath):
2783 if curlret == 0 and self._IsValidBundle(tmpPath, quiet):
2784 platform_utils.rename(tmpPath, dstPath)
2785 return True
2786 else:
2787 platform_utils.remove(tmpPath)
2788 return False
2789 else:
2790 return False
2791
2792 def _IsValidBundle(self, path, quiet):
2793 try:
2794 with open(path, "rb") as f:
2795 if f.read(16) == b"# v2 git bundle\n":
2796 return True
2797 else:
2798 if not quiet:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002799 logger.error("Invalid clone.bundle file; ignoring.")
Gavin Makea2e3302023-03-11 06:46:20 +00002800 return False
2801 except OSError:
2802 return False
2803
2804 def _Checkout(self, rev, quiet=False):
2805 cmd = ["checkout"]
2806 if quiet:
2807 cmd.append("-q")
2808 cmd.append(rev)
2809 cmd.append("--")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002810 if GitCommand(self, cmd).Wait() != 0:
Gavin Makea2e3302023-03-11 06:46:20 +00002811 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002812 raise GitError(
2813 "%s checkout %s " % (self.name, rev), project=self.name
2814 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002815
Gavin Makea2e3302023-03-11 06:46:20 +00002816 def _CherryPick(self, rev, ffonly=False, record_origin=False):
2817 cmd = ["cherry-pick"]
2818 if ffonly:
2819 cmd.append("--ff")
2820 if record_origin:
2821 cmd.append("-x")
2822 cmd.append(rev)
2823 cmd.append("--")
2824 if GitCommand(self, cmd).Wait() != 0:
2825 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002826 raise GitError(
2827 "%s cherry-pick %s " % (self.name, rev), project=self.name
2828 )
Victor Boivie0960b5b2010-11-26 13:42:13 +01002829
Gavin Makea2e3302023-03-11 06:46:20 +00002830 def _LsRemote(self, refs):
2831 cmd = ["ls-remote", self.remote.name, refs]
2832 p = GitCommand(self, cmd, capture_stdout=True)
2833 if p.Wait() == 0:
2834 return p.stdout
2835 return None
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002836
Gavin Makea2e3302023-03-11 06:46:20 +00002837 def _Revert(self, rev):
2838 cmd = ["revert"]
2839 cmd.append("--no-edit")
2840 cmd.append(rev)
2841 cmd.append("--")
2842 if GitCommand(self, cmd).Wait() != 0:
2843 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002844 raise GitError(
2845 "%s revert %s " % (self.name, rev), project=self.name
2846 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002847
Gavin Makea2e3302023-03-11 06:46:20 +00002848 def _ResetHard(self, rev, quiet=True):
2849 cmd = ["reset", "--hard"]
2850 if quiet:
2851 cmd.append("-q")
2852 cmd.append(rev)
2853 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002854 raise GitError(
2855 "%s reset --hard %s " % (self.name, rev), project=self.name
2856 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002857
Gavin Makea2e3302023-03-11 06:46:20 +00002858 def _SyncSubmodules(self, quiet=True):
2859 cmd = ["submodule", "update", "--init", "--recursive"]
2860 if quiet:
2861 cmd.append("-q")
2862 if GitCommand(self, cmd).Wait() != 0:
2863 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07002864 "%s submodule update --init --recursive " % self.name,
2865 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00002866 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002867
Gavin Makea2e3302023-03-11 06:46:20 +00002868 def _Rebase(self, upstream, onto=None):
2869 cmd = ["rebase"]
2870 if onto is not None:
2871 cmd.extend(["--onto", onto])
2872 cmd.append(upstream)
2873 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002874 raise GitError(
2875 "%s rebase %s " % (self.name, upstream), project=self.name
2876 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002877
Gavin Makea2e3302023-03-11 06:46:20 +00002878 def _FastForward(self, head, ffonly=False):
2879 cmd = ["merge", "--no-stat", head]
2880 if ffonly:
2881 cmd.append("--ff-only")
2882 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002883 raise GitError(
2884 "%s merge %s " % (self.name, head), project=self.name
2885 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002886
Gavin Makea2e3302023-03-11 06:46:20 +00002887 def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False):
2888 init_git_dir = not os.path.exists(self.gitdir)
2889 init_obj_dir = not os.path.exists(self.objdir)
2890 try:
2891 # Initialize the bare repository, which contains all of the objects.
2892 if init_obj_dir:
2893 os.makedirs(self.objdir)
2894 self.bare_objdir.init()
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002895
Gavin Makea2e3302023-03-11 06:46:20 +00002896 self._UpdateHooks(quiet=quiet)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002897
Gavin Makea2e3302023-03-11 06:46:20 +00002898 if self.use_git_worktrees:
2899 # Enable per-worktree config file support if possible. This
2900 # is more a nice-to-have feature for users rather than a
2901 # hard requirement.
2902 if git_require((2, 20, 0)):
2903 self.EnableRepositoryExtension("worktreeConfig")
Renaud Paquay788e9622017-01-27 11:41:12 -08002904
Gavin Makea2e3302023-03-11 06:46:20 +00002905 # If we have a separate directory to hold refs, initialize it as
2906 # well.
2907 if self.objdir != self.gitdir:
2908 if init_git_dir:
2909 os.makedirs(self.gitdir)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002910
Gavin Makea2e3302023-03-11 06:46:20 +00002911 if init_obj_dir or init_git_dir:
2912 self._ReferenceGitDir(
2913 self.objdir, self.gitdir, copy_all=True
2914 )
2915 try:
2916 self._CheckDirReference(self.objdir, self.gitdir)
2917 except GitError as e:
2918 if force_sync:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002919 logger.error(
2920 "Retrying clone after deleting %s", self.gitdir
Gavin Makea2e3302023-03-11 06:46:20 +00002921 )
2922 try:
2923 platform_utils.rmtree(
2924 platform_utils.realpath(self.gitdir)
2925 )
2926 if self.worktree and os.path.exists(
2927 platform_utils.realpath(self.worktree)
2928 ):
2929 platform_utils.rmtree(
2930 platform_utils.realpath(self.worktree)
2931 )
2932 return self._InitGitDir(
2933 mirror_git=mirror_git,
2934 force_sync=False,
2935 quiet=quiet,
2936 )
2937 except Exception:
2938 raise e
2939 raise e
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002940
Gavin Makea2e3302023-03-11 06:46:20 +00002941 if init_git_dir:
2942 mp = self.manifest.manifestProject
2943 ref_dir = mp.reference or ""
Julien Camperguedd654222014-01-09 16:21:37 +01002944
Gavin Makea2e3302023-03-11 06:46:20 +00002945 def _expanded_ref_dirs():
2946 """Iterate through possible git reference dir paths."""
2947 name = self.name + ".git"
2948 yield mirror_git or os.path.join(ref_dir, name)
2949 for prefix in "", self.remote.name:
2950 yield os.path.join(
2951 ref_dir, ".repo", "project-objects", prefix, name
2952 )
2953 yield os.path.join(
2954 ref_dir, ".repo", "worktrees", prefix, name
2955 )
2956
2957 if ref_dir or mirror_git:
2958 found_ref_dir = None
2959 for path in _expanded_ref_dirs():
2960 if os.path.exists(path):
2961 found_ref_dir = path
2962 break
2963 ref_dir = found_ref_dir
2964
2965 if ref_dir:
2966 if not os.path.isabs(ref_dir):
2967 # The alternate directory is relative to the object
2968 # database.
2969 ref_dir = os.path.relpath(
2970 ref_dir, os.path.join(self.objdir, "objects")
2971 )
2972 _lwrite(
2973 os.path.join(
2974 self.objdir, "objects/info/alternates"
2975 ),
2976 os.path.join(ref_dir, "objects") + "\n",
2977 )
2978
2979 m = self.manifest.manifestProject.config
2980 for key in ["user.name", "user.email"]:
2981 if m.Has(key, include_defaults=False):
2982 self.config.SetString(key, m.GetString(key))
2983 if not self.manifest.EnableGitLfs:
2984 self.config.SetString(
2985 "filter.lfs.smudge", "git-lfs smudge --skip -- %f"
2986 )
2987 self.config.SetString(
2988 "filter.lfs.process", "git-lfs filter-process --skip"
2989 )
2990 self.config.SetBoolean(
2991 "core.bare", True if self.manifest.IsMirror else None
2992 )
Josip Sokcevic9267d582023-10-19 14:46:11 -07002993
2994 if not init_obj_dir:
2995 # The project might be shared (obj_dir already initialized), but
2996 # such information is not available here. Instead of passing it,
2997 # set it as shared, and rely to be unset down the execution
2998 # path.
2999 if git_require((2, 7, 0)):
3000 self.EnableRepositoryExtension("preciousObjects")
3001 else:
3002 self.config.SetString("gc.pruneExpire", "never")
3003
Gavin Makea2e3302023-03-11 06:46:20 +00003004 except Exception:
3005 if init_obj_dir and os.path.exists(self.objdir):
3006 platform_utils.rmtree(self.objdir)
3007 if init_git_dir and os.path.exists(self.gitdir):
3008 platform_utils.rmtree(self.gitdir)
3009 raise
3010
3011 def _UpdateHooks(self, quiet=False):
3012 if os.path.exists(self.objdir):
3013 self._InitHooks(quiet=quiet)
3014
3015 def _InitHooks(self, quiet=False):
3016 hooks = platform_utils.realpath(os.path.join(self.objdir, "hooks"))
3017 if not os.path.exists(hooks):
3018 os.makedirs(hooks)
3019
3020 # Delete sample hooks. They're noise.
3021 for hook in glob.glob(os.path.join(hooks, "*.sample")):
3022 try:
3023 platform_utils.remove(hook, missing_ok=True)
3024 except PermissionError:
3025 pass
3026
3027 for stock_hook in _ProjectHooks():
3028 name = os.path.basename(stock_hook)
3029
3030 if (
3031 name in ("commit-msg",)
3032 and not self.remote.review
3033 and self is not self.manifest.manifestProject
3034 ):
3035 # Don't install a Gerrit Code Review hook if this
3036 # project does not appear to use it for reviews.
3037 #
3038 # Since the manifest project is one of those, but also
3039 # managed through gerrit, it's excluded.
3040 continue
3041
3042 dst = os.path.join(hooks, name)
3043 if platform_utils.islink(dst):
3044 continue
3045 if os.path.exists(dst):
3046 # If the files are the same, we'll leave it alone. We create
3047 # symlinks below by default but fallback to hardlinks if the OS
3048 # blocks them. So if we're here, it's probably because we made a
3049 # hardlink below.
3050 if not filecmp.cmp(stock_hook, dst, shallow=False):
3051 if not quiet:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00003052 logger.warning(
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00003053 "warn: %s: Not replacing locally modified %s hook",
Gavin Makea2e3302023-03-11 06:46:20 +00003054 self.RelPath(local=False),
3055 name,
3056 )
3057 continue
3058 try:
3059 platform_utils.symlink(
3060 os.path.relpath(stock_hook, os.path.dirname(dst)), dst
3061 )
3062 except OSError as e:
3063 if e.errno == errno.EPERM:
3064 try:
3065 os.link(stock_hook, dst)
3066 except OSError:
Jason Chang32b59562023-07-14 16:45:35 -07003067 raise GitError(
3068 self._get_symlink_error_message(), project=self.name
3069 )
Gavin Makea2e3302023-03-11 06:46:20 +00003070 else:
3071 raise
3072
3073 def _InitRemote(self):
3074 if self.remote.url:
3075 remote = self.GetRemote()
3076 remote.url = self.remote.url
3077 remote.pushUrl = self.remote.pushUrl
3078 remote.review = self.remote.review
3079 remote.projectname = self.name
3080
3081 if self.worktree:
3082 remote.ResetFetch(mirror=False)
3083 else:
3084 remote.ResetFetch(mirror=True)
3085 remote.Save()
3086
3087 def _InitMRef(self):
3088 """Initialize the pseudo m/<manifest branch> ref."""
3089 if self.manifest.branch:
3090 if self.use_git_worktrees:
3091 # Set up the m/ space to point to the worktree-specific ref
3092 # space. We'll update the worktree-specific ref space on each
3093 # checkout.
3094 ref = R_M + self.manifest.branch
3095 if not self.bare_ref.symref(ref):
3096 self.bare_git.symbolic_ref(
3097 "-m",
3098 "redirecting to worktree scope",
3099 ref,
3100 R_WORKTREE_M + self.manifest.branch,
3101 )
3102
3103 # We can't update this ref with git worktrees until it exists.
3104 # We'll wait until the initial checkout to set it.
3105 if not os.path.exists(self.worktree):
3106 return
3107
3108 base = R_WORKTREE_M
3109 active_git = self.work_git
3110
3111 self._InitAnyMRef(HEAD, self.bare_git, detach=True)
3112 else:
3113 base = R_M
3114 active_git = self.bare_git
3115
3116 self._InitAnyMRef(base + self.manifest.branch, active_git)
3117
3118 def _InitMirrorHead(self):
3119 self._InitAnyMRef(HEAD, self.bare_git)
3120
3121 def _InitAnyMRef(self, ref, active_git, detach=False):
3122 """Initialize |ref| in |active_git| to the value in the manifest.
3123
3124 This points |ref| to the <project> setting in the manifest.
3125
3126 Args:
3127 ref: The branch to update.
3128 active_git: The git repository to make updates in.
3129 detach: Whether to update target of symbolic refs, or overwrite the
3130 ref directly (and thus make it non-symbolic).
3131 """
3132 cur = self.bare_ref.symref(ref)
3133
3134 if self.revisionId:
3135 if cur != "" or self.bare_ref.get(ref) != self.revisionId:
3136 msg = "manifest set to %s" % self.revisionId
3137 dst = self.revisionId + "^0"
3138 active_git.UpdateRef(ref, dst, message=msg, detach=True)
Julien Camperguedd654222014-01-09 16:21:37 +01003139 else:
Gavin Makea2e3302023-03-11 06:46:20 +00003140 remote = self.GetRemote()
3141 dst = remote.ToLocal(self.revisionExpr)
3142 if cur != dst:
3143 msg = "manifest set to %s" % self.revisionExpr
3144 if detach:
3145 active_git.UpdateRef(ref, dst, message=msg, detach=True)
3146 else:
3147 active_git.symbolic_ref("-m", msg, ref, dst)
Julien Camperguedd654222014-01-09 16:21:37 +01003148
Gavin Makea2e3302023-03-11 06:46:20 +00003149 def _CheckDirReference(self, srcdir, destdir):
3150 # Git worktrees don't use symlinks to share at all.
3151 if self.use_git_worktrees:
3152 return
Julien Camperguedd654222014-01-09 16:21:37 +01003153
Gavin Makea2e3302023-03-11 06:46:20 +00003154 for name in self.shareable_dirs:
3155 # Try to self-heal a bit in simple cases.
3156 dst_path = os.path.join(destdir, name)
3157 src_path = os.path.join(srcdir, name)
Julien Camperguedd654222014-01-09 16:21:37 +01003158
Gavin Makea2e3302023-03-11 06:46:20 +00003159 dst = platform_utils.realpath(dst_path)
3160 if os.path.lexists(dst):
3161 src = platform_utils.realpath(src_path)
3162 # Fail if the links are pointing to the wrong place.
3163 if src != dst:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00003164 logger.error(
3165 "error: %s is different in %s vs %s",
3166 name,
3167 destdir,
3168 srcdir,
3169 )
Gavin Makea2e3302023-03-11 06:46:20 +00003170 raise GitError(
3171 "--force-sync not enabled; cannot overwrite a local "
3172 "work tree. If you're comfortable with the "
3173 "possibility of losing the work tree's git metadata,"
3174 " use `repo sync --force-sync {0}` to "
Jason Chang32b59562023-07-14 16:45:35 -07003175 "proceed.".format(self.RelPath(local=False)),
3176 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003177 )
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003178
Gavin Makea2e3302023-03-11 06:46:20 +00003179 def _ReferenceGitDir(self, gitdir, dotgit, copy_all):
3180 """Update |dotgit| to reference |gitdir|, using symlinks where possible.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003181
Gavin Makea2e3302023-03-11 06:46:20 +00003182 Args:
3183 gitdir: The bare git repository. Must already be initialized.
3184 dotgit: The repository you would like to initialize.
3185 copy_all: If true, copy all remaining files from |gitdir| ->
3186 |dotgit|. This saves you the effort of initializing |dotgit|
3187 yourself.
3188 """
3189 symlink_dirs = self.shareable_dirs[:]
3190 to_symlink = symlink_dirs
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003191
Gavin Makea2e3302023-03-11 06:46:20 +00003192 to_copy = []
3193 if copy_all:
3194 to_copy = platform_utils.listdir(gitdir)
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003195
Gavin Makea2e3302023-03-11 06:46:20 +00003196 dotgit = platform_utils.realpath(dotgit)
3197 for name in set(to_copy).union(to_symlink):
3198 try:
3199 src = platform_utils.realpath(os.path.join(gitdir, name))
3200 dst = os.path.join(dotgit, name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003201
Gavin Makea2e3302023-03-11 06:46:20 +00003202 if os.path.lexists(dst):
3203 continue
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003204
Gavin Makea2e3302023-03-11 06:46:20 +00003205 # If the source dir doesn't exist, create an empty dir.
3206 if name in symlink_dirs and not os.path.lexists(src):
3207 os.makedirs(src)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003208
Gavin Makea2e3302023-03-11 06:46:20 +00003209 if name in to_symlink:
3210 platform_utils.symlink(
3211 os.path.relpath(src, os.path.dirname(dst)), dst
3212 )
3213 elif copy_all and not platform_utils.islink(dst):
3214 if platform_utils.isdir(src):
3215 shutil.copytree(src, dst)
3216 elif os.path.isfile(src):
3217 shutil.copy(src, dst)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003218
Gavin Makea2e3302023-03-11 06:46:20 +00003219 except OSError as e:
3220 if e.errno == errno.EPERM:
3221 raise DownloadError(self._get_symlink_error_message())
3222 else:
3223 raise
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003224
Gavin Makea2e3302023-03-11 06:46:20 +00003225 def _InitGitWorktree(self):
3226 """Init the project using git worktrees."""
3227 self.bare_git.worktree("prune")
3228 self.bare_git.worktree(
3229 "add",
3230 "-ff",
3231 "--checkout",
3232 "--detach",
3233 "--lock",
3234 self.worktree,
3235 self.GetRevisionId(),
3236 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003237
Gavin Makea2e3302023-03-11 06:46:20 +00003238 # Rewrite the internal state files to use relative paths between the
3239 # checkouts & worktrees.
3240 dotgit = os.path.join(self.worktree, ".git")
Jason R. Coombs034950b2023-10-20 23:32:02 +05453241 with open(dotgit) as fp:
Gavin Makea2e3302023-03-11 06:46:20 +00003242 # Figure out the checkout->worktree path.
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003243 setting = fp.read()
Gavin Makea2e3302023-03-11 06:46:20 +00003244 assert setting.startswith("gitdir:")
3245 git_worktree_path = setting.split(":", 1)[1].strip()
3246 # Some platforms (e.g. Windows) won't let us update dotgit in situ
3247 # because of file permissions. Delete it and recreate it from scratch
3248 # to avoid.
3249 platform_utils.remove(dotgit)
3250 # Use relative path from checkout->worktree & maintain Unix line endings
3251 # on all OS's to match git behavior.
3252 with open(dotgit, "w", newline="\n") as fp:
3253 print(
3254 "gitdir:",
3255 os.path.relpath(git_worktree_path, self.worktree),
3256 file=fp,
3257 )
3258 # Use relative path from worktree->checkout & maintain Unix line endings
3259 # on all OS's to match git behavior.
3260 with open(
3261 os.path.join(git_worktree_path, "gitdir"), "w", newline="\n"
3262 ) as fp:
3263 print(os.path.relpath(dotgit, git_worktree_path), file=fp)
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003264
Gavin Makea2e3302023-03-11 06:46:20 +00003265 self._InitMRef()
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003266
Gavin Makea2e3302023-03-11 06:46:20 +00003267 def _InitWorkTree(self, force_sync=False, submodules=False):
3268 """Setup the worktree .git path.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003269
Gavin Makea2e3302023-03-11 06:46:20 +00003270 This is the user-visible path like src/foo/.git/.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003271
Gavin Makea2e3302023-03-11 06:46:20 +00003272 With non-git-worktrees, this will be a symlink to the .repo/projects/
3273 path. With git-worktrees, this will be a .git file using "gitdir: ..."
3274 syntax.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003275
Gavin Makea2e3302023-03-11 06:46:20 +00003276 Older checkouts had .git/ directories. If we see that, migrate it.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003277
Gavin Makea2e3302023-03-11 06:46:20 +00003278 This also handles changes in the manifest. Maybe this project was
3279 backed by "foo/bar" on the server, but now it's "new/foo/bar". We have
3280 to update the path we point to under .repo/projects/ to match.
3281 """
3282 dotgit = os.path.join(self.worktree, ".git")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003283
Gavin Makea2e3302023-03-11 06:46:20 +00003284 # If using an old layout style (a directory), migrate it.
3285 if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
Jason Chang32b59562023-07-14 16:45:35 -07003286 self._MigrateOldWorkTreeGitDir(dotgit, project=self.name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003287
Gavin Makea2e3302023-03-11 06:46:20 +00003288 init_dotgit = not os.path.exists(dotgit)
3289 if self.use_git_worktrees:
3290 if init_dotgit:
3291 self._InitGitWorktree()
3292 self._CopyAndLinkFiles()
3293 else:
3294 if not init_dotgit:
3295 # See if the project has changed.
3296 if platform_utils.realpath(
3297 self.gitdir
3298 ) != platform_utils.realpath(dotgit):
3299 platform_utils.remove(dotgit)
Doug Anderson37282b42011-03-04 11:54:18 -08003300
Gavin Makea2e3302023-03-11 06:46:20 +00003301 if init_dotgit or not os.path.exists(dotgit):
3302 os.makedirs(self.worktree, exist_ok=True)
3303 platform_utils.symlink(
3304 os.path.relpath(self.gitdir, self.worktree), dotgit
3305 )
Doug Anderson37282b42011-03-04 11:54:18 -08003306
Gavin Makea2e3302023-03-11 06:46:20 +00003307 if init_dotgit:
3308 _lwrite(
3309 os.path.join(dotgit, HEAD), "%s\n" % self.GetRevisionId()
3310 )
Doug Anderson37282b42011-03-04 11:54:18 -08003311
Gavin Makea2e3302023-03-11 06:46:20 +00003312 # Finish checking out the worktree.
3313 cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
3314 if GitCommand(self, cmd).Wait() != 0:
3315 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07003316 "Cannot initialize work tree for " + self.name,
3317 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003318 )
Doug Anderson37282b42011-03-04 11:54:18 -08003319
Gavin Makea2e3302023-03-11 06:46:20 +00003320 if submodules:
3321 self._SyncSubmodules(quiet=True)
3322 self._CopyAndLinkFiles()
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003323
Gavin Makea2e3302023-03-11 06:46:20 +00003324 @classmethod
Jason Chang32b59562023-07-14 16:45:35 -07003325 def _MigrateOldWorkTreeGitDir(cls, dotgit, project=None):
Gavin Makea2e3302023-03-11 06:46:20 +00003326 """Migrate the old worktree .git/ dir style to a symlink.
3327
3328 This logic specifically only uses state from |dotgit| to figure out
3329 where to move content and not |self|. This way if the backing project
3330 also changed places, we only do the .git/ dir to .git symlink migration
3331 here. The path updates will happen independently.
3332 """
3333 # Figure out where in .repo/projects/ it's pointing to.
3334 if not os.path.islink(os.path.join(dotgit, "refs")):
Jason Chang32b59562023-07-14 16:45:35 -07003335 raise GitError(
3336 f"{dotgit}: unsupported checkout state", project=project
3337 )
Gavin Makea2e3302023-03-11 06:46:20 +00003338 gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, "refs")))
3339
3340 # Remove known symlink paths that exist in .repo/projects/.
3341 KNOWN_LINKS = {
3342 "config",
3343 "description",
3344 "hooks",
3345 "info",
3346 "logs",
3347 "objects",
3348 "packed-refs",
3349 "refs",
3350 "rr-cache",
3351 "shallow",
3352 "svn",
3353 }
3354 # Paths that we know will be in both, but are safe to clobber in
3355 # .repo/projects/.
3356 SAFE_TO_CLOBBER = {
3357 "COMMIT_EDITMSG",
3358 "FETCH_HEAD",
3359 "HEAD",
3360 "gc.log",
3361 "gitk.cache",
3362 "index",
3363 "ORIG_HEAD",
3364 }
3365
3366 # First see if we'd succeed before starting the migration.
3367 unknown_paths = []
3368 for name in platform_utils.listdir(dotgit):
3369 # Ignore all temporary/backup names. These are common with vim &
3370 # emacs.
3371 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3372 continue
3373
3374 dotgit_path = os.path.join(dotgit, name)
3375 if name in KNOWN_LINKS:
3376 if not platform_utils.islink(dotgit_path):
3377 unknown_paths.append(f"{dotgit_path}: should be a symlink")
3378 else:
3379 gitdir_path = os.path.join(gitdir, name)
3380 if name not in SAFE_TO_CLOBBER and os.path.exists(gitdir_path):
3381 unknown_paths.append(
3382 f"{dotgit_path}: unknown file; please file a bug"
3383 )
3384 if unknown_paths:
Jason Chang32b59562023-07-14 16:45:35 -07003385 raise GitError(
3386 "Aborting migration: " + "\n".join(unknown_paths),
3387 project=project,
3388 )
Gavin Makea2e3302023-03-11 06:46:20 +00003389
3390 # Now walk the paths and sync the .git/ to .repo/projects/.
3391 for name in platform_utils.listdir(dotgit):
3392 dotgit_path = os.path.join(dotgit, name)
3393
3394 # Ignore all temporary/backup names. These are common with vim &
3395 # emacs.
3396 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3397 platform_utils.remove(dotgit_path)
3398 elif name in KNOWN_LINKS:
3399 platform_utils.remove(dotgit_path)
3400 else:
3401 gitdir_path = os.path.join(gitdir, name)
3402 platform_utils.remove(gitdir_path, missing_ok=True)
3403 platform_utils.rename(dotgit_path, gitdir_path)
3404
3405 # Now that the dir should be empty, clear it out, and symlink it over.
3406 platform_utils.rmdir(dotgit)
3407 platform_utils.symlink(
Jason R. Coombs47944bb2023-09-29 12:42:22 -04003408 os.path.relpath(gitdir, os.path.dirname(os.path.realpath(dotgit))),
3409 dotgit,
Gavin Makea2e3302023-03-11 06:46:20 +00003410 )
3411
3412 def _get_symlink_error_message(self):
3413 if platform_utils.isWindows():
3414 return (
3415 "Unable to create symbolic link. Please re-run the command as "
3416 "Administrator, or see "
3417 "https://github.com/git-for-windows/git/wiki/Symbolic-Links "
3418 "for other options."
3419 )
3420 return "filesystem must support symlinks"
3421
3422 def _revlist(self, *args, **kw):
3423 a = []
3424 a.extend(args)
3425 a.append("--")
3426 return self.work_git.rev_list(*a, **kw)
3427
3428 @property
3429 def _allrefs(self):
3430 return self.bare_ref.all
3431
3432 def _getLogs(
3433 self, rev1, rev2, oneline=False, color=True, pretty_format=None
3434 ):
3435 """Get logs between two revisions of this project."""
3436 comp = ".."
3437 if rev1:
3438 revs = [rev1]
3439 if rev2:
3440 revs.extend([comp, rev2])
3441 cmd = ["log", "".join(revs)]
3442 out = DiffColoring(self.config)
3443 if out.is_on and color:
3444 cmd.append("--color")
3445 if pretty_format is not None:
3446 cmd.append("--pretty=format:%s" % pretty_format)
3447 if oneline:
3448 cmd.append("--oneline")
3449
3450 try:
3451 log = GitCommand(
3452 self, cmd, capture_stdout=True, capture_stderr=True
3453 )
3454 if log.Wait() == 0:
3455 return log.stdout
3456 except GitError:
3457 # worktree may not exist if groups changed for example. In that
3458 # case, try in gitdir instead.
3459 if not os.path.exists(self.worktree):
3460 return self.bare_git.log(*cmd[1:])
3461 else:
3462 raise
3463 return None
3464
3465 def getAddedAndRemovedLogs(
3466 self, toProject, oneline=False, color=True, pretty_format=None
3467 ):
3468 """Get the list of logs from this revision to given revisionId"""
3469 logs = {}
3470 selfId = self.GetRevisionId(self._allrefs)
3471 toId = toProject.GetRevisionId(toProject._allrefs)
3472
3473 logs["added"] = self._getLogs(
3474 selfId,
3475 toId,
3476 oneline=oneline,
3477 color=color,
3478 pretty_format=pretty_format,
3479 )
3480 logs["removed"] = self._getLogs(
3481 toId,
3482 selfId,
3483 oneline=oneline,
3484 color=color,
3485 pretty_format=pretty_format,
3486 )
3487 return logs
3488
Mike Frysingerd4aee652023-10-19 05:13:32 -04003489 class _GitGetByExec:
Gavin Makea2e3302023-03-11 06:46:20 +00003490 def __init__(self, project, bare, gitdir):
3491 self._project = project
3492 self._bare = bare
3493 self._gitdir = gitdir
3494
3495 # __getstate__ and __setstate__ are required for pickling because
3496 # __getattr__ exists.
3497 def __getstate__(self):
3498 return (self._project, self._bare, self._gitdir)
3499
3500 def __setstate__(self, state):
3501 self._project, self._bare, self._gitdir = state
3502
3503 def LsOthers(self):
3504 p = GitCommand(
3505 self._project,
3506 ["ls-files", "-z", "--others", "--exclude-standard"],
3507 bare=False,
3508 gitdir=self._gitdir,
3509 capture_stdout=True,
3510 capture_stderr=True,
3511 )
3512 if p.Wait() == 0:
3513 out = p.stdout
3514 if out:
3515 # Backslash is not anomalous.
3516 return out[:-1].split("\0")
3517 return []
3518
3519 def DiffZ(self, name, *args):
3520 cmd = [name]
3521 cmd.append("-z")
3522 cmd.append("--ignore-submodules")
3523 cmd.extend(args)
3524 p = GitCommand(
3525 self._project,
3526 cmd,
3527 gitdir=self._gitdir,
3528 bare=False,
3529 capture_stdout=True,
3530 capture_stderr=True,
3531 )
3532 p.Wait()
3533 r = {}
3534 out = p.stdout
3535 if out:
3536 out = iter(out[:-1].split("\0"))
3537 while out:
3538 try:
3539 info = next(out)
3540 path = next(out)
3541 except StopIteration:
3542 break
3543
Mike Frysingerd4aee652023-10-19 05:13:32 -04003544 class _Info:
Gavin Makea2e3302023-03-11 06:46:20 +00003545 def __init__(self, path, omode, nmode, oid, nid, state):
3546 self.path = path
3547 self.src_path = None
3548 self.old_mode = omode
3549 self.new_mode = nmode
3550 self.old_id = oid
3551 self.new_id = nid
3552
3553 if len(state) == 1:
3554 self.status = state
3555 self.level = None
3556 else:
3557 self.status = state[:1]
3558 self.level = state[1:]
3559 while self.level.startswith("0"):
3560 self.level = self.level[1:]
3561
3562 info = info[1:].split(" ")
3563 info = _Info(path, *info)
3564 if info.status in ("R", "C"):
3565 info.src_path = info.path
3566 info.path = next(out)
3567 r[info.path] = info
3568 return r
3569
3570 def GetDotgitPath(self, subpath=None):
3571 """Return the full path to the .git dir.
3572
3573 As a convenience, append |subpath| if provided.
3574 """
3575 if self._bare:
3576 dotgit = self._gitdir
3577 else:
3578 dotgit = os.path.join(self._project.worktree, ".git")
3579 if os.path.isfile(dotgit):
3580 # Git worktrees use a "gitdir:" syntax to point to the
3581 # scratch space.
3582 with open(dotgit) as fp:
3583 setting = fp.read()
3584 assert setting.startswith("gitdir:")
3585 gitdir = setting.split(":", 1)[1].strip()
3586 dotgit = os.path.normpath(
3587 os.path.join(self._project.worktree, gitdir)
3588 )
3589
3590 return dotgit if subpath is None else os.path.join(dotgit, subpath)
3591
3592 def GetHead(self):
3593 """Return the ref that HEAD points to."""
3594 path = self.GetDotgitPath(subpath=HEAD)
3595 try:
3596 with open(path) as fd:
3597 line = fd.readline()
Jason R. Coombsae824fb2023-10-20 23:32:40 +05453598 except OSError as e:
Gavin Makea2e3302023-03-11 06:46:20 +00003599 raise NoManifestException(path, str(e))
3600 try:
3601 line = line.decode()
3602 except AttributeError:
3603 pass
3604 if line.startswith("ref: "):
3605 return line[5:-1]
3606 return line[:-1]
3607
3608 def SetHead(self, ref, message=None):
3609 cmdv = []
3610 if message is not None:
3611 cmdv.extend(["-m", message])
3612 cmdv.append(HEAD)
3613 cmdv.append(ref)
3614 self.symbolic_ref(*cmdv)
3615
3616 def DetachHead(self, new, message=None):
3617 cmdv = ["--no-deref"]
3618 if message is not None:
3619 cmdv.extend(["-m", message])
3620 cmdv.append(HEAD)
3621 cmdv.append(new)
3622 self.update_ref(*cmdv)
3623
3624 def UpdateRef(self, name, new, old=None, message=None, detach=False):
3625 cmdv = []
3626 if message is not None:
3627 cmdv.extend(["-m", message])
3628 if detach:
3629 cmdv.append("--no-deref")
3630 cmdv.append(name)
3631 cmdv.append(new)
3632 if old is not None:
3633 cmdv.append(old)
3634 self.update_ref(*cmdv)
3635
3636 def DeleteRef(self, name, old=None):
3637 if not old:
3638 old = self.rev_parse(name)
3639 self.update_ref("-d", name, old)
3640 self._project.bare_ref.deleted(name)
3641
Jason Chang87058c62023-09-27 11:34:43 -07003642 def rev_list(self, *args, log_as_error=True, **kw):
Gavin Makea2e3302023-03-11 06:46:20 +00003643 if "format" in kw:
3644 cmdv = ["log", "--pretty=format:%s" % kw["format"]]
3645 else:
3646 cmdv = ["rev-list"]
3647 cmdv.extend(args)
3648 p = GitCommand(
3649 self._project,
3650 cmdv,
3651 bare=self._bare,
3652 gitdir=self._gitdir,
3653 capture_stdout=True,
3654 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003655 verify_command=True,
Jason Chang87058c62023-09-27 11:34:43 -07003656 log_as_error=log_as_error,
Gavin Makea2e3302023-03-11 06:46:20 +00003657 )
Jason Chang32b59562023-07-14 16:45:35 -07003658 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003659 return p.stdout.splitlines()
3660
3661 def __getattr__(self, name):
3662 """Allow arbitrary git commands using pythonic syntax.
3663
3664 This allows you to do things like:
3665 git_obj.rev_parse('HEAD')
3666
3667 Since we don't have a 'rev_parse' method defined, the __getattr__
3668 will run. We'll replace the '_' with a '-' and try to run a git
3669 command. Any other positional arguments will be passed to the git
3670 command, and the following keyword arguments are supported:
3671 config: An optional dict of git config options to be passed with
3672 '-c'.
3673
3674 Args:
3675 name: The name of the git command to call. Any '_' characters
3676 will be replaced with '-'.
3677
3678 Returns:
3679 A callable object that will try to call git with the named
3680 command.
3681 """
3682 name = name.replace("_", "-")
3683
Jason Chang87058c62023-09-27 11:34:43 -07003684 def runner(*args, log_as_error=True, **kwargs):
Gavin Makea2e3302023-03-11 06:46:20 +00003685 cmdv = []
3686 config = kwargs.pop("config", None)
3687 for k in kwargs:
3688 raise TypeError(
3689 "%s() got an unexpected keyword argument %r" % (name, k)
3690 )
3691 if config is not None:
3692 for k, v in config.items():
3693 cmdv.append("-c")
3694 cmdv.append("%s=%s" % (k, v))
3695 cmdv.append(name)
3696 cmdv.extend(args)
3697 p = GitCommand(
3698 self._project,
3699 cmdv,
3700 bare=self._bare,
3701 gitdir=self._gitdir,
3702 capture_stdout=True,
3703 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003704 verify_command=True,
Jason Chang87058c62023-09-27 11:34:43 -07003705 log_as_error=log_as_error,
Gavin Makea2e3302023-03-11 06:46:20 +00003706 )
Jason Chang32b59562023-07-14 16:45:35 -07003707 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003708 r = p.stdout
3709 if r.endswith("\n") and r.index("\n") == len(r) - 1:
3710 return r[:-1]
3711 return r
3712
3713 return runner
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003714
3715
Jason Chang32b59562023-07-14 16:45:35 -07003716class LocalSyncFail(RepoError):
3717 """Default error when there is an Sync_LocalHalf error."""
3718
3719
3720class _PriorSyncFailedError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003721 def __str__(self):
3722 return "prior sync failed; rebase still in progress"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003723
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003724
Jason Chang32b59562023-07-14 16:45:35 -07003725class _DirtyError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003726 def __str__(self):
3727 return "contains uncommitted changes"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003728
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003729
Mike Frysingerd4aee652023-10-19 05:13:32 -04003730class _InfoMessage:
Gavin Makea2e3302023-03-11 06:46:20 +00003731 def __init__(self, project, text):
3732 self.project = project
3733 self.text = text
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003734
Gavin Makea2e3302023-03-11 06:46:20 +00003735 def Print(self, syncbuf):
3736 syncbuf.out.info(
3737 "%s/: %s", self.project.RelPath(local=False), self.text
3738 )
3739 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003740
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003741
Mike Frysingerd4aee652023-10-19 05:13:32 -04003742class _Failure:
Gavin Makea2e3302023-03-11 06:46:20 +00003743 def __init__(self, project, why):
3744 self.project = project
3745 self.why = why
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003746
Gavin Makea2e3302023-03-11 06:46:20 +00003747 def Print(self, syncbuf):
3748 syncbuf.out.fail(
3749 "error: %s/: %s", self.project.RelPath(local=False), str(self.why)
3750 )
3751 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003752
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003753
Mike Frysingerd4aee652023-10-19 05:13:32 -04003754class _Later:
Gavin Makea2e3302023-03-11 06:46:20 +00003755 def __init__(self, project, action):
3756 self.project = project
3757 self.action = action
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003758
Gavin Makea2e3302023-03-11 06:46:20 +00003759 def Run(self, syncbuf):
3760 out = syncbuf.out
3761 out.project("project %s/", self.project.RelPath(local=False))
3762 out.nl()
3763 try:
3764 self.action()
3765 out.nl()
3766 return True
3767 except GitError:
3768 out.nl()
3769 return False
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003770
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003771
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003772class _SyncColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +00003773 def __init__(self, config):
3774 super().__init__(config, "reposync")
3775 self.project = self.printer("header", attr="bold")
3776 self.info = self.printer("info")
3777 self.fail = self.printer("fail", fg="red")
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003778
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003779
Mike Frysingerd4aee652023-10-19 05:13:32 -04003780class SyncBuffer:
Gavin Makea2e3302023-03-11 06:46:20 +00003781 def __init__(self, config, detach_head=False):
3782 self._messages = []
3783 self._failures = []
3784 self._later_queue1 = []
3785 self._later_queue2 = []
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003786
Gavin Makea2e3302023-03-11 06:46:20 +00003787 self.out = _SyncColoring(config)
3788 self.out.redirect(sys.stderr)
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003789
Gavin Makea2e3302023-03-11 06:46:20 +00003790 self.detach_head = detach_head
3791 self.clean = True
3792 self.recent_clean = True
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003793
Gavin Makea2e3302023-03-11 06:46:20 +00003794 def info(self, project, fmt, *args):
3795 self._messages.append(_InfoMessage(project, fmt % args))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003796
Gavin Makea2e3302023-03-11 06:46:20 +00003797 def fail(self, project, err=None):
3798 self._failures.append(_Failure(project, err))
David Rileye0684ad2017-04-05 00:02:59 -07003799 self._MarkUnclean()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003800
Gavin Makea2e3302023-03-11 06:46:20 +00003801 def later1(self, project, what):
3802 self._later_queue1.append(_Later(project, what))
Mike Frysinger70d861f2019-08-26 15:22:36 -04003803
Gavin Makea2e3302023-03-11 06:46:20 +00003804 def later2(self, project, what):
3805 self._later_queue2.append(_Later(project, what))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003806
Gavin Makea2e3302023-03-11 06:46:20 +00003807 def Finish(self):
3808 self._PrintMessages()
3809 self._RunLater()
3810 self._PrintMessages()
3811 return self.clean
3812
3813 def Recently(self):
3814 recent_clean = self.recent_clean
3815 self.recent_clean = True
3816 return recent_clean
3817
3818 def _MarkUnclean(self):
3819 self.clean = False
3820 self.recent_clean = False
3821
3822 def _RunLater(self):
3823 for q in ["_later_queue1", "_later_queue2"]:
3824 if not self._RunQueue(q):
3825 return
3826
3827 def _RunQueue(self, queue):
3828 for m in getattr(self, queue):
3829 if not m.Run(self):
3830 self._MarkUnclean()
3831 return False
3832 setattr(self, queue, [])
3833 return True
3834
3835 def _PrintMessages(self):
3836 if self._messages or self._failures:
3837 if os.isatty(2):
3838 self.out.write(progress.CSI_ERASE_LINE)
3839 self.out.write("\r")
3840
3841 for m in self._messages:
3842 m.Print(self)
3843 for m in self._failures:
3844 m.Print(self)
3845
3846 self._messages = []
3847 self._failures = []
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003848
3849
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003850class MetaProject(Project):
Gavin Makea2e3302023-03-11 06:46:20 +00003851 """A special project housed under .repo."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003852
Gavin Makea2e3302023-03-11 06:46:20 +00003853 def __init__(self, manifest, name, gitdir, worktree):
3854 Project.__init__(
3855 self,
3856 manifest=manifest,
3857 name=name,
3858 gitdir=gitdir,
3859 objdir=gitdir,
3860 worktree=worktree,
3861 remote=RemoteSpec("origin"),
3862 relpath=".repo/%s" % name,
3863 revisionExpr="refs/heads/master",
3864 revisionId=None,
3865 groups=None,
3866 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003867
Gavin Makea2e3302023-03-11 06:46:20 +00003868 def PreSync(self):
3869 if self.Exists:
3870 cb = self.CurrentBranch
3871 if cb:
3872 base = self.GetBranch(cb).merge
3873 if base:
3874 self.revisionExpr = base
3875 self.revisionId = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003876
Gavin Makea2e3302023-03-11 06:46:20 +00003877 @property
3878 def HasChanges(self):
3879 """Has the remote received new commits not yet checked out?"""
3880 if not self.remote or not self.revisionExpr:
3881 return False
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003882
Gavin Makea2e3302023-03-11 06:46:20 +00003883 all_refs = self.bare_ref.all
3884 revid = self.GetRevisionId(all_refs)
3885 head = self.work_git.GetHead()
3886 if head.startswith(R_HEADS):
3887 try:
3888 head = all_refs[head]
3889 except KeyError:
3890 head = None
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003891
Gavin Makea2e3302023-03-11 06:46:20 +00003892 if revid == head:
3893 return False
3894 elif self._revlist(not_rev(HEAD), revid):
3895 return True
3896 return False
LaMont Jones9b72cf22022-03-29 21:54:22 +00003897
3898
3899class RepoProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003900 """The MetaProject for repo itself."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003901
Gavin Makea2e3302023-03-11 06:46:20 +00003902 @property
3903 def LastFetch(self):
3904 try:
3905 fh = os.path.join(self.gitdir, "FETCH_HEAD")
3906 return os.path.getmtime(fh)
3907 except OSError:
3908 return 0
LaMont Jones9b72cf22022-03-29 21:54:22 +00003909
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +01003910
LaMont Jones9b72cf22022-03-29 21:54:22 +00003911class ManifestProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003912 """The MetaProject for manifests."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003913
Gavin Makea2e3302023-03-11 06:46:20 +00003914 def MetaBranchSwitch(self, submodules=False):
3915 """Prepare for manifest branch switch."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003916
Gavin Makea2e3302023-03-11 06:46:20 +00003917 # detach and delete manifest branch, allowing a new
3918 # branch to take over
3919 syncbuf = SyncBuffer(self.config, detach_head=True)
3920 self.Sync_LocalHalf(syncbuf, submodules=submodules)
3921 syncbuf.Finish()
LaMont Jones9b72cf22022-03-29 21:54:22 +00003922
Gavin Makea2e3302023-03-11 06:46:20 +00003923 return (
3924 GitCommand(
3925 self,
3926 ["update-ref", "-d", "refs/heads/default"],
3927 capture_stdout=True,
3928 capture_stderr=True,
3929 ).Wait()
3930 == 0
3931 )
LaMont Jones9b03f152022-03-29 23:01:18 +00003932
Gavin Makea2e3302023-03-11 06:46:20 +00003933 @property
3934 def standalone_manifest_url(self):
3935 """The URL of the standalone manifest, or None."""
3936 return self.config.GetString("manifest.standalone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003937
Gavin Makea2e3302023-03-11 06:46:20 +00003938 @property
3939 def manifest_groups(self):
3940 """The manifest groups string."""
3941 return self.config.GetString("manifest.groups")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003942
Gavin Makea2e3302023-03-11 06:46:20 +00003943 @property
3944 def reference(self):
3945 """The --reference for this manifest."""
3946 return self.config.GetString("repo.reference")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003947
Gavin Makea2e3302023-03-11 06:46:20 +00003948 @property
3949 def dissociate(self):
3950 """Whether to dissociate."""
3951 return self.config.GetBoolean("repo.dissociate")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003952
Gavin Makea2e3302023-03-11 06:46:20 +00003953 @property
3954 def archive(self):
3955 """Whether we use archive."""
3956 return self.config.GetBoolean("repo.archive")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003957
Gavin Makea2e3302023-03-11 06:46:20 +00003958 @property
3959 def mirror(self):
3960 """Whether we use mirror."""
3961 return self.config.GetBoolean("repo.mirror")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003962
Gavin Makea2e3302023-03-11 06:46:20 +00003963 @property
3964 def use_worktree(self):
3965 """Whether we use worktree."""
3966 return self.config.GetBoolean("repo.worktree")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003967
Gavin Makea2e3302023-03-11 06:46:20 +00003968 @property
3969 def clone_bundle(self):
3970 """Whether we use clone_bundle."""
3971 return self.config.GetBoolean("repo.clonebundle")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003972
Gavin Makea2e3302023-03-11 06:46:20 +00003973 @property
3974 def submodules(self):
3975 """Whether we use submodules."""
3976 return self.config.GetBoolean("repo.submodules")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003977
Gavin Makea2e3302023-03-11 06:46:20 +00003978 @property
3979 def git_lfs(self):
3980 """Whether we use git_lfs."""
3981 return self.config.GetBoolean("repo.git-lfs")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003982
Gavin Makea2e3302023-03-11 06:46:20 +00003983 @property
3984 def use_superproject(self):
3985 """Whether we use superproject."""
3986 return self.config.GetBoolean("repo.superproject")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003987
Gavin Makea2e3302023-03-11 06:46:20 +00003988 @property
3989 def partial_clone(self):
3990 """Whether this is a partial clone."""
3991 return self.config.GetBoolean("repo.partialclone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003992
Gavin Makea2e3302023-03-11 06:46:20 +00003993 @property
3994 def depth(self):
3995 """Partial clone depth."""
Roberto Vladimir Prado Carranza3d58d212023-09-13 10:27:26 +02003996 return self.config.GetInt("repo.depth")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003997
Gavin Makea2e3302023-03-11 06:46:20 +00003998 @property
3999 def clone_filter(self):
4000 """The clone filter."""
4001 return self.config.GetString("repo.clonefilter")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004002
Gavin Makea2e3302023-03-11 06:46:20 +00004003 @property
4004 def partial_clone_exclude(self):
4005 """Partial clone exclude string"""
4006 return self.config.GetString("repo.partialcloneexclude")
LaMont Jones4ada0432022-04-14 15:10:43 +00004007
Gavin Makea2e3302023-03-11 06:46:20 +00004008 @property
Jason Chang17833322023-05-23 13:06:55 -07004009 def clone_filter_for_depth(self):
4010 """Replace shallow clone with partial clone."""
4011 return self.config.GetString("repo.clonefilterfordepth")
4012
4013 @property
Gavin Makea2e3302023-03-11 06:46:20 +00004014 def manifest_platform(self):
4015 """The --platform argument from `repo init`."""
4016 return self.config.GetString("manifest.platform")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004017
Gavin Makea2e3302023-03-11 06:46:20 +00004018 @property
4019 def _platform_name(self):
4020 """Return the name of the platform."""
4021 return platform.system().lower()
LaMont Jones9b03f152022-03-29 23:01:18 +00004022
Gavin Makea2e3302023-03-11 06:46:20 +00004023 def SyncWithPossibleInit(
4024 self,
4025 submanifest,
4026 verbose=False,
4027 current_branch_only=False,
4028 tags="",
4029 git_event_log=None,
4030 ):
4031 """Sync a manifestProject, possibly for the first time.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004032
Gavin Makea2e3302023-03-11 06:46:20 +00004033 Call Sync() with arguments from the most recent `repo init`. If this is
4034 a new sub manifest, then inherit options from the parent's
4035 manifestProject.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004036
Gavin Makea2e3302023-03-11 06:46:20 +00004037 This is used by subcmds.Sync() to do an initial download of new sub
4038 manifests.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004039
Gavin Makea2e3302023-03-11 06:46:20 +00004040 Args:
4041 submanifest: an XmlSubmanifest, the submanifest to re-sync.
4042 verbose: a boolean, whether to show all output, rather than only
4043 errors.
4044 current_branch_only: a boolean, whether to only fetch the current
4045 manifest branch from the server.
4046 tags: a boolean, whether to fetch tags.
4047 git_event_log: an EventLog, for git tracing.
4048 """
4049 # TODO(lamontjones): when refactoring sync (and init?) consider how to
4050 # better get the init options that we should use for new submanifests
4051 # that are added when syncing an existing workspace.
4052 git_event_log = git_event_log or EventLog()
LaMont Jonesb90a4222022-04-14 15:00:09 +00004053 spec = submanifest.ToSubmanifestSpec()
Gavin Makea2e3302023-03-11 06:46:20 +00004054 # Use the init options from the existing manifestProject, or the parent
4055 # if it doesn't exist.
4056 #
4057 # Today, we only support changing manifest_groups on the sub-manifest,
4058 # with no supported-for-the-user way to change the other arguments from
4059 # those specified by the outermost manifest.
4060 #
4061 # TODO(lamontjones): determine which of these should come from the
4062 # outermost manifest and which should come from the parent manifest.
4063 mp = self if self.Exists else submanifest.parent.manifestProject
4064 return self.Sync(
LaMont Jones55ee3042022-04-06 17:10:21 +00004065 manifest_url=spec.manifestUrl,
4066 manifest_branch=spec.revision,
Gavin Makea2e3302023-03-11 06:46:20 +00004067 standalone_manifest=mp.standalone_manifest_url,
4068 groups=mp.manifest_groups,
4069 platform=mp.manifest_platform,
4070 mirror=mp.mirror,
4071 dissociate=mp.dissociate,
4072 reference=mp.reference,
4073 worktree=mp.use_worktree,
4074 submodules=mp.submodules,
4075 archive=mp.archive,
4076 partial_clone=mp.partial_clone,
4077 clone_filter=mp.clone_filter,
4078 partial_clone_exclude=mp.partial_clone_exclude,
4079 clone_bundle=mp.clone_bundle,
4080 git_lfs=mp.git_lfs,
4081 use_superproject=mp.use_superproject,
LaMont Jones55ee3042022-04-06 17:10:21 +00004082 verbose=verbose,
4083 current_branch_only=current_branch_only,
4084 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00004085 depth=mp.depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004086 git_event_log=git_event_log,
4087 manifest_name=spec.manifestName,
Gavin Makea2e3302023-03-11 06:46:20 +00004088 this_manifest_only=True,
LaMont Jones55ee3042022-04-06 17:10:21 +00004089 outer_manifest=False,
Jason Chang17833322023-05-23 13:06:55 -07004090 clone_filter_for_depth=mp.clone_filter_for_depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004091 )
LaMont Jones409407a2022-04-05 21:21:56 +00004092
Gavin Makea2e3302023-03-11 06:46:20 +00004093 def Sync(
4094 self,
4095 _kwargs_only=(),
4096 manifest_url="",
4097 manifest_branch=None,
4098 standalone_manifest=False,
4099 groups="",
4100 mirror=False,
4101 reference="",
4102 dissociate=False,
4103 worktree=False,
4104 submodules=False,
4105 archive=False,
4106 partial_clone=None,
4107 depth=None,
4108 clone_filter="blob:none",
4109 partial_clone_exclude=None,
4110 clone_bundle=None,
4111 git_lfs=None,
4112 use_superproject=None,
4113 verbose=False,
4114 current_branch_only=False,
4115 git_event_log=None,
4116 platform="",
4117 manifest_name="default.xml",
4118 tags="",
4119 this_manifest_only=False,
4120 outer_manifest=True,
Jason Chang17833322023-05-23 13:06:55 -07004121 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00004122 ):
4123 """Sync the manifest and all submanifests.
LaMont Jones409407a2022-04-05 21:21:56 +00004124
Gavin Makea2e3302023-03-11 06:46:20 +00004125 Args:
4126 manifest_url: a string, the URL of the manifest project.
4127 manifest_branch: a string, the manifest branch to use.
4128 standalone_manifest: a boolean, whether to store the manifest as a
4129 static file.
4130 groups: a string, restricts the checkout to projects with the
4131 specified groups.
4132 mirror: a boolean, whether to create a mirror of the remote
4133 repository.
4134 reference: a string, location of a repo instance to use as a
4135 reference.
4136 dissociate: a boolean, whether to dissociate from reference mirrors
4137 after clone.
4138 worktree: a boolean, whether to use git-worktree to manage projects.
4139 submodules: a boolean, whether sync submodules associated with the
4140 manifest project.
4141 archive: a boolean, whether to checkout each project as an archive.
4142 See git-archive.
4143 partial_clone: a boolean, whether to perform a partial clone.
4144 depth: an int, how deep of a shallow clone to create.
4145 clone_filter: a string, filter to use with partial_clone.
4146 partial_clone_exclude : a string, comma-delimeted list of project
4147 names to exclude from partial clone.
4148 clone_bundle: a boolean, whether to enable /clone.bundle on
4149 HTTP/HTTPS.
4150 git_lfs: a boolean, whether to enable git LFS support.
4151 use_superproject: a boolean, whether to use the manifest
4152 superproject to sync projects.
4153 verbose: a boolean, whether to show all output, rather than only
4154 errors.
4155 current_branch_only: a boolean, whether to only fetch the current
4156 manifest branch from the server.
4157 platform: a string, restrict the checkout to projects with the
4158 specified platform group.
4159 git_event_log: an EventLog, for git tracing.
4160 tags: a boolean, whether to fetch tags.
4161 manifest_name: a string, the name of the manifest file to use.
4162 this_manifest_only: a boolean, whether to only operate on the
4163 current sub manifest.
4164 outer_manifest: a boolean, whether to start at the outermost
4165 manifest.
Jason Chang17833322023-05-23 13:06:55 -07004166 clone_filter_for_depth: a string, when specified replaces shallow
4167 clones with partial.
LaMont Jones9b03f152022-03-29 23:01:18 +00004168
Gavin Makea2e3302023-03-11 06:46:20 +00004169 Returns:
4170 a boolean, whether the sync was successful.
4171 """
4172 assert _kwargs_only == (), "Sync only accepts keyword arguments."
LaMont Jones9b03f152022-03-29 23:01:18 +00004173
Gavin Makea2e3302023-03-11 06:46:20 +00004174 groups = groups or self.manifest.GetDefaultGroupsStr(
4175 with_platform=False
4176 )
4177 platform = platform or "auto"
4178 git_event_log = git_event_log or EventLog()
4179 if outer_manifest and self.manifest.is_submanifest:
4180 # In a multi-manifest checkout, use the outer manifest unless we are
4181 # told not to.
4182 return self.client.outer_manifest.manifestProject.Sync(
4183 manifest_url=manifest_url,
4184 manifest_branch=manifest_branch,
4185 standalone_manifest=standalone_manifest,
4186 groups=groups,
4187 platform=platform,
4188 mirror=mirror,
4189 dissociate=dissociate,
4190 reference=reference,
4191 worktree=worktree,
4192 submodules=submodules,
4193 archive=archive,
4194 partial_clone=partial_clone,
4195 clone_filter=clone_filter,
4196 partial_clone_exclude=partial_clone_exclude,
4197 clone_bundle=clone_bundle,
4198 git_lfs=git_lfs,
4199 use_superproject=use_superproject,
4200 verbose=verbose,
4201 current_branch_only=current_branch_only,
4202 tags=tags,
4203 depth=depth,
4204 git_event_log=git_event_log,
4205 manifest_name=manifest_name,
4206 this_manifest_only=this_manifest_only,
4207 outer_manifest=False,
4208 )
LaMont Jones9b03f152022-03-29 23:01:18 +00004209
Gavin Makea2e3302023-03-11 06:46:20 +00004210 # If repo has already been initialized, we take -u with the absence of
4211 # --standalone-manifest to mean "transition to a standard repo set up",
4212 # which necessitates starting fresh.
4213 # If --standalone-manifest is set, we always tear everything down and
4214 # start anew.
4215 if self.Exists:
4216 was_standalone_manifest = self.config.GetString(
4217 "manifest.standalone"
4218 )
4219 if was_standalone_manifest and not manifest_url:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004220 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004221 "fatal: repo was initialized with a standlone manifest, "
4222 "cannot be re-initialized without --manifest-url/-u"
4223 )
4224 return False
4225
4226 if standalone_manifest or (
4227 was_standalone_manifest and manifest_url
4228 ):
4229 self.config.ClearCache()
4230 if self.gitdir and os.path.exists(self.gitdir):
4231 platform_utils.rmtree(self.gitdir)
4232 if self.worktree and os.path.exists(self.worktree):
4233 platform_utils.rmtree(self.worktree)
4234
4235 is_new = not self.Exists
4236 if is_new:
4237 if not manifest_url:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004238 logger.error("fatal: manifest url is required.")
Gavin Makea2e3302023-03-11 06:46:20 +00004239 return False
4240
4241 if verbose:
4242 print(
4243 "Downloading manifest from %s"
4244 % (GitConfig.ForUser().UrlInsteadOf(manifest_url),),
4245 file=sys.stderr,
4246 )
4247
4248 # The manifest project object doesn't keep track of the path on the
4249 # server where this git is located, so let's save that here.
4250 mirrored_manifest_git = None
4251 if reference:
4252 manifest_git_path = urllib.parse.urlparse(manifest_url).path[1:]
4253 mirrored_manifest_git = os.path.join(
4254 reference, manifest_git_path
4255 )
4256 if not mirrored_manifest_git.endswith(".git"):
4257 mirrored_manifest_git += ".git"
4258 if not os.path.exists(mirrored_manifest_git):
4259 mirrored_manifest_git = os.path.join(
4260 reference, ".repo/manifests.git"
4261 )
4262
4263 self._InitGitDir(mirror_git=mirrored_manifest_git)
4264
4265 # If standalone_manifest is set, mark the project as "standalone" --
4266 # we'll still do much of the manifests.git set up, but will avoid actual
4267 # syncs to a remote.
4268 if standalone_manifest:
4269 self.config.SetString("manifest.standalone", manifest_url)
4270 elif not manifest_url and not manifest_branch:
4271 # If -u is set and --standalone-manifest is not, then we're not in
4272 # standalone mode. Otherwise, use config to infer what we were in
4273 # the last init.
4274 standalone_manifest = bool(
4275 self.config.GetString("manifest.standalone")
4276 )
4277 if not standalone_manifest:
4278 self.config.SetString("manifest.standalone", None)
4279
4280 self._ConfigureDepth(depth)
4281
4282 # Set the remote URL before the remote branch as we might need it below.
4283 if manifest_url:
4284 r = self.GetRemote()
4285 r.url = manifest_url
4286 r.ResetFetch()
4287 r.Save()
4288
4289 if not standalone_manifest:
4290 if manifest_branch:
4291 if manifest_branch == "HEAD":
4292 manifest_branch = self.ResolveRemoteHead()
4293 if manifest_branch is None:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004294 logger.error("fatal: unable to resolve HEAD")
Gavin Makea2e3302023-03-11 06:46:20 +00004295 return False
4296 self.revisionExpr = manifest_branch
4297 else:
4298 if is_new:
4299 default_branch = self.ResolveRemoteHead()
4300 if default_branch is None:
4301 # If the remote doesn't have HEAD configured, default to
4302 # master.
4303 default_branch = "refs/heads/master"
4304 self.revisionExpr = default_branch
4305 else:
4306 self.PreSync()
4307
4308 groups = re.split(r"[,\s]+", groups or "")
4309 all_platforms = ["linux", "darwin", "windows"]
4310 platformize = lambda x: "platform-" + x
4311 if platform == "auto":
4312 if not mirror and not self.mirror:
4313 groups.append(platformize(self._platform_name))
4314 elif platform == "all":
4315 groups.extend(map(platformize, all_platforms))
4316 elif platform in all_platforms:
4317 groups.append(platformize(platform))
4318 elif platform != "none":
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004319 logger.error("fatal: invalid platform flag", file=sys.stderr)
Gavin Makea2e3302023-03-11 06:46:20 +00004320 return False
4321 self.config.SetString("manifest.platform", platform)
4322
4323 groups = [x for x in groups if x]
4324 groupstr = ",".join(groups)
4325 if (
4326 platform == "auto"
4327 and groupstr == self.manifest.GetDefaultGroupsStr()
4328 ):
4329 groupstr = None
4330 self.config.SetString("manifest.groups", groupstr)
4331
4332 if reference:
4333 self.config.SetString("repo.reference", reference)
4334
4335 if dissociate:
4336 self.config.SetBoolean("repo.dissociate", dissociate)
4337
4338 if worktree:
4339 if mirror:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004340 logger.error("fatal: --mirror and --worktree are incompatible")
Gavin Makea2e3302023-03-11 06:46:20 +00004341 return False
4342 if submodules:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004343 logger.error(
4344 "fatal: --submodules and --worktree are incompatible"
Gavin Makea2e3302023-03-11 06:46:20 +00004345 )
4346 return False
4347 self.config.SetBoolean("repo.worktree", worktree)
4348 if is_new:
4349 self.use_git_worktrees = True
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004350 logger.warning("warning: --worktree is experimental!")
Gavin Makea2e3302023-03-11 06:46:20 +00004351
4352 if archive:
4353 if is_new:
4354 self.config.SetBoolean("repo.archive", archive)
4355 else:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004356 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004357 "fatal: --archive is only supported when initializing a "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004358 "new workspace."
Gavin Makea2e3302023-03-11 06:46:20 +00004359 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004360 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004361 "Either delete the .repo folder in this workspace, or "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004362 "initialize in another location."
Gavin Makea2e3302023-03-11 06:46:20 +00004363 )
4364 return False
4365
4366 if mirror:
4367 if is_new:
4368 self.config.SetBoolean("repo.mirror", mirror)
4369 else:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004370 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004371 "fatal: --mirror is only supported when initializing a new "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004372 "workspace."
Gavin Makea2e3302023-03-11 06:46:20 +00004373 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004374 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004375 "Either delete the .repo folder in this workspace, or "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004376 "initialize in another location."
Gavin Makea2e3302023-03-11 06:46:20 +00004377 )
4378 return False
4379
4380 if partial_clone is not None:
4381 if mirror:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004382 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004383 "fatal: --mirror and --partial-clone are mutually "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004384 "exclusive"
Gavin Makea2e3302023-03-11 06:46:20 +00004385 )
4386 return False
4387 self.config.SetBoolean("repo.partialclone", partial_clone)
4388 if clone_filter:
4389 self.config.SetString("repo.clonefilter", clone_filter)
4390 elif self.partial_clone:
4391 clone_filter = self.clone_filter
4392 else:
4393 clone_filter = None
4394
4395 if partial_clone_exclude is not None:
4396 self.config.SetString(
4397 "repo.partialcloneexclude", partial_clone_exclude
4398 )
4399
4400 if clone_bundle is None:
4401 clone_bundle = False if partial_clone else True
4402 else:
4403 self.config.SetBoolean("repo.clonebundle", clone_bundle)
4404
4405 if submodules:
4406 self.config.SetBoolean("repo.submodules", submodules)
4407
4408 if git_lfs is not None:
4409 if git_lfs:
4410 git_require((2, 17, 0), fail=True, msg="Git LFS support")
4411
4412 self.config.SetBoolean("repo.git-lfs", git_lfs)
4413 if not is_new:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004414 logger.warning(
Gavin Makea2e3302023-03-11 06:46:20 +00004415 "warning: Changing --git-lfs settings will only affect new "
4416 "project checkouts.\n"
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004417 " Existing projects will require manual updates.\n"
Gavin Makea2e3302023-03-11 06:46:20 +00004418 )
4419
Jason Chang17833322023-05-23 13:06:55 -07004420 if clone_filter_for_depth is not None:
4421 self.ConfigureCloneFilterForDepth(clone_filter_for_depth)
4422
Gavin Makea2e3302023-03-11 06:46:20 +00004423 if use_superproject is not None:
4424 self.config.SetBoolean("repo.superproject", use_superproject)
4425
4426 if not standalone_manifest:
4427 success = self.Sync_NetworkHalf(
4428 is_new=is_new,
4429 quiet=not verbose,
4430 verbose=verbose,
4431 clone_bundle=clone_bundle,
4432 current_branch_only=current_branch_only,
4433 tags=tags,
4434 submodules=submodules,
4435 clone_filter=clone_filter,
4436 partial_clone_exclude=self.manifest.PartialCloneExclude,
Jason Chang17833322023-05-23 13:06:55 -07004437 clone_filter_for_depth=self.manifest.CloneFilterForDepth,
Gavin Makea2e3302023-03-11 06:46:20 +00004438 ).success
4439 if not success:
4440 r = self.GetRemote()
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004441 logger.error("fatal: cannot obtain manifest %s", r.url)
Gavin Makea2e3302023-03-11 06:46:20 +00004442
4443 # Better delete the manifest git dir if we created it; otherwise
4444 # next time (when user fixes problems) we won't go through the
4445 # "is_new" logic.
4446 if is_new:
4447 platform_utils.rmtree(self.gitdir)
4448 return False
4449
4450 if manifest_branch:
4451 self.MetaBranchSwitch(submodules=submodules)
4452
4453 syncbuf = SyncBuffer(self.config)
4454 self.Sync_LocalHalf(syncbuf, submodules=submodules)
4455 syncbuf.Finish()
4456
4457 if is_new or self.CurrentBranch is None:
Jason Chang1a3612f2023-08-08 14:12:53 -07004458 try:
4459 self.StartBranch("default")
4460 except GitError as e:
4461 msg = str(e)
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004462 logger.error(
4463 "fatal: cannot create default in manifest %s", msg
Gavin Makea2e3302023-03-11 06:46:20 +00004464 )
4465 return False
4466
4467 if not manifest_name:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004468 logger.error("fatal: manifest name (-m) is required.")
Gavin Makea2e3302023-03-11 06:46:20 +00004469 return False
4470
4471 elif is_new:
4472 # This is a new standalone manifest.
4473 manifest_name = "default.xml"
4474 manifest_data = fetch.fetch_file(manifest_url, verbose=verbose)
4475 dest = os.path.join(self.worktree, manifest_name)
4476 os.makedirs(os.path.dirname(dest), exist_ok=True)
4477 with open(dest, "wb") as f:
4478 f.write(manifest_data)
4479
4480 try:
4481 self.manifest.Link(manifest_name)
4482 except ManifestParseError as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004483 logger.error("fatal: manifest '%s' not available", manifest_name)
4484 logger.error("fatal: %s", e)
Gavin Makea2e3302023-03-11 06:46:20 +00004485 return False
4486
4487 if not this_manifest_only:
4488 for submanifest in self.manifest.submanifests.values():
4489 spec = submanifest.ToSubmanifestSpec()
4490 submanifest.repo_client.manifestProject.Sync(
4491 manifest_url=spec.manifestUrl,
4492 manifest_branch=spec.revision,
4493 standalone_manifest=standalone_manifest,
4494 groups=self.manifest_groups,
4495 platform=platform,
4496 mirror=mirror,
4497 dissociate=dissociate,
4498 reference=reference,
4499 worktree=worktree,
4500 submodules=submodules,
4501 archive=archive,
4502 partial_clone=partial_clone,
4503 clone_filter=clone_filter,
4504 partial_clone_exclude=partial_clone_exclude,
4505 clone_bundle=clone_bundle,
4506 git_lfs=git_lfs,
4507 use_superproject=use_superproject,
4508 verbose=verbose,
4509 current_branch_only=current_branch_only,
4510 tags=tags,
4511 depth=depth,
4512 git_event_log=git_event_log,
4513 manifest_name=spec.manifestName,
4514 this_manifest_only=False,
4515 outer_manifest=False,
4516 )
4517
4518 # Lastly, if the manifest has a <superproject> then have the
4519 # superproject sync it (if it will be used).
4520 if git_superproject.UseSuperproject(use_superproject, self.manifest):
4521 sync_result = self.manifest.superproject.Sync(git_event_log)
4522 if not sync_result.success:
4523 submanifest = ""
4524 if self.manifest.path_prefix:
4525 submanifest = f"for {self.manifest.path_prefix} "
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004526 logger.warning(
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004527 "warning: git update of superproject %s failed, "
Gavin Makea2e3302023-03-11 06:46:20 +00004528 "repo sync will not use superproject to fetch source; "
4529 "while this error is not fatal, and you can continue to "
4530 "run repo sync, please run repo init with the "
4531 "--no-use-superproject option to stop seeing this warning",
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004532 submanifest,
Gavin Makea2e3302023-03-11 06:46:20 +00004533 )
4534 if sync_result.fatal and use_superproject is not None:
4535 return False
4536
4537 return True
4538
Jason Chang17833322023-05-23 13:06:55 -07004539 def ConfigureCloneFilterForDepth(self, clone_filter_for_depth):
4540 """Configure clone filter to replace shallow clones.
4541
4542 Args:
4543 clone_filter_for_depth: a string or None, e.g. 'blob:none' will
4544 disable shallow clones and replace with partial clone. None will
4545 enable shallow clones.
4546 """
4547 self.config.SetString(
4548 "repo.clonefilterfordepth", clone_filter_for_depth
4549 )
4550
Gavin Makea2e3302023-03-11 06:46:20 +00004551 def _ConfigureDepth(self, depth):
4552 """Configure the depth we'll sync down.
4553
4554 Args:
4555 depth: an int, how deep of a partial clone to create.
4556 """
4557 # Opt.depth will be non-None if user actually passed --depth to repo
4558 # init.
4559 if depth is not None:
4560 if depth > 0:
4561 # Positive values will set the depth.
4562 depth = str(depth)
4563 else:
4564 # Negative numbers will clear the depth; passing None to
4565 # SetString will do that.
4566 depth = None
4567
4568 # We store the depth in the main manifest project.
4569 self.config.SetString("repo.depth", depth)