blob: d887a5587d35affba17f8f6193c1ef0938ad8bf0 [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(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400368 f'{subpath}: "{part}" not allowed in paths'
Gavin Makea2e3302023-03-11 06:46:20 +0000369 )
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(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400374 f"{path}: traversing symlinks not allow"
Gavin Makea2e3302023-03-11 06:46:20 +0000375 )
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(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400380 f"{path}: only regular files & directories allowed"
Gavin Makea2e3302023-03-11 06:46:20 +0000381 )
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(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400412 f"{self.src}: copying from directory not supported"
Gavin Makea2e3302023-03-11 06:46:20 +0000413 )
414 if platform_utils.isdir(dest):
415 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400416 f"{self.dest}: copying to directory not allowed"
Gavin Makea2e3302023-03-11 06:46:20 +0000417 )
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:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400960 line = (
961 f" {i_status}{f_status}\t{i.src_path} => {p} ({i.level}%)"
Gavin Makea2e3302023-03-11 06:46:20 +0000962 )
963 else:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400964 line = f" {i_status}{f_status}\t{p}"
Gavin Makea2e3302023-03-11 06:46:20 +0000965
966 if i and not f:
967 out.added("%s", line)
968 elif (i and f) or (not i and f):
969 out.changed("%s", line)
970 elif not i and not f:
971 out.untracked("%s", line)
972 else:
973 out.write("%s", line)
974 out.nl()
975
976 return "DIRTY"
977
978 def PrintWorkTreeDiff(
979 self, absolute_paths=False, output_redir=None, local=False
980 ):
981 """Prints the status of the repository to stdout."""
982 out = DiffColoring(self.config)
983 if output_redir:
984 out.redirect(output_redir)
985 cmd = ["diff"]
986 if out.is_on:
987 cmd.append("--color")
988 cmd.append(HEAD)
989 if absolute_paths:
990 cmd.append("--src-prefix=a/%s/" % self.RelPath(local))
991 cmd.append("--dst-prefix=b/%s/" % self.RelPath(local))
992 cmd.append("--")
993 try:
994 p = GitCommand(self, cmd, capture_stdout=True, capture_stderr=True)
995 p.Wait()
996 except GitError as e:
997 out.nl()
998 out.project("project %s/" % self.RelPath(local))
999 out.nl()
1000 out.fail("%s", str(e))
1001 out.nl()
1002 return False
1003 if p.stdout:
1004 out.nl()
1005 out.project("project %s/" % self.RelPath(local))
1006 out.nl()
1007 out.write("%s", p.stdout)
1008 return p.Wait() == 0
1009
1010 def WasPublished(self, branch, all_refs=None):
1011 """Was the branch published (uploaded) for code review?
1012 If so, returns the SHA-1 hash of the last published
1013 state for the branch.
1014 """
1015 key = R_PUB + branch
1016 if all_refs is None:
1017 try:
1018 return self.bare_git.rev_parse(key)
1019 except GitError:
1020 return None
1021 else:
1022 try:
1023 return all_refs[key]
1024 except KeyError:
1025 return None
1026
1027 def CleanPublishedCache(self, all_refs=None):
1028 """Prunes any stale published refs."""
1029 if all_refs is None:
1030 all_refs = self._allrefs
1031 heads = set()
1032 canrm = {}
1033 for name, ref_id in all_refs.items():
1034 if name.startswith(R_HEADS):
1035 heads.add(name)
1036 elif name.startswith(R_PUB):
1037 canrm[name] = ref_id
1038
1039 for name, ref_id in canrm.items():
1040 n = name[len(R_PUB) :]
1041 if R_HEADS + n not in heads:
1042 self.bare_git.DeleteRef(name, ref_id)
1043
1044 def GetUploadableBranches(self, selected_branch=None):
1045 """List any branches which can be uploaded for review."""
1046 heads = {}
1047 pubed = {}
1048
1049 for name, ref_id in self._allrefs.items():
1050 if name.startswith(R_HEADS):
1051 heads[name[len(R_HEADS) :]] = ref_id
1052 elif name.startswith(R_PUB):
1053 pubed[name[len(R_PUB) :]] = ref_id
1054
1055 ready = []
1056 for branch, ref_id in heads.items():
1057 if branch in pubed and pubed[branch] == ref_id:
1058 continue
1059 if selected_branch and branch != selected_branch:
1060 continue
1061
1062 rb = self.GetUploadableBranch(branch)
1063 if rb:
1064 ready.append(rb)
1065 return ready
1066
1067 def GetUploadableBranch(self, branch_name):
1068 """Get a single uploadable branch, or None."""
1069 branch = self.GetBranch(branch_name)
1070 base = branch.LocalMerge
1071 if branch.LocalMerge:
1072 rb = ReviewableBranch(self, branch, base)
1073 if rb.commits:
1074 return rb
1075 return None
1076
1077 def UploadForReview(
1078 self,
1079 branch=None,
1080 people=([], []),
1081 dryrun=False,
1082 auto_topic=False,
1083 hashtags=(),
1084 labels=(),
1085 private=False,
1086 notify=None,
1087 wip=False,
1088 ready=False,
1089 dest_branch=None,
1090 validate_certs=True,
1091 push_options=None,
1092 ):
1093 """Uploads the named branch for code review."""
1094 if branch is None:
1095 branch = self.CurrentBranch
1096 if branch is None:
Jason Chang32b59562023-07-14 16:45:35 -07001097 raise GitError("not currently on a branch", project=self.name)
Gavin Makea2e3302023-03-11 06:46:20 +00001098
1099 branch = self.GetBranch(branch)
1100 if not branch.LocalMerge:
Jason Chang32b59562023-07-14 16:45:35 -07001101 raise GitError(
1102 "branch %s does not track a remote" % branch.name,
1103 project=self.name,
1104 )
Gavin Makea2e3302023-03-11 06:46:20 +00001105 if not branch.remote.review:
Jason Chang32b59562023-07-14 16:45:35 -07001106 raise GitError(
1107 "remote %s has no review url" % branch.remote.name,
1108 project=self.name,
1109 )
Gavin Makea2e3302023-03-11 06:46:20 +00001110
1111 # Basic validity check on label syntax.
1112 for label in labels:
1113 if not re.match(r"^.+[+-][0-9]+$", label):
1114 raise UploadError(
1115 f'invalid label syntax "{label}": labels use forms like '
Jason Chang5a3a5f72023-08-17 11:36:41 -07001116 "CodeReview+1 or Verified-1",
1117 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00001118 )
1119
1120 if dest_branch is None:
1121 dest_branch = self.dest_branch
1122 if dest_branch is None:
1123 dest_branch = branch.merge
1124 if not dest_branch.startswith(R_HEADS):
1125 dest_branch = R_HEADS + dest_branch
1126
1127 if not branch.remote.projectname:
1128 branch.remote.projectname = self.name
1129 branch.remote.Save()
1130
1131 url = branch.remote.ReviewUrl(self.UserEmail, validate_certs)
1132 if url is None:
Jason Chang5a3a5f72023-08-17 11:36:41 -07001133 raise UploadError("review not configured", project=self.name)
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +00001134 cmd = ["push", "--progress"]
Gavin Makea2e3302023-03-11 06:46:20 +00001135 if dryrun:
1136 cmd.append("-n")
1137
1138 if url.startswith("ssh://"):
1139 cmd.append("--receive-pack=gerrit receive-pack")
1140
Aravind Vasudevan99ebf622023-04-04 23:44:37 +00001141 # This stops git from pushing all reachable annotated tags when
1142 # push.followTags is configured. Gerrit does not accept any tags
1143 # pushed to a CL.
1144 if git_require((1, 8, 3)):
1145 cmd.append("--no-follow-tags")
1146
Gavin Makea2e3302023-03-11 06:46:20 +00001147 for push_option in push_options or []:
1148 cmd.append("-o")
1149 cmd.append(push_option)
1150
1151 cmd.append(url)
1152
1153 if dest_branch.startswith(R_HEADS):
1154 dest_branch = dest_branch[len(R_HEADS) :]
1155
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001156 ref_spec = f"{R_HEADS + branch.name}:refs/for/{dest_branch}"
Gavin Makea2e3302023-03-11 06:46:20 +00001157 opts = []
1158 if auto_topic:
1159 opts += ["topic=" + branch.name]
1160 opts += ["t=%s" % p for p in hashtags]
1161 # NB: No need to encode labels as they've been validated above.
1162 opts += ["l=%s" % p for p in labels]
1163
1164 opts += ["r=%s" % p for p in people[0]]
1165 opts += ["cc=%s" % p for p in people[1]]
1166 if notify:
1167 opts += ["notify=" + notify]
1168 if private:
1169 opts += ["private"]
1170 if wip:
1171 opts += ["wip"]
1172 if ready:
1173 opts += ["ready"]
1174 if opts:
1175 ref_spec = ref_spec + "%" + ",".join(opts)
1176 cmd.append(ref_spec)
1177
Jason Chang1e9f7b92023-08-25 10:31:04 -07001178 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001179
1180 if not dryrun:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001181 msg = f"posted to {branch.remote.review} for {dest_branch}"
Gavin Makea2e3302023-03-11 06:46:20 +00001182 self.bare_git.UpdateRef(
1183 R_PUB + branch.name, R_HEADS + branch.name, message=msg
1184 )
1185
1186 def _ExtractArchive(self, tarpath, path=None):
1187 """Extract the given tar on its current location
1188
1189 Args:
1190 tarpath: The path to the actual tar file
1191
1192 """
1193 try:
1194 with tarfile.open(tarpath, "r") as tar:
1195 tar.extractall(path=path)
1196 return True
Jason R. Coombsae824fb2023-10-20 23:32:40 +05451197 except (OSError, tarfile.TarError) as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001198 logger.error("error: Cannot extract archive %s: %s", tarpath, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001199 return False
1200
1201 def Sync_NetworkHalf(
1202 self,
1203 quiet=False,
1204 verbose=False,
1205 output_redir=None,
1206 is_new=None,
1207 current_branch_only=None,
1208 force_sync=False,
1209 clone_bundle=True,
1210 tags=None,
1211 archive=False,
1212 optimized_fetch=False,
1213 retry_fetches=0,
1214 prune=False,
1215 submodules=False,
1216 ssh_proxy=None,
1217 clone_filter=None,
1218 partial_clone_exclude=set(),
Jason Chang17833322023-05-23 13:06:55 -07001219 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001220 ):
1221 """Perform only the network IO portion of the sync process.
1222 Local working directory/branch state is not affected.
1223 """
1224 if archive and not isinstance(self, MetaProject):
1225 if self.remote.url.startswith(("http://", "https://")):
Jason Chang32b59562023-07-14 16:45:35 -07001226 msg_template = (
1227 "%s: Cannot fetch archives from http/https remotes."
Gavin Makea2e3302023-03-11 06:46:20 +00001228 )
Jason Chang32b59562023-07-14 16:45:35 -07001229 msg_args = self.name
1230 msg = msg_template % msg_args
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001231 logger.error(msg_template, msg_args)
Jason Chang32b59562023-07-14 16:45:35 -07001232 return SyncNetworkHalfResult(
1233 False, SyncNetworkHalfError(msg, project=self.name)
1234 )
Gavin Makea2e3302023-03-11 06:46:20 +00001235
1236 name = self.relpath.replace("\\", "/")
1237 name = name.replace("/", "_")
1238 tarpath = "%s.tar" % name
1239 topdir = self.manifest.topdir
1240
1241 try:
1242 self._FetchArchive(tarpath, cwd=topdir)
1243 except GitError as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001244 logger.error("error: %s", e)
Jason Chang32b59562023-07-14 16:45:35 -07001245 return SyncNetworkHalfResult(False, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001246
1247 # From now on, we only need absolute tarpath.
1248 tarpath = os.path.join(topdir, tarpath)
1249
1250 if not self._ExtractArchive(tarpath, path=topdir):
Jason Chang32b59562023-07-14 16:45:35 -07001251 return SyncNetworkHalfResult(
1252 True,
1253 SyncNetworkHalfError(
1254 f"Unable to Extract Archive {tarpath}",
1255 project=self.name,
1256 ),
1257 )
Gavin Makea2e3302023-03-11 06:46:20 +00001258 try:
1259 platform_utils.remove(tarpath)
1260 except OSError as e:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00001261 logger.warning("warn: Cannot remove archive %s: %s", tarpath, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001262 self._CopyAndLinkFiles()
Jason Chang32b59562023-07-14 16:45:35 -07001263 return SyncNetworkHalfResult(True)
Gavin Makea2e3302023-03-11 06:46:20 +00001264
1265 # If the shared object dir already exists, don't try to rebootstrap with
1266 # a clone bundle download. We should have the majority of objects
1267 # already.
1268 if clone_bundle and os.path.exists(self.objdir):
1269 clone_bundle = False
1270
1271 if self.name in partial_clone_exclude:
1272 clone_bundle = True
1273 clone_filter = None
1274
1275 if is_new is None:
1276 is_new = not self.Exists
1277 if is_new:
1278 self._InitGitDir(force_sync=force_sync, quiet=quiet)
1279 else:
1280 self._UpdateHooks(quiet=quiet)
1281 self._InitRemote()
1282
1283 if self.UseAlternates:
1284 # If gitdir/objects is a symlink, migrate it from the old layout.
1285 gitdir_objects = os.path.join(self.gitdir, "objects")
1286 if platform_utils.islink(gitdir_objects):
1287 platform_utils.remove(gitdir_objects, missing_ok=True)
1288 gitdir_alt = os.path.join(self.gitdir, "objects/info/alternates")
1289 if not os.path.exists(gitdir_alt):
1290 os.makedirs(os.path.dirname(gitdir_alt), exist_ok=True)
1291 _lwrite(
1292 gitdir_alt,
1293 os.path.join(
1294 os.path.relpath(self.objdir, gitdir_objects), "objects"
1295 )
1296 + "\n",
1297 )
1298
1299 if is_new:
1300 alt = os.path.join(self.objdir, "objects/info/alternates")
1301 try:
1302 with open(alt) as fd:
1303 # This works for both absolute and relative alternate
1304 # directories.
1305 alt_dir = os.path.join(
1306 self.objdir, "objects", fd.readline().rstrip()
1307 )
Jason R. Coombsae824fb2023-10-20 23:32:40 +05451308 except OSError:
Gavin Makea2e3302023-03-11 06:46:20 +00001309 alt_dir = None
1310 else:
1311 alt_dir = None
1312
1313 if (
1314 clone_bundle
1315 and alt_dir is None
1316 and self._ApplyCloneBundle(
1317 initial=is_new, quiet=quiet, verbose=verbose
1318 )
1319 ):
1320 is_new = False
1321
1322 if current_branch_only is None:
1323 if self.sync_c:
1324 current_branch_only = True
1325 elif not self.manifest._loaded:
1326 # Manifest cannot check defaults until it syncs.
1327 current_branch_only = False
1328 elif self.manifest.default.sync_c:
1329 current_branch_only = True
1330
1331 if tags is None:
1332 tags = self.sync_tags
1333
1334 if self.clone_depth:
1335 depth = self.clone_depth
1336 else:
1337 depth = self.manifest.manifestProject.depth
1338
Jason Chang17833322023-05-23 13:06:55 -07001339 if depth and clone_filter_for_depth:
1340 depth = None
1341 clone_filter = clone_filter_for_depth
1342
Gavin Makea2e3302023-03-11 06:46:20 +00001343 # See if we can skip the network fetch entirely.
1344 remote_fetched = False
1345 if not (
1346 optimized_fetch
Sylvain56a5a012023-09-11 13:38:00 +02001347 and IsId(self.revisionExpr)
1348 and self._CheckForImmutableRevision()
Gavin Makea2e3302023-03-11 06:46:20 +00001349 ):
1350 remote_fetched = True
Jason Chang32b59562023-07-14 16:45:35 -07001351 try:
1352 if not self._RemoteFetch(
1353 initial=is_new,
1354 quiet=quiet,
1355 verbose=verbose,
1356 output_redir=output_redir,
1357 alt_dir=alt_dir,
1358 current_branch_only=current_branch_only,
1359 tags=tags,
1360 prune=prune,
1361 depth=depth,
1362 submodules=submodules,
1363 force_sync=force_sync,
1364 ssh_proxy=ssh_proxy,
1365 clone_filter=clone_filter,
1366 retry_fetches=retry_fetches,
1367 ):
1368 return SyncNetworkHalfResult(
1369 remote_fetched,
1370 SyncNetworkHalfError(
1371 f"Unable to remote fetch project {self.name}",
1372 project=self.name,
1373 ),
1374 )
1375 except RepoError as e:
1376 return SyncNetworkHalfResult(
1377 remote_fetched,
1378 e,
1379 )
Gavin Makea2e3302023-03-11 06:46:20 +00001380
1381 mp = self.manifest.manifestProject
1382 dissociate = mp.dissociate
1383 if dissociate:
1384 alternates_file = os.path.join(
1385 self.objdir, "objects/info/alternates"
1386 )
1387 if os.path.exists(alternates_file):
1388 cmd = ["repack", "-a", "-d"]
1389 p = GitCommand(
1390 self,
1391 cmd,
1392 bare=True,
1393 capture_stdout=bool(output_redir),
1394 merge_output=bool(output_redir),
1395 )
1396 if p.stdout and output_redir:
1397 output_redir.write(p.stdout)
1398 if p.Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07001399 return SyncNetworkHalfResult(
1400 remote_fetched,
1401 GitError(
1402 "Unable to repack alternates", project=self.name
1403 ),
1404 )
Gavin Makea2e3302023-03-11 06:46:20 +00001405 platform_utils.remove(alternates_file)
1406
1407 if self.worktree:
1408 self._InitMRef()
1409 else:
1410 self._InitMirrorHead()
1411 platform_utils.remove(
1412 os.path.join(self.gitdir, "FETCH_HEAD"), missing_ok=True
1413 )
Jason Chang32b59562023-07-14 16:45:35 -07001414 return SyncNetworkHalfResult(remote_fetched)
Gavin Makea2e3302023-03-11 06:46:20 +00001415
1416 def PostRepoUpgrade(self):
1417 self._InitHooks()
1418
1419 def _CopyAndLinkFiles(self):
1420 if self.client.isGitcClient:
1421 return
1422 for copyfile in self.copyfiles:
1423 copyfile._Copy()
1424 for linkfile in self.linkfiles:
1425 linkfile._Link()
1426
1427 def GetCommitRevisionId(self):
1428 """Get revisionId of a commit.
1429
1430 Use this method instead of GetRevisionId to get the id of the commit
1431 rather than the id of the current git object (for example, a tag)
1432
1433 """
Sylvaine9cb3912023-09-10 23:35:01 +02001434 if self.revisionId:
1435 return self.revisionId
Gavin Makea2e3302023-03-11 06:46:20 +00001436 if not self.revisionExpr.startswith(R_TAGS):
1437 return self.GetRevisionId(self._allrefs)
1438
1439 try:
1440 return self.bare_git.rev_list(self.revisionExpr, "-1")[0]
1441 except GitError:
1442 raise ManifestInvalidRevisionError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001443 f"revision {self.revisionExpr} in {self.name} not found"
Gavin Makea2e3302023-03-11 06:46:20 +00001444 )
1445
1446 def GetRevisionId(self, all_refs=None):
1447 if self.revisionId:
1448 return self.revisionId
1449
1450 rem = self.GetRemote()
1451 rev = rem.ToLocal(self.revisionExpr)
1452
1453 if all_refs is not None and rev in all_refs:
1454 return all_refs[rev]
1455
1456 try:
1457 return self.bare_git.rev_parse("--verify", "%s^0" % rev)
1458 except GitError:
1459 raise ManifestInvalidRevisionError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001460 f"revision {self.revisionExpr} in {self.name} not found"
Gavin Makea2e3302023-03-11 06:46:20 +00001461 )
1462
1463 def SetRevisionId(self, revisionId):
1464 if self.revisionExpr:
1465 self.upstream = self.revisionExpr
1466
1467 self.revisionId = revisionId
1468
Jason Chang32b59562023-07-14 16:45:35 -07001469 def Sync_LocalHalf(
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001470 self,
1471 syncbuf,
1472 force_sync=False,
1473 submodules=False,
1474 errors=None,
1475 verbose=False,
Jason Chang32b59562023-07-14 16:45:35 -07001476 ):
Gavin Makea2e3302023-03-11 06:46:20 +00001477 """Perform only the local IO portion of the sync process.
1478
1479 Network access is not required.
1480 """
Jason Chang32b59562023-07-14 16:45:35 -07001481 if errors is None:
1482 errors = []
1483
1484 def fail(error: Exception):
1485 errors.append(error)
1486 syncbuf.fail(self, error)
1487
Gavin Makea2e3302023-03-11 06:46:20 +00001488 if not os.path.exists(self.gitdir):
Jason Chang32b59562023-07-14 16:45:35 -07001489 fail(
1490 LocalSyncFail(
1491 "Cannot checkout %s due to missing network sync; Run "
1492 "`repo sync -n %s` first." % (self.name, self.name),
1493 project=self.name,
1494 )
Gavin Makea2e3302023-03-11 06:46:20 +00001495 )
1496 return
1497
1498 self._InitWorkTree(force_sync=force_sync, submodules=submodules)
1499 all_refs = self.bare_ref.all
1500 self.CleanPublishedCache(all_refs)
1501 revid = self.GetRevisionId(all_refs)
1502
1503 # Special case the root of the repo client checkout. Make sure it
1504 # doesn't contain files being checked out to dirs we don't allow.
1505 if self.relpath == ".":
1506 PROTECTED_PATHS = {".repo"}
1507 paths = set(
1508 self.work_git.ls_tree("-z", "--name-only", "--", revid).split(
1509 "\0"
1510 )
1511 )
1512 bad_paths = paths & PROTECTED_PATHS
1513 if bad_paths:
Jason Chang32b59562023-07-14 16:45:35 -07001514 fail(
1515 LocalSyncFail(
1516 "Refusing to checkout project that writes to protected "
1517 "paths: %s" % (", ".join(bad_paths),),
1518 project=self.name,
1519 )
Gavin Makea2e3302023-03-11 06:46:20 +00001520 )
1521 return
1522
1523 def _doff():
1524 self._FastForward(revid)
1525 self._CopyAndLinkFiles()
1526
1527 def _dosubmodules():
1528 self._SyncSubmodules(quiet=True)
1529
1530 head = self.work_git.GetHead()
1531 if head.startswith(R_HEADS):
1532 branch = head[len(R_HEADS) :]
1533 try:
1534 head = all_refs[head]
1535 except KeyError:
1536 head = None
1537 else:
1538 branch = None
1539
1540 if branch is None or syncbuf.detach_head:
1541 # Currently on a detached HEAD. The user is assumed to
1542 # not have any local modifications worth worrying about.
1543 if self.IsRebaseInProgress():
Jason Chang32b59562023-07-14 16:45:35 -07001544 fail(_PriorSyncFailedError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001545 return
1546
1547 if head == revid:
1548 # No changes; don't do anything further.
1549 # Except if the head needs to be detached.
1550 if not syncbuf.detach_head:
1551 # The copy/linkfile config may have changed.
1552 self._CopyAndLinkFiles()
1553 return
1554 else:
1555 lost = self._revlist(not_rev(revid), HEAD)
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001556 if lost and verbose:
Gavin Makea2e3302023-03-11 06:46:20 +00001557 syncbuf.info(self, "discarding %d commits", len(lost))
1558
1559 try:
1560 self._Checkout(revid, quiet=True)
1561 if submodules:
1562 self._SyncSubmodules(quiet=True)
1563 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001564 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001565 return
1566 self._CopyAndLinkFiles()
1567 return
1568
1569 if head == revid:
1570 # No changes; don't do anything further.
1571 #
1572 # The copy/linkfile config may have changed.
1573 self._CopyAndLinkFiles()
1574 return
1575
1576 branch = self.GetBranch(branch)
1577
1578 if not branch.LocalMerge:
1579 # The current branch has no tracking configuration.
1580 # Jump off it to a detached HEAD.
1581 syncbuf.info(
1582 self, "leaving %s; does not track upstream", branch.name
1583 )
1584 try:
1585 self._Checkout(revid, quiet=True)
1586 if submodules:
1587 self._SyncSubmodules(quiet=True)
1588 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001589 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001590 return
1591 self._CopyAndLinkFiles()
1592 return
1593
1594 upstream_gain = self._revlist(not_rev(HEAD), revid)
1595
1596 # See if we can perform a fast forward merge. This can happen if our
1597 # branch isn't in the exact same state as we last published.
1598 try:
Jason Chang87058c62023-09-27 11:34:43 -07001599 self.work_git.merge_base(
1600 "--is-ancestor", HEAD, revid, log_as_error=False
1601 )
Gavin Makea2e3302023-03-11 06:46:20 +00001602 # Skip the published logic.
1603 pub = False
1604 except GitError:
1605 pub = self.WasPublished(branch.name, all_refs)
1606
1607 if pub:
1608 not_merged = self._revlist(not_rev(revid), pub)
1609 if not_merged:
1610 if upstream_gain:
1611 # The user has published this branch and some of those
1612 # commits are not yet merged upstream. We do not want
1613 # to rewrite the published commits so we punt.
Jason Chang32b59562023-07-14 16:45:35 -07001614 fail(
1615 LocalSyncFail(
1616 "branch %s is published (but not merged) and is "
1617 "now %d commits behind"
1618 % (branch.name, len(upstream_gain)),
1619 project=self.name,
1620 )
Gavin Makea2e3302023-03-11 06:46:20 +00001621 )
1622 return
1623 elif pub == head:
1624 # All published commits are merged, and thus we are a
1625 # strict subset. We can fast-forward safely.
1626 syncbuf.later1(self, _doff)
1627 if submodules:
1628 syncbuf.later1(self, _dosubmodules)
1629 return
1630
1631 # Examine the local commits not in the remote. Find the
1632 # last one attributed to this user, if any.
1633 local_changes = self._revlist(not_rev(revid), HEAD, format="%H %ce")
1634 last_mine = None
1635 cnt_mine = 0
1636 for commit in local_changes:
1637 commit_id, committer_email = commit.split(" ", 1)
1638 if committer_email == self.UserEmail:
1639 last_mine = commit_id
1640 cnt_mine += 1
1641
1642 if not upstream_gain and cnt_mine == len(local_changes):
1643 # The copy/linkfile config may have changed.
1644 self._CopyAndLinkFiles()
1645 return
1646
1647 if self.IsDirty(consider_untracked=False):
Jason Chang32b59562023-07-14 16:45:35 -07001648 fail(_DirtyError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001649 return
1650
1651 # If the upstream switched on us, warn the user.
1652 if branch.merge != self.revisionExpr:
1653 if branch.merge and self.revisionExpr:
1654 syncbuf.info(
1655 self,
1656 "manifest switched %s...%s",
1657 branch.merge,
1658 self.revisionExpr,
1659 )
1660 elif branch.merge:
1661 syncbuf.info(self, "manifest no longer tracks %s", branch.merge)
1662
1663 if cnt_mine < len(local_changes):
1664 # Upstream rebased. Not everything in HEAD was created by this user.
1665 syncbuf.info(
1666 self,
1667 "discarding %d commits removed from upstream",
1668 len(local_changes) - cnt_mine,
1669 )
1670
1671 branch.remote = self.GetRemote()
Sylvain56a5a012023-09-11 13:38:00 +02001672 if not IsId(self.revisionExpr):
Gavin Makea2e3302023-03-11 06:46:20 +00001673 # In case of manifest sync the revisionExpr might be a SHA1.
1674 branch.merge = self.revisionExpr
1675 if not branch.merge.startswith("refs/"):
1676 branch.merge = R_HEADS + branch.merge
1677 branch.Save()
1678
1679 if cnt_mine > 0 and self.rebase:
1680
1681 def _docopyandlink():
1682 self._CopyAndLinkFiles()
1683
1684 def _dorebase():
1685 self._Rebase(upstream="%s^1" % last_mine, onto=revid)
1686
1687 syncbuf.later2(self, _dorebase)
1688 if submodules:
1689 syncbuf.later2(self, _dosubmodules)
1690 syncbuf.later2(self, _docopyandlink)
1691 elif local_changes:
1692 try:
1693 self._ResetHard(revid)
1694 if submodules:
1695 self._SyncSubmodules(quiet=True)
1696 self._CopyAndLinkFiles()
1697 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001698 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001699 return
1700 else:
1701 syncbuf.later1(self, _doff)
1702 if submodules:
1703 syncbuf.later1(self, _dosubmodules)
1704
1705 def AddCopyFile(self, src, dest, topdir):
1706 """Mark |src| for copying to |dest| (relative to |topdir|).
1707
1708 No filesystem changes occur here. Actual copying happens later on.
1709
1710 Paths should have basic validation run on them before being queued.
1711 Further checking will be handled when the actual copy happens.
1712 """
1713 self.copyfiles.append(_CopyFile(self.worktree, src, topdir, dest))
1714
1715 def AddLinkFile(self, src, dest, topdir):
1716 """Mark |dest| to create a symlink (relative to |topdir|) pointing to
1717 |src|.
1718
1719 No filesystem changes occur here. Actual linking happens later on.
1720
1721 Paths should have basic validation run on them before being queued.
1722 Further checking will be handled when the actual link happens.
1723 """
1724 self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest))
1725
1726 def AddAnnotation(self, name, value, keep):
1727 self.annotations.append(Annotation(name, value, keep))
1728
1729 def DownloadPatchSet(self, change_id, patch_id):
1730 """Download a single patch set of a single change to FETCH_HEAD."""
1731 remote = self.GetRemote()
1732
1733 cmd = ["fetch", remote.name]
1734 cmd.append(
1735 "refs/changes/%2.2d/%d/%d" % (change_id % 100, change_id, patch_id)
1736 )
Jason Chang1a3612f2023-08-08 14:12:53 -07001737 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001738 return DownloadedChange(
1739 self,
1740 self.GetRevisionId(),
1741 change_id,
1742 patch_id,
1743 self.bare_git.rev_parse("FETCH_HEAD"),
1744 )
1745
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001746 def DeleteWorktree(self, verbose=False, force=False):
Gavin Makea2e3302023-03-11 06:46:20 +00001747 """Delete the source checkout and any other housekeeping tasks.
1748
1749 This currently leaves behind the internal .repo/ cache state. This
1750 helps when switching branches or manifest changes get reverted as we
1751 don't have to redownload all the git objects. But we should do some GC
1752 at some point.
1753
1754 Args:
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001755 verbose: Whether to show verbose messages.
Gavin Makea2e3302023-03-11 06:46:20 +00001756 force: Always delete tree even if dirty.
Doug Anderson3ba5f952011-04-07 12:51:04 -07001757
1758 Returns:
Gavin Makea2e3302023-03-11 06:46:20 +00001759 True if the worktree was completely cleaned out.
1760 """
1761 if self.IsDirty():
1762 if force:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00001763 logger.warning(
Gavin Makea2e3302023-03-11 06:46:20 +00001764 "warning: %s: Removing dirty project: uncommitted changes "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001765 "lost.",
1766 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001767 )
1768 else:
Jason Chang32b59562023-07-14 16:45:35 -07001769 msg = (
1770 "error: %s: Cannot remove project: uncommitted"
1771 "changes are present.\n" % self.RelPath(local=False)
Gavin Makea2e3302023-03-11 06:46:20 +00001772 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001773 logger.error(msg)
Jason Chang32b59562023-07-14 16:45:35 -07001774 raise DeleteDirtyWorktreeError(msg, project=self)
Wink Saville02d79452009-04-10 13:01:24 -07001775
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001776 if verbose:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001777 print(f"{self.RelPath(local=False)}: Deleting obsolete checkout.")
Wink Saville02d79452009-04-10 13:01:24 -07001778
Gavin Makea2e3302023-03-11 06:46:20 +00001779 # Unlock and delink from the main worktree. We don't use git's worktree
1780 # remove because it will recursively delete projects -- we handle that
1781 # ourselves below. https://crbug.com/git/48
1782 if self.use_git_worktrees:
1783 needle = platform_utils.realpath(self.gitdir)
1784 # Find the git worktree commondir under .repo/worktrees/.
1785 output = self.bare_git.worktree("list", "--porcelain").splitlines()[
1786 0
1787 ]
1788 assert output.startswith("worktree "), output
1789 commondir = output[9:]
1790 # Walk each of the git worktrees to see where they point.
1791 configs = os.path.join(commondir, "worktrees")
1792 for name in os.listdir(configs):
1793 gitdir = os.path.join(configs, name, "gitdir")
1794 with open(gitdir) as fp:
1795 relpath = fp.read().strip()
1796 # Resolve the checkout path and see if it matches this project.
1797 fullpath = platform_utils.realpath(
1798 os.path.join(configs, name, relpath)
1799 )
1800 if fullpath == needle:
1801 platform_utils.rmtree(os.path.join(configs, name))
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001802
Gavin Makea2e3302023-03-11 06:46:20 +00001803 # Delete the .git directory first, so we're less likely to have a
1804 # partially working git repository around. There shouldn't be any git
1805 # projects here, so rmtree works.
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001806
Gavin Makea2e3302023-03-11 06:46:20 +00001807 # Try to remove plain files first in case of git worktrees. If this
1808 # fails for any reason, we'll fall back to rmtree, and that'll display
1809 # errors if it can't remove things either.
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001810 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001811 platform_utils.remove(self.gitdir)
1812 except OSError:
1813 pass
1814 try:
1815 platform_utils.rmtree(self.gitdir)
1816 except OSError as e:
1817 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001818 logger.error("error: %s: %s", self.gitdir, e)
1819 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00001820 "error: %s: Failed to delete obsolete checkout; remove "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001821 "manually, then run `repo sync -l`.",
1822 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001823 )
Jason Chang32b59562023-07-14 16:45:35 -07001824 raise DeleteWorktreeError(aggregate_errors=[e])
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001825
Gavin Makea2e3302023-03-11 06:46:20 +00001826 # Delete everything under the worktree, except for directories that
1827 # contain another git project.
1828 dirs_to_remove = []
1829 failed = False
Jason Chang32b59562023-07-14 16:45:35 -07001830 errors = []
Gavin Makea2e3302023-03-11 06:46:20 +00001831 for root, dirs, files in platform_utils.walk(self.worktree):
1832 for f in files:
1833 path = os.path.join(root, f)
1834 try:
1835 platform_utils.remove(path)
1836 except OSError as e:
1837 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001838 logger.error("error: %s: Failed to remove: %s", path, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001839 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001840 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001841 dirs[:] = [
1842 d
1843 for d in dirs
1844 if not os.path.lexists(os.path.join(root, d, ".git"))
1845 ]
1846 dirs_to_remove += [
1847 os.path.join(root, d)
1848 for d in dirs
1849 if os.path.join(root, d) not in dirs_to_remove
1850 ]
1851 for d in reversed(dirs_to_remove):
1852 if platform_utils.islink(d):
1853 try:
1854 platform_utils.remove(d)
1855 except OSError as e:
1856 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001857 logger.error("error: %s: Failed to remove: %s", d, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001858 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001859 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001860 elif not platform_utils.listdir(d):
1861 try:
1862 platform_utils.rmdir(d)
1863 except OSError as e:
1864 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001865 logger.error("error: %s: Failed to remove: %s", d, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001866 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001867 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001868 if failed:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001869 logger.error(
1870 "error: %s: Failed to delete obsolete checkout.",
1871 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001872 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001873 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00001874 " Remove manually, then run `repo sync -l`.",
Gavin Makea2e3302023-03-11 06:46:20 +00001875 )
Jason Chang32b59562023-07-14 16:45:35 -07001876 raise DeleteWorktreeError(aggregate_errors=errors)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07001877
Gavin Makea2e3302023-03-11 06:46:20 +00001878 # Try deleting parent dirs if they are empty.
1879 path = self.worktree
1880 while path != self.manifest.topdir:
1881 try:
1882 platform_utils.rmdir(path)
1883 except OSError as e:
1884 if e.errno != errno.ENOENT:
1885 break
1886 path = os.path.dirname(path)
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001887
Gavin Makea2e3302023-03-11 06:46:20 +00001888 return True
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001889
Gavin Makea2e3302023-03-11 06:46:20 +00001890 def StartBranch(self, name, branch_merge="", revision=None):
1891 """Create a new branch off the manifest's revision."""
1892 if not branch_merge:
1893 branch_merge = self.revisionExpr
1894 head = self.work_git.GetHead()
1895 if head == (R_HEADS + name):
1896 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001897
David Pursehouse8a68ff92012-09-24 12:15:13 +09001898 all_refs = self.bare_ref.all
Gavin Makea2e3302023-03-11 06:46:20 +00001899 if R_HEADS + name in all_refs:
Jason Chang1a3612f2023-08-08 14:12:53 -07001900 GitCommand(
1901 self, ["checkout", "-q", name, "--"], verify_command=True
1902 ).Wait()
1903 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001904
Gavin Makea2e3302023-03-11 06:46:20 +00001905 branch = self.GetBranch(name)
1906 branch.remote = self.GetRemote()
1907 branch.merge = branch_merge
Sylvain56a5a012023-09-11 13:38:00 +02001908 if not branch.merge.startswith("refs/") and not IsId(branch_merge):
Gavin Makea2e3302023-03-11 06:46:20 +00001909 branch.merge = R_HEADS + branch_merge
Shawn O. Pearce88443382010-10-08 10:02:09 +02001910
Gavin Makea2e3302023-03-11 06:46:20 +00001911 if revision is None:
1912 revid = self.GetRevisionId(all_refs)
Shawn O. Pearce88443382010-10-08 10:02:09 +02001913 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001914 revid = self.work_git.rev_parse(revision)
Brian Harring14a66742012-09-28 20:21:57 -07001915
Gavin Makea2e3302023-03-11 06:46:20 +00001916 if head.startswith(R_HEADS):
Kevin Degiabaa7f32014-11-12 11:27:45 -07001917 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001918 head = all_refs[head]
1919 except KeyError:
1920 head = None
1921 if revid and head and revid == head:
1922 ref = R_HEADS + name
1923 self.work_git.update_ref(ref, revid)
1924 self.work_git.symbolic_ref(HEAD, ref)
1925 branch.Save()
1926 return True
Kevin Degib1a07b82015-07-27 13:33:43 -06001927
Jason Chang1a3612f2023-08-08 14:12:53 -07001928 GitCommand(
1929 self,
1930 ["checkout", "-q", "-b", branch.name, revid],
1931 verify_command=True,
1932 ).Wait()
1933 branch.Save()
1934 return True
Kevin Degi384b3c52014-10-16 16:02:58 -06001935
Gavin Makea2e3302023-03-11 06:46:20 +00001936 def CheckoutBranch(self, name):
1937 """Checkout a local topic branch.
Shawn O. Pearce2816d4f2009-03-03 17:53:18 -08001938
Gavin Makea2e3302023-03-11 06:46:20 +00001939 Args:
1940 name: The name of the branch to checkout.
Shawn O. Pearce88443382010-10-08 10:02:09 +02001941
Gavin Makea2e3302023-03-11 06:46:20 +00001942 Returns:
Jason Chang1a3612f2023-08-08 14:12:53 -07001943 True if the checkout succeeded; False if the
1944 branch doesn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00001945 """
1946 rev = R_HEADS + name
1947 head = self.work_git.GetHead()
1948 if head == rev:
1949 # Already on the branch.
1950 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001951
Gavin Makea2e3302023-03-11 06:46:20 +00001952 all_refs = self.bare_ref.all
1953 try:
1954 revid = all_refs[rev]
1955 except KeyError:
1956 # Branch does not exist in this project.
Jason Chang1a3612f2023-08-08 14:12:53 -07001957 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001958
Gavin Makea2e3302023-03-11 06:46:20 +00001959 if head.startswith(R_HEADS):
1960 try:
1961 head = all_refs[head]
1962 except KeyError:
1963 head = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001964
Gavin Makea2e3302023-03-11 06:46:20 +00001965 if head == revid:
1966 # Same revision; just update HEAD to point to the new
1967 # target branch, but otherwise take no other action.
1968 _lwrite(
1969 self.work_git.GetDotgitPath(subpath=HEAD),
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001970 f"ref: {R_HEADS}{name}\n",
Gavin Makea2e3302023-03-11 06:46:20 +00001971 )
1972 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05001973
Jason Chang1a3612f2023-08-08 14:12:53 -07001974 GitCommand(
1975 self,
1976 ["checkout", name, "--"],
1977 capture_stdout=True,
1978 capture_stderr=True,
1979 verify_command=True,
1980 ).Wait()
1981 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05001982
Gavin Makea2e3302023-03-11 06:46:20 +00001983 def AbandonBranch(self, name):
1984 """Destroy a local topic branch.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07001985
Gavin Makea2e3302023-03-11 06:46:20 +00001986 Args:
1987 name: The name of the branch to abandon.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07001988
Gavin Makea2e3302023-03-11 06:46:20 +00001989 Returns:
Jason Changf9aacd42023-08-03 14:38:00 -07001990 True if the abandon succeeded; Raises GitCommandError if it didn't;
1991 None if the branch didn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00001992 """
1993 rev = R_HEADS + name
1994 all_refs = self.bare_ref.all
1995 if rev not in all_refs:
1996 # Doesn't exist
1997 return None
1998
1999 head = self.work_git.GetHead()
2000 if head == rev:
2001 # We can't destroy the branch while we are sitting
2002 # on it. Switch to a detached HEAD.
2003 head = all_refs[head]
2004
2005 revid = self.GetRevisionId(all_refs)
2006 if head == revid:
2007 _lwrite(
2008 self.work_git.GetDotgitPath(subpath=HEAD), "%s\n" % revid
2009 )
2010 else:
2011 self._Checkout(revid, quiet=True)
Jason Changf9aacd42023-08-03 14:38:00 -07002012 GitCommand(
2013 self,
2014 ["branch", "-D", name],
2015 capture_stdout=True,
2016 capture_stderr=True,
2017 verify_command=True,
2018 ).Wait()
2019 return True
Gavin Makea2e3302023-03-11 06:46:20 +00002020
2021 def PruneHeads(self):
2022 """Prune any topic branches already merged into upstream."""
2023 cb = self.CurrentBranch
2024 kill = []
2025 left = self._allrefs
2026 for name in left.keys():
2027 if name.startswith(R_HEADS):
2028 name = name[len(R_HEADS) :]
2029 if cb is None or name != cb:
2030 kill.append(name)
2031
2032 # Minor optimization: If there's nothing to prune, then don't try to
2033 # read any project state.
2034 if not kill and not cb:
2035 return []
2036
2037 rev = self.GetRevisionId(left)
2038 if (
2039 cb is not None
2040 and not self._revlist(HEAD + "..." + rev)
2041 and not self.IsDirty(consider_untracked=False)
2042 ):
2043 self.work_git.DetachHead(HEAD)
2044 kill.append(cb)
2045
2046 if kill:
2047 old = self.bare_git.GetHead()
2048
2049 try:
2050 self.bare_git.DetachHead(rev)
2051
2052 b = ["branch", "-d"]
2053 b.extend(kill)
2054 b = GitCommand(
2055 self, b, bare=True, capture_stdout=True, capture_stderr=True
2056 )
2057 b.Wait()
2058 finally:
Sylvain56a5a012023-09-11 13:38:00 +02002059 if IsId(old):
Gavin Makea2e3302023-03-11 06:46:20 +00002060 self.bare_git.DetachHead(old)
2061 else:
2062 self.bare_git.SetHead(old)
2063 left = self._allrefs
2064
2065 for branch in kill:
2066 if (R_HEADS + branch) not in left:
2067 self.CleanPublishedCache()
2068 break
2069
2070 if cb and cb not in kill:
2071 kill.append(cb)
2072 kill.sort()
2073
2074 kept = []
2075 for branch in kill:
2076 if R_HEADS + branch in left:
2077 branch = self.GetBranch(branch)
2078 base = branch.LocalMerge
2079 if not base:
2080 base = rev
2081 kept.append(ReviewableBranch(self, branch, base))
2082 return kept
2083
2084 def GetRegisteredSubprojects(self):
2085 result = []
2086
2087 def rec(subprojects):
2088 if not subprojects:
2089 return
2090 result.extend(subprojects)
2091 for p in subprojects:
2092 rec(p.subprojects)
2093
2094 rec(self.subprojects)
2095 return result
2096
2097 def _GetSubmodules(self):
2098 # Unfortunately we cannot call `git submodule status --recursive` here
2099 # because the working tree might not exist yet, and it cannot be used
2100 # without a working tree in its current implementation.
2101
2102 def get_submodules(gitdir, rev):
2103 # Parse .gitmodules for submodule sub_paths and sub_urls.
2104 sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
2105 if not sub_paths:
2106 return []
2107 # Run `git ls-tree` to read SHAs of submodule object, which happen
2108 # to be revision of submodule repository.
2109 sub_revs = git_ls_tree(gitdir, rev, sub_paths)
2110 submodules = []
2111 for sub_path, sub_url in zip(sub_paths, sub_urls):
2112 try:
2113 sub_rev = sub_revs[sub_path]
2114 except KeyError:
2115 # Ignore non-exist submodules.
2116 continue
2117 submodules.append((sub_rev, sub_path, sub_url))
2118 return submodules
2119
2120 re_path = re.compile(r"^submodule\.(.+)\.path=(.*)$")
2121 re_url = re.compile(r"^submodule\.(.+)\.url=(.*)$")
2122
2123 def parse_gitmodules(gitdir, rev):
2124 cmd = ["cat-file", "blob", "%s:.gitmodules" % rev]
2125 try:
2126 p = GitCommand(
2127 None,
2128 cmd,
2129 capture_stdout=True,
2130 capture_stderr=True,
2131 bare=True,
2132 gitdir=gitdir,
2133 )
2134 except GitError:
2135 return [], []
2136 if p.Wait() != 0:
2137 return [], []
2138
2139 gitmodules_lines = []
2140 fd, temp_gitmodules_path = tempfile.mkstemp()
2141 try:
2142 os.write(fd, p.stdout.encode("utf-8"))
2143 os.close(fd)
2144 cmd = ["config", "--file", temp_gitmodules_path, "--list"]
2145 p = GitCommand(
2146 None,
2147 cmd,
2148 capture_stdout=True,
2149 capture_stderr=True,
2150 bare=True,
2151 gitdir=gitdir,
2152 )
2153 if p.Wait() != 0:
2154 return [], []
2155 gitmodules_lines = p.stdout.split("\n")
2156 except GitError:
2157 return [], []
2158 finally:
2159 platform_utils.remove(temp_gitmodules_path)
2160
2161 names = set()
2162 paths = {}
2163 urls = {}
2164 for line in gitmodules_lines:
2165 if not line:
2166 continue
2167 m = re_path.match(line)
2168 if m:
2169 names.add(m.group(1))
2170 paths[m.group(1)] = m.group(2)
2171 continue
2172 m = re_url.match(line)
2173 if m:
2174 names.add(m.group(1))
2175 urls[m.group(1)] = m.group(2)
2176 continue
2177 names = sorted(names)
2178 return (
2179 [paths.get(name, "") for name in names],
2180 [urls.get(name, "") for name in names],
2181 )
2182
2183 def git_ls_tree(gitdir, rev, paths):
2184 cmd = ["ls-tree", rev, "--"]
2185 cmd.extend(paths)
2186 try:
2187 p = GitCommand(
2188 None,
2189 cmd,
2190 capture_stdout=True,
2191 capture_stderr=True,
2192 bare=True,
2193 gitdir=gitdir,
2194 )
2195 except GitError:
2196 return []
2197 if p.Wait() != 0:
2198 return []
2199 objects = {}
2200 for line in p.stdout.split("\n"):
2201 if not line.strip():
2202 continue
2203 object_rev, object_path = line.split()[2:4]
2204 objects[object_path] = object_rev
2205 return objects
2206
2207 try:
2208 rev = self.GetRevisionId()
2209 except GitError:
2210 return []
2211 return get_submodules(self.gitdir, rev)
2212
2213 def GetDerivedSubprojects(self):
2214 result = []
2215 if not self.Exists:
2216 # If git repo does not exist yet, querying its submodules will
2217 # mess up its states; so return here.
2218 return result
2219 for rev, path, url in self._GetSubmodules():
2220 name = self.manifest.GetSubprojectName(self, path)
2221 (
2222 relpath,
2223 worktree,
2224 gitdir,
2225 objdir,
2226 ) = self.manifest.GetSubprojectPaths(self, name, path)
2227 project = self.manifest.paths.get(relpath)
2228 if project:
2229 result.extend(project.GetDerivedSubprojects())
2230 continue
2231
2232 if url.startswith(".."):
2233 url = urllib.parse.urljoin("%s/" % self.remote.url, url)
2234 remote = RemoteSpec(
2235 self.remote.name,
2236 url=url,
2237 pushUrl=self.remote.pushUrl,
2238 review=self.remote.review,
2239 revision=self.remote.revision,
2240 )
2241 subproject = Project(
2242 manifest=self.manifest,
2243 name=name,
2244 remote=remote,
2245 gitdir=gitdir,
2246 objdir=objdir,
2247 worktree=worktree,
2248 relpath=relpath,
2249 revisionExpr=rev,
2250 revisionId=rev,
2251 rebase=self.rebase,
2252 groups=self.groups,
2253 sync_c=self.sync_c,
2254 sync_s=self.sync_s,
2255 sync_tags=self.sync_tags,
2256 parent=self,
2257 is_derived=True,
2258 )
2259 result.append(subproject)
2260 result.extend(subproject.GetDerivedSubprojects())
2261 return result
2262
2263 def EnableRepositoryExtension(self, key, value="true", version=1):
2264 """Enable git repository extension |key| with |value|.
2265
2266 Args:
2267 key: The extension to enabled. Omit the "extensions." prefix.
2268 value: The value to use for the extension.
2269 version: The minimum git repository version needed.
2270 """
2271 # Make sure the git repo version is new enough already.
2272 found_version = self.config.GetInt("core.repositoryFormatVersion")
2273 if found_version is None:
2274 found_version = 0
2275 if found_version < version:
2276 self.config.SetString("core.repositoryFormatVersion", str(version))
2277
2278 # Enable the extension!
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002279 self.config.SetString(f"extensions.{key}", value)
Gavin Makea2e3302023-03-11 06:46:20 +00002280
2281 def ResolveRemoteHead(self, name=None):
2282 """Find out what the default branch (HEAD) points to.
2283
2284 Normally this points to refs/heads/master, but projects are moving to
2285 main. Support whatever the server uses rather than hardcoding "master"
2286 ourselves.
2287 """
2288 if name is None:
2289 name = self.remote.name
2290
2291 # The output will look like (NB: tabs are separators):
2292 # ref: refs/heads/master HEAD
2293 # 5f6803b100bb3cd0f534e96e88c91373e8ed1c44 HEAD
2294 output = self.bare_git.ls_remote(
2295 "-q", "--symref", "--exit-code", name, "HEAD"
2296 )
2297
2298 for line in output.splitlines():
2299 lhs, rhs = line.split("\t", 1)
2300 if rhs == "HEAD" and lhs.startswith("ref:"):
2301 return lhs[4:].strip()
2302
2303 return None
2304
2305 def _CheckForImmutableRevision(self):
2306 try:
2307 # if revision (sha or tag) is not present then following function
2308 # throws an error.
2309 self.bare_git.rev_list(
Jason Chang87058c62023-09-27 11:34:43 -07002310 "-1",
2311 "--missing=allow-any",
2312 "%s^0" % self.revisionExpr,
2313 "--",
2314 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002315 )
2316 if self.upstream:
2317 rev = self.GetRemote().ToLocal(self.upstream)
2318 self.bare_git.rev_list(
Jason Chang87058c62023-09-27 11:34:43 -07002319 "-1",
2320 "--missing=allow-any",
2321 "%s^0" % rev,
2322 "--",
2323 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002324 )
2325 self.bare_git.merge_base(
Jason Chang87058c62023-09-27 11:34:43 -07002326 "--is-ancestor",
2327 self.revisionExpr,
2328 rev,
2329 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002330 )
2331 return True
2332 except GitError:
2333 # There is no such persistent revision. We have to fetch it.
2334 return False
2335
2336 def _FetchArchive(self, tarpath, cwd=None):
2337 cmd = ["archive", "-v", "-o", tarpath]
2338 cmd.append("--remote=%s" % self.remote.url)
2339 cmd.append("--prefix=%s/" % self.RelPath(local=False))
2340 cmd.append(self.revisionExpr)
2341
2342 command = GitCommand(
Jason Chang32b59562023-07-14 16:45:35 -07002343 self,
2344 cmd,
2345 cwd=cwd,
2346 capture_stdout=True,
2347 capture_stderr=True,
2348 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00002349 )
Jason Chang32b59562023-07-14 16:45:35 -07002350 command.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00002351
2352 def _RemoteFetch(
2353 self,
2354 name=None,
2355 current_branch_only=False,
2356 initial=False,
2357 quiet=False,
2358 verbose=False,
2359 output_redir=None,
2360 alt_dir=None,
2361 tags=True,
2362 prune=False,
2363 depth=None,
2364 submodules=False,
2365 ssh_proxy=None,
2366 force_sync=False,
2367 clone_filter=None,
2368 retry_fetches=2,
2369 retry_sleep_initial_sec=4.0,
2370 retry_exp_factor=2.0,
Jason Chang32b59562023-07-14 16:45:35 -07002371 ) -> bool:
Gavin Makea2e3302023-03-11 06:46:20 +00002372 tag_name = None
2373 # The depth should not be used when fetching to a mirror because
2374 # it will result in a shallow repository that cannot be cloned or
2375 # fetched from.
2376 # The repo project should also never be synced with partial depth.
2377 if self.manifest.IsMirror or self.relpath == ".repo/repo":
2378 depth = None
2379
2380 if depth:
2381 current_branch_only = True
2382
Sylvain56a5a012023-09-11 13:38:00 +02002383 is_sha1 = bool(IsId(self.revisionExpr))
Gavin Makea2e3302023-03-11 06:46:20 +00002384
2385 if current_branch_only:
2386 if self.revisionExpr.startswith(R_TAGS):
2387 # This is a tag and its commit id should never change.
2388 tag_name = self.revisionExpr[len(R_TAGS) :]
2389 elif self.upstream and self.upstream.startswith(R_TAGS):
2390 # This is a tag and its commit id should never change.
2391 tag_name = self.upstream[len(R_TAGS) :]
2392
2393 if is_sha1 or tag_name is not None:
2394 if self._CheckForImmutableRevision():
2395 if verbose:
2396 print(
2397 "Skipped fetching project %s (already have "
2398 "persistent ref)" % self.name
2399 )
2400 return True
2401 if is_sha1 and not depth:
2402 # When syncing a specific commit and --depth is not set:
2403 # * if upstream is explicitly specified and is not a sha1, fetch
2404 # only upstream as users expect only upstream to be fetch.
2405 # Note: The commit might not be in upstream in which case the
2406 # sync will fail.
2407 # * otherwise, fetch all branches to make sure we end up with
2408 # the specific commit.
2409 if self.upstream:
Sylvain56a5a012023-09-11 13:38:00 +02002410 current_branch_only = not IsId(self.upstream)
Gavin Makea2e3302023-03-11 06:46:20 +00002411 else:
2412 current_branch_only = False
2413
2414 if not name:
2415 name = self.remote.name
2416
2417 remote = self.GetRemote(name)
2418 if not remote.PreConnectFetch(ssh_proxy):
2419 ssh_proxy = None
2420
2421 if initial:
2422 if alt_dir and "objects" == os.path.basename(alt_dir):
2423 ref_dir = os.path.dirname(alt_dir)
2424 packed_refs = os.path.join(self.gitdir, "packed-refs")
2425
2426 all_refs = self.bare_ref.all
2427 ids = set(all_refs.values())
2428 tmp = set()
2429
2430 for r, ref_id in GitRefs(ref_dir).all.items():
2431 if r not in all_refs:
2432 if r.startswith(R_TAGS) or remote.WritesTo(r):
2433 all_refs[r] = ref_id
2434 ids.add(ref_id)
2435 continue
2436
2437 if ref_id in ids:
2438 continue
2439
2440 r = "refs/_alt/%s" % ref_id
2441 all_refs[r] = ref_id
2442 ids.add(ref_id)
2443 tmp.add(r)
2444
2445 tmp_packed_lines = []
2446 old_packed_lines = []
2447
2448 for r in sorted(all_refs):
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002449 line = f"{all_refs[r]} {r}\n"
Gavin Makea2e3302023-03-11 06:46:20 +00002450 tmp_packed_lines.append(line)
2451 if r not in tmp:
2452 old_packed_lines.append(line)
2453
2454 tmp_packed = "".join(tmp_packed_lines)
2455 old_packed = "".join(old_packed_lines)
2456 _lwrite(packed_refs, tmp_packed)
2457 else:
2458 alt_dir = None
2459
2460 cmd = ["fetch"]
2461
2462 if clone_filter:
2463 git_require((2, 19, 0), fail=True, msg="partial clones")
2464 cmd.append("--filter=%s" % clone_filter)
2465 self.EnableRepositoryExtension("partialclone", self.remote.name)
2466
2467 if depth:
2468 cmd.append("--depth=%s" % depth)
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002469 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002470 # If this repo has shallow objects, then we don't know which refs
2471 # have shallow objects or not. Tell git to unshallow all fetched
2472 # refs. Don't do this with projects that don't have shallow
2473 # objects, since it is less efficient.
2474 if os.path.exists(os.path.join(self.gitdir, "shallow")):
2475 cmd.append("--depth=2147483647")
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002476
Gavin Makea2e3302023-03-11 06:46:20 +00002477 if not verbose:
2478 cmd.append("--quiet")
2479 if not quiet and sys.stdout.isatty():
2480 cmd.append("--progress")
2481 if not self.worktree:
2482 cmd.append("--update-head-ok")
2483 cmd.append(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002484
Gavin Makea2e3302023-03-11 06:46:20 +00002485 if force_sync:
2486 cmd.append("--force")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002487
Gavin Makea2e3302023-03-11 06:46:20 +00002488 if prune:
2489 cmd.append("--prune")
Mike Frysinger29626b42021-05-01 09:37:13 -04002490
Gavin Makea2e3302023-03-11 06:46:20 +00002491 # Always pass something for --recurse-submodules, git with GIT_DIR
2492 # behaves incorrectly when not given `--recurse-submodules=no`.
2493 # (b/218891912)
2494 cmd.append(
2495 f'--recurse-submodules={"on-demand" if submodules else "no"}'
2496 )
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002497
Gavin Makea2e3302023-03-11 06:46:20 +00002498 spec = []
2499 if not current_branch_only:
2500 # Fetch whole repo.
2501 spec.append(
2502 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2503 )
2504 elif tag_name is not None:
2505 spec.append("tag")
2506 spec.append(tag_name)
Remy Böhmer1469c282020-12-15 18:49:02 +01002507
Gavin Makea2e3302023-03-11 06:46:20 +00002508 if self.manifest.IsMirror and not current_branch_only:
2509 branch = None
Remy Böhmer1469c282020-12-15 18:49:02 +01002510 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002511 branch = self.revisionExpr
2512 if (
2513 not self.manifest.IsMirror
2514 and is_sha1
2515 and depth
2516 and git_require((1, 8, 3))
2517 ):
2518 # Shallow checkout of a specific commit, fetch from that commit and
2519 # not the heads only as the commit might be deeper in the history.
2520 spec.append(branch)
2521 if self.upstream:
2522 spec.append(self.upstream)
David James8d201162013-10-11 17:03:19 -07002523 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002524 if is_sha1:
2525 branch = self.upstream
2526 if branch is not None and branch.strip():
2527 if not branch.startswith("refs/"):
2528 branch = R_HEADS + branch
2529 spec.append(str(("+%s:" % branch) + remote.ToLocal(branch)))
David James8d201162013-10-11 17:03:19 -07002530
Gavin Makea2e3302023-03-11 06:46:20 +00002531 # If mirroring repo and we cannot deduce the tag or branch to fetch,
2532 # fetch whole repo.
2533 if self.manifest.IsMirror and not spec:
2534 spec.append(
2535 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2536 )
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002537
Gavin Makea2e3302023-03-11 06:46:20 +00002538 # If using depth then we should not get all the tags since they may
2539 # be outside of the depth.
2540 if not tags or depth:
2541 cmd.append("--no-tags")
2542 else:
2543 cmd.append("--tags")
2544 spec.append(str(("+refs/tags/*:") + remote.ToLocal("refs/tags/*")))
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002545
Gavin Makea2e3302023-03-11 06:46:20 +00002546 cmd.extend(spec)
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002547
Gavin Makea2e3302023-03-11 06:46:20 +00002548 # At least one retry minimum due to git remote prune.
2549 retry_fetches = max(retry_fetches, 2)
2550 retry_cur_sleep = retry_sleep_initial_sec
2551 ok = prune_tried = False
2552 for try_n in range(retry_fetches):
Jason Chang32b59562023-07-14 16:45:35 -07002553 verify_command = try_n == retry_fetches - 1
Gavin Makea2e3302023-03-11 06:46:20 +00002554 gitcmd = GitCommand(
2555 self,
2556 cmd,
2557 bare=True,
2558 objdir=os.path.join(self.objdir, "objects"),
2559 ssh_proxy=ssh_proxy,
2560 merge_output=True,
2561 capture_stdout=quiet or bool(output_redir),
Jason Chang32b59562023-07-14 16:45:35 -07002562 verify_command=verify_command,
Gavin Makea2e3302023-03-11 06:46:20 +00002563 )
2564 if gitcmd.stdout and not quiet and output_redir:
2565 output_redir.write(gitcmd.stdout)
2566 ret = gitcmd.Wait()
2567 if ret == 0:
2568 ok = True
2569 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002570
Gavin Makea2e3302023-03-11 06:46:20 +00002571 # Retry later due to HTTP 429 Too Many Requests.
2572 elif (
2573 gitcmd.stdout
2574 and "error:" in gitcmd.stdout
2575 and "HTTP 429" in gitcmd.stdout
2576 ):
2577 # Fallthru to sleep+retry logic at the bottom.
2578 pass
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002579
Gavin Makea2e3302023-03-11 06:46:20 +00002580 # Try to prune remote branches once in case there are conflicts.
2581 # For example, if the remote had refs/heads/upstream, but deleted
2582 # that and now has refs/heads/upstream/foo.
2583 elif (
2584 gitcmd.stdout
2585 and "error:" in gitcmd.stdout
2586 and "git remote prune" in gitcmd.stdout
2587 and not prune_tried
2588 ):
2589 prune_tried = True
2590 prunecmd = GitCommand(
2591 self,
2592 ["remote", "prune", name],
2593 bare=True,
2594 ssh_proxy=ssh_proxy,
2595 )
2596 ret = prunecmd.Wait()
2597 if ret:
2598 break
2599 print(
2600 "retrying fetch after pruning remote branches",
2601 file=output_redir,
2602 )
2603 # Continue right away so we don't sleep as we shouldn't need to.
2604 continue
2605 elif current_branch_only and is_sha1 and ret == 128:
2606 # Exit code 128 means "couldn't find the ref you asked for"; if
2607 # we're in sha1 mode, we just tried sync'ing from the upstream
2608 # field; it doesn't exist, thus abort the optimization attempt
2609 # and do a full sync.
2610 break
2611 elif ret < 0:
2612 # Git died with a signal, exit immediately.
2613 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002614
Gavin Makea2e3302023-03-11 06:46:20 +00002615 # Figure out how long to sleep before the next attempt, if there is
2616 # one.
2617 if not verbose and gitcmd.stdout:
2618 print(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002619 f"\n{self.name}:\n{gitcmd.stdout}",
Gavin Makea2e3302023-03-11 06:46:20 +00002620 end="",
2621 file=output_redir,
2622 )
2623 if try_n < retry_fetches - 1:
2624 print(
2625 "%s: sleeping %s seconds before retrying"
2626 % (self.name, retry_cur_sleep),
2627 file=output_redir,
2628 )
2629 time.sleep(retry_cur_sleep)
2630 retry_cur_sleep = min(
2631 retry_exp_factor * retry_cur_sleep, MAXIMUM_RETRY_SLEEP_SEC
2632 )
2633 retry_cur_sleep *= 1 - random.uniform(
2634 -RETRY_JITTER_PERCENT, RETRY_JITTER_PERCENT
2635 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002636
Gavin Makea2e3302023-03-11 06:46:20 +00002637 if initial:
2638 if alt_dir:
2639 if old_packed != "":
2640 _lwrite(packed_refs, old_packed)
2641 else:
2642 platform_utils.remove(packed_refs)
2643 self.bare_git.pack_refs("--all", "--prune")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002644
Gavin Makea2e3302023-03-11 06:46:20 +00002645 if is_sha1 and current_branch_only:
2646 # We just synced the upstream given branch; verify we
2647 # got what we wanted, else trigger a second run of all
2648 # refs.
2649 if not self._CheckForImmutableRevision():
2650 # Sync the current branch only with depth set to None.
2651 # We always pass depth=None down to avoid infinite recursion.
2652 return self._RemoteFetch(
2653 name=name,
2654 quiet=quiet,
2655 verbose=verbose,
2656 output_redir=output_redir,
2657 current_branch_only=current_branch_only and depth,
2658 initial=False,
2659 alt_dir=alt_dir,
Nasser Grainawiaafed292023-05-24 12:51:03 -06002660 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00002661 depth=None,
2662 ssh_proxy=ssh_proxy,
2663 clone_filter=clone_filter,
2664 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002665
Gavin Makea2e3302023-03-11 06:46:20 +00002666 return ok
Mike Frysingerf4545122019-11-11 04:34:16 -05002667
Gavin Makea2e3302023-03-11 06:46:20 +00002668 def _ApplyCloneBundle(self, initial=False, quiet=False, verbose=False):
2669 if initial and (
2670 self.manifest.manifestProject.depth or self.clone_depth
2671 ):
2672 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002673
Gavin Makea2e3302023-03-11 06:46:20 +00002674 remote = self.GetRemote()
2675 bundle_url = remote.url + "/clone.bundle"
2676 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
2677 if GetSchemeFromUrl(bundle_url) not in (
2678 "http",
2679 "https",
2680 "persistent-http",
2681 "persistent-https",
2682 ):
2683 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06002684
Gavin Makea2e3302023-03-11 06:46:20 +00002685 bundle_dst = os.path.join(self.gitdir, "clone.bundle")
2686 bundle_tmp = os.path.join(self.gitdir, "clone.bundle.tmp")
2687
2688 exist_dst = os.path.exists(bundle_dst)
2689 exist_tmp = os.path.exists(bundle_tmp)
2690
2691 if not initial and not exist_dst and not exist_tmp:
2692 return False
2693
2694 if not exist_dst:
2695 exist_dst = self._FetchBundle(
2696 bundle_url, bundle_tmp, bundle_dst, quiet, verbose
2697 )
2698 if not exist_dst:
2699 return False
2700
2701 cmd = ["fetch"]
2702 if not verbose:
2703 cmd.append("--quiet")
2704 if not quiet and sys.stdout.isatty():
2705 cmd.append("--progress")
2706 if not self.worktree:
2707 cmd.append("--update-head-ok")
2708 cmd.append(bundle_dst)
2709 for f in remote.fetch:
2710 cmd.append(str(f))
2711 cmd.append("+refs/tags/*:refs/tags/*")
2712
2713 ok = (
2714 GitCommand(
2715 self,
2716 cmd,
2717 bare=True,
2718 objdir=os.path.join(self.objdir, "objects"),
2719 ).Wait()
2720 == 0
2721 )
2722 platform_utils.remove(bundle_dst, missing_ok=True)
2723 platform_utils.remove(bundle_tmp, missing_ok=True)
2724 return ok
2725
2726 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
2727 platform_utils.remove(dstPath, missing_ok=True)
2728
2729 cmd = ["curl", "--fail", "--output", tmpPath, "--netrc", "--location"]
2730 if quiet:
2731 cmd += ["--silent", "--show-error"]
2732 if os.path.exists(tmpPath):
2733 size = os.stat(tmpPath).st_size
2734 if size >= 1024:
2735 cmd += ["--continue-at", "%d" % (size,)]
2736 else:
2737 platform_utils.remove(tmpPath)
2738 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
2739 if cookiefile:
2740 cmd += ["--cookie", cookiefile]
2741 if proxy:
2742 cmd += ["--proxy", proxy]
2743 elif "http_proxy" in os.environ and "darwin" == sys.platform:
2744 cmd += ["--proxy", os.environ["http_proxy"]]
2745 if srcUrl.startswith("persistent-https"):
2746 srcUrl = "http" + srcUrl[len("persistent-https") :]
2747 elif srcUrl.startswith("persistent-http"):
2748 srcUrl = "http" + srcUrl[len("persistent-http") :]
2749 cmd += [srcUrl]
2750
2751 proc = None
2752 with Trace("Fetching bundle: %s", " ".join(cmd)):
2753 if verbose:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002754 print(f"{self.name}: Downloading bundle: {srcUrl}")
Gavin Makea2e3302023-03-11 06:46:20 +00002755 stdout = None if verbose else subprocess.PIPE
2756 stderr = None if verbose else subprocess.STDOUT
2757 try:
2758 proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
2759 except OSError:
2760 return False
2761
2762 (output, _) = proc.communicate()
2763 curlret = proc.returncode
2764
2765 if curlret == 22:
2766 # From curl man page:
2767 # 22: HTTP page not retrieved. The requested url was not found
2768 # or returned another error with the HTTP error code being 400
2769 # or above. This return code only appears if -f, --fail is used.
2770 if verbose:
2771 print(
2772 "%s: Unable to retrieve clone.bundle; ignoring."
2773 % self.name
2774 )
2775 if output:
2776 print("Curl output:\n%s" % output)
2777 return False
2778 elif curlret and not verbose and output:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002779 logger.error("%s", output)
Gavin Makea2e3302023-03-11 06:46:20 +00002780
2781 if os.path.exists(tmpPath):
2782 if curlret == 0 and self._IsValidBundle(tmpPath, quiet):
2783 platform_utils.rename(tmpPath, dstPath)
2784 return True
2785 else:
2786 platform_utils.remove(tmpPath)
2787 return False
2788 else:
2789 return False
2790
2791 def _IsValidBundle(self, path, quiet):
2792 try:
2793 with open(path, "rb") as f:
2794 if f.read(16) == b"# v2 git bundle\n":
2795 return True
2796 else:
2797 if not quiet:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002798 logger.error("Invalid clone.bundle file; ignoring.")
Gavin Makea2e3302023-03-11 06:46:20 +00002799 return False
2800 except OSError:
2801 return False
2802
2803 def _Checkout(self, rev, quiet=False):
2804 cmd = ["checkout"]
2805 if quiet:
2806 cmd.append("-q")
2807 cmd.append(rev)
2808 cmd.append("--")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002809 if GitCommand(self, cmd).Wait() != 0:
Gavin Makea2e3302023-03-11 06:46:20 +00002810 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002811 raise GitError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002812 f"{self.name} checkout {rev} ", project=self.name
Jason Chang32b59562023-07-14 16:45:35 -07002813 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002814
Gavin Makea2e3302023-03-11 06:46:20 +00002815 def _CherryPick(self, rev, ffonly=False, record_origin=False):
2816 cmd = ["cherry-pick"]
2817 if ffonly:
2818 cmd.append("--ff")
2819 if record_origin:
2820 cmd.append("-x")
2821 cmd.append(rev)
2822 cmd.append("--")
2823 if GitCommand(self, cmd).Wait() != 0:
2824 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002825 raise GitError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002826 f"{self.name} cherry-pick {rev} ", project=self.name
Jason Chang32b59562023-07-14 16:45:35 -07002827 )
Victor Boivie0960b5b2010-11-26 13:42:13 +01002828
Gavin Makea2e3302023-03-11 06:46:20 +00002829 def _LsRemote(self, refs):
2830 cmd = ["ls-remote", self.remote.name, refs]
2831 p = GitCommand(self, cmd, capture_stdout=True)
2832 if p.Wait() == 0:
2833 return p.stdout
2834 return None
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002835
Gavin Makea2e3302023-03-11 06:46:20 +00002836 def _Revert(self, rev):
2837 cmd = ["revert"]
2838 cmd.append("--no-edit")
2839 cmd.append(rev)
2840 cmd.append("--")
2841 if GitCommand(self, cmd).Wait() != 0:
2842 if self._allrefs:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002843 raise GitError(f"{self.name} revert {rev} ", project=self.name)
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002844
Gavin Makea2e3302023-03-11 06:46:20 +00002845 def _ResetHard(self, rev, quiet=True):
2846 cmd = ["reset", "--hard"]
2847 if quiet:
2848 cmd.append("-q")
2849 cmd.append(rev)
2850 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002851 raise GitError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002852 f"{self.name} reset --hard {rev} ", project=self.name
Jason Chang32b59562023-07-14 16:45:35 -07002853 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002854
Gavin Makea2e3302023-03-11 06:46:20 +00002855 def _SyncSubmodules(self, quiet=True):
2856 cmd = ["submodule", "update", "--init", "--recursive"]
2857 if quiet:
2858 cmd.append("-q")
2859 if GitCommand(self, cmd).Wait() != 0:
2860 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07002861 "%s submodule update --init --recursive " % self.name,
2862 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00002863 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002864
Gavin Makea2e3302023-03-11 06:46:20 +00002865 def _Rebase(self, upstream, onto=None):
2866 cmd = ["rebase"]
2867 if onto is not None:
2868 cmd.extend(["--onto", onto])
2869 cmd.append(upstream)
2870 if GitCommand(self, cmd).Wait() != 0:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002871 raise GitError(f"{self.name} rebase {upstream} ", project=self.name)
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002872
Gavin Makea2e3302023-03-11 06:46:20 +00002873 def _FastForward(self, head, ffonly=False):
2874 cmd = ["merge", "--no-stat", head]
2875 if ffonly:
2876 cmd.append("--ff-only")
2877 if GitCommand(self, cmd).Wait() != 0:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002878 raise GitError(f"{self.name} merge {head} ", project=self.name)
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002879
Gavin Makea2e3302023-03-11 06:46:20 +00002880 def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False):
2881 init_git_dir = not os.path.exists(self.gitdir)
2882 init_obj_dir = not os.path.exists(self.objdir)
2883 try:
2884 # Initialize the bare repository, which contains all of the objects.
2885 if init_obj_dir:
2886 os.makedirs(self.objdir)
2887 self.bare_objdir.init()
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002888
Gavin Makea2e3302023-03-11 06:46:20 +00002889 self._UpdateHooks(quiet=quiet)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002890
Gavin Makea2e3302023-03-11 06:46:20 +00002891 if self.use_git_worktrees:
2892 # Enable per-worktree config file support if possible. This
2893 # is more a nice-to-have feature for users rather than a
2894 # hard requirement.
2895 if git_require((2, 20, 0)):
2896 self.EnableRepositoryExtension("worktreeConfig")
Renaud Paquay788e9622017-01-27 11:41:12 -08002897
Gavin Makea2e3302023-03-11 06:46:20 +00002898 # If we have a separate directory to hold refs, initialize it as
2899 # well.
2900 if self.objdir != self.gitdir:
2901 if init_git_dir:
2902 os.makedirs(self.gitdir)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002903
Gavin Makea2e3302023-03-11 06:46:20 +00002904 if init_obj_dir or init_git_dir:
2905 self._ReferenceGitDir(
2906 self.objdir, self.gitdir, copy_all=True
2907 )
2908 try:
2909 self._CheckDirReference(self.objdir, self.gitdir)
2910 except GitError as e:
2911 if force_sync:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002912 logger.error(
2913 "Retrying clone after deleting %s", self.gitdir
Gavin Makea2e3302023-03-11 06:46:20 +00002914 )
2915 try:
2916 platform_utils.rmtree(
2917 platform_utils.realpath(self.gitdir)
2918 )
2919 if self.worktree and os.path.exists(
2920 platform_utils.realpath(self.worktree)
2921 ):
2922 platform_utils.rmtree(
2923 platform_utils.realpath(self.worktree)
2924 )
2925 return self._InitGitDir(
2926 mirror_git=mirror_git,
2927 force_sync=False,
2928 quiet=quiet,
2929 )
2930 except Exception:
2931 raise e
2932 raise e
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002933
Gavin Makea2e3302023-03-11 06:46:20 +00002934 if init_git_dir:
2935 mp = self.manifest.manifestProject
2936 ref_dir = mp.reference or ""
Julien Camperguedd654222014-01-09 16:21:37 +01002937
Gavin Makea2e3302023-03-11 06:46:20 +00002938 def _expanded_ref_dirs():
2939 """Iterate through possible git reference dir paths."""
2940 name = self.name + ".git"
2941 yield mirror_git or os.path.join(ref_dir, name)
2942 for prefix in "", self.remote.name:
2943 yield os.path.join(
2944 ref_dir, ".repo", "project-objects", prefix, name
2945 )
2946 yield os.path.join(
2947 ref_dir, ".repo", "worktrees", prefix, name
2948 )
2949
2950 if ref_dir or mirror_git:
2951 found_ref_dir = None
2952 for path in _expanded_ref_dirs():
2953 if os.path.exists(path):
2954 found_ref_dir = path
2955 break
2956 ref_dir = found_ref_dir
2957
2958 if ref_dir:
2959 if not os.path.isabs(ref_dir):
2960 # The alternate directory is relative to the object
2961 # database.
2962 ref_dir = os.path.relpath(
2963 ref_dir, os.path.join(self.objdir, "objects")
2964 )
2965 _lwrite(
2966 os.path.join(
2967 self.objdir, "objects/info/alternates"
2968 ),
2969 os.path.join(ref_dir, "objects") + "\n",
2970 )
2971
2972 m = self.manifest.manifestProject.config
2973 for key in ["user.name", "user.email"]:
2974 if m.Has(key, include_defaults=False):
2975 self.config.SetString(key, m.GetString(key))
2976 if not self.manifest.EnableGitLfs:
2977 self.config.SetString(
2978 "filter.lfs.smudge", "git-lfs smudge --skip -- %f"
2979 )
2980 self.config.SetString(
2981 "filter.lfs.process", "git-lfs filter-process --skip"
2982 )
2983 self.config.SetBoolean(
2984 "core.bare", True if self.manifest.IsMirror else None
2985 )
Josip Sokcevic9267d582023-10-19 14:46:11 -07002986
2987 if not init_obj_dir:
2988 # The project might be shared (obj_dir already initialized), but
2989 # such information is not available here. Instead of passing it,
2990 # set it as shared, and rely to be unset down the execution
2991 # path.
2992 if git_require((2, 7, 0)):
2993 self.EnableRepositoryExtension("preciousObjects")
2994 else:
2995 self.config.SetString("gc.pruneExpire", "never")
2996
Gavin Makea2e3302023-03-11 06:46:20 +00002997 except Exception:
2998 if init_obj_dir and os.path.exists(self.objdir):
2999 platform_utils.rmtree(self.objdir)
3000 if init_git_dir and os.path.exists(self.gitdir):
3001 platform_utils.rmtree(self.gitdir)
3002 raise
3003
3004 def _UpdateHooks(self, quiet=False):
3005 if os.path.exists(self.objdir):
3006 self._InitHooks(quiet=quiet)
3007
3008 def _InitHooks(self, quiet=False):
3009 hooks = platform_utils.realpath(os.path.join(self.objdir, "hooks"))
3010 if not os.path.exists(hooks):
3011 os.makedirs(hooks)
3012
3013 # Delete sample hooks. They're noise.
3014 for hook in glob.glob(os.path.join(hooks, "*.sample")):
3015 try:
3016 platform_utils.remove(hook, missing_ok=True)
3017 except PermissionError:
3018 pass
3019
3020 for stock_hook in _ProjectHooks():
3021 name = os.path.basename(stock_hook)
3022
3023 if (
3024 name in ("commit-msg",)
3025 and not self.remote.review
3026 and self is not self.manifest.manifestProject
3027 ):
3028 # Don't install a Gerrit Code Review hook if this
3029 # project does not appear to use it for reviews.
3030 #
3031 # Since the manifest project is one of those, but also
3032 # managed through gerrit, it's excluded.
3033 continue
3034
3035 dst = os.path.join(hooks, name)
3036 if platform_utils.islink(dst):
3037 continue
3038 if os.path.exists(dst):
3039 # If the files are the same, we'll leave it alone. We create
3040 # symlinks below by default but fallback to hardlinks if the OS
3041 # blocks them. So if we're here, it's probably because we made a
3042 # hardlink below.
3043 if not filecmp.cmp(stock_hook, dst, shallow=False):
3044 if not quiet:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00003045 logger.warning(
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00003046 "warn: %s: Not replacing locally modified %s hook",
Gavin Makea2e3302023-03-11 06:46:20 +00003047 self.RelPath(local=False),
3048 name,
3049 )
3050 continue
3051 try:
3052 platform_utils.symlink(
3053 os.path.relpath(stock_hook, os.path.dirname(dst)), dst
3054 )
3055 except OSError as e:
3056 if e.errno == errno.EPERM:
3057 try:
3058 os.link(stock_hook, dst)
3059 except OSError:
Jason Chang32b59562023-07-14 16:45:35 -07003060 raise GitError(
3061 self._get_symlink_error_message(), project=self.name
3062 )
Gavin Makea2e3302023-03-11 06:46:20 +00003063 else:
3064 raise
3065
3066 def _InitRemote(self):
3067 if self.remote.url:
3068 remote = self.GetRemote()
3069 remote.url = self.remote.url
3070 remote.pushUrl = self.remote.pushUrl
3071 remote.review = self.remote.review
3072 remote.projectname = self.name
3073
3074 if self.worktree:
3075 remote.ResetFetch(mirror=False)
3076 else:
3077 remote.ResetFetch(mirror=True)
3078 remote.Save()
3079
3080 def _InitMRef(self):
3081 """Initialize the pseudo m/<manifest branch> ref."""
3082 if self.manifest.branch:
3083 if self.use_git_worktrees:
3084 # Set up the m/ space to point to the worktree-specific ref
3085 # space. We'll update the worktree-specific ref space on each
3086 # checkout.
3087 ref = R_M + self.manifest.branch
3088 if not self.bare_ref.symref(ref):
3089 self.bare_git.symbolic_ref(
3090 "-m",
3091 "redirecting to worktree scope",
3092 ref,
3093 R_WORKTREE_M + self.manifest.branch,
3094 )
3095
3096 # We can't update this ref with git worktrees until it exists.
3097 # We'll wait until the initial checkout to set it.
3098 if not os.path.exists(self.worktree):
3099 return
3100
3101 base = R_WORKTREE_M
3102 active_git = self.work_git
3103
3104 self._InitAnyMRef(HEAD, self.bare_git, detach=True)
3105 else:
3106 base = R_M
3107 active_git = self.bare_git
3108
3109 self._InitAnyMRef(base + self.manifest.branch, active_git)
3110
3111 def _InitMirrorHead(self):
3112 self._InitAnyMRef(HEAD, self.bare_git)
3113
3114 def _InitAnyMRef(self, ref, active_git, detach=False):
3115 """Initialize |ref| in |active_git| to the value in the manifest.
3116
3117 This points |ref| to the <project> setting in the manifest.
3118
3119 Args:
3120 ref: The branch to update.
3121 active_git: The git repository to make updates in.
3122 detach: Whether to update target of symbolic refs, or overwrite the
3123 ref directly (and thus make it non-symbolic).
3124 """
3125 cur = self.bare_ref.symref(ref)
3126
3127 if self.revisionId:
3128 if cur != "" or self.bare_ref.get(ref) != self.revisionId:
3129 msg = "manifest set to %s" % self.revisionId
3130 dst = self.revisionId + "^0"
3131 active_git.UpdateRef(ref, dst, message=msg, detach=True)
Julien Camperguedd654222014-01-09 16:21:37 +01003132 else:
Gavin Makea2e3302023-03-11 06:46:20 +00003133 remote = self.GetRemote()
3134 dst = remote.ToLocal(self.revisionExpr)
3135 if cur != dst:
3136 msg = "manifest set to %s" % self.revisionExpr
3137 if detach:
3138 active_git.UpdateRef(ref, dst, message=msg, detach=True)
3139 else:
3140 active_git.symbolic_ref("-m", msg, ref, dst)
Julien Camperguedd654222014-01-09 16:21:37 +01003141
Gavin Makea2e3302023-03-11 06:46:20 +00003142 def _CheckDirReference(self, srcdir, destdir):
3143 # Git worktrees don't use symlinks to share at all.
3144 if self.use_git_worktrees:
3145 return
Julien Camperguedd654222014-01-09 16:21:37 +01003146
Gavin Makea2e3302023-03-11 06:46:20 +00003147 for name in self.shareable_dirs:
3148 # Try to self-heal a bit in simple cases.
3149 dst_path = os.path.join(destdir, name)
3150 src_path = os.path.join(srcdir, name)
Julien Camperguedd654222014-01-09 16:21:37 +01003151
Gavin Makea2e3302023-03-11 06:46:20 +00003152 dst = platform_utils.realpath(dst_path)
3153 if os.path.lexists(dst):
3154 src = platform_utils.realpath(src_path)
3155 # Fail if the links are pointing to the wrong place.
3156 if src != dst:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00003157 logger.error(
3158 "error: %s is different in %s vs %s",
3159 name,
3160 destdir,
3161 srcdir,
3162 )
Gavin Makea2e3302023-03-11 06:46:20 +00003163 raise GitError(
3164 "--force-sync not enabled; cannot overwrite a local "
3165 "work tree. If you're comfortable with the "
3166 "possibility of losing the work tree's git metadata,"
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003167 " use "
3168 f"`repo sync --force-sync {self.RelPath(local=False)}` "
3169 "to proceed.",
Jason Chang32b59562023-07-14 16:45:35 -07003170 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003171 )
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003172
Gavin Makea2e3302023-03-11 06:46:20 +00003173 def _ReferenceGitDir(self, gitdir, dotgit, copy_all):
3174 """Update |dotgit| to reference |gitdir|, using symlinks where possible.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003175
Gavin Makea2e3302023-03-11 06:46:20 +00003176 Args:
3177 gitdir: The bare git repository. Must already be initialized.
3178 dotgit: The repository you would like to initialize.
3179 copy_all: If true, copy all remaining files from |gitdir| ->
3180 |dotgit|. This saves you the effort of initializing |dotgit|
3181 yourself.
3182 """
3183 symlink_dirs = self.shareable_dirs[:]
3184 to_symlink = symlink_dirs
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003185
Gavin Makea2e3302023-03-11 06:46:20 +00003186 to_copy = []
3187 if copy_all:
3188 to_copy = platform_utils.listdir(gitdir)
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003189
Gavin Makea2e3302023-03-11 06:46:20 +00003190 dotgit = platform_utils.realpath(dotgit)
3191 for name in set(to_copy).union(to_symlink):
3192 try:
3193 src = platform_utils.realpath(os.path.join(gitdir, name))
3194 dst = os.path.join(dotgit, name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003195
Gavin Makea2e3302023-03-11 06:46:20 +00003196 if os.path.lexists(dst):
3197 continue
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003198
Gavin Makea2e3302023-03-11 06:46:20 +00003199 # If the source dir doesn't exist, create an empty dir.
3200 if name in symlink_dirs and not os.path.lexists(src):
3201 os.makedirs(src)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003202
Gavin Makea2e3302023-03-11 06:46:20 +00003203 if name in to_symlink:
3204 platform_utils.symlink(
3205 os.path.relpath(src, os.path.dirname(dst)), dst
3206 )
3207 elif copy_all and not platform_utils.islink(dst):
3208 if platform_utils.isdir(src):
3209 shutil.copytree(src, dst)
3210 elif os.path.isfile(src):
3211 shutil.copy(src, dst)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003212
Gavin Makea2e3302023-03-11 06:46:20 +00003213 except OSError as e:
3214 if e.errno == errno.EPERM:
3215 raise DownloadError(self._get_symlink_error_message())
3216 else:
3217 raise
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003218
Gavin Makea2e3302023-03-11 06:46:20 +00003219 def _InitGitWorktree(self):
3220 """Init the project using git worktrees."""
3221 self.bare_git.worktree("prune")
3222 self.bare_git.worktree(
3223 "add",
3224 "-ff",
3225 "--checkout",
3226 "--detach",
3227 "--lock",
3228 self.worktree,
3229 self.GetRevisionId(),
3230 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003231
Gavin Makea2e3302023-03-11 06:46:20 +00003232 # Rewrite the internal state files to use relative paths between the
3233 # checkouts & worktrees.
3234 dotgit = os.path.join(self.worktree, ".git")
Jason R. Coombs034950b2023-10-20 23:32:02 +05453235 with open(dotgit) as fp:
Gavin Makea2e3302023-03-11 06:46:20 +00003236 # Figure out the checkout->worktree path.
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003237 setting = fp.read()
Gavin Makea2e3302023-03-11 06:46:20 +00003238 assert setting.startswith("gitdir:")
3239 git_worktree_path = setting.split(":", 1)[1].strip()
3240 # Some platforms (e.g. Windows) won't let us update dotgit in situ
3241 # because of file permissions. Delete it and recreate it from scratch
3242 # to avoid.
3243 platform_utils.remove(dotgit)
3244 # Use relative path from checkout->worktree & maintain Unix line endings
3245 # on all OS's to match git behavior.
3246 with open(dotgit, "w", newline="\n") as fp:
3247 print(
3248 "gitdir:",
3249 os.path.relpath(git_worktree_path, self.worktree),
3250 file=fp,
3251 )
3252 # Use relative path from worktree->checkout & maintain Unix line endings
3253 # on all OS's to match git behavior.
3254 with open(
3255 os.path.join(git_worktree_path, "gitdir"), "w", newline="\n"
3256 ) as fp:
3257 print(os.path.relpath(dotgit, git_worktree_path), file=fp)
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003258
Gavin Makea2e3302023-03-11 06:46:20 +00003259 self._InitMRef()
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003260
Gavin Makea2e3302023-03-11 06:46:20 +00003261 def _InitWorkTree(self, force_sync=False, submodules=False):
3262 """Setup the worktree .git path.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003263
Gavin Makea2e3302023-03-11 06:46:20 +00003264 This is the user-visible path like src/foo/.git/.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003265
Gavin Makea2e3302023-03-11 06:46:20 +00003266 With non-git-worktrees, this will be a symlink to the .repo/projects/
3267 path. With git-worktrees, this will be a .git file using "gitdir: ..."
3268 syntax.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003269
Gavin Makea2e3302023-03-11 06:46:20 +00003270 Older checkouts had .git/ directories. If we see that, migrate it.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003271
Gavin Makea2e3302023-03-11 06:46:20 +00003272 This also handles changes in the manifest. Maybe this project was
3273 backed by "foo/bar" on the server, but now it's "new/foo/bar". We have
3274 to update the path we point to under .repo/projects/ to match.
3275 """
3276 dotgit = os.path.join(self.worktree, ".git")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003277
Gavin Makea2e3302023-03-11 06:46:20 +00003278 # If using an old layout style (a directory), migrate it.
3279 if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
Jason Chang32b59562023-07-14 16:45:35 -07003280 self._MigrateOldWorkTreeGitDir(dotgit, project=self.name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003281
Gavin Makea2e3302023-03-11 06:46:20 +00003282 init_dotgit = not os.path.exists(dotgit)
3283 if self.use_git_worktrees:
3284 if init_dotgit:
3285 self._InitGitWorktree()
3286 self._CopyAndLinkFiles()
3287 else:
3288 if not init_dotgit:
3289 # See if the project has changed.
3290 if platform_utils.realpath(
3291 self.gitdir
3292 ) != platform_utils.realpath(dotgit):
3293 platform_utils.remove(dotgit)
Doug Anderson37282b42011-03-04 11:54:18 -08003294
Gavin Makea2e3302023-03-11 06:46:20 +00003295 if init_dotgit or not os.path.exists(dotgit):
3296 os.makedirs(self.worktree, exist_ok=True)
3297 platform_utils.symlink(
3298 os.path.relpath(self.gitdir, self.worktree), dotgit
3299 )
Doug Anderson37282b42011-03-04 11:54:18 -08003300
Gavin Makea2e3302023-03-11 06:46:20 +00003301 if init_dotgit:
3302 _lwrite(
3303 os.path.join(dotgit, HEAD), "%s\n" % self.GetRevisionId()
3304 )
Doug Anderson37282b42011-03-04 11:54:18 -08003305
Gavin Makea2e3302023-03-11 06:46:20 +00003306 # Finish checking out the worktree.
3307 cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
3308 if GitCommand(self, cmd).Wait() != 0:
3309 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07003310 "Cannot initialize work tree for " + self.name,
3311 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003312 )
Doug Anderson37282b42011-03-04 11:54:18 -08003313
Gavin Makea2e3302023-03-11 06:46:20 +00003314 if submodules:
3315 self._SyncSubmodules(quiet=True)
3316 self._CopyAndLinkFiles()
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003317
Gavin Makea2e3302023-03-11 06:46:20 +00003318 @classmethod
Jason Chang32b59562023-07-14 16:45:35 -07003319 def _MigrateOldWorkTreeGitDir(cls, dotgit, project=None):
Gavin Makea2e3302023-03-11 06:46:20 +00003320 """Migrate the old worktree .git/ dir style to a symlink.
3321
3322 This logic specifically only uses state from |dotgit| to figure out
3323 where to move content and not |self|. This way if the backing project
3324 also changed places, we only do the .git/ dir to .git symlink migration
3325 here. The path updates will happen independently.
3326 """
3327 # Figure out where in .repo/projects/ it's pointing to.
3328 if not os.path.islink(os.path.join(dotgit, "refs")):
Jason Chang32b59562023-07-14 16:45:35 -07003329 raise GitError(
3330 f"{dotgit}: unsupported checkout state", project=project
3331 )
Gavin Makea2e3302023-03-11 06:46:20 +00003332 gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, "refs")))
3333
3334 # Remove known symlink paths that exist in .repo/projects/.
3335 KNOWN_LINKS = {
3336 "config",
3337 "description",
3338 "hooks",
3339 "info",
3340 "logs",
3341 "objects",
3342 "packed-refs",
3343 "refs",
3344 "rr-cache",
3345 "shallow",
3346 "svn",
3347 }
3348 # Paths that we know will be in both, but are safe to clobber in
3349 # .repo/projects/.
3350 SAFE_TO_CLOBBER = {
3351 "COMMIT_EDITMSG",
3352 "FETCH_HEAD",
3353 "HEAD",
3354 "gc.log",
3355 "gitk.cache",
3356 "index",
3357 "ORIG_HEAD",
3358 }
3359
3360 # First see if we'd succeed before starting the migration.
3361 unknown_paths = []
3362 for name in platform_utils.listdir(dotgit):
3363 # Ignore all temporary/backup names. These are common with vim &
3364 # emacs.
3365 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3366 continue
3367
3368 dotgit_path = os.path.join(dotgit, name)
3369 if name in KNOWN_LINKS:
3370 if not platform_utils.islink(dotgit_path):
3371 unknown_paths.append(f"{dotgit_path}: should be a symlink")
3372 else:
3373 gitdir_path = os.path.join(gitdir, name)
3374 if name not in SAFE_TO_CLOBBER and os.path.exists(gitdir_path):
3375 unknown_paths.append(
3376 f"{dotgit_path}: unknown file; please file a bug"
3377 )
3378 if unknown_paths:
Jason Chang32b59562023-07-14 16:45:35 -07003379 raise GitError(
3380 "Aborting migration: " + "\n".join(unknown_paths),
3381 project=project,
3382 )
Gavin Makea2e3302023-03-11 06:46:20 +00003383
3384 # Now walk the paths and sync the .git/ to .repo/projects/.
3385 for name in platform_utils.listdir(dotgit):
3386 dotgit_path = os.path.join(dotgit, name)
3387
3388 # Ignore all temporary/backup names. These are common with vim &
3389 # emacs.
3390 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3391 platform_utils.remove(dotgit_path)
3392 elif name in KNOWN_LINKS:
3393 platform_utils.remove(dotgit_path)
3394 else:
3395 gitdir_path = os.path.join(gitdir, name)
3396 platform_utils.remove(gitdir_path, missing_ok=True)
3397 platform_utils.rename(dotgit_path, gitdir_path)
3398
3399 # Now that the dir should be empty, clear it out, and symlink it over.
3400 platform_utils.rmdir(dotgit)
3401 platform_utils.symlink(
Jason R. Coombs47944bb2023-09-29 12:42:22 -04003402 os.path.relpath(gitdir, os.path.dirname(os.path.realpath(dotgit))),
3403 dotgit,
Gavin Makea2e3302023-03-11 06:46:20 +00003404 )
3405
3406 def _get_symlink_error_message(self):
3407 if platform_utils.isWindows():
3408 return (
3409 "Unable to create symbolic link. Please re-run the command as "
3410 "Administrator, or see "
3411 "https://github.com/git-for-windows/git/wiki/Symbolic-Links "
3412 "for other options."
3413 )
3414 return "filesystem must support symlinks"
3415
3416 def _revlist(self, *args, **kw):
3417 a = []
3418 a.extend(args)
3419 a.append("--")
3420 return self.work_git.rev_list(*a, **kw)
3421
3422 @property
3423 def _allrefs(self):
3424 return self.bare_ref.all
3425
3426 def _getLogs(
3427 self, rev1, rev2, oneline=False, color=True, pretty_format=None
3428 ):
3429 """Get logs between two revisions of this project."""
3430 comp = ".."
3431 if rev1:
3432 revs = [rev1]
3433 if rev2:
3434 revs.extend([comp, rev2])
3435 cmd = ["log", "".join(revs)]
3436 out = DiffColoring(self.config)
3437 if out.is_on and color:
3438 cmd.append("--color")
3439 if pretty_format is not None:
3440 cmd.append("--pretty=format:%s" % pretty_format)
3441 if oneline:
3442 cmd.append("--oneline")
3443
3444 try:
3445 log = GitCommand(
3446 self, cmd, capture_stdout=True, capture_stderr=True
3447 )
3448 if log.Wait() == 0:
3449 return log.stdout
3450 except GitError:
3451 # worktree may not exist if groups changed for example. In that
3452 # case, try in gitdir instead.
3453 if not os.path.exists(self.worktree):
3454 return self.bare_git.log(*cmd[1:])
3455 else:
3456 raise
3457 return None
3458
3459 def getAddedAndRemovedLogs(
3460 self, toProject, oneline=False, color=True, pretty_format=None
3461 ):
3462 """Get the list of logs from this revision to given revisionId"""
3463 logs = {}
3464 selfId = self.GetRevisionId(self._allrefs)
3465 toId = toProject.GetRevisionId(toProject._allrefs)
3466
3467 logs["added"] = self._getLogs(
3468 selfId,
3469 toId,
3470 oneline=oneline,
3471 color=color,
3472 pretty_format=pretty_format,
3473 )
3474 logs["removed"] = self._getLogs(
3475 toId,
3476 selfId,
3477 oneline=oneline,
3478 color=color,
3479 pretty_format=pretty_format,
3480 )
3481 return logs
3482
Mike Frysingerd4aee652023-10-19 05:13:32 -04003483 class _GitGetByExec:
Gavin Makea2e3302023-03-11 06:46:20 +00003484 def __init__(self, project, bare, gitdir):
3485 self._project = project
3486 self._bare = bare
3487 self._gitdir = gitdir
3488
3489 # __getstate__ and __setstate__ are required for pickling because
3490 # __getattr__ exists.
3491 def __getstate__(self):
3492 return (self._project, self._bare, self._gitdir)
3493
3494 def __setstate__(self, state):
3495 self._project, self._bare, self._gitdir = state
3496
3497 def LsOthers(self):
3498 p = GitCommand(
3499 self._project,
3500 ["ls-files", "-z", "--others", "--exclude-standard"],
3501 bare=False,
3502 gitdir=self._gitdir,
3503 capture_stdout=True,
3504 capture_stderr=True,
3505 )
3506 if p.Wait() == 0:
3507 out = p.stdout
3508 if out:
3509 # Backslash is not anomalous.
3510 return out[:-1].split("\0")
3511 return []
3512
3513 def DiffZ(self, name, *args):
3514 cmd = [name]
3515 cmd.append("-z")
3516 cmd.append("--ignore-submodules")
3517 cmd.extend(args)
3518 p = GitCommand(
3519 self._project,
3520 cmd,
3521 gitdir=self._gitdir,
3522 bare=False,
3523 capture_stdout=True,
3524 capture_stderr=True,
3525 )
3526 p.Wait()
3527 r = {}
3528 out = p.stdout
3529 if out:
3530 out = iter(out[:-1].split("\0"))
3531 while out:
3532 try:
3533 info = next(out)
3534 path = next(out)
3535 except StopIteration:
3536 break
3537
Mike Frysingerd4aee652023-10-19 05:13:32 -04003538 class _Info:
Gavin Makea2e3302023-03-11 06:46:20 +00003539 def __init__(self, path, omode, nmode, oid, nid, state):
3540 self.path = path
3541 self.src_path = None
3542 self.old_mode = omode
3543 self.new_mode = nmode
3544 self.old_id = oid
3545 self.new_id = nid
3546
3547 if len(state) == 1:
3548 self.status = state
3549 self.level = None
3550 else:
3551 self.status = state[:1]
3552 self.level = state[1:]
3553 while self.level.startswith("0"):
3554 self.level = self.level[1:]
3555
3556 info = info[1:].split(" ")
3557 info = _Info(path, *info)
3558 if info.status in ("R", "C"):
3559 info.src_path = info.path
3560 info.path = next(out)
3561 r[info.path] = info
3562 return r
3563
3564 def GetDotgitPath(self, subpath=None):
3565 """Return the full path to the .git dir.
3566
3567 As a convenience, append |subpath| if provided.
3568 """
3569 if self._bare:
3570 dotgit = self._gitdir
3571 else:
3572 dotgit = os.path.join(self._project.worktree, ".git")
3573 if os.path.isfile(dotgit):
3574 # Git worktrees use a "gitdir:" syntax to point to the
3575 # scratch space.
3576 with open(dotgit) as fp:
3577 setting = fp.read()
3578 assert setting.startswith("gitdir:")
3579 gitdir = setting.split(":", 1)[1].strip()
3580 dotgit = os.path.normpath(
3581 os.path.join(self._project.worktree, gitdir)
3582 )
3583
3584 return dotgit if subpath is None else os.path.join(dotgit, subpath)
3585
3586 def GetHead(self):
3587 """Return the ref that HEAD points to."""
3588 path = self.GetDotgitPath(subpath=HEAD)
3589 try:
3590 with open(path) as fd:
3591 line = fd.readline()
Jason R. Coombsae824fb2023-10-20 23:32:40 +05453592 except OSError as e:
Gavin Makea2e3302023-03-11 06:46:20 +00003593 raise NoManifestException(path, str(e))
3594 try:
3595 line = line.decode()
3596 except AttributeError:
3597 pass
3598 if line.startswith("ref: "):
3599 return line[5:-1]
3600 return line[:-1]
3601
3602 def SetHead(self, ref, message=None):
3603 cmdv = []
3604 if message is not None:
3605 cmdv.extend(["-m", message])
3606 cmdv.append(HEAD)
3607 cmdv.append(ref)
3608 self.symbolic_ref(*cmdv)
3609
3610 def DetachHead(self, new, message=None):
3611 cmdv = ["--no-deref"]
3612 if message is not None:
3613 cmdv.extend(["-m", message])
3614 cmdv.append(HEAD)
3615 cmdv.append(new)
3616 self.update_ref(*cmdv)
3617
3618 def UpdateRef(self, name, new, old=None, message=None, detach=False):
3619 cmdv = []
3620 if message is not None:
3621 cmdv.extend(["-m", message])
3622 if detach:
3623 cmdv.append("--no-deref")
3624 cmdv.append(name)
3625 cmdv.append(new)
3626 if old is not None:
3627 cmdv.append(old)
3628 self.update_ref(*cmdv)
3629
3630 def DeleteRef(self, name, old=None):
3631 if not old:
3632 old = self.rev_parse(name)
3633 self.update_ref("-d", name, old)
3634 self._project.bare_ref.deleted(name)
3635
Jason Chang87058c62023-09-27 11:34:43 -07003636 def rev_list(self, *args, log_as_error=True, **kw):
Gavin Makea2e3302023-03-11 06:46:20 +00003637 if "format" in kw:
3638 cmdv = ["log", "--pretty=format:%s" % kw["format"]]
3639 else:
3640 cmdv = ["rev-list"]
3641 cmdv.extend(args)
3642 p = GitCommand(
3643 self._project,
3644 cmdv,
3645 bare=self._bare,
3646 gitdir=self._gitdir,
3647 capture_stdout=True,
3648 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003649 verify_command=True,
Jason Chang87058c62023-09-27 11:34:43 -07003650 log_as_error=log_as_error,
Gavin Makea2e3302023-03-11 06:46:20 +00003651 )
Jason Chang32b59562023-07-14 16:45:35 -07003652 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003653 return p.stdout.splitlines()
3654
3655 def __getattr__(self, name):
3656 """Allow arbitrary git commands using pythonic syntax.
3657
3658 This allows you to do things like:
3659 git_obj.rev_parse('HEAD')
3660
3661 Since we don't have a 'rev_parse' method defined, the __getattr__
3662 will run. We'll replace the '_' with a '-' and try to run a git
3663 command. Any other positional arguments will be passed to the git
3664 command, and the following keyword arguments are supported:
3665 config: An optional dict of git config options to be passed with
3666 '-c'.
3667
3668 Args:
3669 name: The name of the git command to call. Any '_' characters
3670 will be replaced with '-'.
3671
3672 Returns:
3673 A callable object that will try to call git with the named
3674 command.
3675 """
3676 name = name.replace("_", "-")
3677
Jason Chang87058c62023-09-27 11:34:43 -07003678 def runner(*args, log_as_error=True, **kwargs):
Gavin Makea2e3302023-03-11 06:46:20 +00003679 cmdv = []
3680 config = kwargs.pop("config", None)
3681 for k in kwargs:
3682 raise TypeError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003683 f"{name}() got an unexpected keyword argument {k!r}"
Gavin Makea2e3302023-03-11 06:46:20 +00003684 )
3685 if config is not None:
3686 for k, v in config.items():
3687 cmdv.append("-c")
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003688 cmdv.append(f"{k}={v}")
Gavin Makea2e3302023-03-11 06:46:20 +00003689 cmdv.append(name)
3690 cmdv.extend(args)
3691 p = GitCommand(
3692 self._project,
3693 cmdv,
3694 bare=self._bare,
3695 gitdir=self._gitdir,
3696 capture_stdout=True,
3697 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003698 verify_command=True,
Jason Chang87058c62023-09-27 11:34:43 -07003699 log_as_error=log_as_error,
Gavin Makea2e3302023-03-11 06:46:20 +00003700 )
Jason Chang32b59562023-07-14 16:45:35 -07003701 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003702 r = p.stdout
3703 if r.endswith("\n") and r.index("\n") == len(r) - 1:
3704 return r[:-1]
3705 return r
3706
3707 return runner
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003708
3709
Jason Chang32b59562023-07-14 16:45:35 -07003710class LocalSyncFail(RepoError):
3711 """Default error when there is an Sync_LocalHalf error."""
3712
3713
3714class _PriorSyncFailedError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003715 def __str__(self):
3716 return "prior sync failed; rebase still in progress"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003717
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003718
Jason Chang32b59562023-07-14 16:45:35 -07003719class _DirtyError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003720 def __str__(self):
3721 return "contains uncommitted changes"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003722
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003723
Mike Frysingerd4aee652023-10-19 05:13:32 -04003724class _InfoMessage:
Gavin Makea2e3302023-03-11 06:46:20 +00003725 def __init__(self, project, text):
3726 self.project = project
3727 self.text = text
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003728
Gavin Makea2e3302023-03-11 06:46:20 +00003729 def Print(self, syncbuf):
3730 syncbuf.out.info(
3731 "%s/: %s", self.project.RelPath(local=False), self.text
3732 )
3733 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003734
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003735
Mike Frysingerd4aee652023-10-19 05:13:32 -04003736class _Failure:
Gavin Makea2e3302023-03-11 06:46:20 +00003737 def __init__(self, project, why):
3738 self.project = project
3739 self.why = why
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003740
Gavin Makea2e3302023-03-11 06:46:20 +00003741 def Print(self, syncbuf):
3742 syncbuf.out.fail(
3743 "error: %s/: %s", self.project.RelPath(local=False), str(self.why)
3744 )
3745 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003746
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003747
Mike Frysingerd4aee652023-10-19 05:13:32 -04003748class _Later:
Gavin Makea2e3302023-03-11 06:46:20 +00003749 def __init__(self, project, action):
3750 self.project = project
3751 self.action = action
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003752
Gavin Makea2e3302023-03-11 06:46:20 +00003753 def Run(self, syncbuf):
3754 out = syncbuf.out
3755 out.project("project %s/", self.project.RelPath(local=False))
3756 out.nl()
3757 try:
3758 self.action()
3759 out.nl()
3760 return True
3761 except GitError:
3762 out.nl()
3763 return False
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003764
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003765
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003766class _SyncColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +00003767 def __init__(self, config):
3768 super().__init__(config, "reposync")
3769 self.project = self.printer("header", attr="bold")
3770 self.info = self.printer("info")
3771 self.fail = self.printer("fail", fg="red")
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003772
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003773
Mike Frysingerd4aee652023-10-19 05:13:32 -04003774class SyncBuffer:
Gavin Makea2e3302023-03-11 06:46:20 +00003775 def __init__(self, config, detach_head=False):
3776 self._messages = []
3777 self._failures = []
3778 self._later_queue1 = []
3779 self._later_queue2 = []
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003780
Gavin Makea2e3302023-03-11 06:46:20 +00003781 self.out = _SyncColoring(config)
3782 self.out.redirect(sys.stderr)
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003783
Gavin Makea2e3302023-03-11 06:46:20 +00003784 self.detach_head = detach_head
3785 self.clean = True
3786 self.recent_clean = True
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003787
Gavin Makea2e3302023-03-11 06:46:20 +00003788 def info(self, project, fmt, *args):
3789 self._messages.append(_InfoMessage(project, fmt % args))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003790
Gavin Makea2e3302023-03-11 06:46:20 +00003791 def fail(self, project, err=None):
3792 self._failures.append(_Failure(project, err))
David Rileye0684ad2017-04-05 00:02:59 -07003793 self._MarkUnclean()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003794
Gavin Makea2e3302023-03-11 06:46:20 +00003795 def later1(self, project, what):
3796 self._later_queue1.append(_Later(project, what))
Mike Frysinger70d861f2019-08-26 15:22:36 -04003797
Gavin Makea2e3302023-03-11 06:46:20 +00003798 def later2(self, project, what):
3799 self._later_queue2.append(_Later(project, what))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003800
Gavin Makea2e3302023-03-11 06:46:20 +00003801 def Finish(self):
3802 self._PrintMessages()
3803 self._RunLater()
3804 self._PrintMessages()
3805 return self.clean
3806
3807 def Recently(self):
3808 recent_clean = self.recent_clean
3809 self.recent_clean = True
3810 return recent_clean
3811
3812 def _MarkUnclean(self):
3813 self.clean = False
3814 self.recent_clean = False
3815
3816 def _RunLater(self):
3817 for q in ["_later_queue1", "_later_queue2"]:
3818 if not self._RunQueue(q):
3819 return
3820
3821 def _RunQueue(self, queue):
3822 for m in getattr(self, queue):
3823 if not m.Run(self):
3824 self._MarkUnclean()
3825 return False
3826 setattr(self, queue, [])
3827 return True
3828
3829 def _PrintMessages(self):
3830 if self._messages or self._failures:
3831 if os.isatty(2):
3832 self.out.write(progress.CSI_ERASE_LINE)
3833 self.out.write("\r")
3834
3835 for m in self._messages:
3836 m.Print(self)
3837 for m in self._failures:
3838 m.Print(self)
3839
3840 self._messages = []
3841 self._failures = []
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003842
3843
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003844class MetaProject(Project):
Gavin Makea2e3302023-03-11 06:46:20 +00003845 """A special project housed under .repo."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003846
Gavin Makea2e3302023-03-11 06:46:20 +00003847 def __init__(self, manifest, name, gitdir, worktree):
3848 Project.__init__(
3849 self,
3850 manifest=manifest,
3851 name=name,
3852 gitdir=gitdir,
3853 objdir=gitdir,
3854 worktree=worktree,
3855 remote=RemoteSpec("origin"),
3856 relpath=".repo/%s" % name,
3857 revisionExpr="refs/heads/master",
3858 revisionId=None,
3859 groups=None,
3860 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003861
Gavin Makea2e3302023-03-11 06:46:20 +00003862 def PreSync(self):
3863 if self.Exists:
3864 cb = self.CurrentBranch
3865 if cb:
3866 base = self.GetBranch(cb).merge
3867 if base:
3868 self.revisionExpr = base
3869 self.revisionId = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003870
Gavin Makea2e3302023-03-11 06:46:20 +00003871 @property
3872 def HasChanges(self):
3873 """Has the remote received new commits not yet checked out?"""
3874 if not self.remote or not self.revisionExpr:
3875 return False
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003876
Gavin Makea2e3302023-03-11 06:46:20 +00003877 all_refs = self.bare_ref.all
3878 revid = self.GetRevisionId(all_refs)
3879 head = self.work_git.GetHead()
3880 if head.startswith(R_HEADS):
3881 try:
3882 head = all_refs[head]
3883 except KeyError:
3884 head = None
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003885
Gavin Makea2e3302023-03-11 06:46:20 +00003886 if revid == head:
3887 return False
3888 elif self._revlist(not_rev(HEAD), revid):
3889 return True
3890 return False
LaMont Jones9b72cf22022-03-29 21:54:22 +00003891
3892
3893class RepoProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003894 """The MetaProject for repo itself."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003895
Gavin Makea2e3302023-03-11 06:46:20 +00003896 @property
3897 def LastFetch(self):
3898 try:
3899 fh = os.path.join(self.gitdir, "FETCH_HEAD")
3900 return os.path.getmtime(fh)
3901 except OSError:
3902 return 0
LaMont Jones9b72cf22022-03-29 21:54:22 +00003903
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +01003904
LaMont Jones9b72cf22022-03-29 21:54:22 +00003905class ManifestProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003906 """The MetaProject for manifests."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003907
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08003908 def MetaBranchSwitch(self, submodules=False, verbose=False):
Gavin Makea2e3302023-03-11 06:46:20 +00003909 """Prepare for manifest branch switch."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003910
Gavin Makea2e3302023-03-11 06:46:20 +00003911 # detach and delete manifest branch, allowing a new
3912 # branch to take over
3913 syncbuf = SyncBuffer(self.config, detach_head=True)
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08003914 self.Sync_LocalHalf(syncbuf, submodules=submodules, verbose=verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00003915 syncbuf.Finish()
LaMont Jones9b72cf22022-03-29 21:54:22 +00003916
Gavin Makea2e3302023-03-11 06:46:20 +00003917 return (
3918 GitCommand(
3919 self,
3920 ["update-ref", "-d", "refs/heads/default"],
3921 capture_stdout=True,
3922 capture_stderr=True,
3923 ).Wait()
3924 == 0
3925 )
LaMont Jones9b03f152022-03-29 23:01:18 +00003926
Gavin Makea2e3302023-03-11 06:46:20 +00003927 @property
3928 def standalone_manifest_url(self):
3929 """The URL of the standalone manifest, or None."""
3930 return self.config.GetString("manifest.standalone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003931
Gavin Makea2e3302023-03-11 06:46:20 +00003932 @property
3933 def manifest_groups(self):
3934 """The manifest groups string."""
3935 return self.config.GetString("manifest.groups")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003936
Gavin Makea2e3302023-03-11 06:46:20 +00003937 @property
3938 def reference(self):
3939 """The --reference for this manifest."""
3940 return self.config.GetString("repo.reference")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003941
Gavin Makea2e3302023-03-11 06:46:20 +00003942 @property
3943 def dissociate(self):
3944 """Whether to dissociate."""
3945 return self.config.GetBoolean("repo.dissociate")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003946
Gavin Makea2e3302023-03-11 06:46:20 +00003947 @property
3948 def archive(self):
3949 """Whether we use archive."""
3950 return self.config.GetBoolean("repo.archive")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003951
Gavin Makea2e3302023-03-11 06:46:20 +00003952 @property
3953 def mirror(self):
3954 """Whether we use mirror."""
3955 return self.config.GetBoolean("repo.mirror")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003956
Gavin Makea2e3302023-03-11 06:46:20 +00003957 @property
3958 def use_worktree(self):
3959 """Whether we use worktree."""
3960 return self.config.GetBoolean("repo.worktree")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003961
Gavin Makea2e3302023-03-11 06:46:20 +00003962 @property
3963 def clone_bundle(self):
3964 """Whether we use clone_bundle."""
3965 return self.config.GetBoolean("repo.clonebundle")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003966
Gavin Makea2e3302023-03-11 06:46:20 +00003967 @property
3968 def submodules(self):
3969 """Whether we use submodules."""
3970 return self.config.GetBoolean("repo.submodules")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003971
Gavin Makea2e3302023-03-11 06:46:20 +00003972 @property
3973 def git_lfs(self):
3974 """Whether we use git_lfs."""
3975 return self.config.GetBoolean("repo.git-lfs")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003976
Gavin Makea2e3302023-03-11 06:46:20 +00003977 @property
3978 def use_superproject(self):
3979 """Whether we use superproject."""
3980 return self.config.GetBoolean("repo.superproject")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003981
Gavin Makea2e3302023-03-11 06:46:20 +00003982 @property
3983 def partial_clone(self):
3984 """Whether this is a partial clone."""
3985 return self.config.GetBoolean("repo.partialclone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003986
Gavin Makea2e3302023-03-11 06:46:20 +00003987 @property
3988 def depth(self):
3989 """Partial clone depth."""
Roberto Vladimir Prado Carranza3d58d212023-09-13 10:27:26 +02003990 return self.config.GetInt("repo.depth")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003991
Gavin Makea2e3302023-03-11 06:46:20 +00003992 @property
3993 def clone_filter(self):
3994 """The clone filter."""
3995 return self.config.GetString("repo.clonefilter")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003996
Gavin Makea2e3302023-03-11 06:46:20 +00003997 @property
3998 def partial_clone_exclude(self):
3999 """Partial clone exclude string"""
4000 return self.config.GetString("repo.partialcloneexclude")
LaMont Jones4ada0432022-04-14 15:10:43 +00004001
Gavin Makea2e3302023-03-11 06:46:20 +00004002 @property
Jason Chang17833322023-05-23 13:06:55 -07004003 def clone_filter_for_depth(self):
4004 """Replace shallow clone with partial clone."""
4005 return self.config.GetString("repo.clonefilterfordepth")
4006
4007 @property
Gavin Makea2e3302023-03-11 06:46:20 +00004008 def manifest_platform(self):
4009 """The --platform argument from `repo init`."""
4010 return self.config.GetString("manifest.platform")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004011
Gavin Makea2e3302023-03-11 06:46:20 +00004012 @property
4013 def _platform_name(self):
4014 """Return the name of the platform."""
4015 return platform.system().lower()
LaMont Jones9b03f152022-03-29 23:01:18 +00004016
Gavin Makea2e3302023-03-11 06:46:20 +00004017 def SyncWithPossibleInit(
4018 self,
4019 submanifest,
4020 verbose=False,
4021 current_branch_only=False,
4022 tags="",
4023 git_event_log=None,
4024 ):
4025 """Sync a manifestProject, possibly for the first time.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004026
Gavin Makea2e3302023-03-11 06:46:20 +00004027 Call Sync() with arguments from the most recent `repo init`. If this is
4028 a new sub manifest, then inherit options from the parent's
4029 manifestProject.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004030
Gavin Makea2e3302023-03-11 06:46:20 +00004031 This is used by subcmds.Sync() to do an initial download of new sub
4032 manifests.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004033
Gavin Makea2e3302023-03-11 06:46:20 +00004034 Args:
4035 submanifest: an XmlSubmanifest, the submanifest to re-sync.
4036 verbose: a boolean, whether to show all output, rather than only
4037 errors.
4038 current_branch_only: a boolean, whether to only fetch the current
4039 manifest branch from the server.
4040 tags: a boolean, whether to fetch tags.
4041 git_event_log: an EventLog, for git tracing.
4042 """
4043 # TODO(lamontjones): when refactoring sync (and init?) consider how to
4044 # better get the init options that we should use for new submanifests
4045 # that are added when syncing an existing workspace.
4046 git_event_log = git_event_log or EventLog()
LaMont Jonesb90a4222022-04-14 15:00:09 +00004047 spec = submanifest.ToSubmanifestSpec()
Gavin Makea2e3302023-03-11 06:46:20 +00004048 # Use the init options from the existing manifestProject, or the parent
4049 # if it doesn't exist.
4050 #
4051 # Today, we only support changing manifest_groups on the sub-manifest,
4052 # with no supported-for-the-user way to change the other arguments from
4053 # those specified by the outermost manifest.
4054 #
4055 # TODO(lamontjones): determine which of these should come from the
4056 # outermost manifest and which should come from the parent manifest.
4057 mp = self if self.Exists else submanifest.parent.manifestProject
4058 return self.Sync(
LaMont Jones55ee3042022-04-06 17:10:21 +00004059 manifest_url=spec.manifestUrl,
4060 manifest_branch=spec.revision,
Gavin Makea2e3302023-03-11 06:46:20 +00004061 standalone_manifest=mp.standalone_manifest_url,
4062 groups=mp.manifest_groups,
4063 platform=mp.manifest_platform,
4064 mirror=mp.mirror,
4065 dissociate=mp.dissociate,
4066 reference=mp.reference,
4067 worktree=mp.use_worktree,
4068 submodules=mp.submodules,
4069 archive=mp.archive,
4070 partial_clone=mp.partial_clone,
4071 clone_filter=mp.clone_filter,
4072 partial_clone_exclude=mp.partial_clone_exclude,
4073 clone_bundle=mp.clone_bundle,
4074 git_lfs=mp.git_lfs,
4075 use_superproject=mp.use_superproject,
LaMont Jones55ee3042022-04-06 17:10:21 +00004076 verbose=verbose,
4077 current_branch_only=current_branch_only,
4078 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00004079 depth=mp.depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004080 git_event_log=git_event_log,
4081 manifest_name=spec.manifestName,
Gavin Makea2e3302023-03-11 06:46:20 +00004082 this_manifest_only=True,
LaMont Jones55ee3042022-04-06 17:10:21 +00004083 outer_manifest=False,
Jason Chang17833322023-05-23 13:06:55 -07004084 clone_filter_for_depth=mp.clone_filter_for_depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004085 )
LaMont Jones409407a2022-04-05 21:21:56 +00004086
Gavin Makea2e3302023-03-11 06:46:20 +00004087 def Sync(
4088 self,
4089 _kwargs_only=(),
4090 manifest_url="",
4091 manifest_branch=None,
4092 standalone_manifest=False,
4093 groups="",
4094 mirror=False,
4095 reference="",
4096 dissociate=False,
4097 worktree=False,
4098 submodules=False,
4099 archive=False,
4100 partial_clone=None,
4101 depth=None,
4102 clone_filter="blob:none",
4103 partial_clone_exclude=None,
4104 clone_bundle=None,
4105 git_lfs=None,
4106 use_superproject=None,
4107 verbose=False,
4108 current_branch_only=False,
4109 git_event_log=None,
4110 platform="",
4111 manifest_name="default.xml",
4112 tags="",
4113 this_manifest_only=False,
4114 outer_manifest=True,
Jason Chang17833322023-05-23 13:06:55 -07004115 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00004116 ):
4117 """Sync the manifest and all submanifests.
LaMont Jones409407a2022-04-05 21:21:56 +00004118
Gavin Makea2e3302023-03-11 06:46:20 +00004119 Args:
4120 manifest_url: a string, the URL of the manifest project.
4121 manifest_branch: a string, the manifest branch to use.
4122 standalone_manifest: a boolean, whether to store the manifest as a
4123 static file.
4124 groups: a string, restricts the checkout to projects with the
4125 specified groups.
4126 mirror: a boolean, whether to create a mirror of the remote
4127 repository.
4128 reference: a string, location of a repo instance to use as a
4129 reference.
4130 dissociate: a boolean, whether to dissociate from reference mirrors
4131 after clone.
4132 worktree: a boolean, whether to use git-worktree to manage projects.
4133 submodules: a boolean, whether sync submodules associated with the
4134 manifest project.
4135 archive: a boolean, whether to checkout each project as an archive.
4136 See git-archive.
4137 partial_clone: a boolean, whether to perform a partial clone.
4138 depth: an int, how deep of a shallow clone to create.
4139 clone_filter: a string, filter to use with partial_clone.
4140 partial_clone_exclude : a string, comma-delimeted list of project
4141 names to exclude from partial clone.
4142 clone_bundle: a boolean, whether to enable /clone.bundle on
4143 HTTP/HTTPS.
4144 git_lfs: a boolean, whether to enable git LFS support.
4145 use_superproject: a boolean, whether to use the manifest
4146 superproject to sync projects.
4147 verbose: a boolean, whether to show all output, rather than only
4148 errors.
4149 current_branch_only: a boolean, whether to only fetch the current
4150 manifest branch from the server.
4151 platform: a string, restrict the checkout to projects with the
4152 specified platform group.
4153 git_event_log: an EventLog, for git tracing.
4154 tags: a boolean, whether to fetch tags.
4155 manifest_name: a string, the name of the manifest file to use.
4156 this_manifest_only: a boolean, whether to only operate on the
4157 current sub manifest.
4158 outer_manifest: a boolean, whether to start at the outermost
4159 manifest.
Jason Chang17833322023-05-23 13:06:55 -07004160 clone_filter_for_depth: a string, when specified replaces shallow
4161 clones with partial.
LaMont Jones9b03f152022-03-29 23:01:18 +00004162
Gavin Makea2e3302023-03-11 06:46:20 +00004163 Returns:
4164 a boolean, whether the sync was successful.
4165 """
4166 assert _kwargs_only == (), "Sync only accepts keyword arguments."
LaMont Jones9b03f152022-03-29 23:01:18 +00004167
Gavin Makea2e3302023-03-11 06:46:20 +00004168 groups = groups or self.manifest.GetDefaultGroupsStr(
4169 with_platform=False
4170 )
4171 platform = platform or "auto"
4172 git_event_log = git_event_log or EventLog()
4173 if outer_manifest and self.manifest.is_submanifest:
4174 # In a multi-manifest checkout, use the outer manifest unless we are
4175 # told not to.
4176 return self.client.outer_manifest.manifestProject.Sync(
4177 manifest_url=manifest_url,
4178 manifest_branch=manifest_branch,
4179 standalone_manifest=standalone_manifest,
4180 groups=groups,
4181 platform=platform,
4182 mirror=mirror,
4183 dissociate=dissociate,
4184 reference=reference,
4185 worktree=worktree,
4186 submodules=submodules,
4187 archive=archive,
4188 partial_clone=partial_clone,
4189 clone_filter=clone_filter,
4190 partial_clone_exclude=partial_clone_exclude,
4191 clone_bundle=clone_bundle,
4192 git_lfs=git_lfs,
4193 use_superproject=use_superproject,
4194 verbose=verbose,
4195 current_branch_only=current_branch_only,
4196 tags=tags,
4197 depth=depth,
4198 git_event_log=git_event_log,
4199 manifest_name=manifest_name,
4200 this_manifest_only=this_manifest_only,
4201 outer_manifest=False,
4202 )
LaMont Jones9b03f152022-03-29 23:01:18 +00004203
Gavin Makea2e3302023-03-11 06:46:20 +00004204 # If repo has already been initialized, we take -u with the absence of
4205 # --standalone-manifest to mean "transition to a standard repo set up",
4206 # which necessitates starting fresh.
4207 # If --standalone-manifest is set, we always tear everything down and
4208 # start anew.
4209 if self.Exists:
4210 was_standalone_manifest = self.config.GetString(
4211 "manifest.standalone"
4212 )
4213 if was_standalone_manifest and not manifest_url:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004214 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004215 "fatal: repo was initialized with a standlone manifest, "
4216 "cannot be re-initialized without --manifest-url/-u"
4217 )
4218 return False
4219
4220 if standalone_manifest or (
4221 was_standalone_manifest and manifest_url
4222 ):
4223 self.config.ClearCache()
4224 if self.gitdir and os.path.exists(self.gitdir):
4225 platform_utils.rmtree(self.gitdir)
4226 if self.worktree and os.path.exists(self.worktree):
4227 platform_utils.rmtree(self.worktree)
4228
4229 is_new = not self.Exists
4230 if is_new:
4231 if not manifest_url:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004232 logger.error("fatal: manifest url is required.")
Gavin Makea2e3302023-03-11 06:46:20 +00004233 return False
4234
4235 if verbose:
4236 print(
4237 "Downloading manifest from %s"
4238 % (GitConfig.ForUser().UrlInsteadOf(manifest_url),),
4239 file=sys.stderr,
4240 )
4241
4242 # The manifest project object doesn't keep track of the path on the
4243 # server where this git is located, so let's save that here.
4244 mirrored_manifest_git = None
4245 if reference:
4246 manifest_git_path = urllib.parse.urlparse(manifest_url).path[1:]
4247 mirrored_manifest_git = os.path.join(
4248 reference, manifest_git_path
4249 )
4250 if not mirrored_manifest_git.endswith(".git"):
4251 mirrored_manifest_git += ".git"
4252 if not os.path.exists(mirrored_manifest_git):
4253 mirrored_manifest_git = os.path.join(
4254 reference, ".repo/manifests.git"
4255 )
4256
4257 self._InitGitDir(mirror_git=mirrored_manifest_git)
4258
4259 # If standalone_manifest is set, mark the project as "standalone" --
4260 # we'll still do much of the manifests.git set up, but will avoid actual
4261 # syncs to a remote.
4262 if standalone_manifest:
4263 self.config.SetString("manifest.standalone", manifest_url)
4264 elif not manifest_url and not manifest_branch:
4265 # If -u is set and --standalone-manifest is not, then we're not in
4266 # standalone mode. Otherwise, use config to infer what we were in
4267 # the last init.
4268 standalone_manifest = bool(
4269 self.config.GetString("manifest.standalone")
4270 )
4271 if not standalone_manifest:
4272 self.config.SetString("manifest.standalone", None)
4273
4274 self._ConfigureDepth(depth)
4275
4276 # Set the remote URL before the remote branch as we might need it below.
4277 if manifest_url:
4278 r = self.GetRemote()
4279 r.url = manifest_url
4280 r.ResetFetch()
4281 r.Save()
4282
4283 if not standalone_manifest:
4284 if manifest_branch:
4285 if manifest_branch == "HEAD":
4286 manifest_branch = self.ResolveRemoteHead()
4287 if manifest_branch is None:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004288 logger.error("fatal: unable to resolve HEAD")
Gavin Makea2e3302023-03-11 06:46:20 +00004289 return False
4290 self.revisionExpr = manifest_branch
4291 else:
4292 if is_new:
4293 default_branch = self.ResolveRemoteHead()
4294 if default_branch is None:
4295 # If the remote doesn't have HEAD configured, default to
4296 # master.
4297 default_branch = "refs/heads/master"
4298 self.revisionExpr = default_branch
4299 else:
4300 self.PreSync()
4301
4302 groups = re.split(r"[,\s]+", groups or "")
4303 all_platforms = ["linux", "darwin", "windows"]
4304 platformize = lambda x: "platform-" + x
4305 if platform == "auto":
4306 if not mirror and not self.mirror:
4307 groups.append(platformize(self._platform_name))
4308 elif platform == "all":
4309 groups.extend(map(platformize, all_platforms))
4310 elif platform in all_platforms:
4311 groups.append(platformize(platform))
4312 elif platform != "none":
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004313 logger.error("fatal: invalid platform flag", file=sys.stderr)
Gavin Makea2e3302023-03-11 06:46:20 +00004314 return False
4315 self.config.SetString("manifest.platform", platform)
4316
4317 groups = [x for x in groups if x]
4318 groupstr = ",".join(groups)
4319 if (
4320 platform == "auto"
4321 and groupstr == self.manifest.GetDefaultGroupsStr()
4322 ):
4323 groupstr = None
4324 self.config.SetString("manifest.groups", groupstr)
4325
4326 if reference:
4327 self.config.SetString("repo.reference", reference)
4328
4329 if dissociate:
4330 self.config.SetBoolean("repo.dissociate", dissociate)
4331
4332 if worktree:
4333 if mirror:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004334 logger.error("fatal: --mirror and --worktree are incompatible")
Gavin Makea2e3302023-03-11 06:46:20 +00004335 return False
4336 if submodules:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004337 logger.error(
4338 "fatal: --submodules and --worktree are incompatible"
Gavin Makea2e3302023-03-11 06:46:20 +00004339 )
4340 return False
4341 self.config.SetBoolean("repo.worktree", worktree)
4342 if is_new:
4343 self.use_git_worktrees = True
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004344 logger.warning("warning: --worktree is experimental!")
Gavin Makea2e3302023-03-11 06:46:20 +00004345
4346 if archive:
4347 if is_new:
4348 self.config.SetBoolean("repo.archive", archive)
4349 else:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004350 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004351 "fatal: --archive is only supported when initializing a "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004352 "new workspace."
Gavin Makea2e3302023-03-11 06:46:20 +00004353 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004354 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004355 "Either delete the .repo folder in this workspace, or "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004356 "initialize in another location."
Gavin Makea2e3302023-03-11 06:46:20 +00004357 )
4358 return False
4359
4360 if mirror:
4361 if is_new:
4362 self.config.SetBoolean("repo.mirror", mirror)
4363 else:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004364 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004365 "fatal: --mirror is only supported when initializing a new "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004366 "workspace."
Gavin Makea2e3302023-03-11 06:46:20 +00004367 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004368 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004369 "Either delete the .repo folder in this workspace, or "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004370 "initialize in another location."
Gavin Makea2e3302023-03-11 06:46:20 +00004371 )
4372 return False
4373
4374 if partial_clone is not None:
4375 if mirror:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004376 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004377 "fatal: --mirror and --partial-clone are mutually "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004378 "exclusive"
Gavin Makea2e3302023-03-11 06:46:20 +00004379 )
4380 return False
4381 self.config.SetBoolean("repo.partialclone", partial_clone)
4382 if clone_filter:
4383 self.config.SetString("repo.clonefilter", clone_filter)
4384 elif self.partial_clone:
4385 clone_filter = self.clone_filter
4386 else:
4387 clone_filter = None
4388
4389 if partial_clone_exclude is not None:
4390 self.config.SetString(
4391 "repo.partialcloneexclude", partial_clone_exclude
4392 )
4393
4394 if clone_bundle is None:
4395 clone_bundle = False if partial_clone else True
4396 else:
4397 self.config.SetBoolean("repo.clonebundle", clone_bundle)
4398
4399 if submodules:
4400 self.config.SetBoolean("repo.submodules", submodules)
4401
4402 if git_lfs is not None:
4403 if git_lfs:
4404 git_require((2, 17, 0), fail=True, msg="Git LFS support")
4405
4406 self.config.SetBoolean("repo.git-lfs", git_lfs)
4407 if not is_new:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004408 logger.warning(
Gavin Makea2e3302023-03-11 06:46:20 +00004409 "warning: Changing --git-lfs settings will only affect new "
4410 "project checkouts.\n"
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004411 " Existing projects will require manual updates.\n"
Gavin Makea2e3302023-03-11 06:46:20 +00004412 )
4413
Jason Chang17833322023-05-23 13:06:55 -07004414 if clone_filter_for_depth is not None:
4415 self.ConfigureCloneFilterForDepth(clone_filter_for_depth)
4416
Gavin Makea2e3302023-03-11 06:46:20 +00004417 if use_superproject is not None:
4418 self.config.SetBoolean("repo.superproject", use_superproject)
4419
4420 if not standalone_manifest:
4421 success = self.Sync_NetworkHalf(
4422 is_new=is_new,
4423 quiet=not verbose,
4424 verbose=verbose,
4425 clone_bundle=clone_bundle,
4426 current_branch_only=current_branch_only,
4427 tags=tags,
4428 submodules=submodules,
4429 clone_filter=clone_filter,
4430 partial_clone_exclude=self.manifest.PartialCloneExclude,
Jason Chang17833322023-05-23 13:06:55 -07004431 clone_filter_for_depth=self.manifest.CloneFilterForDepth,
Gavin Makea2e3302023-03-11 06:46:20 +00004432 ).success
4433 if not success:
4434 r = self.GetRemote()
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004435 logger.error("fatal: cannot obtain manifest %s", r.url)
Gavin Makea2e3302023-03-11 06:46:20 +00004436
4437 # Better delete the manifest git dir if we created it; otherwise
4438 # next time (when user fixes problems) we won't go through the
4439 # "is_new" logic.
4440 if is_new:
4441 platform_utils.rmtree(self.gitdir)
4442 return False
4443
4444 if manifest_branch:
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08004445 self.MetaBranchSwitch(submodules=submodules, verbose=verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00004446
4447 syncbuf = SyncBuffer(self.config)
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08004448 self.Sync_LocalHalf(syncbuf, submodules=submodules, verbose=verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00004449 syncbuf.Finish()
4450
4451 if is_new or self.CurrentBranch is None:
Jason Chang1a3612f2023-08-08 14:12:53 -07004452 try:
4453 self.StartBranch("default")
4454 except GitError as e:
4455 msg = str(e)
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004456 logger.error(
4457 "fatal: cannot create default in manifest %s", msg
Gavin Makea2e3302023-03-11 06:46:20 +00004458 )
4459 return False
4460
4461 if not manifest_name:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004462 logger.error("fatal: manifest name (-m) is required.")
Gavin Makea2e3302023-03-11 06:46:20 +00004463 return False
4464
4465 elif is_new:
4466 # This is a new standalone manifest.
4467 manifest_name = "default.xml"
4468 manifest_data = fetch.fetch_file(manifest_url, verbose=verbose)
4469 dest = os.path.join(self.worktree, manifest_name)
4470 os.makedirs(os.path.dirname(dest), exist_ok=True)
4471 with open(dest, "wb") as f:
4472 f.write(manifest_data)
4473
4474 try:
4475 self.manifest.Link(manifest_name)
4476 except ManifestParseError as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004477 logger.error("fatal: manifest '%s' not available", manifest_name)
4478 logger.error("fatal: %s", e)
Gavin Makea2e3302023-03-11 06:46:20 +00004479 return False
4480
4481 if not this_manifest_only:
4482 for submanifest in self.manifest.submanifests.values():
4483 spec = submanifest.ToSubmanifestSpec()
4484 submanifest.repo_client.manifestProject.Sync(
4485 manifest_url=spec.manifestUrl,
4486 manifest_branch=spec.revision,
4487 standalone_manifest=standalone_manifest,
4488 groups=self.manifest_groups,
4489 platform=platform,
4490 mirror=mirror,
4491 dissociate=dissociate,
4492 reference=reference,
4493 worktree=worktree,
4494 submodules=submodules,
4495 archive=archive,
4496 partial_clone=partial_clone,
4497 clone_filter=clone_filter,
4498 partial_clone_exclude=partial_clone_exclude,
4499 clone_bundle=clone_bundle,
4500 git_lfs=git_lfs,
4501 use_superproject=use_superproject,
4502 verbose=verbose,
4503 current_branch_only=current_branch_only,
4504 tags=tags,
4505 depth=depth,
4506 git_event_log=git_event_log,
4507 manifest_name=spec.manifestName,
4508 this_manifest_only=False,
4509 outer_manifest=False,
4510 )
4511
4512 # Lastly, if the manifest has a <superproject> then have the
4513 # superproject sync it (if it will be used).
4514 if git_superproject.UseSuperproject(use_superproject, self.manifest):
4515 sync_result = self.manifest.superproject.Sync(git_event_log)
4516 if not sync_result.success:
4517 submanifest = ""
4518 if self.manifest.path_prefix:
4519 submanifest = f"for {self.manifest.path_prefix} "
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004520 logger.warning(
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004521 "warning: git update of superproject %s failed, "
Gavin Makea2e3302023-03-11 06:46:20 +00004522 "repo sync will not use superproject to fetch source; "
4523 "while this error is not fatal, and you can continue to "
4524 "run repo sync, please run repo init with the "
4525 "--no-use-superproject option to stop seeing this warning",
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004526 submanifest,
Gavin Makea2e3302023-03-11 06:46:20 +00004527 )
4528 if sync_result.fatal and use_superproject is not None:
4529 return False
4530
4531 return True
4532
Jason Chang17833322023-05-23 13:06:55 -07004533 def ConfigureCloneFilterForDepth(self, clone_filter_for_depth):
4534 """Configure clone filter to replace shallow clones.
4535
4536 Args:
4537 clone_filter_for_depth: a string or None, e.g. 'blob:none' will
4538 disable shallow clones and replace with partial clone. None will
4539 enable shallow clones.
4540 """
4541 self.config.SetString(
4542 "repo.clonefilterfordepth", clone_filter_for_depth
4543 )
4544
Gavin Makea2e3302023-03-11 06:46:20 +00004545 def _ConfigureDepth(self, depth):
4546 """Configure the depth we'll sync down.
4547
4548 Args:
4549 depth: an int, how deep of a partial clone to create.
4550 """
4551 # Opt.depth will be non-None if user actually passed --depth to repo
4552 # init.
4553 if depth is not None:
4554 if depth > 0:
4555 # Positive values will set the depth.
4556 depth = str(depth)
4557 else:
4558 # Negative numbers will clear the depth; passing None to
4559 # SetString will do that.
4560 depth = None
4561
4562 # We store the depth in the main manifest project.
4563 self.config.SetString("repo.depth", depth)