blob: a61c43ed5f8b04bf72b8d445c023af73cf91be6a [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
Kaushik Lingarkar50c62262025-11-03 19:58:26 -080015import datetime
Shawn O. Pearce438ee1c2008-11-03 09:59:36 -080016import errno
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070017import filecmp
Wink Saville4c426ef2015-06-03 08:05:17 -070018import glob
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070019import os
LaMont Jonesd82be3e2022-04-05 19:30:46 +000020import platform
Shawn O. Pearcec325dc32011-10-03 08:30:24 -070021import random
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070022import re
23import shutil
24import stat
Sergiy Belozorov96edb9b2024-03-04 19:48:52 +010025import string
Shawn O. Pearce5e7127d2012-08-02 14:57:37 -070026import subprocess
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070027import sys
Julien Campergue335f5ef2013-10-16 11:02:35 +020028import tarfile
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +080029import tempfile
Shawn O. Pearcec325dc32011-10-03 08:30:24 -070030import time
Mike Frysinger64477332023-08-21 21:20:32 -040031from typing import List, NamedTuple
Mike Frysingeracf63b22019-06-13 02:24:21 -040032import urllib.parse
Shawn O. Pearcedf5ee522011-10-11 14:05:21 -070033
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070034from color import Coloring
Mike Frysinger64477332023-08-21 21:20:32 -040035from error import DownloadError
Josip Sokcevicf7f9dd42024-10-03 20:32:04 +000036from error import GitAuthError
Mike Frysinger64477332023-08-21 21:20:32 -040037from error import GitError
38from error import ManifestInvalidPathError
39from error import ManifestInvalidRevisionError
40from error import ManifestParseError
41from error import NoManifestException
42from error import RepoError
43from error import UploadError
LaMont Jones0de4fc32022-04-21 17:18:35 +000044import fetch
Mike Frysinger64477332023-08-21 21:20:32 -040045from git_command import git_require
46from git_command import GitCommand
47from git_config import GetSchemeFromUrl
48from git_config import GetUrlCookieFile
49from git_config import GitConfig
Mike Frysinger64477332023-08-21 21:20:32 -040050from git_config import IsId
51from git_refs import GitRefs
52from git_refs import HEAD
53from git_refs import R_HEADS
54from git_refs import R_M
55from git_refs import R_PUB
56from git_refs import R_TAGS
57from git_refs import R_WORKTREE_M
LaMont Jonesff6b1da2022-06-01 21:03:34 +000058import git_superproject
LaMont Jones55ee3042022-04-06 17:10:21 +000059from git_trace2_event_log import EventLog
Renaud Paquayd5cec5e2016-11-01 11:24:03 -070060import platform_utils
Mike Frysinger70d861f2019-08-26 15:22:36 -040061import progress
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +000062from repo_logging import RepoLogger
Joanna Wanga6c52f52022-11-03 16:51:19 -040063from repo_trace import Trace
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070064
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -070065
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +000066logger = RepoLogger(__file__)
67
68
LaMont Jones1eddca82022-09-01 15:15:04 +000069class SyncNetworkHalfResult(NamedTuple):
Gavin Makea2e3302023-03-11 06:46:20 +000070 """Sync_NetworkHalf return value."""
71
Gavin Makea2e3302023-03-11 06:46:20 +000072 # Did we query the remote? False when optimized_fetch is True and we have
73 # the commit already present.
74 remote_fetched: bool
Jason Chang32b59562023-07-14 16:45:35 -070075 # Error from SyncNetworkHalf
76 error: Exception = None
77
78 @property
79 def success(self) -> bool:
80 return not self.error
81
82
83class SyncNetworkHalfError(RepoError):
84 """Failure trying to sync."""
85
86
87class DeleteWorktreeError(RepoError):
88 """Failure to delete worktree."""
89
90 def __init__(
91 self, *args, aggregate_errors: List[Exception] = None, **kwargs
92 ) -> None:
93 super().__init__(*args, **kwargs)
94 self.aggregate_errors = aggregate_errors or []
95
96
97class DeleteDirtyWorktreeError(DeleteWorktreeError):
98 """Failure to delete worktree due to uncommitted changes."""
LaMont Jones1eddca82022-09-01 15:15:04 +000099
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +0100100
George Engelbrecht9bc283e2020-04-02 12:36:09 -0600101# Maximum sleep time allowed during retries.
102MAXIMUM_RETRY_SLEEP_SEC = 3600.0
103# +-10% random jitter is added to each Fetches retry sleep duration.
104RETRY_JITTER_PERCENT = 0.1
105
LaMont Jonesfa8d9392022-11-02 22:01:29 +0000106# Whether to use alternates. Switching back and forth is *NOT* supported.
Mike Frysinger1d00a7e2021-12-21 00:40:31 -0500107# TODO(vapier): Remove knob once behavior is verified.
Gavin Makea2e3302023-03-11 06:46:20 +0000108_ALTERNATES = os.environ.get("REPO_USE_ALTERNATES") == "1"
George Engelbrecht9bc283e2020-04-02 12:36:09 -0600109
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +0100110
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700111def _lwrite(path, content):
Gavin Makea2e3302023-03-11 06:46:20 +0000112 lock = "%s.lock" % path
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700113
Gavin Makea2e3302023-03-11 06:46:20 +0000114 # Maintain Unix line endings on all OS's to match git behavior.
115 with open(lock, "w", newline="\n") as fd:
116 fd.write(content)
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700117
Gavin Makea2e3302023-03-11 06:46:20 +0000118 try:
119 platform_utils.rename(lock, path)
120 except OSError:
121 platform_utils.remove(lock)
122 raise
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700123
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700124
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700125def not_rev(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000126 return "^" + r
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700127
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700128
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800129def sq(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000130 return "'" + r.replace("'", "'''") + "'"
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -0800131
David Pursehouse819827a2020-02-12 15:20:19 +0900132
Jonathan Nieder93719792015-03-17 11:29:58 -0700133_project_hook_list = None
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700134
135
Jonathan Nieder93719792015-03-17 11:29:58 -0700136def _ProjectHooks():
Gavin Makea2e3302023-03-11 06:46:20 +0000137 """List the hooks present in the 'hooks' directory.
Jonathan Nieder93719792015-03-17 11:29:58 -0700138
Gavin Makea2e3302023-03-11 06:46:20 +0000139 These hooks are project hooks and are copied to the '.git/hooks' directory
140 of all subprojects.
Jonathan Nieder93719792015-03-17 11:29:58 -0700141
Gavin Makea2e3302023-03-11 06:46:20 +0000142 This function caches the list of hooks (based on the contents of the
143 'repo/hooks' directory) on the first call.
Jonathan Nieder93719792015-03-17 11:29:58 -0700144
Gavin Makea2e3302023-03-11 06:46:20 +0000145 Returns:
146 A list of absolute paths to all of the files in the hooks directory.
147 """
148 global _project_hook_list
149 if _project_hook_list is None:
Kaiyi Li46819a72024-03-27 07:21:43 -0700150 d = os.path.realpath(os.path.abspath(os.path.dirname(__file__)))
Gavin Makea2e3302023-03-11 06:46:20 +0000151 d = os.path.join(d, "hooks")
152 _project_hook_list = [
153 os.path.join(d, x) for x in platform_utils.listdir(d)
154 ]
155 return _project_hook_list
Jonathan Nieder93719792015-03-17 11:29:58 -0700156
157
Mike Frysingerd4aee652023-10-19 05:13:32 -0400158class DownloadedChange:
Gavin Makea2e3302023-03-11 06:46:20 +0000159 _commit_cache = None
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700160
Gavin Makea2e3302023-03-11 06:46:20 +0000161 def __init__(self, project, base, change_id, ps_id, commit):
162 self.project = project
163 self.base = base
164 self.change_id = change_id
165 self.ps_id = ps_id
166 self.commit = commit
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700167
Gavin Makea2e3302023-03-11 06:46:20 +0000168 @property
169 def commits(self):
170 if self._commit_cache is None:
171 self._commit_cache = self.project.bare_git.rev_list(
172 "--abbrev=8",
173 "--abbrev-commit",
174 "--pretty=oneline",
175 "--reverse",
176 "--date-order",
177 not_rev(self.base),
178 self.commit,
179 "--",
180 )
181 return self._commit_cache
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700182
183
Mike Frysingerd4aee652023-10-19 05:13:32 -0400184class ReviewableBranch:
Gavin Makea2e3302023-03-11 06:46:20 +0000185 _commit_cache = None
186 _base_exists = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700187
Gavin Makea2e3302023-03-11 06:46:20 +0000188 def __init__(self, project, branch, base):
189 self.project = project
190 self.branch = branch
191 self.base = base
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700192
Gavin Makea2e3302023-03-11 06:46:20 +0000193 @property
194 def name(self):
195 return self.branch.name
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700196
Gavin Makea2e3302023-03-11 06:46:20 +0000197 @property
198 def commits(self):
199 if self._commit_cache is None:
200 args = (
201 "--abbrev=8",
202 "--abbrev-commit",
203 "--pretty=oneline",
204 "--reverse",
205 "--date-order",
206 not_rev(self.base),
207 R_HEADS + self.name,
208 "--",
209 )
210 try:
Jason Chang87058c62023-09-27 11:34:43 -0700211 self._commit_cache = self.project.bare_git.rev_list(
212 *args, log_as_error=self.base_exists
213 )
Gavin Makea2e3302023-03-11 06:46:20 +0000214 except GitError:
215 # We weren't able to probe the commits for this branch. Was it
216 # tracking a branch that no longer exists? If so, return no
217 # commits. Otherwise, rethrow the error as we don't know what's
218 # going on.
219 if self.base_exists:
220 raise
Mike Frysinger6da17752019-09-11 18:43:17 -0400221
Gavin Makea2e3302023-03-11 06:46:20 +0000222 self._commit_cache = []
Mike Frysinger6da17752019-09-11 18:43:17 -0400223
Gavin Makea2e3302023-03-11 06:46:20 +0000224 return self._commit_cache
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700225
Gavin Makea2e3302023-03-11 06:46:20 +0000226 @property
227 def unabbrev_commits(self):
228 r = dict()
229 for commit in self.project.bare_git.rev_list(
230 not_rev(self.base), R_HEADS + self.name, "--"
231 ):
232 r[commit[0:8]] = commit
233 return r
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800234
Gavin Makea2e3302023-03-11 06:46:20 +0000235 @property
236 def date(self):
237 return self.project.bare_git.log(
238 "--pretty=format:%cd", "-n", "1", R_HEADS + self.name, "--"
239 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700240
Gavin Makea2e3302023-03-11 06:46:20 +0000241 @property
242 def base_exists(self):
243 """Whether the branch we're tracking exists.
Mike Frysinger6da17752019-09-11 18:43:17 -0400244
Gavin Makea2e3302023-03-11 06:46:20 +0000245 Normally it should, but sometimes branches we track can get deleted.
246 """
247 if self._base_exists is None:
248 try:
249 self.project.bare_git.rev_parse("--verify", not_rev(self.base))
250 # If we're still here, the base branch exists.
251 self._base_exists = True
252 except GitError:
253 # If we failed to verify, the base branch doesn't exist.
254 self._base_exists = False
Mike Frysinger6da17752019-09-11 18:43:17 -0400255
Gavin Makea2e3302023-03-11 06:46:20 +0000256 return self._base_exists
Mike Frysinger6da17752019-09-11 18:43:17 -0400257
Gavin Makea2e3302023-03-11 06:46:20 +0000258 def UploadForReview(
259 self,
260 people,
261 dryrun=False,
Mike Frysinger87f52f32024-06-30 20:12:07 -0400262 topic=None,
Gavin Makea2e3302023-03-11 06:46:20 +0000263 hashtags=(),
264 labels=(),
265 private=False,
266 notify=None,
267 wip=False,
268 ready=False,
269 dest_branch=None,
270 validate_certs=True,
271 push_options=None,
Sergiy Belozorov96edb9b2024-03-04 19:48:52 +0100272 patchset_description=None,
Gavin Makea2e3302023-03-11 06:46:20 +0000273 ):
274 self.project.UploadForReview(
275 branch=self.name,
276 people=people,
277 dryrun=dryrun,
Mike Frysinger87f52f32024-06-30 20:12:07 -0400278 topic=topic,
Gavin Makea2e3302023-03-11 06:46:20 +0000279 hashtags=hashtags,
280 labels=labels,
281 private=private,
282 notify=notify,
283 wip=wip,
284 ready=ready,
285 dest_branch=dest_branch,
286 validate_certs=validate_certs,
287 push_options=push_options,
Sergiy Belozorov96edb9b2024-03-04 19:48:52 +0100288 patchset_description=patchset_description,
Gavin Makea2e3302023-03-11 06:46:20 +0000289 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700290
Gavin Makea2e3302023-03-11 06:46:20 +0000291 def GetPublishedRefs(self):
292 refs = {}
293 output = self.project.bare_git.ls_remote(
294 self.branch.remote.SshReviewUrl(self.project.UserEmail),
295 "refs/changes/*",
296 )
297 for line in output.split("\n"):
298 try:
299 (sha, ref) = line.split()
300 refs[sha] = ref
301 except ValueError:
302 pass
Ficus Kirkpatrickbc7ef672009-05-04 12:45:11 -0700303
Gavin Makea2e3302023-03-11 06:46:20 +0000304 return refs
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700305
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700306
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700307class StatusColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000308 def __init__(self, config):
309 super().__init__(config, "status")
310 self.project = self.printer("header", attr="bold")
311 self.branch = self.printer("header", attr="bold")
312 self.nobranch = self.printer("nobranch", fg="red")
313 self.important = self.printer("important", fg="red")
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700314
Gavin Makea2e3302023-03-11 06:46:20 +0000315 self.added = self.printer("added", fg="green")
316 self.changed = self.printer("changed", fg="red")
317 self.untracked = self.printer("untracked", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700318
319
320class DiffColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000321 def __init__(self, config):
322 super().__init__(config, "diff")
323 self.project = self.printer("header", attr="bold")
324 self.fail = self.printer("fail", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700325
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700326
Mike Frysingerd4aee652023-10-19 05:13:32 -0400327class Annotation:
Gavin Makea2e3302023-03-11 06:46:20 +0000328 def __init__(self, name, value, keep):
329 self.name = name
330 self.value = value
331 self.keep = keep
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700332
Gavin Makea2e3302023-03-11 06:46:20 +0000333 def __eq__(self, other):
334 if not isinstance(other, Annotation):
335 return False
336 return self.__dict__ == other.__dict__
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700337
Gavin Makea2e3302023-03-11 06:46:20 +0000338 def __lt__(self, other):
339 # This exists just so that lists of Annotation objects can be sorted,
340 # for use in comparisons.
341 if not isinstance(other, Annotation):
342 raise ValueError("comparison is not between two Annotation objects")
343 if self.name == other.name:
344 if self.value == other.value:
345 return self.keep < other.keep
346 return self.value < other.value
347 return self.name < other.name
Jack Neus6ea0cae2021-07-20 20:52:33 +0000348
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700349
Mike Frysingere6a202f2019-08-02 15:57:57 -0400350def _SafeExpandPath(base, subpath, skipfinal=False):
Gavin Makea2e3302023-03-11 06:46:20 +0000351 """Make sure |subpath| is completely safe under |base|.
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700352
Gavin Makea2e3302023-03-11 06:46:20 +0000353 We make sure no intermediate symlinks are traversed, and that the final path
354 is not a special file (e.g. not a socket or fifo).
Mike Frysingere6a202f2019-08-02 15:57:57 -0400355
Gavin Makea2e3302023-03-11 06:46:20 +0000356 NB: We rely on a number of paths already being filtered out while parsing
357 the manifest. See the validation logic in manifest_xml.py for more details.
358 """
359 # Split up the path by its components. We can't use os.path.sep exclusively
360 # as some platforms (like Windows) will convert / to \ and that bypasses all
361 # our constructed logic here. Especially since manifest authors only use
362 # / in their paths.
363 resep = re.compile(r"[/%s]" % re.escape(os.path.sep))
364 components = resep.split(subpath)
365 if skipfinal:
366 # Whether the caller handles the final component itself.
367 finalpart = components.pop()
Mike Frysingere6a202f2019-08-02 15:57:57 -0400368
Gavin Makea2e3302023-03-11 06:46:20 +0000369 path = base
370 for part in components:
371 if part in {".", ".."}:
372 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400373 f'{subpath}: "{part}" not allowed in paths'
Gavin Makea2e3302023-03-11 06:46:20 +0000374 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400375
Gavin Makea2e3302023-03-11 06:46:20 +0000376 path = os.path.join(path, part)
377 if platform_utils.islink(path):
378 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400379 f"{path}: traversing symlinks not allow"
Gavin Makea2e3302023-03-11 06:46:20 +0000380 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400381
Gavin Makea2e3302023-03-11 06:46:20 +0000382 if os.path.exists(path):
383 if not os.path.isfile(path) and not platform_utils.isdir(path):
384 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400385 f"{path}: only regular files & directories allowed"
Gavin Makea2e3302023-03-11 06:46:20 +0000386 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400387
Gavin Makea2e3302023-03-11 06:46:20 +0000388 if skipfinal:
389 path = os.path.join(path, finalpart)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400390
Gavin Makea2e3302023-03-11 06:46:20 +0000391 return path
Mike Frysingere6a202f2019-08-02 15:57:57 -0400392
393
Peter Kjellerstedt412367b2025-11-08 00:06:16 +0100394class _CopyFile(NamedTuple):
Gavin Makea2e3302023-03-11 06:46:20 +0000395 """Container for <copyfile> manifest element."""
Mike Frysingere6a202f2019-08-02 15:57:57 -0400396
Peter Kjellerstedt412367b2025-11-08 00:06:16 +0100397 # Absolute path to the git project checkout.
398 git_worktree: str
399 # Relative path under |git_worktree| of file to read.
400 src: str
401 # Absolute path to the top of the repo client checkout.
402 topdir: str
403 # Relative path under |topdir| of file to write.
404 dest: str
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
Peter Kjellerstedt412367b2025-11-08 00:06:16 +0100438class _LinkFile(NamedTuple):
Gavin Makea2e3302023-03-11 06:46:20 +0000439 """Container for <linkfile> manifest element."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700440
Peter Kjellerstedt412367b2025-11-08 00:06:16 +0100441 # Absolute path to the git project checkout.
442 git_worktree: str
443 # Target of symlink relative to path under |git_worktree|.
444 src: str
445 # Absolute path to the top of the repo client checkout.
446 topdir: str
447 # Relative path under |topdir| of symlink to create.
448 dest: str
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500449
Gavin Makea2e3302023-03-11 06:46:20 +0000450 def __linkIt(self, relSrc, absDest):
451 # Link file if it does not exist or is out of date.
452 if not platform_utils.islink(absDest) or (
453 platform_utils.readlink(absDest) != relSrc
454 ):
455 try:
456 # Remove existing file first, since it might be read-only.
457 if os.path.lexists(absDest):
458 platform_utils.remove(absDest)
459 else:
460 dest_dir = os.path.dirname(absDest)
461 if not platform_utils.isdir(dest_dir):
462 os.makedirs(dest_dir)
463 platform_utils.symlink(relSrc, absDest)
Jason R. Coombsae824fb2023-10-20 23:32:40 +0545464 except OSError:
Peter Kjellerstedt1e4b2882025-12-02 21:00:12 +0100465 logger.error("error: Cannot symlink %s to %s", absDest, relSrc)
Gavin Makea2e3302023-03-11 06:46:20 +0000466
467 def _Link(self):
468 """Link the self.src & self.dest paths.
469
470 Handles wild cards on the src linking all of the files in the source in
471 to the destination directory.
472 """
473 # Some people use src="." to create stable links to projects. Let's
474 # allow that but reject all other uses of "." to keep things simple.
475 if self.src == ".":
476 src = self.git_worktree
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500477 else:
Gavin Makea2e3302023-03-11 06:46:20 +0000478 src = _SafeExpandPath(self.git_worktree, self.src)
Wink Saville4c426ef2015-06-03 08:05:17 -0700479
Gavin Makea2e3302023-03-11 06:46:20 +0000480 if not glob.has_magic(src):
481 # Entity does not contain a wild card so just a simple one to one
482 # link operation.
483 dest = _SafeExpandPath(self.topdir, self.dest, skipfinal=True)
484 # dest & src are absolute paths at this point. Make sure the target
485 # of the symlink is relative in the context of the repo client
486 # checkout.
487 relpath = os.path.relpath(src, os.path.dirname(dest))
488 self.__linkIt(relpath, dest)
489 else:
490 dest = _SafeExpandPath(self.topdir, self.dest)
491 # Entity contains a wild card.
492 if os.path.exists(dest) and not platform_utils.isdir(dest):
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +0000493 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +0000494 "Link error: src with wildcard, %s must be a directory",
495 dest,
496 )
497 else:
498 for absSrcFile in glob.glob(src):
499 # Create a releative path from source dir to destination
500 # dir.
501 absSrcDir = os.path.dirname(absSrcFile)
502 relSrcDir = os.path.relpath(absSrcDir, dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400503
Gavin Makea2e3302023-03-11 06:46:20 +0000504 # Get the source file name.
505 srcFile = os.path.basename(absSrcFile)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400506
Gavin Makea2e3302023-03-11 06:46:20 +0000507 # Now form the final full paths to srcFile. They will be
508 # absolute for the desintaiton and relative for the source.
509 absDest = os.path.join(dest, srcFile)
510 relSrc = os.path.join(relSrcDir, srcFile)
511 self.__linkIt(relSrc, absDest)
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500512
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700513
Mike Frysingerd4aee652023-10-19 05:13:32 -0400514class RemoteSpec:
Gavin Makea2e3302023-03-11 06:46:20 +0000515 def __init__(
516 self,
517 name,
518 url=None,
519 pushUrl=None,
520 review=None,
521 revision=None,
522 orig_name=None,
523 fetchUrl=None,
524 ):
525 self.name = name
526 self.url = url
527 self.pushUrl = pushUrl
528 self.review = review
529 self.revision = revision
530 self.orig_name = orig_name
531 self.fetchUrl = fetchUrl
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700532
Ian Kasprzak0286e312021-02-05 10:06:18 -0800533
Mike Frysingerd4aee652023-10-19 05:13:32 -0400534class Project:
Gavin Makea2e3302023-03-11 06:46:20 +0000535 # These objects can be shared between several working trees.
536 @property
537 def shareable_dirs(self):
538 """Return the shareable directories"""
539 if self.UseAlternates:
540 return ["hooks", "rr-cache"]
541 else:
542 return ["hooks", "objects", "rr-cache"]
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700543
Gavin Makea2e3302023-03-11 06:46:20 +0000544 def __init__(
545 self,
546 manifest,
547 name,
548 remote,
549 gitdir,
550 objdir,
551 worktree,
552 relpath,
553 revisionExpr,
554 revisionId,
555 rebase=True,
Peter Kjellerstedt75773b82025-11-08 02:36:56 +0100556 groups=set(),
Gavin Makea2e3302023-03-11 06:46:20 +0000557 sync_c=False,
558 sync_s=False,
559 sync_tags=True,
560 clone_depth=None,
561 upstream=None,
562 parent=None,
563 use_git_worktrees=False,
564 is_derived=False,
565 dest_branch=None,
566 optimized_fetch=False,
567 retry_fetches=0,
Gavin Makea2e3302023-03-11 06:46:20 +0000568 ):
569 """Init a Project object.
Doug Anderson3ba5f952011-04-07 12:51:04 -0700570
571 Args:
Gavin Makea2e3302023-03-11 06:46:20 +0000572 manifest: The XmlManifest object.
573 name: The `name` attribute of manifest.xml's project element.
574 remote: RemoteSpec object specifying its remote's properties.
575 gitdir: Absolute path of git directory.
576 objdir: Absolute path of directory to store git objects.
577 worktree: Absolute path of git working tree.
578 relpath: Relative path of git working tree to repo's top directory.
579 revisionExpr: The `revision` attribute of manifest.xml's project
580 element.
581 revisionId: git commit id for checking out.
582 rebase: The `rebase` attribute of manifest.xml's project element.
583 groups: The `groups` attribute of manifest.xml's project element.
584 sync_c: The `sync-c` attribute of manifest.xml's project element.
585 sync_s: The `sync-s` attribute of manifest.xml's project element.
586 sync_tags: The `sync-tags` attribute of manifest.xml's project
587 element.
588 upstream: The `upstream` attribute of manifest.xml's project
589 element.
590 parent: The parent Project object.
591 use_git_worktrees: Whether to use `git worktree` for this project.
592 is_derived: False if the project was explicitly defined in the
593 manifest; True if the project is a discovered submodule.
594 dest_branch: The branch to which to push changes for review by
595 default.
596 optimized_fetch: If True, when a project is set to a sha1 revision,
597 only fetch from the remote if the sha1 is not present locally.
598 retry_fetches: Retry remote fetches n times upon receiving transient
599 error with exponential backoff and jitter.
Gavin Makea2e3302023-03-11 06:46:20 +0000600 """
601 self.client = self.manifest = manifest
602 self.name = name
603 self.remote = remote
604 self.UpdatePaths(relpath, worktree, gitdir, objdir)
605 self.SetRevision(revisionExpr, revisionId=revisionId)
606
607 self.rebase = rebase
608 self.groups = groups
609 self.sync_c = sync_c
610 self.sync_s = sync_s
611 self.sync_tags = sync_tags
612 self.clone_depth = clone_depth
613 self.upstream = upstream
614 self.parent = parent
615 # NB: Do not use this setting in __init__ to change behavior so that the
616 # manifest.git checkout can inspect & change it after instantiating.
617 # See the XmlManifest init code for more info.
618 self.use_git_worktrees = use_git_worktrees
619 self.is_derived = is_derived
620 self.optimized_fetch = optimized_fetch
621 self.retry_fetches = max(0, retry_fetches)
622 self.subprojects = []
623
624 self.snapshots = {}
Peter Kjellerstedt412367b2025-11-08 00:06:16 +0100625 # Use dicts to dedupe while maintaining declared order.
626 self.copyfiles = {}
627 self.linkfiles = {}
Gavin Makea2e3302023-03-11 06:46:20 +0000628 self.annotations = []
629 self.dest_branch = dest_branch
Gavin Makea2e3302023-03-11 06:46:20 +0000630
631 # This will be filled in if a project is later identified to be the
632 # project containing repo hooks.
633 self.enabled_repo_hooks = []
634
Gavin Makea2e3302023-03-11 06:46:20 +0000635 def RelPath(self, local=True):
636 """Return the path for the project relative to a manifest.
637
638 Args:
639 local: a boolean, if True, the path is relative to the local
640 (sub)manifest. If false, the path is relative to the outermost
641 manifest.
642 """
643 if local:
644 return self.relpath
645 return os.path.join(self.manifest.path_prefix, self.relpath)
646
647 def SetRevision(self, revisionExpr, revisionId=None):
648 """Set revisionId based on revision expression and id"""
649 self.revisionExpr = revisionExpr
650 if revisionId is None and revisionExpr and IsId(revisionExpr):
651 self.revisionId = self.revisionExpr
652 else:
653 self.revisionId = revisionId
654
655 def UpdatePaths(self, relpath, worktree, gitdir, objdir):
656 """Update paths used by this project"""
657 self.gitdir = gitdir.replace("\\", "/")
658 self.objdir = objdir.replace("\\", "/")
659 if worktree:
660 self.worktree = os.path.normpath(worktree).replace("\\", "/")
661 else:
662 self.worktree = None
663 self.relpath = relpath
664
665 self.config = GitConfig.ForRepository(
666 gitdir=self.gitdir, defaults=self.manifest.globalConfig
667 )
668
669 if self.worktree:
670 self.work_git = self._GitGetByExec(
671 self, bare=False, gitdir=self.gitdir
672 )
673 else:
674 self.work_git = None
675 self.bare_git = self._GitGetByExec(self, bare=True, gitdir=self.gitdir)
676 self.bare_ref = GitRefs(self.gitdir)
677 self.bare_objdir = self._GitGetByExec(
678 self, bare=True, gitdir=self.objdir
679 )
680
681 @property
682 def UseAlternates(self):
683 """Whether git alternates are in use.
684
685 This will be removed once migration to alternates is complete.
686 """
687 return _ALTERNATES or self.manifest.is_multimanifest
688
689 @property
690 def Derived(self):
691 return self.is_derived
692
693 @property
694 def Exists(self):
695 return platform_utils.isdir(self.gitdir) and platform_utils.isdir(
696 self.objdir
697 )
698
699 @property
700 def CurrentBranch(self):
701 """Obtain the name of the currently checked out branch.
702
703 The branch name omits the 'refs/heads/' prefix.
704 None is returned if the project is on a detached HEAD, or if the
705 work_git is otheriwse inaccessible (e.g. an incomplete sync).
706 """
707 try:
708 b = self.work_git.GetHead()
709 except NoManifestException:
710 # If the local checkout is in a bad state, don't barf. Let the
711 # callers process this like the head is unreadable.
712 return None
713 if b.startswith(R_HEADS):
714 return b[len(R_HEADS) :]
715 return None
716
717 def IsRebaseInProgress(self):
Erik Elmeke562cea72024-04-29 11:26:53 +0200718 """Returns true if a rebase or "am" is in progress"""
719 # "rebase-apply" is used for "git rebase".
720 # "rebase-merge" is used for "git am".
Gavin Makea2e3302023-03-11 06:46:20 +0000721 return (
722 os.path.exists(self.work_git.GetDotgitPath("rebase-apply"))
723 or os.path.exists(self.work_git.GetDotgitPath("rebase-merge"))
724 or os.path.exists(os.path.join(self.worktree, ".dotest"))
725 )
726
Erik Elmeke562cea72024-04-29 11:26:53 +0200727 def IsCherryPickInProgress(self):
728 """Returns True if a cherry-pick is in progress."""
729 return os.path.exists(self.work_git.GetDotgitPath("CHERRY_PICK_HEAD"))
730
731 def _AbortRebase(self):
732 """Abort ongoing rebase, cherry-pick or patch apply (am).
733
734 If no rebase, cherry-pick or patch apply was in progress, this method
735 ignores the status and continues.
736 """
737
738 def _git(*args):
739 # Ignore return code, in case there was no rebase in progress.
Erik Elmeke4592a632024-08-27 16:16:26 +0200740 GitCommand(self, args, log_as_error=False).Wait()
Erik Elmeke562cea72024-04-29 11:26:53 +0200741
742 _git("cherry-pick", "--abort")
743 _git("rebase", "--abort")
744 _git("am", "--abort")
745
Gavin Makea2e3302023-03-11 06:46:20 +0000746 def IsDirty(self, consider_untracked=True):
747 """Is the working directory modified in some way?"""
748 self.work_git.update_index(
749 "-q", "--unmerged", "--ignore-missing", "--refresh"
750 )
751 if self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD):
752 return True
753 if self.work_git.DiffZ("diff-files"):
754 return True
755 if consider_untracked and self.UntrackedFiles():
756 return True
757 return False
758
759 _userident_name = None
760 _userident_email = None
761
762 @property
763 def UserName(self):
764 """Obtain the user's personal name."""
765 if self._userident_name is None:
766 self._LoadUserIdentity()
767 return self._userident_name
768
769 @property
770 def UserEmail(self):
771 """Obtain the user's email address. This is very likely
772 to be their Gerrit login.
773 """
774 if self._userident_email is None:
775 self._LoadUserIdentity()
776 return self._userident_email
777
778 def _LoadUserIdentity(self):
779 u = self.bare_git.var("GIT_COMMITTER_IDENT")
780 m = re.compile("^(.*) <([^>]*)> ").match(u)
781 if m:
782 self._userident_name = m.group(1)
783 self._userident_email = m.group(2)
784 else:
785 self._userident_name = ""
786 self._userident_email = ""
787
788 def GetRemote(self, name=None):
789 """Get the configuration for a single remote.
790
791 Defaults to the current project's remote.
792 """
793 if name is None:
794 name = self.remote.name
795 return self.config.GetRemote(name)
796
797 def GetBranch(self, name):
798 """Get the configuration for a single branch."""
799 return self.config.GetBranch(name)
800
801 def GetBranches(self):
802 """Get all existing local branches."""
803 current = self.CurrentBranch
804 all_refs = self._allrefs
805 heads = {}
806
807 for name, ref_id in all_refs.items():
808 if name.startswith(R_HEADS):
809 name = name[len(R_HEADS) :]
810 b = self.GetBranch(name)
811 b.current = name == current
812 b.published = None
813 b.revision = ref_id
814 heads[name] = b
815
816 for name, ref_id in all_refs.items():
817 if name.startswith(R_PUB):
818 name = name[len(R_PUB) :]
819 b = heads.get(name)
820 if b:
821 b.published = ref_id
822
823 return heads
824
825 def MatchesGroups(self, manifest_groups):
826 """Returns true if the manifest groups specified at init should cause
827 this project to be synced.
828 Prefixing a manifest group with "-" inverts the meaning of a group.
829 All projects are implicitly labelled with "all".
830
831 labels are resolved in order. In the example case of
832 project_groups: "all,group1,group2"
833 manifest_groups: "-group1,group2"
834 the project will be matched.
835
836 The special manifest group "default" will match any project that
837 does not have the special project group "notdefault"
838 """
839 default_groups = self.manifest.default_groups or ["default"]
840 expanded_manifest_groups = manifest_groups or default_groups
Peter Kjellerstedt75773b82025-11-08 02:36:56 +0100841 expanded_project_groups = {"all"} | self.groups
Gavin Makea2e3302023-03-11 06:46:20 +0000842 if "notdefault" not in expanded_project_groups:
Peter Kjellerstedt75773b82025-11-08 02:36:56 +0100843 expanded_project_groups |= {"default"}
Gavin Makea2e3302023-03-11 06:46:20 +0000844
845 matched = False
846 for group in expanded_manifest_groups:
847 if group.startswith("-") and group[1:] in expanded_project_groups:
848 matched = False
849 elif group in expanded_project_groups:
850 matched = True
851
852 return matched
853
854 def UncommitedFiles(self, get_all=True):
855 """Returns a list of strings, uncommitted files in the git tree.
856
857 Args:
858 get_all: a boolean, if True - get information about all different
859 uncommitted files. If False - return as soon as any kind of
860 uncommitted files is detected.
861 """
862 details = []
863 self.work_git.update_index(
864 "-q", "--unmerged", "--ignore-missing", "--refresh"
865 )
866 if self.IsRebaseInProgress():
867 details.append("rebase in progress")
868 if not get_all:
869 return details
870
871 changes = self.work_git.DiffZ("diff-index", "--cached", HEAD).keys()
872 if changes:
873 details.extend(changes)
874 if not get_all:
875 return details
876
877 changes = self.work_git.DiffZ("diff-files").keys()
878 if changes:
879 details.extend(changes)
880 if not get_all:
881 return details
882
883 changes = self.UntrackedFiles()
884 if changes:
885 details.extend(changes)
886
887 return details
888
889 def UntrackedFiles(self):
890 """Returns a list of strings, untracked files in the git tree."""
891 return self.work_git.LsOthers()
892
893 def HasChanges(self):
894 """Returns true if there are uncommitted changes."""
895 return bool(self.UncommitedFiles(get_all=False))
896
897 def PrintWorkTreeStatus(self, output_redir=None, quiet=False, local=False):
898 """Prints the status of the repository to stdout.
899
900 Args:
901 output_redir: If specified, redirect the output to this object.
902 quiet: If True then only print the project name. Do not print
903 the modified files, branch name, etc.
904 local: a boolean, if True, the path is relative to the local
905 (sub)manifest. If false, the path is relative to the outermost
906 manifest.
907 """
908 if not platform_utils.isdir(self.worktree):
909 if output_redir is None:
910 output_redir = sys.stdout
911 print(file=output_redir)
912 print("project %s/" % self.RelPath(local), file=output_redir)
913 print(' missing (run "repo sync")', file=output_redir)
914 return
915
916 self.work_git.update_index(
917 "-q", "--unmerged", "--ignore-missing", "--refresh"
918 )
919 rb = self.IsRebaseInProgress()
920 di = self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD)
921 df = self.work_git.DiffZ("diff-files")
922 do = self.work_git.LsOthers()
923 if not rb and not di and not df and not do and not self.CurrentBranch:
924 return "CLEAN"
925
926 out = StatusColoring(self.config)
927 if output_redir is not None:
928 out.redirect(output_redir)
929 out.project("project %-40s", self.RelPath(local) + "/ ")
930
931 if quiet:
932 out.nl()
933 return "DIRTY"
934
935 branch = self.CurrentBranch
936 if branch is None:
937 out.nobranch("(*** NO BRANCH ***)")
938 else:
939 out.branch("branch %s", branch)
940 out.nl()
941
942 if rb:
943 out.important("prior sync failed; rebase still in progress")
944 out.nl()
945
946 paths = list()
947 paths.extend(di.keys())
948 paths.extend(df.keys())
949 paths.extend(do)
950
951 for p in sorted(set(paths)):
952 try:
953 i = di[p]
954 except KeyError:
955 i = None
956
957 try:
958 f = df[p]
959 except KeyError:
960 f = None
961
962 if i:
963 i_status = i.status.upper()
964 else:
965 i_status = "-"
966
967 if f:
968 f_status = f.status.lower()
969 else:
970 f_status = "-"
971
972 if i and i.src_path:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400973 line = (
974 f" {i_status}{f_status}\t{i.src_path} => {p} ({i.level}%)"
Gavin Makea2e3302023-03-11 06:46:20 +0000975 )
976 else:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400977 line = f" {i_status}{f_status}\t{p}"
Gavin Makea2e3302023-03-11 06:46:20 +0000978
979 if i and not f:
980 out.added("%s", line)
981 elif (i and f) or (not i and f):
982 out.changed("%s", line)
983 elif not i and not f:
984 out.untracked("%s", line)
985 else:
986 out.write("%s", line)
987 out.nl()
988
989 return "DIRTY"
990
991 def PrintWorkTreeDiff(
992 self, absolute_paths=False, output_redir=None, local=False
993 ):
994 """Prints the status of the repository to stdout."""
995 out = DiffColoring(self.config)
996 if output_redir:
997 out.redirect(output_redir)
998 cmd = ["diff"]
999 if out.is_on:
1000 cmd.append("--color")
1001 cmd.append(HEAD)
1002 if absolute_paths:
1003 cmd.append("--src-prefix=a/%s/" % self.RelPath(local))
1004 cmd.append("--dst-prefix=b/%s/" % self.RelPath(local))
1005 cmd.append("--")
1006 try:
1007 p = GitCommand(self, cmd, capture_stdout=True, capture_stderr=True)
1008 p.Wait()
1009 except GitError as e:
1010 out.nl()
1011 out.project("project %s/" % self.RelPath(local))
1012 out.nl()
1013 out.fail("%s", str(e))
1014 out.nl()
1015 return False
1016 if p.stdout:
1017 out.nl()
1018 out.project("project %s/" % self.RelPath(local))
1019 out.nl()
1020 out.write("%s", p.stdout)
1021 return p.Wait() == 0
1022
1023 def WasPublished(self, branch, all_refs=None):
1024 """Was the branch published (uploaded) for code review?
1025 If so, returns the SHA-1 hash of the last published
1026 state for the branch.
1027 """
1028 key = R_PUB + branch
1029 if all_refs is None:
1030 try:
1031 return self.bare_git.rev_parse(key)
1032 except GitError:
1033 return None
1034 else:
1035 try:
1036 return all_refs[key]
1037 except KeyError:
1038 return None
1039
1040 def CleanPublishedCache(self, all_refs=None):
1041 """Prunes any stale published refs."""
1042 if all_refs is None:
1043 all_refs = self._allrefs
1044 heads = set()
1045 canrm = {}
1046 for name, ref_id in all_refs.items():
1047 if name.startswith(R_HEADS):
1048 heads.add(name)
1049 elif name.startswith(R_PUB):
1050 canrm[name] = ref_id
1051
1052 for name, ref_id in canrm.items():
1053 n = name[len(R_PUB) :]
1054 if R_HEADS + n not in heads:
1055 self.bare_git.DeleteRef(name, ref_id)
1056
1057 def GetUploadableBranches(self, selected_branch=None):
1058 """List any branches which can be uploaded for review."""
1059 heads = {}
1060 pubed = {}
1061
1062 for name, ref_id in self._allrefs.items():
1063 if name.startswith(R_HEADS):
1064 heads[name[len(R_HEADS) :]] = ref_id
1065 elif name.startswith(R_PUB):
1066 pubed[name[len(R_PUB) :]] = ref_id
1067
1068 ready = []
1069 for branch, ref_id in heads.items():
1070 if branch in pubed and pubed[branch] == ref_id:
1071 continue
1072 if selected_branch and branch != selected_branch:
1073 continue
1074
1075 rb = self.GetUploadableBranch(branch)
1076 if rb:
1077 ready.append(rb)
1078 return ready
1079
1080 def GetUploadableBranch(self, branch_name):
1081 """Get a single uploadable branch, or None."""
1082 branch = self.GetBranch(branch_name)
1083 base = branch.LocalMerge
1084 if branch.LocalMerge:
1085 rb = ReviewableBranch(self, branch, base)
1086 if rb.commits:
1087 return rb
1088 return None
1089
1090 def UploadForReview(
1091 self,
1092 branch=None,
1093 people=([], []),
1094 dryrun=False,
Mike Frysinger87f52f32024-06-30 20:12:07 -04001095 topic=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001096 hashtags=(),
1097 labels=(),
1098 private=False,
1099 notify=None,
1100 wip=False,
1101 ready=False,
1102 dest_branch=None,
1103 validate_certs=True,
1104 push_options=None,
Sergiy Belozorov96edb9b2024-03-04 19:48:52 +01001105 patchset_description=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001106 ):
1107 """Uploads the named branch for code review."""
1108 if branch is None:
1109 branch = self.CurrentBranch
1110 if branch is None:
Jason Chang32b59562023-07-14 16:45:35 -07001111 raise GitError("not currently on a branch", project=self.name)
Gavin Makea2e3302023-03-11 06:46:20 +00001112
1113 branch = self.GetBranch(branch)
1114 if not branch.LocalMerge:
Jason Chang32b59562023-07-14 16:45:35 -07001115 raise GitError(
1116 "branch %s does not track a remote" % branch.name,
1117 project=self.name,
1118 )
Gavin Makea2e3302023-03-11 06:46:20 +00001119 if not branch.remote.review:
Jason Chang32b59562023-07-14 16:45:35 -07001120 raise GitError(
1121 "remote %s has no review url" % branch.remote.name,
1122 project=self.name,
1123 )
Gavin Makea2e3302023-03-11 06:46:20 +00001124
1125 # Basic validity check on label syntax.
1126 for label in labels:
1127 if not re.match(r"^.+[+-][0-9]+$", label):
1128 raise UploadError(
1129 f'invalid label syntax "{label}": labels use forms like '
Jason Chang5a3a5f72023-08-17 11:36:41 -07001130 "CodeReview+1 or Verified-1",
1131 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00001132 )
1133
1134 if dest_branch is None:
1135 dest_branch = self.dest_branch
1136 if dest_branch is None:
1137 dest_branch = branch.merge
1138 if not dest_branch.startswith(R_HEADS):
1139 dest_branch = R_HEADS + dest_branch
1140
1141 if not branch.remote.projectname:
1142 branch.remote.projectname = self.name
1143 branch.remote.Save()
1144
1145 url = branch.remote.ReviewUrl(self.UserEmail, validate_certs)
1146 if url is None:
Jason Chang5a3a5f72023-08-17 11:36:41 -07001147 raise UploadError("review not configured", project=self.name)
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +00001148 cmd = ["push", "--progress"]
Gavin Makea2e3302023-03-11 06:46:20 +00001149 if dryrun:
1150 cmd.append("-n")
1151
1152 if url.startswith("ssh://"):
1153 cmd.append("--receive-pack=gerrit receive-pack")
1154
Aravind Vasudevan99ebf622023-04-04 23:44:37 +00001155 # This stops git from pushing all reachable annotated tags when
1156 # push.followTags is configured. Gerrit does not accept any tags
1157 # pushed to a CL.
Mike Frysinger12f6dc42024-03-21 13:06:11 -04001158 cmd.append("--no-follow-tags")
Aravind Vasudevan99ebf622023-04-04 23:44:37 +00001159
Gavin Makea2e3302023-03-11 06:46:20 +00001160 for push_option in push_options or []:
1161 cmd.append("-o")
1162 cmd.append(push_option)
1163
1164 cmd.append(url)
1165
1166 if dest_branch.startswith(R_HEADS):
1167 dest_branch = dest_branch[len(R_HEADS) :]
1168
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001169 ref_spec = f"{R_HEADS + branch.name}:refs/for/{dest_branch}"
Gavin Makea2e3302023-03-11 06:46:20 +00001170 opts = []
Mike Frysinger87f52f32024-06-30 20:12:07 -04001171 if topic is not None:
1172 opts += [f"topic={topic}"]
Gavin Makea2e3302023-03-11 06:46:20 +00001173 opts += ["t=%s" % p for p in hashtags]
1174 # NB: No need to encode labels as they've been validated above.
1175 opts += ["l=%s" % p for p in labels]
1176
1177 opts += ["r=%s" % p for p in people[0]]
1178 opts += ["cc=%s" % p for p in people[1]]
1179 if notify:
1180 opts += ["notify=" + notify]
1181 if private:
1182 opts += ["private"]
1183 if wip:
1184 opts += ["wip"]
1185 if ready:
1186 opts += ["ready"]
Sergiy Belozorov96edb9b2024-03-04 19:48:52 +01001187 if patchset_description:
1188 opts += [
1189 f"m={self._encode_patchset_description(patchset_description)}"
1190 ]
Gavin Makea2e3302023-03-11 06:46:20 +00001191 if opts:
1192 ref_spec = ref_spec + "%" + ",".join(opts)
1193 cmd.append(ref_spec)
1194
Jason Chang1e9f7b92023-08-25 10:31:04 -07001195 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001196
1197 if not dryrun:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001198 msg = f"posted to {branch.remote.review} for {dest_branch}"
Gavin Makea2e3302023-03-11 06:46:20 +00001199 self.bare_git.UpdateRef(
1200 R_PUB + branch.name, R_HEADS + branch.name, message=msg
1201 )
1202
Sergiy Belozorov96edb9b2024-03-04 19:48:52 +01001203 @staticmethod
1204 def _encode_patchset_description(original):
1205 """Applies percent-encoding for strings sent as patchset description.
1206
1207 The encoding used is based on but stricter than URL encoding (Section
1208 2.1 of RFC 3986). The only non-escaped characters are alphanumerics, and
1209 'SPACE' (U+0020) can be represented as 'LOW LINE' (U+005F) or
1210 'PLUS SIGN' (U+002B).
1211
1212 For more information, see the Gerrit docs here:
1213 https://gerrit-review.googlesource.com/Documentation/user-upload.html#patch_set_description
1214 """
1215 SAFE = {ord(x) for x in string.ascii_letters + string.digits}
1216
1217 def _enc(b):
1218 if b in SAFE:
1219 return chr(b)
1220 elif b == ord(" "):
1221 return "_"
1222 else:
1223 return f"%{b:02x}"
1224
1225 return "".join(_enc(x) for x in original.encode("utf-8"))
1226
Gavin Makea2e3302023-03-11 06:46:20 +00001227 def _ExtractArchive(self, tarpath, path=None):
1228 """Extract the given tar on its current location
1229
1230 Args:
1231 tarpath: The path to the actual tar file
1232
1233 """
1234 try:
1235 with tarfile.open(tarpath, "r") as tar:
1236 tar.extractall(path=path)
1237 return True
Jason R. Coombsae824fb2023-10-20 23:32:40 +05451238 except (OSError, tarfile.TarError) as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001239 logger.error("error: Cannot extract archive %s: %s", tarpath, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001240 return False
1241
1242 def Sync_NetworkHalf(
1243 self,
1244 quiet=False,
1245 verbose=False,
1246 output_redir=None,
1247 is_new=None,
1248 current_branch_only=None,
1249 force_sync=False,
1250 clone_bundle=True,
1251 tags=None,
1252 archive=False,
1253 optimized_fetch=False,
1254 retry_fetches=0,
1255 prune=False,
1256 submodules=False,
1257 ssh_proxy=None,
1258 clone_filter=None,
1259 partial_clone_exclude=set(),
Jason Chang17833322023-05-23 13:06:55 -07001260 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001261 ):
1262 """Perform only the network IO portion of the sync process.
1263 Local working directory/branch state is not affected.
1264 """
1265 if archive and not isinstance(self, MetaProject):
1266 if self.remote.url.startswith(("http://", "https://")):
Jason Chang32b59562023-07-14 16:45:35 -07001267 msg_template = (
1268 "%s: Cannot fetch archives from http/https remotes."
Gavin Makea2e3302023-03-11 06:46:20 +00001269 )
Jason Chang32b59562023-07-14 16:45:35 -07001270 msg_args = self.name
1271 msg = msg_template % msg_args
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001272 logger.error(msg_template, msg_args)
Jason Chang32b59562023-07-14 16:45:35 -07001273 return SyncNetworkHalfResult(
1274 False, SyncNetworkHalfError(msg, project=self.name)
1275 )
Gavin Makea2e3302023-03-11 06:46:20 +00001276
1277 name = self.relpath.replace("\\", "/")
1278 name = name.replace("/", "_")
1279 tarpath = "%s.tar" % name
1280 topdir = self.manifest.topdir
1281
1282 try:
1283 self._FetchArchive(tarpath, cwd=topdir)
1284 except GitError as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001285 logger.error("error: %s", e)
Jason Chang32b59562023-07-14 16:45:35 -07001286 return SyncNetworkHalfResult(False, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001287
1288 # From now on, we only need absolute tarpath.
1289 tarpath = os.path.join(topdir, tarpath)
1290
1291 if not self._ExtractArchive(tarpath, path=topdir):
Jason Chang32b59562023-07-14 16:45:35 -07001292 return SyncNetworkHalfResult(
1293 True,
1294 SyncNetworkHalfError(
1295 f"Unable to Extract Archive {tarpath}",
1296 project=self.name,
1297 ),
1298 )
Gavin Makea2e3302023-03-11 06:46:20 +00001299 try:
1300 platform_utils.remove(tarpath)
1301 except OSError as e:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00001302 logger.warning("warn: Cannot remove archive %s: %s", tarpath, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001303 self._CopyAndLinkFiles()
Jason Chang32b59562023-07-14 16:45:35 -07001304 return SyncNetworkHalfResult(True)
Gavin Makea2e3302023-03-11 06:46:20 +00001305
1306 # If the shared object dir already exists, don't try to rebootstrap with
1307 # a clone bundle download. We should have the majority of objects
1308 # already.
1309 if clone_bundle and os.path.exists(self.objdir):
1310 clone_bundle = False
1311
1312 if self.name in partial_clone_exclude:
1313 clone_bundle = True
1314 clone_filter = None
1315
1316 if is_new is None:
1317 is_new = not self.Exists
1318 if is_new:
1319 self._InitGitDir(force_sync=force_sync, quiet=quiet)
1320 else:
Josip Sokcevic9b57aa02023-12-01 23:01:52 +00001321 try:
1322 # At this point, it's possible that gitdir points to an old
1323 # objdir (e.g. name changed, but objdir exists). Check
1324 # references to ensure that's not the case. See
1325 # https://issues.gerritcodereview.com/40013418 for more
1326 # details.
1327 self._CheckDirReference(self.objdir, self.gitdir)
1328
1329 self._UpdateHooks(quiet=quiet)
1330 except GitError as e:
1331 if not force_sync:
1332 raise e
1333 # Let _InitGitDir fix the issue, force_sync is always True here.
1334 self._InitGitDir(force_sync=True, quiet=quiet)
Gavin Makea2e3302023-03-11 06:46:20 +00001335 self._InitRemote()
1336
1337 if self.UseAlternates:
1338 # If gitdir/objects is a symlink, migrate it from the old layout.
1339 gitdir_objects = os.path.join(self.gitdir, "objects")
1340 if platform_utils.islink(gitdir_objects):
1341 platform_utils.remove(gitdir_objects, missing_ok=True)
1342 gitdir_alt = os.path.join(self.gitdir, "objects/info/alternates")
1343 if not os.path.exists(gitdir_alt):
1344 os.makedirs(os.path.dirname(gitdir_alt), exist_ok=True)
1345 _lwrite(
1346 gitdir_alt,
1347 os.path.join(
1348 os.path.relpath(self.objdir, gitdir_objects), "objects"
1349 )
1350 + "\n",
1351 )
1352
1353 if is_new:
1354 alt = os.path.join(self.objdir, "objects/info/alternates")
1355 try:
1356 with open(alt) as fd:
1357 # This works for both absolute and relative alternate
1358 # directories.
1359 alt_dir = os.path.join(
1360 self.objdir, "objects", fd.readline().rstrip()
1361 )
Jason R. Coombsae824fb2023-10-20 23:32:40 +05451362 except OSError:
Gavin Makea2e3302023-03-11 06:46:20 +00001363 alt_dir = None
1364 else:
1365 alt_dir = None
1366
1367 if (
1368 clone_bundle
1369 and alt_dir is None
1370 and self._ApplyCloneBundle(
1371 initial=is_new, quiet=quiet, verbose=verbose
1372 )
1373 ):
1374 is_new = False
1375
1376 if current_branch_only is None:
1377 if self.sync_c:
1378 current_branch_only = True
1379 elif not self.manifest._loaded:
1380 # Manifest cannot check defaults until it syncs.
1381 current_branch_only = False
1382 elif self.manifest.default.sync_c:
1383 current_branch_only = True
1384
1385 if tags is None:
1386 tags = self.sync_tags
1387
1388 if self.clone_depth:
1389 depth = self.clone_depth
1390 else:
1391 depth = self.manifest.manifestProject.depth
1392
Jason Chang17833322023-05-23 13:06:55 -07001393 if depth and clone_filter_for_depth:
1394 depth = None
1395 clone_filter = clone_filter_for_depth
1396
Gavin Makea2e3302023-03-11 06:46:20 +00001397 # See if we can skip the network fetch entirely.
1398 remote_fetched = False
1399 if not (
1400 optimized_fetch
Sylvain56a5a012023-09-11 13:38:00 +02001401 and IsId(self.revisionExpr)
1402 and self._CheckForImmutableRevision()
Gavin Makea2e3302023-03-11 06:46:20 +00001403 ):
1404 remote_fetched = True
Jason Chang32b59562023-07-14 16:45:35 -07001405 try:
1406 if not self._RemoteFetch(
1407 initial=is_new,
1408 quiet=quiet,
1409 verbose=verbose,
1410 output_redir=output_redir,
1411 alt_dir=alt_dir,
1412 current_branch_only=current_branch_only,
1413 tags=tags,
1414 prune=prune,
1415 depth=depth,
1416 submodules=submodules,
1417 force_sync=force_sync,
1418 ssh_proxy=ssh_proxy,
1419 clone_filter=clone_filter,
1420 retry_fetches=retry_fetches,
1421 ):
1422 return SyncNetworkHalfResult(
1423 remote_fetched,
1424 SyncNetworkHalfError(
1425 f"Unable to remote fetch project {self.name}",
1426 project=self.name,
1427 ),
1428 )
1429 except RepoError as e:
1430 return SyncNetworkHalfResult(
1431 remote_fetched,
1432 e,
1433 )
Gavin Makea2e3302023-03-11 06:46:20 +00001434
1435 mp = self.manifest.manifestProject
1436 dissociate = mp.dissociate
1437 if dissociate:
1438 alternates_file = os.path.join(
1439 self.objdir, "objects/info/alternates"
1440 )
1441 if os.path.exists(alternates_file):
1442 cmd = ["repack", "-a", "-d"]
1443 p = GitCommand(
1444 self,
1445 cmd,
1446 bare=True,
1447 capture_stdout=bool(output_redir),
1448 merge_output=bool(output_redir),
1449 )
1450 if p.stdout and output_redir:
1451 output_redir.write(p.stdout)
1452 if p.Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07001453 return SyncNetworkHalfResult(
1454 remote_fetched,
1455 GitError(
1456 "Unable to repack alternates", project=self.name
1457 ),
1458 )
Gavin Makea2e3302023-03-11 06:46:20 +00001459 platform_utils.remove(alternates_file)
1460
1461 if self.worktree:
1462 self._InitMRef()
1463 else:
1464 self._InitMirrorHead()
1465 platform_utils.remove(
1466 os.path.join(self.gitdir, "FETCH_HEAD"), missing_ok=True
1467 )
Jason Chang32b59562023-07-14 16:45:35 -07001468 return SyncNetworkHalfResult(remote_fetched)
Gavin Makea2e3302023-03-11 06:46:20 +00001469
1470 def PostRepoUpgrade(self):
1471 self._InitHooks()
1472
1473 def _CopyAndLinkFiles(self):
Gavin Makea2e3302023-03-11 06:46:20 +00001474 for copyfile in self.copyfiles:
1475 copyfile._Copy()
1476 for linkfile in self.linkfiles:
1477 linkfile._Link()
1478
1479 def GetCommitRevisionId(self):
1480 """Get revisionId of a commit.
1481
1482 Use this method instead of GetRevisionId to get the id of the commit
1483 rather than the id of the current git object (for example, a tag)
1484
1485 """
Sylvaine9cb3912023-09-10 23:35:01 +02001486 if self.revisionId:
1487 return self.revisionId
Gavin Makea2e3302023-03-11 06:46:20 +00001488 if not self.revisionExpr.startswith(R_TAGS):
1489 return self.GetRevisionId(self._allrefs)
1490
1491 try:
1492 return self.bare_git.rev_list(self.revisionExpr, "-1")[0]
1493 except GitError:
1494 raise ManifestInvalidRevisionError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001495 f"revision {self.revisionExpr} in {self.name} not found"
Gavin Makea2e3302023-03-11 06:46:20 +00001496 )
1497
1498 def GetRevisionId(self, all_refs=None):
1499 if self.revisionId:
1500 return self.revisionId
1501
1502 rem = self.GetRemote()
1503 rev = rem.ToLocal(self.revisionExpr)
1504
1505 if all_refs is not None and rev in all_refs:
1506 return all_refs[rev]
1507
1508 try:
1509 return self.bare_git.rev_parse("--verify", "%s^0" % rev)
1510 except GitError:
1511 raise ManifestInvalidRevisionError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001512 f"revision {self.revisionExpr} in {self.name} not found"
Gavin Makea2e3302023-03-11 06:46:20 +00001513 )
1514
1515 def SetRevisionId(self, revisionId):
1516 if self.revisionExpr:
1517 self.upstream = self.revisionExpr
1518
1519 self.revisionId = revisionId
1520
Jason Chang32b59562023-07-14 16:45:35 -07001521 def Sync_LocalHalf(
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001522 self,
1523 syncbuf,
1524 force_sync=False,
Josip Sokcevicedadb252024-02-29 09:48:37 -08001525 force_checkout=False,
Jeroen Dhollanderc44ad092024-08-20 10:28:41 +02001526 force_rebase=False,
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001527 submodules=False,
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001528 verbose=False,
Jason Chang32b59562023-07-14 16:45:35 -07001529 ):
Gavin Makea2e3302023-03-11 06:46:20 +00001530 """Perform only the local IO portion of the sync process.
1531
1532 Network access is not required.
1533 """
Jason Chang32b59562023-07-14 16:45:35 -07001534
1535 def fail(error: Exception):
Jason Chang32b59562023-07-14 16:45:35 -07001536 syncbuf.fail(self, error)
1537
Gavin Makea2e3302023-03-11 06:46:20 +00001538 if not os.path.exists(self.gitdir):
Jason Chang32b59562023-07-14 16:45:35 -07001539 fail(
1540 LocalSyncFail(
1541 "Cannot checkout %s due to missing network sync; Run "
1542 "`repo sync -n %s` first." % (self.name, self.name),
1543 project=self.name,
1544 )
Gavin Makea2e3302023-03-11 06:46:20 +00001545 )
1546 return
1547
1548 self._InitWorkTree(force_sync=force_sync, submodules=submodules)
Kaushik Lingarkar99eca452024-12-17 15:16:39 -08001549 # TODO(https://git-scm.com/docs/git-worktree#_bugs): Re-evaluate if
1550 # submodules can be init when using worktrees once its support is
1551 # complete.
Kaushik Lingarkar46232642025-09-10 17:07:35 -07001552 if self.parent and not self.use_git_worktrees:
1553 self._InitSubmodule()
Gavin Makea2e3302023-03-11 06:46:20 +00001554 all_refs = self.bare_ref.all
1555 self.CleanPublishedCache(all_refs)
1556 revid = self.GetRevisionId(all_refs)
1557
1558 # Special case the root of the repo client checkout. Make sure it
1559 # doesn't contain files being checked out to dirs we don't allow.
1560 if self.relpath == ".":
1561 PROTECTED_PATHS = {".repo"}
1562 paths = set(
1563 self.work_git.ls_tree("-z", "--name-only", "--", revid).split(
1564 "\0"
1565 )
1566 )
1567 bad_paths = paths & PROTECTED_PATHS
1568 if bad_paths:
Jason Chang32b59562023-07-14 16:45:35 -07001569 fail(
1570 LocalSyncFail(
1571 "Refusing to checkout project that writes to protected "
1572 "paths: %s" % (", ".join(bad_paths),),
1573 project=self.name,
1574 )
Gavin Makea2e3302023-03-11 06:46:20 +00001575 )
1576 return
1577
1578 def _doff():
1579 self._FastForward(revid)
1580 self._CopyAndLinkFiles()
1581
Jeroen Dhollandere4872ac2025-10-15 17:27:09 +02001582 def _dorebase():
1583 self._Rebase(upstream="@{upstream}")
1584
Gavin Makea2e3302023-03-11 06:46:20 +00001585 def _dosubmodules():
1586 self._SyncSubmodules(quiet=True)
1587
1588 head = self.work_git.GetHead()
1589 if head.startswith(R_HEADS):
1590 branch = head[len(R_HEADS) :]
1591 try:
1592 head = all_refs[head]
1593 except KeyError:
1594 head = None
1595 else:
1596 branch = None
1597
1598 if branch is None or syncbuf.detach_head:
1599 # Currently on a detached HEAD. The user is assumed to
1600 # not have any local modifications worth worrying about.
Erik Elmeke562cea72024-04-29 11:26:53 +02001601 rebase_in_progress = (
1602 self.IsRebaseInProgress() or self.IsCherryPickInProgress()
1603 )
1604 if rebase_in_progress and force_checkout:
1605 self._AbortRebase()
1606 rebase_in_progress = (
1607 self.IsRebaseInProgress() or self.IsCherryPickInProgress()
1608 )
1609 if rebase_in_progress:
Jason Chang32b59562023-07-14 16:45:35 -07001610 fail(_PriorSyncFailedError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001611 return
1612
1613 if head == revid:
1614 # No changes; don't do anything further.
1615 # Except if the head needs to be detached.
1616 if not syncbuf.detach_head:
1617 # The copy/linkfile config may have changed.
1618 self._CopyAndLinkFiles()
1619 return
1620 else:
1621 lost = self._revlist(not_rev(revid), HEAD)
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001622 if lost and verbose:
Gavin Makea2e3302023-03-11 06:46:20 +00001623 syncbuf.info(self, "discarding %d commits", len(lost))
1624
1625 try:
Josip Sokcevicedadb252024-02-29 09:48:37 -08001626 self._Checkout(revid, force_checkout=force_checkout, quiet=True)
Gavin Makea2e3302023-03-11 06:46:20 +00001627 if submodules:
1628 self._SyncSubmodules(quiet=True)
1629 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001630 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001631 return
1632 self._CopyAndLinkFiles()
1633 return
1634
1635 if head == revid:
1636 # No changes; don't do anything further.
1637 #
1638 # The copy/linkfile config may have changed.
1639 self._CopyAndLinkFiles()
1640 return
1641
1642 branch = self.GetBranch(branch)
1643
1644 if not branch.LocalMerge:
1645 # The current branch has no tracking configuration.
1646 # Jump off it to a detached HEAD.
1647 syncbuf.info(
1648 self, "leaving %s; does not track upstream", branch.name
1649 )
1650 try:
1651 self._Checkout(revid, quiet=True)
1652 if submodules:
1653 self._SyncSubmodules(quiet=True)
1654 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001655 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001656 return
1657 self._CopyAndLinkFiles()
1658 return
1659
1660 upstream_gain = self._revlist(not_rev(HEAD), revid)
1661
1662 # See if we can perform a fast forward merge. This can happen if our
1663 # branch isn't in the exact same state as we last published.
1664 try:
Jason Chang87058c62023-09-27 11:34:43 -07001665 self.work_git.merge_base(
1666 "--is-ancestor", HEAD, revid, log_as_error=False
1667 )
Gavin Makea2e3302023-03-11 06:46:20 +00001668 # Skip the published logic.
1669 pub = False
1670 except GitError:
1671 pub = self.WasPublished(branch.name, all_refs)
1672
1673 if pub:
1674 not_merged = self._revlist(not_rev(revid), pub)
1675 if not_merged:
Jeroen Dhollandere4872ac2025-10-15 17:27:09 +02001676 if upstream_gain:
1677 if force_rebase:
1678 # Try to rebase local published but not merged changes
1679 # on top of the upstream changes.
1680 syncbuf.later1(self, _dorebase, not verbose)
1681 else:
1682 # The user has published this branch and some of those
1683 # commits are not yet merged upstream. We do not want
1684 # to rewrite the published commits so we punt.
1685 fail(
1686 LocalSyncFail(
1687 "branch %s is published (but not merged) and "
1688 "is now %d commits behind. Fix this manually "
1689 "or rerun with the --rebase option to force a "
1690 "rebase." % (branch.name, len(upstream_gain)),
1691 project=self.name,
1692 )
Jason Chang32b59562023-07-14 16:45:35 -07001693 )
Brian Norrisb5774442024-09-17 15:53:37 -07001694 return
1695 syncbuf.later1(self, _doff, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001696 return
1697 elif pub == head:
1698 # All published commits are merged, and thus we are a
1699 # strict subset. We can fast-forward safely.
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001700 syncbuf.later1(self, _doff, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001701 if submodules:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001702 syncbuf.later1(self, _dosubmodules, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001703 return
1704
1705 # Examine the local commits not in the remote. Find the
1706 # last one attributed to this user, if any.
1707 local_changes = self._revlist(not_rev(revid), HEAD, format="%H %ce")
1708 last_mine = None
1709 cnt_mine = 0
1710 for commit in local_changes:
1711 commit_id, committer_email = commit.split(" ", 1)
1712 if committer_email == self.UserEmail:
1713 last_mine = commit_id
1714 cnt_mine += 1
1715
1716 if not upstream_gain and cnt_mine == len(local_changes):
1717 # The copy/linkfile config may have changed.
1718 self._CopyAndLinkFiles()
1719 return
1720
1721 if self.IsDirty(consider_untracked=False):
Jason Chang32b59562023-07-14 16:45:35 -07001722 fail(_DirtyError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001723 return
1724
1725 # If the upstream switched on us, warn the user.
1726 if branch.merge != self.revisionExpr:
1727 if branch.merge and self.revisionExpr:
1728 syncbuf.info(
1729 self,
1730 "manifest switched %s...%s",
1731 branch.merge,
1732 self.revisionExpr,
1733 )
1734 elif branch.merge:
1735 syncbuf.info(self, "manifest no longer tracks %s", branch.merge)
1736
1737 if cnt_mine < len(local_changes):
1738 # Upstream rebased. Not everything in HEAD was created by this user.
1739 syncbuf.info(
1740 self,
1741 "discarding %d commits removed from upstream",
1742 len(local_changes) - cnt_mine,
1743 )
1744
1745 branch.remote = self.GetRemote()
Sylvain56a5a012023-09-11 13:38:00 +02001746 if not IsId(self.revisionExpr):
Gavin Makea2e3302023-03-11 06:46:20 +00001747 # In case of manifest sync the revisionExpr might be a SHA1.
1748 branch.merge = self.revisionExpr
1749 if not branch.merge.startswith("refs/"):
1750 branch.merge = R_HEADS + branch.merge
1751 branch.Save()
1752
1753 if cnt_mine > 0 and self.rebase:
1754
1755 def _docopyandlink():
1756 self._CopyAndLinkFiles()
1757
1758 def _dorebase():
1759 self._Rebase(upstream="%s^1" % last_mine, onto=revid)
1760
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001761 syncbuf.later2(self, _dorebase, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001762 if submodules:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001763 syncbuf.later2(self, _dosubmodules, not verbose)
1764 syncbuf.later2(self, _docopyandlink, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001765 elif local_changes:
1766 try:
1767 self._ResetHard(revid)
1768 if submodules:
1769 self._SyncSubmodules(quiet=True)
1770 self._CopyAndLinkFiles()
1771 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001772 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001773 return
1774 else:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001775 syncbuf.later1(self, _doff, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001776 if submodules:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001777 syncbuf.later1(self, _dosubmodules, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001778
1779 def AddCopyFile(self, src, dest, topdir):
1780 """Mark |src| for copying to |dest| (relative to |topdir|).
1781
1782 No filesystem changes occur here. Actual copying happens later on.
1783
1784 Paths should have basic validation run on them before being queued.
1785 Further checking will be handled when the actual copy happens.
1786 """
Peter Kjellerstedt412367b2025-11-08 00:06:16 +01001787 self.copyfiles[_CopyFile(self.worktree, src, topdir, dest)] = True
Gavin Makea2e3302023-03-11 06:46:20 +00001788
1789 def AddLinkFile(self, src, dest, topdir):
1790 """Mark |dest| to create a symlink (relative to |topdir|) pointing to
1791 |src|.
1792
1793 No filesystem changes occur here. Actual linking happens later on.
1794
1795 Paths should have basic validation run on them before being queued.
1796 Further checking will be handled when the actual link happens.
1797 """
Peter Kjellerstedt412367b2025-11-08 00:06:16 +01001798 self.linkfiles[_LinkFile(self.worktree, src, topdir, dest)] = True
Gavin Makea2e3302023-03-11 06:46:20 +00001799
1800 def AddAnnotation(self, name, value, keep):
1801 self.annotations.append(Annotation(name, value, keep))
1802
1803 def DownloadPatchSet(self, change_id, patch_id):
1804 """Download a single patch set of a single change to FETCH_HEAD."""
1805 remote = self.GetRemote()
1806
1807 cmd = ["fetch", remote.name]
1808 cmd.append(
1809 "refs/changes/%2.2d/%d/%d" % (change_id % 100, change_id, patch_id)
1810 )
Jason Chang1a3612f2023-08-08 14:12:53 -07001811 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001812 return DownloadedChange(
1813 self,
1814 self.GetRevisionId(),
1815 change_id,
1816 patch_id,
1817 self.bare_git.rev_parse("FETCH_HEAD"),
1818 )
1819
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001820 def DeleteWorktree(self, verbose=False, force=False):
Gavin Makea2e3302023-03-11 06:46:20 +00001821 """Delete the source checkout and any other housekeeping tasks.
1822
1823 This currently leaves behind the internal .repo/ cache state. This
1824 helps when switching branches or manifest changes get reverted as we
1825 don't have to redownload all the git objects. But we should do some GC
1826 at some point.
1827
1828 Args:
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001829 verbose: Whether to show verbose messages.
Gavin Makea2e3302023-03-11 06:46:20 +00001830 force: Always delete tree even if dirty.
Doug Anderson3ba5f952011-04-07 12:51:04 -07001831
1832 Returns:
Gavin Makea2e3302023-03-11 06:46:20 +00001833 True if the worktree was completely cleaned out.
1834 """
1835 if self.IsDirty():
1836 if force:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00001837 logger.warning(
Gavin Makea2e3302023-03-11 06:46:20 +00001838 "warning: %s: Removing dirty project: uncommitted changes "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001839 "lost.",
1840 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001841 )
1842 else:
Jason Chang32b59562023-07-14 16:45:35 -07001843 msg = (
Josip Sokcevica3a73722024-03-14 23:50:33 +00001844 "error: %s: Cannot remove project: uncommitted "
Jason Chang32b59562023-07-14 16:45:35 -07001845 "changes are present.\n" % self.RelPath(local=False)
Gavin Makea2e3302023-03-11 06:46:20 +00001846 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001847 logger.error(msg)
Josip Sokcevica3a73722024-03-14 23:50:33 +00001848 raise DeleteDirtyWorktreeError(msg, project=self.name)
Wink Saville02d79452009-04-10 13:01:24 -07001849
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001850 if verbose:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001851 print(f"{self.RelPath(local=False)}: Deleting obsolete checkout.")
Wink Saville02d79452009-04-10 13:01:24 -07001852
Gavin Makea2e3302023-03-11 06:46:20 +00001853 # Unlock and delink from the main worktree. We don't use git's worktree
1854 # remove because it will recursively delete projects -- we handle that
1855 # ourselves below. https://crbug.com/git/48
1856 if self.use_git_worktrees:
Kaiyi Li46819a72024-03-27 07:21:43 -07001857 needle = os.path.realpath(self.gitdir)
Gavin Makea2e3302023-03-11 06:46:20 +00001858 # Find the git worktree commondir under .repo/worktrees/.
1859 output = self.bare_git.worktree("list", "--porcelain").splitlines()[
1860 0
1861 ]
1862 assert output.startswith("worktree "), output
1863 commondir = output[9:]
1864 # Walk each of the git worktrees to see where they point.
1865 configs = os.path.join(commondir, "worktrees")
1866 for name in os.listdir(configs):
1867 gitdir = os.path.join(configs, name, "gitdir")
1868 with open(gitdir) as fp:
1869 relpath = fp.read().strip()
1870 # Resolve the checkout path and see if it matches this project.
Kaiyi Li46819a72024-03-27 07:21:43 -07001871 fullpath = os.path.realpath(
Gavin Makea2e3302023-03-11 06:46:20 +00001872 os.path.join(configs, name, relpath)
1873 )
1874 if fullpath == needle:
1875 platform_utils.rmtree(os.path.join(configs, name))
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001876
Gavin Makea2e3302023-03-11 06:46:20 +00001877 # Delete the .git directory first, so we're less likely to have a
1878 # partially working git repository around. There shouldn't be any git
1879 # projects here, so rmtree works.
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001880
Gavin Makea2e3302023-03-11 06:46:20 +00001881 # Try to remove plain files first in case of git worktrees. If this
1882 # fails for any reason, we'll fall back to rmtree, and that'll display
1883 # errors if it can't remove things either.
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001884 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001885 platform_utils.remove(self.gitdir)
1886 except OSError:
1887 pass
1888 try:
1889 platform_utils.rmtree(self.gitdir)
1890 except OSError as e:
1891 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001892 logger.error("error: %s: %s", self.gitdir, e)
1893 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00001894 "error: %s: Failed to delete obsolete checkout; remove "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001895 "manually, then run `repo sync -l`.",
1896 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001897 )
Jason Chang32b59562023-07-14 16:45:35 -07001898 raise DeleteWorktreeError(aggregate_errors=[e])
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001899
Gavin Makea2e3302023-03-11 06:46:20 +00001900 # Delete everything under the worktree, except for directories that
1901 # contain another git project.
1902 dirs_to_remove = []
1903 failed = False
Jason Chang32b59562023-07-14 16:45:35 -07001904 errors = []
Gavin Makea2e3302023-03-11 06:46:20 +00001905 for root, dirs, files in platform_utils.walk(self.worktree):
1906 for f in files:
1907 path = os.path.join(root, f)
1908 try:
1909 platform_utils.remove(path)
1910 except OSError as e:
1911 if e.errno != errno.ENOENT:
Josip Sokcevic4217a822024-01-24 13:54:25 -08001912 logger.warning("%s: Failed to remove: %s", path, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001913 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001914 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001915 dirs[:] = [
1916 d
1917 for d in dirs
1918 if not os.path.lexists(os.path.join(root, d, ".git"))
1919 ]
1920 dirs_to_remove += [
1921 os.path.join(root, d)
1922 for d in dirs
1923 if os.path.join(root, d) not in dirs_to_remove
1924 ]
1925 for d in reversed(dirs_to_remove):
1926 if platform_utils.islink(d):
1927 try:
1928 platform_utils.remove(d)
1929 except OSError as e:
1930 if e.errno != errno.ENOENT:
Josip Sokcevic4217a822024-01-24 13:54:25 -08001931 logger.warning("%s: Failed to remove: %s", d, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001932 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001933 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001934 elif not platform_utils.listdir(d):
1935 try:
1936 platform_utils.rmdir(d)
1937 except OSError as e:
1938 if e.errno != errno.ENOENT:
Josip Sokcevic4217a822024-01-24 13:54:25 -08001939 logger.warning("%s: Failed to remove: %s", d, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001940 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001941 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001942 if failed:
Josip Sokcevic4217a822024-01-24 13:54:25 -08001943 rename_path = (
1944 f"{self.worktree}_repo_to_be_deleted_{int(time.time())}"
Gavin Makea2e3302023-03-11 06:46:20 +00001945 )
Josip Sokcevic4217a822024-01-24 13:54:25 -08001946 try:
1947 platform_utils.rename(self.worktree, rename_path)
1948 logger.warning(
1949 "warning: renamed %s to %s. You can delete it, but you "
1950 "might need elevated permissions (e.g. root)",
1951 self.worktree,
1952 rename_path,
1953 )
1954 # Rename successful! Clear the errors.
1955 errors = []
1956 except OSError:
1957 logger.error(
1958 "%s: Failed to delete obsolete checkout.\n",
1959 " Remove manually, then run `repo sync -l`.",
1960 self.RelPath(local=False),
1961 )
1962 raise DeleteWorktreeError(aggregate_errors=errors)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07001963
Gavin Makea2e3302023-03-11 06:46:20 +00001964 # Try deleting parent dirs if they are empty.
1965 path = self.worktree
1966 while path != self.manifest.topdir:
1967 try:
1968 platform_utils.rmdir(path)
1969 except OSError as e:
1970 if e.errno != errno.ENOENT:
1971 break
1972 path = os.path.dirname(path)
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001973
Gavin Makea2e3302023-03-11 06:46:20 +00001974 return True
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001975
Gavin Makea2e3302023-03-11 06:46:20 +00001976 def StartBranch(self, name, branch_merge="", revision=None):
1977 """Create a new branch off the manifest's revision."""
1978 if not branch_merge:
1979 branch_merge = self.revisionExpr
1980 head = self.work_git.GetHead()
1981 if head == (R_HEADS + name):
1982 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001983
David Pursehouse8a68ff92012-09-24 12:15:13 +09001984 all_refs = self.bare_ref.all
Gavin Makea2e3302023-03-11 06:46:20 +00001985 if R_HEADS + name in all_refs:
Jason Chang1a3612f2023-08-08 14:12:53 -07001986 GitCommand(
1987 self, ["checkout", "-q", name, "--"], verify_command=True
1988 ).Wait()
1989 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001990
Gavin Makea2e3302023-03-11 06:46:20 +00001991 branch = self.GetBranch(name)
1992 branch.remote = self.GetRemote()
1993 branch.merge = branch_merge
Sylvain56a5a012023-09-11 13:38:00 +02001994 if not branch.merge.startswith("refs/") and not IsId(branch_merge):
Gavin Makea2e3302023-03-11 06:46:20 +00001995 branch.merge = R_HEADS + branch_merge
Shawn O. Pearce88443382010-10-08 10:02:09 +02001996
Gavin Makea2e3302023-03-11 06:46:20 +00001997 if revision is None:
1998 revid = self.GetRevisionId(all_refs)
Shawn O. Pearce88443382010-10-08 10:02:09 +02001999 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002000 revid = self.work_git.rev_parse(revision)
Brian Harring14a66742012-09-28 20:21:57 -07002001
Gavin Makea2e3302023-03-11 06:46:20 +00002002 if head.startswith(R_HEADS):
Kevin Degiabaa7f32014-11-12 11:27:45 -07002003 try:
Gavin Makea2e3302023-03-11 06:46:20 +00002004 head = all_refs[head]
2005 except KeyError:
2006 head = None
2007 if revid and head and revid == head:
2008 ref = R_HEADS + name
2009 self.work_git.update_ref(ref, revid)
2010 self.work_git.symbolic_ref(HEAD, ref)
2011 branch.Save()
2012 return True
Kevin Degib1a07b82015-07-27 13:33:43 -06002013
Jason Chang1a3612f2023-08-08 14:12:53 -07002014 GitCommand(
2015 self,
2016 ["checkout", "-q", "-b", branch.name, revid],
2017 verify_command=True,
2018 ).Wait()
2019 branch.Save()
2020 return True
Kevin Degi384b3c52014-10-16 16:02:58 -06002021
Gavin Makea2e3302023-03-11 06:46:20 +00002022 def CheckoutBranch(self, name):
2023 """Checkout a local topic branch.
Shawn O. Pearce2816d4f2009-03-03 17:53:18 -08002024
Gavin Makea2e3302023-03-11 06:46:20 +00002025 Args:
2026 name: The name of the branch to checkout.
Shawn O. Pearce88443382010-10-08 10:02:09 +02002027
Gavin Makea2e3302023-03-11 06:46:20 +00002028 Returns:
Jason Chang1a3612f2023-08-08 14:12:53 -07002029 True if the checkout succeeded; False if the
2030 branch doesn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00002031 """
2032 rev = R_HEADS + name
2033 head = self.work_git.GetHead()
2034 if head == rev:
2035 # Already on the branch.
2036 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02002037
Gavin Makea2e3302023-03-11 06:46:20 +00002038 all_refs = self.bare_ref.all
2039 try:
2040 revid = all_refs[rev]
2041 except KeyError:
2042 # Branch does not exist in this project.
Jason Chang1a3612f2023-08-08 14:12:53 -07002043 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002044
Gavin Makea2e3302023-03-11 06:46:20 +00002045 if head.startswith(R_HEADS):
2046 try:
2047 head = all_refs[head]
2048 except KeyError:
2049 head = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002050
Gavin Makea2e3302023-03-11 06:46:20 +00002051 if head == revid:
2052 # Same revision; just update HEAD to point to the new
2053 # target branch, but otherwise take no other action.
Gavin Mak74edacd2025-07-17 17:54:22 +00002054 self.work_git.SetHead(R_HEADS + name)
Gavin Makea2e3302023-03-11 06:46:20 +00002055 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05002056
Jason Chang1a3612f2023-08-08 14:12:53 -07002057 GitCommand(
2058 self,
2059 ["checkout", name, "--"],
2060 capture_stdout=True,
2061 capture_stderr=True,
2062 verify_command=True,
2063 ).Wait()
2064 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05002065
Gavin Makea2e3302023-03-11 06:46:20 +00002066 def AbandonBranch(self, name):
2067 """Destroy a local topic branch.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002068
Gavin Makea2e3302023-03-11 06:46:20 +00002069 Args:
2070 name: The name of the branch to abandon.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002071
Gavin Makea2e3302023-03-11 06:46:20 +00002072 Returns:
Jason Changf9aacd42023-08-03 14:38:00 -07002073 True if the abandon succeeded; Raises GitCommandError if it didn't;
2074 None if the branch didn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00002075 """
2076 rev = R_HEADS + name
2077 all_refs = self.bare_ref.all
2078 if rev not in all_refs:
2079 # Doesn't exist
2080 return None
2081
2082 head = self.work_git.GetHead()
2083 if head == rev:
2084 # We can't destroy the branch while we are sitting
2085 # on it. Switch to a detached HEAD.
2086 head = all_refs[head]
2087
2088 revid = self.GetRevisionId(all_refs)
2089 if head == revid:
Gavin Mak74edacd2025-07-17 17:54:22 +00002090 self.work_git.DetachHead(revid)
Gavin Makea2e3302023-03-11 06:46:20 +00002091 else:
2092 self._Checkout(revid, quiet=True)
Jason Changf9aacd42023-08-03 14:38:00 -07002093 GitCommand(
2094 self,
2095 ["branch", "-D", name],
2096 capture_stdout=True,
2097 capture_stderr=True,
2098 verify_command=True,
2099 ).Wait()
2100 return True
Gavin Makea2e3302023-03-11 06:46:20 +00002101
2102 def PruneHeads(self):
2103 """Prune any topic branches already merged into upstream."""
2104 cb = self.CurrentBranch
2105 kill = []
2106 left = self._allrefs
2107 for name in left.keys():
2108 if name.startswith(R_HEADS):
2109 name = name[len(R_HEADS) :]
2110 if cb is None or name != cb:
2111 kill.append(name)
2112
2113 # Minor optimization: If there's nothing to prune, then don't try to
2114 # read any project state.
2115 if not kill and not cb:
2116 return []
2117
2118 rev = self.GetRevisionId(left)
2119 if (
2120 cb is not None
2121 and not self._revlist(HEAD + "..." + rev)
2122 and not self.IsDirty(consider_untracked=False)
2123 ):
2124 self.work_git.DetachHead(HEAD)
2125 kill.append(cb)
2126
2127 if kill:
2128 old = self.bare_git.GetHead()
2129
2130 try:
2131 self.bare_git.DetachHead(rev)
2132
2133 b = ["branch", "-d"]
2134 b.extend(kill)
2135 b = GitCommand(
2136 self, b, bare=True, capture_stdout=True, capture_stderr=True
2137 )
2138 b.Wait()
2139 finally:
Sylvain56a5a012023-09-11 13:38:00 +02002140 if IsId(old):
Gavin Makea2e3302023-03-11 06:46:20 +00002141 self.bare_git.DetachHead(old)
2142 else:
2143 self.bare_git.SetHead(old)
2144 left = self._allrefs
2145
2146 for branch in kill:
2147 if (R_HEADS + branch) not in left:
2148 self.CleanPublishedCache()
2149 break
2150
2151 if cb and cb not in kill:
2152 kill.append(cb)
2153 kill.sort()
2154
2155 kept = []
2156 for branch in kill:
2157 if R_HEADS + branch in left:
2158 branch = self.GetBranch(branch)
2159 base = branch.LocalMerge
2160 if not base:
2161 base = rev
2162 kept.append(ReviewableBranch(self, branch, base))
2163 return kept
2164
2165 def GetRegisteredSubprojects(self):
2166 result = []
2167
2168 def rec(subprojects):
2169 if not subprojects:
2170 return
2171 result.extend(subprojects)
2172 for p in subprojects:
2173 rec(p.subprojects)
2174
2175 rec(self.subprojects)
2176 return result
2177
2178 def _GetSubmodules(self):
2179 # Unfortunately we cannot call `git submodule status --recursive` here
2180 # because the working tree might not exist yet, and it cannot be used
2181 # without a working tree in its current implementation.
2182
2183 def get_submodules(gitdir, rev):
2184 # Parse .gitmodules for submodule sub_paths and sub_urls.
Albert Akmukhametov4b94e772025-02-17 17:04:42 +03002185 sub_paths, sub_urls, sub_shallows = parse_gitmodules(gitdir, rev)
Gavin Makea2e3302023-03-11 06:46:20 +00002186 if not sub_paths:
2187 return []
2188 # Run `git ls-tree` to read SHAs of submodule object, which happen
2189 # to be revision of submodule repository.
2190 sub_revs = git_ls_tree(gitdir, rev, sub_paths)
2191 submodules = []
Albert Akmukhametov4b94e772025-02-17 17:04:42 +03002192 for sub_path, sub_url, sub_shallow in zip(
2193 sub_paths, sub_urls, sub_shallows
2194 ):
Gavin Makea2e3302023-03-11 06:46:20 +00002195 try:
2196 sub_rev = sub_revs[sub_path]
2197 except KeyError:
2198 # Ignore non-exist submodules.
2199 continue
Albert Akmukhametov4b94e772025-02-17 17:04:42 +03002200 submodules.append((sub_rev, sub_path, sub_url, sub_shallow))
Gavin Makea2e3302023-03-11 06:46:20 +00002201 return submodules
2202
2203 re_path = re.compile(r"^submodule\.(.+)\.path=(.*)$")
2204 re_url = re.compile(r"^submodule\.(.+)\.url=(.*)$")
Albert Akmukhametov4b94e772025-02-17 17:04:42 +03002205 re_shallow = re.compile(r"^submodule\.(.+)\.shallow=(.*)$")
Gavin Makea2e3302023-03-11 06:46:20 +00002206
2207 def parse_gitmodules(gitdir, rev):
2208 cmd = ["cat-file", "blob", "%s:.gitmodules" % rev]
2209 try:
2210 p = GitCommand(
2211 None,
2212 cmd,
2213 capture_stdout=True,
2214 capture_stderr=True,
2215 bare=True,
2216 gitdir=gitdir,
2217 )
2218 except GitError:
Albert Akmukhametov4b94e772025-02-17 17:04:42 +03002219 return [], [], []
Gavin Makea2e3302023-03-11 06:46:20 +00002220 if p.Wait() != 0:
Albert Akmukhametov4b94e772025-02-17 17:04:42 +03002221 return [], [], []
Gavin Makea2e3302023-03-11 06:46:20 +00002222
2223 gitmodules_lines = []
2224 fd, temp_gitmodules_path = tempfile.mkstemp()
2225 try:
2226 os.write(fd, p.stdout.encode("utf-8"))
2227 os.close(fd)
2228 cmd = ["config", "--file", temp_gitmodules_path, "--list"]
2229 p = GitCommand(
2230 None,
2231 cmd,
2232 capture_stdout=True,
2233 capture_stderr=True,
2234 bare=True,
2235 gitdir=gitdir,
2236 )
2237 if p.Wait() != 0:
Albert Akmukhametov4b94e772025-02-17 17:04:42 +03002238 return [], [], []
Gavin Makea2e3302023-03-11 06:46:20 +00002239 gitmodules_lines = p.stdout.split("\n")
2240 except GitError:
Albert Akmukhametov4b94e772025-02-17 17:04:42 +03002241 return [], [], []
Gavin Makea2e3302023-03-11 06:46:20 +00002242 finally:
2243 platform_utils.remove(temp_gitmodules_path)
2244
2245 names = set()
2246 paths = {}
2247 urls = {}
Albert Akmukhametov4b94e772025-02-17 17:04:42 +03002248 shallows = {}
Gavin Makea2e3302023-03-11 06:46:20 +00002249 for line in gitmodules_lines:
2250 if not line:
2251 continue
2252 m = re_path.match(line)
2253 if m:
2254 names.add(m.group(1))
2255 paths[m.group(1)] = m.group(2)
2256 continue
2257 m = re_url.match(line)
2258 if m:
2259 names.add(m.group(1))
2260 urls[m.group(1)] = m.group(2)
2261 continue
Albert Akmukhametov4b94e772025-02-17 17:04:42 +03002262 m = re_shallow.match(line)
2263 if m:
2264 names.add(m.group(1))
2265 shallows[m.group(1)] = m.group(2)
2266 continue
Gavin Makea2e3302023-03-11 06:46:20 +00002267 names = sorted(names)
2268 return (
2269 [paths.get(name, "") for name in names],
2270 [urls.get(name, "") for name in names],
Albert Akmukhametov4b94e772025-02-17 17:04:42 +03002271 [shallows.get(name, "") for name in names],
Gavin Makea2e3302023-03-11 06:46:20 +00002272 )
2273
2274 def git_ls_tree(gitdir, rev, paths):
2275 cmd = ["ls-tree", rev, "--"]
2276 cmd.extend(paths)
2277 try:
2278 p = GitCommand(
2279 None,
2280 cmd,
2281 capture_stdout=True,
2282 capture_stderr=True,
2283 bare=True,
2284 gitdir=gitdir,
2285 )
2286 except GitError:
2287 return []
2288 if p.Wait() != 0:
2289 return []
2290 objects = {}
2291 for line in p.stdout.split("\n"):
2292 if not line.strip():
2293 continue
2294 object_rev, object_path = line.split()[2:4]
2295 objects[object_path] = object_rev
2296 return objects
2297
2298 try:
2299 rev = self.GetRevisionId()
Kaushik Lingarkar584863f2024-10-16 14:17:24 -07002300 except (GitError, ManifestInvalidRevisionError):
2301 # The git repo may be outdated (i.e. not fetched yet) and querying
2302 # its submodules using the revision may not work; so return here.
Gavin Makea2e3302023-03-11 06:46:20 +00002303 return []
2304 return get_submodules(self.gitdir, rev)
2305
2306 def GetDerivedSubprojects(self):
2307 result = []
2308 if not self.Exists:
2309 # If git repo does not exist yet, querying its submodules will
2310 # mess up its states; so return here.
2311 return result
Albert Akmukhametov4b94e772025-02-17 17:04:42 +03002312 for rev, path, url, shallow in self._GetSubmodules():
Gavin Makea2e3302023-03-11 06:46:20 +00002313 name = self.manifest.GetSubprojectName(self, path)
2314 (
2315 relpath,
2316 worktree,
2317 gitdir,
2318 objdir,
2319 ) = self.manifest.GetSubprojectPaths(self, name, path)
2320 project = self.manifest.paths.get(relpath)
2321 if project:
2322 result.extend(project.GetDerivedSubprojects())
2323 continue
2324
2325 if url.startswith(".."):
2326 url = urllib.parse.urljoin("%s/" % self.remote.url, url)
2327 remote = RemoteSpec(
2328 self.remote.name,
2329 url=url,
2330 pushUrl=self.remote.pushUrl,
2331 review=self.remote.review,
2332 revision=self.remote.revision,
2333 )
Albert Akmukhametov4b94e772025-02-17 17:04:42 +03002334 clone_depth = 1 if shallow.lower() == "true" else None
Gavin Makea2e3302023-03-11 06:46:20 +00002335 subproject = Project(
2336 manifest=self.manifest,
2337 name=name,
2338 remote=remote,
2339 gitdir=gitdir,
2340 objdir=objdir,
2341 worktree=worktree,
2342 relpath=relpath,
2343 revisionExpr=rev,
2344 revisionId=rev,
2345 rebase=self.rebase,
2346 groups=self.groups,
2347 sync_c=self.sync_c,
2348 sync_s=self.sync_s,
2349 sync_tags=self.sync_tags,
2350 parent=self,
Albert Akmukhametov4b94e772025-02-17 17:04:42 +03002351 clone_depth=clone_depth,
Gavin Makea2e3302023-03-11 06:46:20 +00002352 is_derived=True,
2353 )
2354 result.append(subproject)
2355 result.extend(subproject.GetDerivedSubprojects())
Gavin Makea2e3302023-03-11 06:46:20 +00002356 return result
2357
2358 def EnableRepositoryExtension(self, key, value="true", version=1):
2359 """Enable git repository extension |key| with |value|.
2360
2361 Args:
2362 key: The extension to enabled. Omit the "extensions." prefix.
2363 value: The value to use for the extension.
2364 version: The minimum git repository version needed.
2365 """
2366 # Make sure the git repo version is new enough already.
2367 found_version = self.config.GetInt("core.repositoryFormatVersion")
2368 if found_version is None:
2369 found_version = 0
2370 if found_version < version:
2371 self.config.SetString("core.repositoryFormatVersion", str(version))
2372
2373 # Enable the extension!
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002374 self.config.SetString(f"extensions.{key}", value)
Gavin Makea2e3302023-03-11 06:46:20 +00002375
2376 def ResolveRemoteHead(self, name=None):
2377 """Find out what the default branch (HEAD) points to.
2378
2379 Normally this points to refs/heads/master, but projects are moving to
2380 main. Support whatever the server uses rather than hardcoding "master"
2381 ourselves.
2382 """
2383 if name is None:
2384 name = self.remote.name
2385
2386 # The output will look like (NB: tabs are separators):
2387 # ref: refs/heads/master HEAD
2388 # 5f6803b100bb3cd0f534e96e88c91373e8ed1c44 HEAD
2389 output = self.bare_git.ls_remote(
2390 "-q", "--symref", "--exit-code", name, "HEAD"
2391 )
2392
2393 for line in output.splitlines():
2394 lhs, rhs = line.split("\t", 1)
2395 if rhs == "HEAD" and lhs.startswith("ref:"):
2396 return lhs[4:].strip()
2397
2398 return None
2399
2400 def _CheckForImmutableRevision(self):
2401 try:
2402 # if revision (sha or tag) is not present then following function
2403 # throws an error.
Josip Sokcevic454fdaf2024-10-07 17:33:38 +00002404 revs = [f"{self.revisionExpr}^0"]
2405 upstream_rev = None
Kaushik Lingarkar67383bd2025-09-09 13:14:34 -07002406
2407 # Only check upstream when using superproject.
2408 if self.upstream and self.manifest.manifestProject.use_superproject:
Josip Sokcevic454fdaf2024-10-07 17:33:38 +00002409 upstream_rev = self.GetRemote().ToLocal(self.upstream)
2410 revs.append(upstream_rev)
2411
Gavin Makea2e3302023-03-11 06:46:20 +00002412 self.bare_git.rev_list(
Jason Chang87058c62023-09-27 11:34:43 -07002413 "-1",
2414 "--missing=allow-any",
Josip Sokcevic454fdaf2024-10-07 17:33:38 +00002415 *revs,
Jason Chang87058c62023-09-27 11:34:43 -07002416 "--",
2417 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002418 )
Josip Sokcevic454fdaf2024-10-07 17:33:38 +00002419
Kaushik Lingarkar67383bd2025-09-09 13:14:34 -07002420 # Only verify upstream relationship for superproject scenarios
2421 # without affecting plain usage.
2422 if self.upstream and self.manifest.manifestProject.use_superproject:
Gavin Makea2e3302023-03-11 06:46:20 +00002423 self.bare_git.merge_base(
Jason Chang87058c62023-09-27 11:34:43 -07002424 "--is-ancestor",
2425 self.revisionExpr,
Josip Sokcevic454fdaf2024-10-07 17:33:38 +00002426 upstream_rev,
Jason Chang87058c62023-09-27 11:34:43 -07002427 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002428 )
2429 return True
2430 except GitError:
2431 # There is no such persistent revision. We have to fetch it.
2432 return False
2433
2434 def _FetchArchive(self, tarpath, cwd=None):
2435 cmd = ["archive", "-v", "-o", tarpath]
2436 cmd.append("--remote=%s" % self.remote.url)
2437 cmd.append("--prefix=%s/" % self.RelPath(local=False))
2438 cmd.append(self.revisionExpr)
2439
2440 command = GitCommand(
Jason Chang32b59562023-07-14 16:45:35 -07002441 self,
2442 cmd,
2443 cwd=cwd,
2444 capture_stdout=True,
2445 capture_stderr=True,
2446 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00002447 )
Jason Chang32b59562023-07-14 16:45:35 -07002448 command.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00002449
2450 def _RemoteFetch(
2451 self,
2452 name=None,
2453 current_branch_only=False,
2454 initial=False,
2455 quiet=False,
2456 verbose=False,
2457 output_redir=None,
2458 alt_dir=None,
2459 tags=True,
2460 prune=False,
2461 depth=None,
2462 submodules=False,
2463 ssh_proxy=None,
2464 force_sync=False,
2465 clone_filter=None,
2466 retry_fetches=2,
2467 retry_sleep_initial_sec=4.0,
2468 retry_exp_factor=2.0,
Jason Chang32b59562023-07-14 16:45:35 -07002469 ) -> bool:
Gavin Makea2e3302023-03-11 06:46:20 +00002470 tag_name = None
2471 # The depth should not be used when fetching to a mirror because
2472 # it will result in a shallow repository that cannot be cloned or
2473 # fetched from.
2474 # The repo project should also never be synced with partial depth.
2475 if self.manifest.IsMirror or self.relpath == ".repo/repo":
2476 depth = None
2477
2478 if depth:
2479 current_branch_only = True
2480
Sylvain56a5a012023-09-11 13:38:00 +02002481 is_sha1 = bool(IsId(self.revisionExpr))
Gavin Makea2e3302023-03-11 06:46:20 +00002482
2483 if current_branch_only:
2484 if self.revisionExpr.startswith(R_TAGS):
2485 # This is a tag and its commit id should never change.
2486 tag_name = self.revisionExpr[len(R_TAGS) :]
2487 elif self.upstream and self.upstream.startswith(R_TAGS):
2488 # This is a tag and its commit id should never change.
2489 tag_name = self.upstream[len(R_TAGS) :]
2490
2491 if is_sha1 or tag_name is not None:
2492 if self._CheckForImmutableRevision():
2493 if verbose:
2494 print(
2495 "Skipped fetching project %s (already have "
2496 "persistent ref)" % self.name
2497 )
2498 return True
2499 if is_sha1 and not depth:
2500 # When syncing a specific commit and --depth is not set:
2501 # * if upstream is explicitly specified and is not a sha1, fetch
2502 # only upstream as users expect only upstream to be fetch.
2503 # Note: The commit might not be in upstream in which case the
2504 # sync will fail.
2505 # * otherwise, fetch all branches to make sure we end up with
2506 # the specific commit.
2507 if self.upstream:
Sylvain56a5a012023-09-11 13:38:00 +02002508 current_branch_only = not IsId(self.upstream)
Gavin Makea2e3302023-03-11 06:46:20 +00002509 else:
2510 current_branch_only = False
2511
2512 if not name:
2513 name = self.remote.name
2514
2515 remote = self.GetRemote(name)
2516 if not remote.PreConnectFetch(ssh_proxy):
2517 ssh_proxy = None
2518
2519 if initial:
2520 if alt_dir and "objects" == os.path.basename(alt_dir):
2521 ref_dir = os.path.dirname(alt_dir)
2522 packed_refs = os.path.join(self.gitdir, "packed-refs")
2523
2524 all_refs = self.bare_ref.all
2525 ids = set(all_refs.values())
2526 tmp = set()
2527
2528 for r, ref_id in GitRefs(ref_dir).all.items():
2529 if r not in all_refs:
2530 if r.startswith(R_TAGS) or remote.WritesTo(r):
2531 all_refs[r] = ref_id
2532 ids.add(ref_id)
2533 continue
2534
2535 if ref_id in ids:
2536 continue
2537
2538 r = "refs/_alt/%s" % ref_id
2539 all_refs[r] = ref_id
2540 ids.add(ref_id)
2541 tmp.add(r)
2542
2543 tmp_packed_lines = []
2544 old_packed_lines = []
2545
2546 for r in sorted(all_refs):
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002547 line = f"{all_refs[r]} {r}\n"
Gavin Makea2e3302023-03-11 06:46:20 +00002548 tmp_packed_lines.append(line)
2549 if r not in tmp:
2550 old_packed_lines.append(line)
2551
2552 tmp_packed = "".join(tmp_packed_lines)
2553 old_packed = "".join(old_packed_lines)
2554 _lwrite(packed_refs, tmp_packed)
2555 else:
2556 alt_dir = None
2557
2558 cmd = ["fetch"]
2559
2560 if clone_filter:
2561 git_require((2, 19, 0), fail=True, msg="partial clones")
2562 cmd.append("--filter=%s" % clone_filter)
2563 self.EnableRepositoryExtension("partialclone", self.remote.name)
2564
2565 if depth:
2566 cmd.append("--depth=%s" % depth)
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002567 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002568 # If this repo has shallow objects, then we don't know which refs
2569 # have shallow objects or not. Tell git to unshallow all fetched
2570 # refs. Don't do this with projects that don't have shallow
2571 # objects, since it is less efficient.
2572 if os.path.exists(os.path.join(self.gitdir, "shallow")):
2573 cmd.append("--depth=2147483647")
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002574
Gavin Mak7f87c542025-12-06 00:06:44 +00002575 # Use clone-depth="1" as a heuristic for repositories containing
2576 # large binaries and disable auto GC to prevent potential hangs.
2577 # Check the configured depth because the `depth` argument might be None
2578 # if REPO_ALLOW_SHALLOW=0 converted it to a partial clone.
2579 effective_depth = (
2580 self.clone_depth or self.manifest.manifestProject.depth
2581 )
2582 if effective_depth == 1:
2583 cmd.append("--no-auto-gc")
2584
Gavin Makea2e3302023-03-11 06:46:20 +00002585 if not verbose:
2586 cmd.append("--quiet")
2587 if not quiet and sys.stdout.isatty():
2588 cmd.append("--progress")
2589 if not self.worktree:
2590 cmd.append("--update-head-ok")
2591 cmd.append(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002592
Gavin Makea2e3302023-03-11 06:46:20 +00002593 if force_sync:
2594 cmd.append("--force")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002595
Gavin Makea2e3302023-03-11 06:46:20 +00002596 if prune:
2597 cmd.append("--prune")
Mike Frysinger29626b42021-05-01 09:37:13 -04002598
Gavin Makea2e3302023-03-11 06:46:20 +00002599 # Always pass something for --recurse-submodules, git with GIT_DIR
2600 # behaves incorrectly when not given `--recurse-submodules=no`.
2601 # (b/218891912)
2602 cmd.append(
2603 f'--recurse-submodules={"on-demand" if submodules else "no"}'
2604 )
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002605
Gavin Makea2e3302023-03-11 06:46:20 +00002606 spec = []
2607 if not current_branch_only:
2608 # Fetch whole repo.
2609 spec.append(
2610 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2611 )
2612 elif tag_name is not None:
2613 spec.append("tag")
2614 spec.append(tag_name)
Remy Böhmer1469c282020-12-15 18:49:02 +01002615
Gavin Makea2e3302023-03-11 06:46:20 +00002616 if self.manifest.IsMirror and not current_branch_only:
2617 branch = None
Remy Böhmer1469c282020-12-15 18:49:02 +01002618 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002619 branch = self.revisionExpr
Mike Frysinger12f6dc42024-03-21 13:06:11 -04002620 if not self.manifest.IsMirror and is_sha1 and depth:
Gavin Makea2e3302023-03-11 06:46:20 +00002621 # Shallow checkout of a specific commit, fetch from that commit and
2622 # not the heads only as the commit might be deeper in the history.
2623 spec.append(branch)
2624 if self.upstream:
2625 spec.append(self.upstream)
David James8d201162013-10-11 17:03:19 -07002626 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002627 if is_sha1:
2628 branch = self.upstream
2629 if branch is not None and branch.strip():
2630 if not branch.startswith("refs/"):
2631 branch = R_HEADS + branch
2632 spec.append(str(("+%s:" % branch) + remote.ToLocal(branch)))
David James8d201162013-10-11 17:03:19 -07002633
Gavin Makea2e3302023-03-11 06:46:20 +00002634 # If mirroring repo and we cannot deduce the tag or branch to fetch,
2635 # fetch whole repo.
2636 if self.manifest.IsMirror and not spec:
2637 spec.append(
2638 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2639 )
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002640
Gavin Makea2e3302023-03-11 06:46:20 +00002641 # If using depth then we should not get all the tags since they may
2642 # be outside of the depth.
2643 if not tags or depth:
2644 cmd.append("--no-tags")
2645 else:
2646 cmd.append("--tags")
2647 spec.append(str(("+refs/tags/*:") + remote.ToLocal("refs/tags/*")))
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002648
Gavin Makea2e3302023-03-11 06:46:20 +00002649 cmd.extend(spec)
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002650
Gavin Makea2e3302023-03-11 06:46:20 +00002651 # At least one retry minimum due to git remote prune.
2652 retry_fetches = max(retry_fetches, 2)
2653 retry_cur_sleep = retry_sleep_initial_sec
2654 ok = prune_tried = False
2655 for try_n in range(retry_fetches):
Jason Chang32b59562023-07-14 16:45:35 -07002656 verify_command = try_n == retry_fetches - 1
Gavin Makea2e3302023-03-11 06:46:20 +00002657 gitcmd = GitCommand(
2658 self,
2659 cmd,
2660 bare=True,
2661 objdir=os.path.join(self.objdir, "objects"),
2662 ssh_proxy=ssh_proxy,
2663 merge_output=True,
2664 capture_stdout=quiet or bool(output_redir),
Jason Chang32b59562023-07-14 16:45:35 -07002665 verify_command=verify_command,
Gavin Makea2e3302023-03-11 06:46:20 +00002666 )
2667 if gitcmd.stdout and not quiet and output_redir:
2668 output_redir.write(gitcmd.stdout)
2669 ret = gitcmd.Wait()
2670 if ret == 0:
2671 ok = True
2672 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002673
Gavin Makea2e3302023-03-11 06:46:20 +00002674 # Retry later due to HTTP 429 Too Many Requests.
2675 elif (
2676 gitcmd.stdout
2677 and "error:" in gitcmd.stdout
2678 and "HTTP 429" in gitcmd.stdout
2679 ):
2680 # Fallthru to sleep+retry logic at the bottom.
2681 pass
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002682
Josip Sokcevice59e2ae2024-09-12 04:32:25 +00002683 # TODO(b/360889369#comment24): git may gc commits incorrectly.
2684 # Until the root cause is fixed, retry fetch with --refetch which
2685 # will bring the repository into a good state.
Josip Sokceviccfe30952024-10-02 18:05:48 +00002686 elif gitcmd.stdout and (
2687 "could not parse commit" in gitcmd.stdout
2688 or "unable to parse commit" in gitcmd.stdout
2689 ):
Josip Sokcevice59e2ae2024-09-12 04:32:25 +00002690 cmd.insert(1, "--refetch")
2691 print(
2692 "could not parse commit error, retrying with refetch",
2693 file=output_redir,
2694 )
2695 continue
2696
Gavin Makea2e3302023-03-11 06:46:20 +00002697 # Try to prune remote branches once in case there are conflicts.
2698 # For example, if the remote had refs/heads/upstream, but deleted
2699 # that and now has refs/heads/upstream/foo.
2700 elif (
2701 gitcmd.stdout
2702 and "error:" in gitcmd.stdout
2703 and "git remote prune" in gitcmd.stdout
2704 and not prune_tried
2705 ):
2706 prune_tried = True
2707 prunecmd = GitCommand(
2708 self,
2709 ["remote", "prune", name],
2710 bare=True,
2711 ssh_proxy=ssh_proxy,
2712 )
2713 ret = prunecmd.Wait()
2714 if ret:
2715 break
2716 print(
2717 "retrying fetch after pruning remote branches",
2718 file=output_redir,
2719 )
2720 # Continue right away so we don't sleep as we shouldn't need to.
2721 continue
Josip Sokcevic621de7e2024-09-26 21:55:10 +00002722 elif (
2723 ret == 128
2724 and gitcmd.stdout
2725 and "fatal: could not read Username" in gitcmd.stdout
2726 ):
2727 # User needs to be authenticated, and Git wants to prompt for
2728 # username and password.
2729 print(
2730 "git requires authentication, but repo cannot perform "
2731 "interactive authentication. Check git credentials.",
2732 file=output_redir,
2733 )
2734 break
Josip Sokcevicf7f9dd42024-10-03 20:32:04 +00002735 elif (
2736 ret == 128
2737 and gitcmd.stdout
2738 and "remote helper 'sso' aborted session" in gitcmd.stdout
2739 ):
2740 # User needs to be authenticated, and Git wants to prompt for
2741 # username and password.
2742 print(
2743 "git requires authentication, but repo cannot perform "
2744 "interactive authentication.",
2745 file=output_redir,
2746 )
2747 raise GitAuthError(gitcmd.stdout)
2748 break
Gavin Makea2e3302023-03-11 06:46:20 +00002749 elif current_branch_only and is_sha1 and ret == 128:
2750 # Exit code 128 means "couldn't find the ref you asked for"; if
2751 # we're in sha1 mode, we just tried sync'ing from the upstream
2752 # field; it doesn't exist, thus abort the optimization attempt
2753 # and do a full sync.
2754 break
Kaushik Lingarkara94457d2025-04-07 17:08:07 -07002755 elif depth and is_sha1 and ret == 1:
2756 # In sha1 mode, when depth is enabled, syncing the revision
2757 # from upstream may not work because some servers only allow
2758 # fetching named refs. Fetching a specific sha1 may result
2759 # in an error like 'server does not allow request for
2760 # unadvertised object'. In this case, attempt a full sync
2761 # without depth.
2762 break
Gavin Makea2e3302023-03-11 06:46:20 +00002763 elif ret < 0:
2764 # Git died with a signal, exit immediately.
2765 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002766
Gavin Makea2e3302023-03-11 06:46:20 +00002767 # Figure out how long to sleep before the next attempt, if there is
2768 # one.
2769 if not verbose and gitcmd.stdout:
2770 print(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002771 f"\n{self.name}:\n{gitcmd.stdout}",
Gavin Makea2e3302023-03-11 06:46:20 +00002772 end="",
2773 file=output_redir,
2774 )
2775 if try_n < retry_fetches - 1:
2776 print(
2777 "%s: sleeping %s seconds before retrying"
2778 % (self.name, retry_cur_sleep),
2779 file=output_redir,
2780 )
2781 time.sleep(retry_cur_sleep)
2782 retry_cur_sleep = min(
2783 retry_exp_factor * retry_cur_sleep, MAXIMUM_RETRY_SLEEP_SEC
2784 )
2785 retry_cur_sleep *= 1 - random.uniform(
2786 -RETRY_JITTER_PERCENT, RETRY_JITTER_PERCENT
2787 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002788
Gavin Makea2e3302023-03-11 06:46:20 +00002789 if initial:
2790 if alt_dir:
2791 if old_packed != "":
2792 _lwrite(packed_refs, old_packed)
2793 else:
2794 platform_utils.remove(packed_refs)
2795 self.bare_git.pack_refs("--all", "--prune")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002796
Gavin Makea2e3302023-03-11 06:46:20 +00002797 if is_sha1 and current_branch_only:
2798 # We just synced the upstream given branch; verify we
2799 # got what we wanted, else trigger a second run of all
2800 # refs.
2801 if not self._CheckForImmutableRevision():
2802 # Sync the current branch only with depth set to None.
2803 # We always pass depth=None down to avoid infinite recursion.
2804 return self._RemoteFetch(
2805 name=name,
2806 quiet=quiet,
2807 verbose=verbose,
2808 output_redir=output_redir,
2809 current_branch_only=current_branch_only and depth,
2810 initial=False,
2811 alt_dir=alt_dir,
Nasser Grainawiaafed292023-05-24 12:51:03 -06002812 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00002813 depth=None,
2814 ssh_proxy=ssh_proxy,
2815 clone_filter=clone_filter,
2816 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002817
Gavin Makea2e3302023-03-11 06:46:20 +00002818 return ok
Mike Frysingerf4545122019-11-11 04:34:16 -05002819
Gavin Makea2e3302023-03-11 06:46:20 +00002820 def _ApplyCloneBundle(self, initial=False, quiet=False, verbose=False):
2821 if initial and (
2822 self.manifest.manifestProject.depth or self.clone_depth
2823 ):
2824 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002825
Gavin Makea2e3302023-03-11 06:46:20 +00002826 remote = self.GetRemote()
2827 bundle_url = remote.url + "/clone.bundle"
2828 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
2829 if GetSchemeFromUrl(bundle_url) not in (
2830 "http",
2831 "https",
2832 "persistent-http",
2833 "persistent-https",
2834 ):
2835 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06002836
Gavin Makea2e3302023-03-11 06:46:20 +00002837 bundle_dst = os.path.join(self.gitdir, "clone.bundle")
2838 bundle_tmp = os.path.join(self.gitdir, "clone.bundle.tmp")
2839
2840 exist_dst = os.path.exists(bundle_dst)
2841 exist_tmp = os.path.exists(bundle_tmp)
2842
2843 if not initial and not exist_dst and not exist_tmp:
2844 return False
2845
2846 if not exist_dst:
2847 exist_dst = self._FetchBundle(
2848 bundle_url, bundle_tmp, bundle_dst, quiet, verbose
2849 )
2850 if not exist_dst:
2851 return False
2852
2853 cmd = ["fetch"]
2854 if not verbose:
2855 cmd.append("--quiet")
2856 if not quiet and sys.stdout.isatty():
2857 cmd.append("--progress")
2858 if not self.worktree:
2859 cmd.append("--update-head-ok")
2860 cmd.append(bundle_dst)
2861 for f in remote.fetch:
2862 cmd.append(str(f))
2863 cmd.append("+refs/tags/*:refs/tags/*")
2864
2865 ok = (
2866 GitCommand(
2867 self,
2868 cmd,
2869 bare=True,
2870 objdir=os.path.join(self.objdir, "objects"),
2871 ).Wait()
2872 == 0
2873 )
2874 platform_utils.remove(bundle_dst, missing_ok=True)
2875 platform_utils.remove(bundle_tmp, missing_ok=True)
2876 return ok
2877
2878 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
2879 platform_utils.remove(dstPath, missing_ok=True)
2880
Mike Frysinger0444ddf2024-07-02 14:52:22 -04002881 # We do not use curl's --retry option since it generally doesn't
2882 # actually retry anything; code 18 for example, it will not retry on.
Gavin Mak97dc5c12025-04-10 18:09:41 +00002883 cmd = [
2884 "curl",
2885 "--fail",
2886 "--output",
2887 tmpPath,
2888 "--netrc-optional",
2889 "--location",
2890 ]
Gavin Makea2e3302023-03-11 06:46:20 +00002891 if quiet:
2892 cmd += ["--silent", "--show-error"]
2893 if os.path.exists(tmpPath):
2894 size = os.stat(tmpPath).st_size
2895 if size >= 1024:
2896 cmd += ["--continue-at", "%d" % (size,)]
2897 else:
2898 platform_utils.remove(tmpPath)
2899 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
2900 if cookiefile:
2901 cmd += ["--cookie", cookiefile]
2902 if proxy:
2903 cmd += ["--proxy", proxy]
2904 elif "http_proxy" in os.environ and "darwin" == sys.platform:
2905 cmd += ["--proxy", os.environ["http_proxy"]]
2906 if srcUrl.startswith("persistent-https"):
2907 srcUrl = "http" + srcUrl[len("persistent-https") :]
2908 elif srcUrl.startswith("persistent-http"):
2909 srcUrl = "http" + srcUrl[len("persistent-http") :]
2910 cmd += [srcUrl]
2911
2912 proc = None
2913 with Trace("Fetching bundle: %s", " ".join(cmd)):
2914 if verbose:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002915 print(f"{self.name}: Downloading bundle: {srcUrl}")
Gavin Makea2e3302023-03-11 06:46:20 +00002916 stdout = None if verbose else subprocess.PIPE
2917 stderr = None if verbose else subprocess.STDOUT
2918 try:
2919 proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
2920 except OSError:
2921 return False
2922
2923 (output, _) = proc.communicate()
2924 curlret = proc.returncode
2925
Mike Frysinger0444ddf2024-07-02 14:52:22 -04002926 if curlret in (22, 35, 56, 92):
2927 # We use --fail so curl exits with unique status.
Gavin Makea2e3302023-03-11 06:46:20 +00002928 # From curl man page:
Mike Frysinger0444ddf2024-07-02 14:52:22 -04002929 # 22: HTTP page not retrieved. The requested url was not found
2930 # or returned another error with the HTTP error code being
2931 # 400 or above.
2932 # 35: SSL connect error. The SSL handshaking failed. This can
2933 # be thrown by Google storage sometimes.
2934 # 56: Failure in receiving network data. This shows up with
2935 # HTTP/404 on Google storage.
2936 # 92: Stream error in HTTP/2 framing layer. Basically the same
2937 # as 22 -- Google storage sometimes throws 500's.
Gavin Makea2e3302023-03-11 06:46:20 +00002938 if verbose:
2939 print(
2940 "%s: Unable to retrieve clone.bundle; ignoring."
2941 % self.name
2942 )
2943 if output:
2944 print("Curl output:\n%s" % output)
2945 return False
2946 elif curlret and not verbose and output:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002947 logger.error("%s", output)
Gavin Makea2e3302023-03-11 06:46:20 +00002948
2949 if os.path.exists(tmpPath):
2950 if curlret == 0 and self._IsValidBundle(tmpPath, quiet):
2951 platform_utils.rename(tmpPath, dstPath)
2952 return True
2953 else:
2954 platform_utils.remove(tmpPath)
2955 return False
2956 else:
2957 return False
2958
2959 def _IsValidBundle(self, path, quiet):
2960 try:
2961 with open(path, "rb") as f:
2962 if f.read(16) == b"# v2 git bundle\n":
2963 return True
2964 else:
2965 if not quiet:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002966 logger.error("Invalid clone.bundle file; ignoring.")
Gavin Makea2e3302023-03-11 06:46:20 +00002967 return False
2968 except OSError:
2969 return False
2970
Josip Sokcevicedadb252024-02-29 09:48:37 -08002971 def _Checkout(self, rev, force_checkout=False, quiet=False):
Gavin Makea2e3302023-03-11 06:46:20 +00002972 cmd = ["checkout"]
2973 if quiet:
2974 cmd.append("-q")
Josip Sokcevicedadb252024-02-29 09:48:37 -08002975 if force_checkout:
2976 cmd.append("-f")
Gavin Makea2e3302023-03-11 06:46:20 +00002977 cmd.append(rev)
2978 cmd.append("--")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002979 if GitCommand(self, cmd).Wait() != 0:
Gavin Makea2e3302023-03-11 06:46:20 +00002980 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002981 raise GitError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002982 f"{self.name} checkout {rev} ", project=self.name
Jason Chang32b59562023-07-14 16:45:35 -07002983 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002984
Gavin Makea2e3302023-03-11 06:46:20 +00002985 def _CherryPick(self, rev, ffonly=False, record_origin=False):
2986 cmd = ["cherry-pick"]
2987 if ffonly:
2988 cmd.append("--ff")
2989 if record_origin:
2990 cmd.append("-x")
2991 cmd.append(rev)
2992 cmd.append("--")
2993 if GitCommand(self, cmd).Wait() != 0:
2994 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002995 raise GitError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002996 f"{self.name} cherry-pick {rev} ", project=self.name
Jason Chang32b59562023-07-14 16:45:35 -07002997 )
Victor Boivie0960b5b2010-11-26 13:42:13 +01002998
Gavin Makea2e3302023-03-11 06:46:20 +00002999 def _LsRemote(self, refs):
3000 cmd = ["ls-remote", self.remote.name, refs]
3001 p = GitCommand(self, cmd, capture_stdout=True)
3002 if p.Wait() == 0:
3003 return p.stdout
3004 return None
Mike Frysinger2a089cf2021-11-13 23:29:42 -05003005
Gavin Makea2e3302023-03-11 06:46:20 +00003006 def _Revert(self, rev):
3007 cmd = ["revert"]
3008 cmd.append("--no-edit")
3009 cmd.append(rev)
3010 cmd.append("--")
3011 if GitCommand(self, cmd).Wait() != 0:
3012 if self._allrefs:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003013 raise GitError(f"{self.name} revert {rev} ", project=self.name)
Mike Frysinger2a089cf2021-11-13 23:29:42 -05003014
Gavin Makea2e3302023-03-11 06:46:20 +00003015 def _ResetHard(self, rev, quiet=True):
3016 cmd = ["reset", "--hard"]
3017 if quiet:
3018 cmd.append("-q")
3019 cmd.append(rev)
3020 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07003021 raise GitError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003022 f"{self.name} reset --hard {rev} ", project=self.name
Jason Chang32b59562023-07-14 16:45:35 -07003023 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05003024
Gavin Makea2e3302023-03-11 06:46:20 +00003025 def _SyncSubmodules(self, quiet=True):
3026 cmd = ["submodule", "update", "--init", "--recursive"]
3027 if quiet:
3028 cmd.append("-q")
3029 if GitCommand(self, cmd).Wait() != 0:
3030 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07003031 "%s submodule update --init --recursive " % self.name,
3032 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003033 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05003034
Kaushik Lingarkar46232642025-09-10 17:07:35 -07003035 def _InitSubmodule(self, quiet=True):
3036 """Initialize the submodule."""
Kaushik Lingarkar99eca452024-12-17 15:16:39 -08003037 cmd = ["submodule", "init"]
3038 if quiet:
3039 cmd.append("-q")
Kaushik Lingarkar46232642025-09-10 17:07:35 -07003040 cmd.extend(["--", self.worktree])
3041 max_retries = 3
3042 base_delay_secs = 1
3043 jitter_ratio = 1 / 3
3044 for attempt in range(max_retries):
3045 git_cmd = GitCommand(
3046 None,
3047 cmd,
3048 cwd=self.parent.worktree,
3049 capture_stdout=True,
3050 capture_stderr=True,
Kaushik Lingarkar99eca452024-12-17 15:16:39 -08003051 )
Kaushik Lingarkar46232642025-09-10 17:07:35 -07003052 if git_cmd.Wait() == 0:
3053 return
3054 error = git_cmd.stderr or git_cmd.stdout
3055 if "lock" in error:
3056 delay = base_delay_secs * (2**attempt)
3057 delay += random.uniform(0, delay * jitter_ratio)
3058 logger.warning(
3059 f"Attempt {attempt+1}/{max_retries}: "
3060 + f"git {' '.join(cmd)} failed."
3061 + f" Error: {error}."
3062 + f" Sleeping {delay:.2f}s before retrying."
3063 )
3064 time.sleep(delay)
3065 else:
3066 break
3067 git_cmd.VerifyCommand()
Kaushik Lingarkar99eca452024-12-17 15:16:39 -08003068
Gavin Makea2e3302023-03-11 06:46:20 +00003069 def _Rebase(self, upstream, onto=None):
3070 cmd = ["rebase"]
3071 if onto is not None:
3072 cmd.extend(["--onto", onto])
3073 cmd.append(upstream)
3074 if GitCommand(self, cmd).Wait() != 0:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003075 raise GitError(f"{self.name} rebase {upstream} ", project=self.name)
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05003076
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003077 def _FastForward(self, head, ffonly=False, quiet=True):
Gavin Makea2e3302023-03-11 06:46:20 +00003078 cmd = ["merge", "--no-stat", head]
3079 if ffonly:
3080 cmd.append("--ff-only")
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003081 if quiet:
3082 cmd.append("-q")
Gavin Makea2e3302023-03-11 06:46:20 +00003083 if GitCommand(self, cmd).Wait() != 0:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003084 raise GitError(f"{self.name} merge {head} ", project=self.name)
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05003085
Gavin Makea2e3302023-03-11 06:46:20 +00003086 def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False):
Kaushik Lingarkar50c62262025-11-03 19:58:26 -08003087 # Prefix for temporary directories created during gitdir initialization.
3088 TMP_GITDIR_PREFIX = ".tmp-project-initgitdir-"
Gavin Makea2e3302023-03-11 06:46:20 +00003089 init_git_dir = not os.path.exists(self.gitdir)
3090 init_obj_dir = not os.path.exists(self.objdir)
Kaushik Lingarkar50c62262025-11-03 19:58:26 -08003091 tmp_gitdir = None
3092 curr_gitdir = self.gitdir
3093 curr_config = self.config
Gavin Makea2e3302023-03-11 06:46:20 +00003094 try:
3095 # Initialize the bare repository, which contains all of the objects.
3096 if init_obj_dir:
3097 os.makedirs(self.objdir)
3098 self.bare_objdir.init()
Mike Frysinger2a089cf2021-11-13 23:29:42 -05003099
Gavin Makea2e3302023-03-11 06:46:20 +00003100 self._UpdateHooks(quiet=quiet)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003101
Gavin Makea2e3302023-03-11 06:46:20 +00003102 if self.use_git_worktrees:
3103 # Enable per-worktree config file support if possible. This
3104 # is more a nice-to-have feature for users rather than a
3105 # hard requirement.
3106 if git_require((2, 20, 0)):
3107 self.EnableRepositoryExtension("worktreeConfig")
Renaud Paquay788e9622017-01-27 11:41:12 -08003108
Gavin Makea2e3302023-03-11 06:46:20 +00003109 # If we have a separate directory to hold refs, initialize it as
3110 # well.
3111 if self.objdir != self.gitdir:
3112 if init_git_dir:
Kaushik Lingarkar50c62262025-11-03 19:58:26 -08003113 os.makedirs(os.path.dirname(self.gitdir), exist_ok=True)
3114 tmp_gitdir = tempfile.mkdtemp(
3115 prefix=TMP_GITDIR_PREFIX,
3116 dir=os.path.dirname(self.gitdir),
3117 )
3118 curr_config = GitConfig.ForRepository(
3119 gitdir=tmp_gitdir, defaults=self.manifest.globalConfig
3120 )
3121 curr_gitdir = tmp_gitdir
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003122
Gavin Makea2e3302023-03-11 06:46:20 +00003123 if init_obj_dir or init_git_dir:
3124 self._ReferenceGitDir(
Kaushik Lingarkar50c62262025-11-03 19:58:26 -08003125 self.objdir, curr_gitdir, copy_all=True
Gavin Makea2e3302023-03-11 06:46:20 +00003126 )
3127 try:
Kaushik Lingarkar50c62262025-11-03 19:58:26 -08003128 self._CheckDirReference(self.objdir, curr_gitdir)
Gavin Makea2e3302023-03-11 06:46:20 +00003129 except GitError as e:
3130 if force_sync:
Gavin Makea2e3302023-03-11 06:46:20 +00003131 try:
Kaushik Lingarkar50c62262025-11-03 19:58:26 -08003132 rm_dirs = (
3133 tmp_gitdir,
3134 self.gitdir,
3135 self.worktree,
3136 )
3137 for d in rm_dirs:
3138 if d and os.path.exists(d):
3139 platform_utils.rmtree(os.path.realpath(d))
Gavin Makea2e3302023-03-11 06:46:20 +00003140 return self._InitGitDir(
3141 mirror_git=mirror_git,
3142 force_sync=False,
3143 quiet=quiet,
3144 )
3145 except Exception:
3146 raise e
3147 raise e
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003148
Gavin Makea2e3302023-03-11 06:46:20 +00003149 if init_git_dir:
3150 mp = self.manifest.manifestProject
3151 ref_dir = mp.reference or ""
Julien Camperguedd654222014-01-09 16:21:37 +01003152
Gavin Makea2e3302023-03-11 06:46:20 +00003153 def _expanded_ref_dirs():
3154 """Iterate through possible git reference dir paths."""
3155 name = self.name + ".git"
3156 yield mirror_git or os.path.join(ref_dir, name)
3157 for prefix in "", self.remote.name:
3158 yield os.path.join(
3159 ref_dir, ".repo", "project-objects", prefix, name
3160 )
3161 yield os.path.join(
3162 ref_dir, ".repo", "worktrees", prefix, name
3163 )
3164
3165 if ref_dir or mirror_git:
3166 found_ref_dir = None
3167 for path in _expanded_ref_dirs():
3168 if os.path.exists(path):
3169 found_ref_dir = path
3170 break
3171 ref_dir = found_ref_dir
3172
3173 if ref_dir:
3174 if not os.path.isabs(ref_dir):
3175 # The alternate directory is relative to the object
3176 # database.
3177 ref_dir = os.path.relpath(
3178 ref_dir, os.path.join(self.objdir, "objects")
3179 )
3180 _lwrite(
3181 os.path.join(
3182 self.objdir, "objects/info/alternates"
3183 ),
3184 os.path.join(ref_dir, "objects") + "\n",
3185 )
3186
3187 m = self.manifest.manifestProject.config
3188 for key in ["user.name", "user.email"]:
3189 if m.Has(key, include_defaults=False):
Kaushik Lingarkar50c62262025-11-03 19:58:26 -08003190 curr_config.SetString(key, m.GetString(key))
Gavin Makea2e3302023-03-11 06:46:20 +00003191 if not self.manifest.EnableGitLfs:
Kaushik Lingarkar50c62262025-11-03 19:58:26 -08003192 curr_config.SetString(
Gavin Makea2e3302023-03-11 06:46:20 +00003193 "filter.lfs.smudge", "git-lfs smudge --skip -- %f"
3194 )
Kaushik Lingarkar50c62262025-11-03 19:58:26 -08003195 curr_config.SetString(
Gavin Makea2e3302023-03-11 06:46:20 +00003196 "filter.lfs.process", "git-lfs filter-process --skip"
3197 )
Kaushik Lingarkar50c62262025-11-03 19:58:26 -08003198 curr_config.SetBoolean(
Gavin Makea2e3302023-03-11 06:46:20 +00003199 "core.bare", True if self.manifest.IsMirror else None
3200 )
Josip Sokcevic9267d582023-10-19 14:46:11 -07003201
Kaushik Lingarkar50c62262025-11-03 19:58:26 -08003202 if tmp_gitdir:
3203 platform_utils.rename(tmp_gitdir, self.gitdir)
3204 tmp_gitdir = None
Josip Sokcevic9267d582023-10-19 14:46:11 -07003205 if not init_obj_dir:
3206 # The project might be shared (obj_dir already initialized), but
3207 # such information is not available here. Instead of passing it,
3208 # set it as shared, and rely to be unset down the execution
3209 # path.
3210 if git_require((2, 7, 0)):
3211 self.EnableRepositoryExtension("preciousObjects")
3212 else:
3213 self.config.SetString("gc.pruneExpire", "never")
3214
Gavin Makea2e3302023-03-11 06:46:20 +00003215 except Exception:
3216 if init_obj_dir and os.path.exists(self.objdir):
3217 platform_utils.rmtree(self.objdir)
3218 if init_git_dir and os.path.exists(self.gitdir):
3219 platform_utils.rmtree(self.gitdir)
3220 raise
Kaushik Lingarkar50c62262025-11-03 19:58:26 -08003221 finally:
3222 # Clean up the temporary directory created during the process,
3223 # as well as any stale ones left over from previous attempts.
3224 if tmp_gitdir and os.path.exists(tmp_gitdir):
3225 platform_utils.rmtree(tmp_gitdir)
3226
3227 age_threshold = datetime.timedelta(days=1)
3228 now = datetime.datetime.now()
3229 for tmp_dir in glob.glob(
3230 os.path.join(
3231 os.path.dirname(self.gitdir), f"{TMP_GITDIR_PREFIX}*"
3232 )
3233 ):
3234 try:
3235 mtime = datetime.datetime.fromtimestamp(
3236 os.path.getmtime(tmp_dir)
3237 )
3238 if now - mtime > age_threshold:
3239 platform_utils.rmtree(tmp_dir)
3240 except OSError:
3241 pass
Gavin Makea2e3302023-03-11 06:46:20 +00003242
3243 def _UpdateHooks(self, quiet=False):
3244 if os.path.exists(self.objdir):
3245 self._InitHooks(quiet=quiet)
3246
3247 def _InitHooks(self, quiet=False):
Kaiyi Li46819a72024-03-27 07:21:43 -07003248 hooks = os.path.realpath(os.path.join(self.objdir, "hooks"))
Gavin Makea2e3302023-03-11 06:46:20 +00003249 if not os.path.exists(hooks):
3250 os.makedirs(hooks)
3251
3252 # Delete sample hooks. They're noise.
3253 for hook in glob.glob(os.path.join(hooks, "*.sample")):
3254 try:
3255 platform_utils.remove(hook, missing_ok=True)
3256 except PermissionError:
3257 pass
3258
3259 for stock_hook in _ProjectHooks():
3260 name = os.path.basename(stock_hook)
3261
3262 if (
3263 name in ("commit-msg",)
3264 and not self.remote.review
3265 and self is not self.manifest.manifestProject
3266 ):
3267 # Don't install a Gerrit Code Review hook if this
3268 # project does not appear to use it for reviews.
3269 #
3270 # Since the manifest project is one of those, but also
3271 # managed through gerrit, it's excluded.
3272 continue
3273
3274 dst = os.path.join(hooks, name)
3275 if platform_utils.islink(dst):
3276 continue
3277 if os.path.exists(dst):
3278 # If the files are the same, we'll leave it alone. We create
3279 # symlinks below by default but fallback to hardlinks if the OS
3280 # blocks them. So if we're here, it's probably because we made a
3281 # hardlink below.
3282 if not filecmp.cmp(stock_hook, dst, shallow=False):
3283 if not quiet:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00003284 logger.warning(
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00003285 "warn: %s: Not replacing locally modified %s hook",
Gavin Makea2e3302023-03-11 06:46:20 +00003286 self.RelPath(local=False),
3287 name,
3288 )
3289 continue
3290 try:
3291 platform_utils.symlink(
3292 os.path.relpath(stock_hook, os.path.dirname(dst)), dst
3293 )
3294 except OSError as e:
3295 if e.errno == errno.EPERM:
3296 try:
3297 os.link(stock_hook, dst)
3298 except OSError:
Jason Chang32b59562023-07-14 16:45:35 -07003299 raise GitError(
3300 self._get_symlink_error_message(), project=self.name
3301 )
Gavin Makea2e3302023-03-11 06:46:20 +00003302 else:
3303 raise
3304
3305 def _InitRemote(self):
3306 if self.remote.url:
3307 remote = self.GetRemote()
3308 remote.url = self.remote.url
3309 remote.pushUrl = self.remote.pushUrl
3310 remote.review = self.remote.review
3311 remote.projectname = self.name
3312
3313 if self.worktree:
3314 remote.ResetFetch(mirror=False)
3315 else:
3316 remote.ResetFetch(mirror=True)
3317 remote.Save()
3318
3319 def _InitMRef(self):
3320 """Initialize the pseudo m/<manifest branch> ref."""
3321 if self.manifest.branch:
3322 if self.use_git_worktrees:
3323 # Set up the m/ space to point to the worktree-specific ref
3324 # space. We'll update the worktree-specific ref space on each
3325 # checkout.
3326 ref = R_M + self.manifest.branch
3327 if not self.bare_ref.symref(ref):
3328 self.bare_git.symbolic_ref(
3329 "-m",
3330 "redirecting to worktree scope",
3331 ref,
3332 R_WORKTREE_M + self.manifest.branch,
3333 )
3334
3335 # We can't update this ref with git worktrees until it exists.
3336 # We'll wait until the initial checkout to set it.
3337 if not os.path.exists(self.worktree):
3338 return
3339
3340 base = R_WORKTREE_M
3341 active_git = self.work_git
3342
3343 self._InitAnyMRef(HEAD, self.bare_git, detach=True)
3344 else:
3345 base = R_M
3346 active_git = self.bare_git
3347
3348 self._InitAnyMRef(base + self.manifest.branch, active_git)
3349
3350 def _InitMirrorHead(self):
3351 self._InitAnyMRef(HEAD, self.bare_git)
3352
3353 def _InitAnyMRef(self, ref, active_git, detach=False):
3354 """Initialize |ref| in |active_git| to the value in the manifest.
3355
3356 This points |ref| to the <project> setting in the manifest.
3357
3358 Args:
3359 ref: The branch to update.
3360 active_git: The git repository to make updates in.
3361 detach: Whether to update target of symbolic refs, or overwrite the
3362 ref directly (and thus make it non-symbolic).
3363 """
3364 cur = self.bare_ref.symref(ref)
3365
3366 if self.revisionId:
3367 if cur != "" or self.bare_ref.get(ref) != self.revisionId:
3368 msg = "manifest set to %s" % self.revisionId
3369 dst = self.revisionId + "^0"
3370 active_git.UpdateRef(ref, dst, message=msg, detach=True)
Julien Camperguedd654222014-01-09 16:21:37 +01003371 else:
Gavin Makea2e3302023-03-11 06:46:20 +00003372 remote = self.GetRemote()
3373 dst = remote.ToLocal(self.revisionExpr)
3374 if cur != dst:
3375 msg = "manifest set to %s" % self.revisionExpr
3376 if detach:
3377 active_git.UpdateRef(ref, dst, message=msg, detach=True)
3378 else:
3379 active_git.symbolic_ref("-m", msg, ref, dst)
Julien Camperguedd654222014-01-09 16:21:37 +01003380
Gavin Makea2e3302023-03-11 06:46:20 +00003381 def _CheckDirReference(self, srcdir, destdir):
3382 # Git worktrees don't use symlinks to share at all.
3383 if self.use_git_worktrees:
3384 return
Julien Camperguedd654222014-01-09 16:21:37 +01003385
Gavin Makea2e3302023-03-11 06:46:20 +00003386 for name in self.shareable_dirs:
3387 # Try to self-heal a bit in simple cases.
3388 dst_path = os.path.join(destdir, name)
3389 src_path = os.path.join(srcdir, name)
Julien Camperguedd654222014-01-09 16:21:37 +01003390
Kaiyi Li46819a72024-03-27 07:21:43 -07003391 dst = os.path.realpath(dst_path)
Gavin Makea2e3302023-03-11 06:46:20 +00003392 if os.path.lexists(dst):
Kaiyi Li46819a72024-03-27 07:21:43 -07003393 src = os.path.realpath(src_path)
Gavin Makea2e3302023-03-11 06:46:20 +00003394 # Fail if the links are pointing to the wrong place.
3395 if src != dst:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00003396 logger.error(
3397 "error: %s is different in %s vs %s",
3398 name,
3399 destdir,
3400 srcdir,
3401 )
Gavin Makea2e3302023-03-11 06:46:20 +00003402 raise GitError(
3403 "--force-sync not enabled; cannot overwrite a local "
3404 "work tree. If you're comfortable with the "
3405 "possibility of losing the work tree's git metadata,"
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003406 " use "
3407 f"`repo sync --force-sync {self.RelPath(local=False)}` "
3408 "to proceed.",
Jason Chang32b59562023-07-14 16:45:35 -07003409 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003410 )
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003411
Gavin Makea2e3302023-03-11 06:46:20 +00003412 def _ReferenceGitDir(self, gitdir, dotgit, copy_all):
3413 """Update |dotgit| to reference |gitdir|, using symlinks where possible.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003414
Gavin Makea2e3302023-03-11 06:46:20 +00003415 Args:
3416 gitdir: The bare git repository. Must already be initialized.
3417 dotgit: The repository you would like to initialize.
3418 copy_all: If true, copy all remaining files from |gitdir| ->
3419 |dotgit|. This saves you the effort of initializing |dotgit|
3420 yourself.
3421 """
3422 symlink_dirs = self.shareable_dirs[:]
3423 to_symlink = symlink_dirs
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003424
Gavin Makea2e3302023-03-11 06:46:20 +00003425 to_copy = []
3426 if copy_all:
3427 to_copy = platform_utils.listdir(gitdir)
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003428
Kaiyi Li46819a72024-03-27 07:21:43 -07003429 dotgit = os.path.realpath(dotgit)
Gavin Makea2e3302023-03-11 06:46:20 +00003430 for name in set(to_copy).union(to_symlink):
3431 try:
Kaiyi Li46819a72024-03-27 07:21:43 -07003432 src = os.path.realpath(os.path.join(gitdir, name))
Gavin Makea2e3302023-03-11 06:46:20 +00003433 dst = os.path.join(dotgit, name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003434
Gavin Makea2e3302023-03-11 06:46:20 +00003435 if os.path.lexists(dst):
3436 continue
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003437
Gavin Makea2e3302023-03-11 06:46:20 +00003438 # If the source dir doesn't exist, create an empty dir.
3439 if name in symlink_dirs and not os.path.lexists(src):
3440 os.makedirs(src)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003441
Gavin Makea2e3302023-03-11 06:46:20 +00003442 if name in to_symlink:
3443 platform_utils.symlink(
3444 os.path.relpath(src, os.path.dirname(dst)), dst
3445 )
3446 elif copy_all and not platform_utils.islink(dst):
3447 if platform_utils.isdir(src):
3448 shutil.copytree(src, dst)
3449 elif os.path.isfile(src):
3450 shutil.copy(src, dst)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003451
Gavin Makea2e3302023-03-11 06:46:20 +00003452 except OSError as e:
3453 if e.errno == errno.EPERM:
3454 raise DownloadError(self._get_symlink_error_message())
3455 else:
3456 raise
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003457
Gavin Makea2e3302023-03-11 06:46:20 +00003458 def _InitGitWorktree(self):
3459 """Init the project using git worktrees."""
3460 self.bare_git.worktree("prune")
3461 self.bare_git.worktree(
3462 "add",
3463 "-ff",
3464 "--checkout",
3465 "--detach",
3466 "--lock",
3467 self.worktree,
3468 self.GetRevisionId(),
3469 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003470
Gavin Makea2e3302023-03-11 06:46:20 +00003471 # Rewrite the internal state files to use relative paths between the
3472 # checkouts & worktrees.
3473 dotgit = os.path.join(self.worktree, ".git")
Jason R. Coombs034950b2023-10-20 23:32:02 +05453474 with open(dotgit) as fp:
Gavin Makea2e3302023-03-11 06:46:20 +00003475 # Figure out the checkout->worktree path.
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003476 setting = fp.read()
Gavin Makea2e3302023-03-11 06:46:20 +00003477 assert setting.startswith("gitdir:")
3478 git_worktree_path = setting.split(":", 1)[1].strip()
Allen Webb1d509862024-10-29 13:24:05 -05003479
3480 # `gitdir` maybe be either relative or absolute depending on the
3481 # behavior of the local copy of git, so only convert the path to
3482 # relative if it needs to be converted.
3483 if os.path.isabs(git_worktree_path):
3484 # Some platforms (e.g. Windows) won't let us update dotgit in situ
3485 # because of file permissions. Delete it and recreate it from
3486 # scratch to avoid.
3487 platform_utils.remove(dotgit)
3488 # Use relative path from checkout->worktree & maintain Unix line
3489 # endings on all OS's to match git behavior.
3490 with open(dotgit, "w", newline="\n") as fp:
3491 print(
3492 "gitdir:",
3493 os.path.relpath(git_worktree_path, self.worktree),
3494 file=fp,
3495 )
3496 # Use relative path from worktree->checkout & maintain Unix line
3497 # endings on all OS's to match git behavior.
3498 with open(
3499 os.path.join(git_worktree_path, "gitdir"), "w", newline="\n"
3500 ) as fp:
3501 print(os.path.relpath(dotgit, git_worktree_path), file=fp)
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003502
Gavin Makea2e3302023-03-11 06:46:20 +00003503 self._InitMRef()
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003504
Gavin Makea2e3302023-03-11 06:46:20 +00003505 def _InitWorkTree(self, force_sync=False, submodules=False):
3506 """Setup the worktree .git path.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003507
Gavin Makea2e3302023-03-11 06:46:20 +00003508 This is the user-visible path like src/foo/.git/.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003509
Gavin Makea2e3302023-03-11 06:46:20 +00003510 With non-git-worktrees, this will be a symlink to the .repo/projects/
3511 path. With git-worktrees, this will be a .git file using "gitdir: ..."
3512 syntax.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003513
Gavin Makea2e3302023-03-11 06:46:20 +00003514 Older checkouts had .git/ directories. If we see that, migrate it.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003515
Gavin Makea2e3302023-03-11 06:46:20 +00003516 This also handles changes in the manifest. Maybe this project was
3517 backed by "foo/bar" on the server, but now it's "new/foo/bar". We have
3518 to update the path we point to under .repo/projects/ to match.
3519 """
3520 dotgit = os.path.join(self.worktree, ".git")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003521
Kaushik Lingarkarcf9a2a22024-12-17 12:49:14 -08003522 # If bare checkout of the submodule is stored under the subproject dir,
3523 # migrate it.
3524 if self.parent:
3525 self._MigrateOldSubmoduleDir()
3526
Gavin Makea2e3302023-03-11 06:46:20 +00003527 # If using an old layout style (a directory), migrate it.
3528 if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
Jason Chang32b59562023-07-14 16:45:35 -07003529 self._MigrateOldWorkTreeGitDir(dotgit, project=self.name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003530
Josip Sokcevic73356f12024-03-20 21:48:34 +00003531 init_dotgit = not os.path.lexists(dotgit)
Gavin Makea2e3302023-03-11 06:46:20 +00003532 if self.use_git_worktrees:
3533 if init_dotgit:
3534 self._InitGitWorktree()
3535 self._CopyAndLinkFiles()
3536 else:
Kaushik Lingarkar66685f02024-12-17 13:49:19 -08003537 # Remove old directory symbolic links for submodules.
3538 if self.parent and platform_utils.islink(dotgit):
3539 platform_utils.remove(dotgit)
3540 init_dotgit = True
3541
Gavin Makea2e3302023-03-11 06:46:20 +00003542 if not init_dotgit:
3543 # See if the project has changed.
Kaushik Lingarkar66685f02024-12-17 13:49:19 -08003544 self._removeBadGitDirLink(dotgit)
Doug Anderson37282b42011-03-04 11:54:18 -08003545
Gavin Makea2e3302023-03-11 06:46:20 +00003546 if init_dotgit or not os.path.exists(dotgit):
Kaushik Lingarkar66685f02024-12-17 13:49:19 -08003547 self._createDotGit(dotgit)
Doug Anderson37282b42011-03-04 11:54:18 -08003548
Gavin Makea2e3302023-03-11 06:46:20 +00003549 if init_dotgit:
Gavin Mak74edacd2025-07-17 17:54:22 +00003550 self.work_git.UpdateRef(HEAD, self.GetRevisionId(), detach=True)
Doug Anderson37282b42011-03-04 11:54:18 -08003551
Gavin Makea2e3302023-03-11 06:46:20 +00003552 # Finish checking out the worktree.
3553 cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
Josip Sokcevicdb111d32025-01-15 23:43:22 +00003554 try:
3555 if GitCommand(self, cmd).Wait() != 0:
3556 raise GitError(
3557 "Cannot initialize work tree for " + self.name,
3558 project=self.name,
3559 )
3560 except Exception as e:
3561 # Something went wrong with read-tree (perhaps fetching
3562 # missing blobs), so remove .git to avoid half initialized
3563 # workspace from which repo can't recover on its own.
3564 platform_utils.remove(dotgit)
3565 raise e
Doug Anderson37282b42011-03-04 11:54:18 -08003566
Gavin Makea2e3302023-03-11 06:46:20 +00003567 if submodules:
3568 self._SyncSubmodules(quiet=True)
3569 self._CopyAndLinkFiles()
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003570
Kaushik Lingarkar66685f02024-12-17 13:49:19 -08003571 def _createDotGit(self, dotgit):
3572 """Initialize .git path.
3573
3574 For submodule projects, create a '.git' file using the gitfile
3575 mechanism, and for the rest, create a symbolic link.
3576 """
3577 os.makedirs(self.worktree, exist_ok=True)
3578 if self.parent:
3579 _lwrite(
3580 dotgit,
3581 f"gitdir: {os.path.relpath(self.gitdir, self.worktree)}\n",
3582 )
3583 else:
3584 platform_utils.symlink(
3585 os.path.relpath(self.gitdir, self.worktree), dotgit
3586 )
3587
3588 def _removeBadGitDirLink(self, dotgit):
3589 """Verify .git is initialized correctly, otherwise delete it."""
3590 if self.parent and os.path.isfile(dotgit):
3591 with open(dotgit) as fp:
3592 setting = fp.read()
3593 if not setting.startswith("gitdir:"):
3594 raise GitError(
3595 f"'.git' in {self.worktree} must start with 'gitdir:'",
3596 project=self.name,
3597 )
3598 gitdir = setting.split(":", 1)[1].strip()
3599 dotgit_path = os.path.normpath(os.path.join(self.worktree, gitdir))
3600 else:
3601 dotgit_path = os.path.realpath(dotgit)
3602 if os.path.realpath(self.gitdir) != dotgit_path:
3603 platform_utils.remove(dotgit)
3604
Gavin Makea2e3302023-03-11 06:46:20 +00003605 @classmethod
Jason Chang32b59562023-07-14 16:45:35 -07003606 def _MigrateOldWorkTreeGitDir(cls, dotgit, project=None):
Gavin Makea2e3302023-03-11 06:46:20 +00003607 """Migrate the old worktree .git/ dir style to a symlink.
3608
3609 This logic specifically only uses state from |dotgit| to figure out
3610 where to move content and not |self|. This way if the backing project
3611 also changed places, we only do the .git/ dir to .git symlink migration
3612 here. The path updates will happen independently.
3613 """
3614 # Figure out where in .repo/projects/ it's pointing to.
3615 if not os.path.islink(os.path.join(dotgit, "refs")):
Jason Chang32b59562023-07-14 16:45:35 -07003616 raise GitError(
3617 f"{dotgit}: unsupported checkout state", project=project
3618 )
Gavin Makea2e3302023-03-11 06:46:20 +00003619 gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, "refs")))
3620
3621 # Remove known symlink paths that exist in .repo/projects/.
3622 KNOWN_LINKS = {
3623 "config",
3624 "description",
3625 "hooks",
3626 "info",
3627 "logs",
3628 "objects",
3629 "packed-refs",
3630 "refs",
3631 "rr-cache",
3632 "shallow",
3633 "svn",
3634 }
3635 # Paths that we know will be in both, but are safe to clobber in
3636 # .repo/projects/.
3637 SAFE_TO_CLOBBER = {
3638 "COMMIT_EDITMSG",
3639 "FETCH_HEAD",
3640 "HEAD",
3641 "gc.log",
3642 "gitk.cache",
3643 "index",
3644 "ORIG_HEAD",
3645 }
3646
3647 # First see if we'd succeed before starting the migration.
3648 unknown_paths = []
3649 for name in platform_utils.listdir(dotgit):
3650 # Ignore all temporary/backup names. These are common with vim &
3651 # emacs.
3652 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3653 continue
3654
3655 dotgit_path = os.path.join(dotgit, name)
3656 if name in KNOWN_LINKS:
3657 if not platform_utils.islink(dotgit_path):
3658 unknown_paths.append(f"{dotgit_path}: should be a symlink")
3659 else:
3660 gitdir_path = os.path.join(gitdir, name)
3661 if name not in SAFE_TO_CLOBBER and os.path.exists(gitdir_path):
3662 unknown_paths.append(
3663 f"{dotgit_path}: unknown file; please file a bug"
3664 )
3665 if unknown_paths:
Jason Chang32b59562023-07-14 16:45:35 -07003666 raise GitError(
3667 "Aborting migration: " + "\n".join(unknown_paths),
3668 project=project,
3669 )
Gavin Makea2e3302023-03-11 06:46:20 +00003670
3671 # Now walk the paths and sync the .git/ to .repo/projects/.
3672 for name in platform_utils.listdir(dotgit):
3673 dotgit_path = os.path.join(dotgit, name)
3674
3675 # Ignore all temporary/backup names. These are common with vim &
3676 # emacs.
3677 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3678 platform_utils.remove(dotgit_path)
3679 elif name in KNOWN_LINKS:
3680 platform_utils.remove(dotgit_path)
3681 else:
3682 gitdir_path = os.path.join(gitdir, name)
3683 platform_utils.remove(gitdir_path, missing_ok=True)
3684 platform_utils.rename(dotgit_path, gitdir_path)
3685
3686 # Now that the dir should be empty, clear it out, and symlink it over.
3687 platform_utils.rmdir(dotgit)
3688 platform_utils.symlink(
Jason R. Coombs47944bb2023-09-29 12:42:22 -04003689 os.path.relpath(gitdir, os.path.dirname(os.path.realpath(dotgit))),
3690 dotgit,
Gavin Makea2e3302023-03-11 06:46:20 +00003691 )
3692
Kaushik Lingarkarcf9a2a22024-12-17 12:49:14 -08003693 def _MigrateOldSubmoduleDir(self):
3694 """Move the old bare checkout in 'subprojects' to 'modules'
3695 as bare checkouts of submodules are now in 'modules' dir.
3696 """
3697 subprojects = os.path.join(self.parent.gitdir, "subprojects")
3698 if not platform_utils.isdir(subprojects):
3699 return
3700
3701 modules = os.path.join(self.parent.gitdir, "modules")
3702 old = self.gitdir
3703 new = os.path.splitext(self.gitdir.replace(subprojects, modules))[0]
3704
3705 if all(map(platform_utils.isdir, [old, new])):
3706 platform_utils.rmtree(old, ignore_errors=True)
3707 else:
3708 os.makedirs(modules, exist_ok=True)
3709 platform_utils.rename(old, new)
3710 self.gitdir = new
3711 self.UpdatePaths(self.relpath, self.worktree, self.gitdir, self.objdir)
3712 if platform_utils.isdir(subprojects) and not os.listdir(subprojects):
3713 platform_utils.rmtree(subprojects, ignore_errors=True)
3714
Gavin Makea2e3302023-03-11 06:46:20 +00003715 def _get_symlink_error_message(self):
3716 if platform_utils.isWindows():
3717 return (
3718 "Unable to create symbolic link. Please re-run the command as "
3719 "Administrator, or see "
3720 "https://github.com/git-for-windows/git/wiki/Symbolic-Links "
3721 "for other options."
3722 )
3723 return "filesystem must support symlinks"
3724
3725 def _revlist(self, *args, **kw):
3726 a = []
3727 a.extend(args)
3728 a.append("--")
3729 return self.work_git.rev_list(*a, **kw)
3730
3731 @property
3732 def _allrefs(self):
3733 return self.bare_ref.all
3734
3735 def _getLogs(
3736 self, rev1, rev2, oneline=False, color=True, pretty_format=None
3737 ):
3738 """Get logs between two revisions of this project."""
3739 comp = ".."
3740 if rev1:
3741 revs = [rev1]
3742 if rev2:
3743 revs.extend([comp, rev2])
3744 cmd = ["log", "".join(revs)]
3745 out = DiffColoring(self.config)
3746 if out.is_on and color:
3747 cmd.append("--color")
3748 if pretty_format is not None:
3749 cmd.append("--pretty=format:%s" % pretty_format)
3750 if oneline:
3751 cmd.append("--oneline")
3752
3753 try:
3754 log = GitCommand(
3755 self, cmd, capture_stdout=True, capture_stderr=True
3756 )
3757 if log.Wait() == 0:
3758 return log.stdout
3759 except GitError:
3760 # worktree may not exist if groups changed for example. In that
3761 # case, try in gitdir instead.
3762 if not os.path.exists(self.worktree):
3763 return self.bare_git.log(*cmd[1:])
3764 else:
3765 raise
3766 return None
3767
3768 def getAddedAndRemovedLogs(
3769 self, toProject, oneline=False, color=True, pretty_format=None
3770 ):
3771 """Get the list of logs from this revision to given revisionId"""
3772 logs = {}
3773 selfId = self.GetRevisionId(self._allrefs)
3774 toId = toProject.GetRevisionId(toProject._allrefs)
3775
3776 logs["added"] = self._getLogs(
3777 selfId,
3778 toId,
3779 oneline=oneline,
3780 color=color,
3781 pretty_format=pretty_format,
3782 )
3783 logs["removed"] = self._getLogs(
3784 toId,
3785 selfId,
3786 oneline=oneline,
3787 color=color,
3788 pretty_format=pretty_format,
3789 )
3790 return logs
3791
Mike Frysingerd4aee652023-10-19 05:13:32 -04003792 class _GitGetByExec:
Gavin Makea2e3302023-03-11 06:46:20 +00003793 def __init__(self, project, bare, gitdir):
3794 self._project = project
3795 self._bare = bare
3796 self._gitdir = gitdir
3797
3798 # __getstate__ and __setstate__ are required for pickling because
3799 # __getattr__ exists.
3800 def __getstate__(self):
3801 return (self._project, self._bare, self._gitdir)
3802
3803 def __setstate__(self, state):
3804 self._project, self._bare, self._gitdir = state
3805
3806 def LsOthers(self):
3807 p = GitCommand(
3808 self._project,
3809 ["ls-files", "-z", "--others", "--exclude-standard"],
3810 bare=False,
3811 gitdir=self._gitdir,
3812 capture_stdout=True,
3813 capture_stderr=True,
3814 )
3815 if p.Wait() == 0:
3816 out = p.stdout
3817 if out:
3818 # Backslash is not anomalous.
3819 return out[:-1].split("\0")
3820 return []
3821
3822 def DiffZ(self, name, *args):
3823 cmd = [name]
3824 cmd.append("-z")
3825 cmd.append("--ignore-submodules")
3826 cmd.extend(args)
3827 p = GitCommand(
3828 self._project,
3829 cmd,
3830 gitdir=self._gitdir,
3831 bare=False,
3832 capture_stdout=True,
3833 capture_stderr=True,
3834 )
3835 p.Wait()
3836 r = {}
3837 out = p.stdout
3838 if out:
3839 out = iter(out[:-1].split("\0"))
3840 while out:
3841 try:
3842 info = next(out)
3843 path = next(out)
3844 except StopIteration:
3845 break
3846
Mike Frysingerd4aee652023-10-19 05:13:32 -04003847 class _Info:
Gavin Makea2e3302023-03-11 06:46:20 +00003848 def __init__(self, path, omode, nmode, oid, nid, state):
3849 self.path = path
3850 self.src_path = None
3851 self.old_mode = omode
3852 self.new_mode = nmode
3853 self.old_id = oid
3854 self.new_id = nid
3855
3856 if len(state) == 1:
3857 self.status = state
3858 self.level = None
3859 else:
3860 self.status = state[:1]
3861 self.level = state[1:]
3862 while self.level.startswith("0"):
3863 self.level = self.level[1:]
3864
3865 info = info[1:].split(" ")
3866 info = _Info(path, *info)
3867 if info.status in ("R", "C"):
3868 info.src_path = info.path
3869 info.path = next(out)
3870 r[info.path] = info
3871 return r
3872
3873 def GetDotgitPath(self, subpath=None):
3874 """Return the full path to the .git dir.
3875
3876 As a convenience, append |subpath| if provided.
3877 """
3878 if self._bare:
3879 dotgit = self._gitdir
3880 else:
3881 dotgit = os.path.join(self._project.worktree, ".git")
3882 if os.path.isfile(dotgit):
3883 # Git worktrees use a "gitdir:" syntax to point to the
3884 # scratch space.
3885 with open(dotgit) as fp:
3886 setting = fp.read()
3887 assert setting.startswith("gitdir:")
3888 gitdir = setting.split(":", 1)[1].strip()
3889 dotgit = os.path.normpath(
3890 os.path.join(self._project.worktree, gitdir)
3891 )
3892
3893 return dotgit if subpath is None else os.path.join(dotgit, subpath)
3894
3895 def GetHead(self):
3896 """Return the ref that HEAD points to."""
Gavin Makea2e3302023-03-11 06:46:20 +00003897 try:
Gavin Mak7f7d70e2025-07-25 18:21:50 +00003898 symbolic_head = self.rev_parse("--symbolic-full-name", HEAD)
3899 if symbolic_head == HEAD:
3900 # Detached HEAD. Return the commit SHA instead.
3901 return self.rev_parse(HEAD)
3902 return symbolic_head
Gavin Mak52bab0b2025-07-21 13:07:37 -07003903 except GitError as e:
Gavin Mak8c3585f2025-08-04 12:08:13 -07003904 logger.warning(
3905 "project %s: unparseable HEAD; trying to recover.\n"
3906 "Check that HEAD ref in .git/HEAD is valid. The error "
3907 "was: %s",
3908 self._project.RelPath(local=False),
3909 e,
3910 )
3911
3912 # Fallback to direct file reading for compatibility with broken
3913 # repos, e.g. if HEAD points to an unborn branch.
Gavin Mak52bab0b2025-07-21 13:07:37 -07003914 path = self.GetDotgitPath(subpath=HEAD)
Gavin Mak8c3585f2025-08-04 12:08:13 -07003915 try:
3916 with open(path) as fd:
3917 line = fd.readline()
3918 except OSError:
3919 raise NoManifestException(path, str(e))
3920 try:
3921 line = line.decode()
3922 except AttributeError:
3923 pass
3924 if line.startswith("ref: "):
3925 return line[5:-1]
3926 return line[:-1]
Gavin Makea2e3302023-03-11 06:46:20 +00003927
3928 def SetHead(self, ref, message=None):
3929 cmdv = []
3930 if message is not None:
3931 cmdv.extend(["-m", message])
3932 cmdv.append(HEAD)
3933 cmdv.append(ref)
3934 self.symbolic_ref(*cmdv)
3935
3936 def DetachHead(self, new, message=None):
3937 cmdv = ["--no-deref"]
3938 if message is not None:
3939 cmdv.extend(["-m", message])
3940 cmdv.append(HEAD)
3941 cmdv.append(new)
3942 self.update_ref(*cmdv)
3943
3944 def UpdateRef(self, name, new, old=None, message=None, detach=False):
3945 cmdv = []
3946 if message is not None:
3947 cmdv.extend(["-m", message])
3948 if detach:
3949 cmdv.append("--no-deref")
3950 cmdv.append(name)
3951 cmdv.append(new)
3952 if old is not None:
3953 cmdv.append(old)
3954 self.update_ref(*cmdv)
3955
3956 def DeleteRef(self, name, old=None):
3957 if not old:
3958 old = self.rev_parse(name)
3959 self.update_ref("-d", name, old)
3960 self._project.bare_ref.deleted(name)
3961
Jason Chang87058c62023-09-27 11:34:43 -07003962 def rev_list(self, *args, log_as_error=True, **kw):
Gavin Makea2e3302023-03-11 06:46:20 +00003963 if "format" in kw:
3964 cmdv = ["log", "--pretty=format:%s" % kw["format"]]
3965 else:
3966 cmdv = ["rev-list"]
3967 cmdv.extend(args)
3968 p = GitCommand(
3969 self._project,
3970 cmdv,
3971 bare=self._bare,
3972 gitdir=self._gitdir,
3973 capture_stdout=True,
3974 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003975 verify_command=True,
Jason Chang87058c62023-09-27 11:34:43 -07003976 log_as_error=log_as_error,
Gavin Makea2e3302023-03-11 06:46:20 +00003977 )
Jason Chang32b59562023-07-14 16:45:35 -07003978 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003979 return p.stdout.splitlines()
3980
3981 def __getattr__(self, name):
3982 """Allow arbitrary git commands using pythonic syntax.
3983
3984 This allows you to do things like:
3985 git_obj.rev_parse('HEAD')
3986
3987 Since we don't have a 'rev_parse' method defined, the __getattr__
3988 will run. We'll replace the '_' with a '-' and try to run a git
3989 command. Any other positional arguments will be passed to the git
3990 command, and the following keyword arguments are supported:
3991 config: An optional dict of git config options to be passed with
3992 '-c'.
3993
3994 Args:
3995 name: The name of the git command to call. Any '_' characters
3996 will be replaced with '-'.
3997
3998 Returns:
3999 A callable object that will try to call git with the named
4000 command.
4001 """
4002 name = name.replace("_", "-")
4003
Jason Chang87058c62023-09-27 11:34:43 -07004004 def runner(*args, log_as_error=True, **kwargs):
Gavin Makea2e3302023-03-11 06:46:20 +00004005 cmdv = []
4006 config = kwargs.pop("config", None)
4007 for k in kwargs:
4008 raise TypeError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04004009 f"{name}() got an unexpected keyword argument {k!r}"
Gavin Makea2e3302023-03-11 06:46:20 +00004010 )
4011 if config is not None:
4012 for k, v in config.items():
4013 cmdv.append("-c")
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04004014 cmdv.append(f"{k}={v}")
Gavin Makea2e3302023-03-11 06:46:20 +00004015 cmdv.append(name)
4016 cmdv.extend(args)
4017 p = GitCommand(
4018 self._project,
4019 cmdv,
4020 bare=self._bare,
4021 gitdir=self._gitdir,
4022 capture_stdout=True,
4023 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07004024 verify_command=True,
Jason Chang87058c62023-09-27 11:34:43 -07004025 log_as_error=log_as_error,
Gavin Makea2e3302023-03-11 06:46:20 +00004026 )
Jason Chang32b59562023-07-14 16:45:35 -07004027 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00004028 r = p.stdout
4029 if r.endswith("\n") and r.index("\n") == len(r) - 1:
4030 return r[:-1]
4031 return r
4032
4033 return runner
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07004034
4035
Jason Chang32b59562023-07-14 16:45:35 -07004036class LocalSyncFail(RepoError):
4037 """Default error when there is an Sync_LocalHalf error."""
4038
4039
4040class _PriorSyncFailedError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00004041 def __str__(self):
4042 return "prior sync failed; rebase still in progress"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07004043
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07004044
Jason Chang32b59562023-07-14 16:45:35 -07004045class _DirtyError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00004046 def __str__(self):
4047 return "contains uncommitted changes"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07004048
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07004049
Mike Frysingerd4aee652023-10-19 05:13:32 -04004050class _InfoMessage:
Gavin Makea2e3302023-03-11 06:46:20 +00004051 def __init__(self, project, text):
4052 self.project = project
4053 self.text = text
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07004054
Gavin Makea2e3302023-03-11 06:46:20 +00004055 def Print(self, syncbuf):
4056 syncbuf.out.info(
4057 "%s/: %s", self.project.RelPath(local=False), self.text
4058 )
4059 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07004060
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07004061
Mike Frysingerd4aee652023-10-19 05:13:32 -04004062class _Failure:
Gavin Makea2e3302023-03-11 06:46:20 +00004063 def __init__(self, project, why):
4064 self.project = project
4065 self.why = why
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07004066
Gavin Makea2e3302023-03-11 06:46:20 +00004067 def Print(self, syncbuf):
4068 syncbuf.out.fail(
4069 "error: %s/: %s", self.project.RelPath(local=False), str(self.why)
4070 )
4071 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07004072
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07004073
Mike Frysingerd4aee652023-10-19 05:13:32 -04004074class _Later:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08004075 def __init__(self, project, action, quiet):
Gavin Makea2e3302023-03-11 06:46:20 +00004076 self.project = project
4077 self.action = action
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08004078 self.quiet = quiet
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07004079
Gavin Makea2e3302023-03-11 06:46:20 +00004080 def Run(self, syncbuf):
4081 out = syncbuf.out
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08004082 if not self.quiet:
4083 out.project("project %s/", self.project.RelPath(local=False))
4084 out.nl()
Gavin Makea2e3302023-03-11 06:46:20 +00004085 try:
4086 self.action()
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08004087 if not self.quiet:
4088 out.nl()
Gavin Makea2e3302023-03-11 06:46:20 +00004089 return True
Gavin Maka64149a2025-08-13 22:48:36 -07004090 except GitError as e:
4091 syncbuf.fail(self.project, e)
Gavin Makea2e3302023-03-11 06:46:20 +00004092 out.nl()
4093 return False
Shawn O. Pearce350cde42009-04-16 11:21:18 -07004094
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07004095
Shawn O. Pearce350cde42009-04-16 11:21:18 -07004096class _SyncColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +00004097 def __init__(self, config):
4098 super().__init__(config, "reposync")
4099 self.project = self.printer("header", attr="bold")
4100 self.info = self.printer("info")
4101 self.fail = self.printer("fail", fg="red")
Shawn O. Pearce350cde42009-04-16 11:21:18 -07004102
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07004103
Mike Frysingerd4aee652023-10-19 05:13:32 -04004104class SyncBuffer:
Gavin Makea2e3302023-03-11 06:46:20 +00004105 def __init__(self, config, detach_head=False):
4106 self._messages = []
Gavin Maka64149a2025-08-13 22:48:36 -07004107
4108 # Failures that have not yet been printed. Cleared after printing.
4109 self._pending_failures = []
4110 # A persistent record of all failures during the buffer's lifetime.
4111 self._all_failures = []
4112
Gavin Makea2e3302023-03-11 06:46:20 +00004113 self._later_queue1 = []
4114 self._later_queue2 = []
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07004115
Gavin Makea2e3302023-03-11 06:46:20 +00004116 self.out = _SyncColoring(config)
4117 self.out.redirect(sys.stderr)
Shawn O. Pearce350cde42009-04-16 11:21:18 -07004118
Gavin Makea2e3302023-03-11 06:46:20 +00004119 self.detach_head = detach_head
4120 self.clean = True
4121 self.recent_clean = True
Shawn O. Pearce350cde42009-04-16 11:21:18 -07004122
Gavin Makea2e3302023-03-11 06:46:20 +00004123 def info(self, project, fmt, *args):
4124 self._messages.append(_InfoMessage(project, fmt % args))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07004125
Gavin Makea2e3302023-03-11 06:46:20 +00004126 def fail(self, project, err=None):
Gavin Maka64149a2025-08-13 22:48:36 -07004127 failure = _Failure(project, err)
4128 self._pending_failures.append(failure)
4129 self._all_failures.append(failure)
David Rileye0684ad2017-04-05 00:02:59 -07004130 self._MarkUnclean()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07004131
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08004132 def later1(self, project, what, quiet):
4133 self._later_queue1.append(_Later(project, what, quiet))
Mike Frysinger70d861f2019-08-26 15:22:36 -04004134
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08004135 def later2(self, project, what, quiet):
4136 self._later_queue2.append(_Later(project, what, quiet))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07004137
Gavin Makea2e3302023-03-11 06:46:20 +00004138 def Finish(self):
4139 self._PrintMessages()
4140 self._RunLater()
4141 self._PrintMessages()
4142 return self.clean
4143
4144 def Recently(self):
4145 recent_clean = self.recent_clean
4146 self.recent_clean = True
4147 return recent_clean
4148
Gavin Maka64149a2025-08-13 22:48:36 -07004149 @property
4150 def errors(self):
4151 """Returns a list of all exceptions accumulated in the buffer."""
4152 return [f.why for f in self._all_failures if f.why]
4153
Gavin Makea2e3302023-03-11 06:46:20 +00004154 def _MarkUnclean(self):
4155 self.clean = False
4156 self.recent_clean = False
4157
4158 def _RunLater(self):
4159 for q in ["_later_queue1", "_later_queue2"]:
4160 if not self._RunQueue(q):
4161 return
4162
4163 def _RunQueue(self, queue):
4164 for m in getattr(self, queue):
4165 if not m.Run(self):
4166 self._MarkUnclean()
4167 return False
4168 setattr(self, queue, [])
4169 return True
4170
4171 def _PrintMessages(self):
Gavin Maka64149a2025-08-13 22:48:36 -07004172 if self._messages or self._pending_failures:
Gavin Makea2e3302023-03-11 06:46:20 +00004173 if os.isatty(2):
4174 self.out.write(progress.CSI_ERASE_LINE)
4175 self.out.write("\r")
4176
4177 for m in self._messages:
4178 m.Print(self)
Gavin Maka64149a2025-08-13 22:48:36 -07004179 for m in self._pending_failures:
Gavin Makea2e3302023-03-11 06:46:20 +00004180 m.Print(self)
4181
4182 self._messages = []
Gavin Maka64149a2025-08-13 22:48:36 -07004183 self._pending_failures = []
Shawn O. Pearce350cde42009-04-16 11:21:18 -07004184
4185
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07004186class MetaProject(Project):
Gavin Makea2e3302023-03-11 06:46:20 +00004187 """A special project housed under .repo."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07004188
Gavin Makea2e3302023-03-11 06:46:20 +00004189 def __init__(self, manifest, name, gitdir, worktree):
4190 Project.__init__(
4191 self,
4192 manifest=manifest,
4193 name=name,
4194 gitdir=gitdir,
4195 objdir=gitdir,
4196 worktree=worktree,
4197 remote=RemoteSpec("origin"),
4198 relpath=".repo/%s" % name,
4199 revisionExpr="refs/heads/master",
4200 revisionId=None,
4201 groups=None,
4202 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07004203
Gavin Makea2e3302023-03-11 06:46:20 +00004204 def PreSync(self):
4205 if self.Exists:
4206 cb = self.CurrentBranch
4207 if cb:
4208 base = self.GetBranch(cb).merge
4209 if base:
4210 self.revisionExpr = base
4211 self.revisionId = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07004212
Gavin Makea2e3302023-03-11 06:46:20 +00004213 @property
4214 def HasChanges(self):
4215 """Has the remote received new commits not yet checked out?"""
4216 if not self.remote or not self.revisionExpr:
4217 return False
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07004218
Gavin Makea2e3302023-03-11 06:46:20 +00004219 all_refs = self.bare_ref.all
4220 revid = self.GetRevisionId(all_refs)
4221 head = self.work_git.GetHead()
4222 if head.startswith(R_HEADS):
4223 try:
4224 head = all_refs[head]
4225 except KeyError:
4226 head = None
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07004227
Gavin Makea2e3302023-03-11 06:46:20 +00004228 if revid == head:
4229 return False
4230 elif self._revlist(not_rev(HEAD), revid):
4231 return True
4232 return False
LaMont Jones9b72cf22022-03-29 21:54:22 +00004233
4234
4235class RepoProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00004236 """The MetaProject for repo itself."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00004237
Gavin Makea2e3302023-03-11 06:46:20 +00004238 @property
4239 def LastFetch(self):
4240 try:
4241 fh = os.path.join(self.gitdir, "FETCH_HEAD")
4242 return os.path.getmtime(fh)
4243 except OSError:
4244 return 0
LaMont Jones9b72cf22022-03-29 21:54:22 +00004245
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +01004246
LaMont Jones9b72cf22022-03-29 21:54:22 +00004247class ManifestProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00004248 """The MetaProject for manifests."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00004249
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08004250 def MetaBranchSwitch(self, submodules=False, verbose=False):
Gavin Makea2e3302023-03-11 06:46:20 +00004251 """Prepare for manifest branch switch."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00004252
Gavin Makea2e3302023-03-11 06:46:20 +00004253 # detach and delete manifest branch, allowing a new
4254 # branch to take over
4255 syncbuf = SyncBuffer(self.config, detach_head=True)
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08004256 self.Sync_LocalHalf(syncbuf, submodules=submodules, verbose=verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00004257 syncbuf.Finish()
LaMont Jones9b72cf22022-03-29 21:54:22 +00004258
Gavin Makea2e3302023-03-11 06:46:20 +00004259 return (
4260 GitCommand(
4261 self,
4262 ["update-ref", "-d", "refs/heads/default"],
4263 capture_stdout=True,
4264 capture_stderr=True,
4265 ).Wait()
4266 == 0
4267 )
LaMont Jones9b03f152022-03-29 23:01:18 +00004268
Gavin Makea2e3302023-03-11 06:46:20 +00004269 @property
4270 def standalone_manifest_url(https://gerrit.googlesource.com/self):
4271 """The URL of the standalone manifest, or None."""
4272 return self.config.GetString("manifest.standalone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004273
Gavin Makea2e3302023-03-11 06:46:20 +00004274 @property
4275 def manifest_groups(self):
4276 """The manifest groups string."""
4277 return self.config.GetString("manifest.groups")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004278
Gavin Makea2e3302023-03-11 06:46:20 +00004279 @property
4280 def reference(self):
4281 """The --reference for this manifest."""
4282 return self.config.GetString("repo.reference")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004283
Gavin Makea2e3302023-03-11 06:46:20 +00004284 @property
4285 def dissociate(self):
4286 """Whether to dissociate."""
4287 return self.config.GetBoolean("repo.dissociate")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004288
Gavin Makea2e3302023-03-11 06:46:20 +00004289 @property
4290 def archive(self):
4291 """Whether we use archive."""
4292 return self.config.GetBoolean("repo.archive")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004293
Gavin Makea2e3302023-03-11 06:46:20 +00004294 @property
4295 def mirror(self):
4296 """Whether we use mirror."""
4297 return self.config.GetBoolean("repo.mirror")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004298
Gavin Makea2e3302023-03-11 06:46:20 +00004299 @property
4300 def use_worktree(self):
4301 """Whether we use worktree."""
4302 return self.config.GetBoolean("repo.worktree")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004303
Gavin Makea2e3302023-03-11 06:46:20 +00004304 @property
4305 def clone_bundle(self):
4306 """Whether we use clone_bundle."""
4307 return self.config.GetBoolean("repo.clonebundle")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004308
Gavin Makea2e3302023-03-11 06:46:20 +00004309 @property
4310 def submodules(self):
4311 """Whether we use submodules."""
4312 return self.config.GetBoolean("repo.submodules")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004313
Gavin Makea2e3302023-03-11 06:46:20 +00004314 @property
4315 def git_lfs(self):
4316 """Whether we use git_lfs."""
4317 return self.config.GetBoolean("repo.git-lfs")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004318
Gavin Makea2e3302023-03-11 06:46:20 +00004319 @property
4320 def use_superproject(self):
4321 """Whether we use superproject."""
4322 return self.config.GetBoolean("repo.superproject")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004323
Gavin Makea2e3302023-03-11 06:46:20 +00004324 @property
4325 def partial_clone(self):
4326 """Whether this is a partial clone."""
4327 return self.config.GetBoolean("repo.partialclone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004328
Gavin Makea2e3302023-03-11 06:46:20 +00004329 @property
4330 def depth(self):
4331 """Partial clone depth."""
Roberto Vladimir Prado Carranza3d58d212023-09-13 10:27:26 +02004332 return self.config.GetInt("repo.depth")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004333
Gavin Makea2e3302023-03-11 06:46:20 +00004334 @property
4335 def clone_filter(self):
4336 """The clone filter."""
4337 return self.config.GetString("repo.clonefilter")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004338
Gavin Makea2e3302023-03-11 06:46:20 +00004339 @property
4340 def partial_clone_exclude(self):
4341 """Partial clone exclude string"""
4342 return self.config.GetString("repo.partialcloneexclude")
LaMont Jones4ada0432022-04-14 15:10:43 +00004343
Gavin Makea2e3302023-03-11 06:46:20 +00004344 @property
Jason Chang17833322023-05-23 13:06:55 -07004345 def clone_filter_for_depth(self):
4346 """Replace shallow clone with partial clone."""
4347 return self.config.GetString("repo.clonefilterfordepth")
4348
4349 @property
Gavin Makea2e3302023-03-11 06:46:20 +00004350 def manifest_platform(self):
4351 """The --platform argument from `repo init`."""
4352 return self.config.GetString("manifest.platform")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004353
Gavin Makea2e3302023-03-11 06:46:20 +00004354 @property
4355 def _platform_name(self):
4356 """Return the name of the platform."""
4357 return platform.system().lower()
LaMont Jones9b03f152022-03-29 23:01:18 +00004358
Gavin Makea2e3302023-03-11 06:46:20 +00004359 def SyncWithPossibleInit(
4360 self,
4361 submanifest,
4362 verbose=False,
4363 current_branch_only=False,
4364 tags="",
4365 git_event_log=None,
4366 ):
4367 """Sync a manifestProject, possibly for the first time.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004368
Gavin Makea2e3302023-03-11 06:46:20 +00004369 Call Sync() with arguments from the most recent `repo init`. If this is
4370 a new sub manifest, then inherit options from the parent's
4371 manifestProject.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004372
Gavin Makea2e3302023-03-11 06:46:20 +00004373 This is used by subcmds.Sync() to do an initial download of new sub
4374 manifests.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004375
Gavin Makea2e3302023-03-11 06:46:20 +00004376 Args:
4377 submanifest: an XmlSubmanifest, the submanifest to re-sync.
4378 verbose: a boolean, whether to show all output, rather than only
4379 errors.
4380 current_branch_only: a boolean, whether to only fetch the current
4381 manifest branch from the server.
4382 tags: a boolean, whether to fetch tags.
4383 git_event_log: an EventLog, for git tracing.
4384 """
4385 # TODO(lamontjones): when refactoring sync (and init?) consider how to
4386 # better get the init options that we should use for new submanifests
4387 # that are added when syncing an existing workspace.
4388 git_event_log = git_event_log or EventLog()
LaMont Jonesb90a4222022-04-14 15:00:09 +00004389 spec = submanifest.ToSubmanifestSpec()
Gavin Makea2e3302023-03-11 06:46:20 +00004390 # Use the init options from the existing manifestProject, or the parent
4391 # if it doesn't exist.
4392 #
4393 # Today, we only support changing manifest_groups on the sub-manifest,
4394 # with no supported-for-the-user way to change the other arguments from
4395 # those specified by the outermost manifest.
4396 #
4397 # TODO(lamontjones): determine which of these should come from the
4398 # outermost manifest and which should come from the parent manifest.
4399 mp = self if self.Exists else submanifest.parent.manifestProject
4400 return self.Sync(
LaMont Jones55ee3042022-04-06 17:10:21 +00004401 manifest_url=spec.manifestUrl,
4402 manifest_branch=spec.revision,
Gavin Makea2e3302023-03-11 06:46:20 +00004403 standalone_manifest=mp.standalone_manifest_url,
4404 groups=mp.manifest_groups,
4405 platform=mp.manifest_platform,
4406 mirror=mp.mirror,
4407 dissociate=mp.dissociate,
4408 reference=mp.reference,
4409 worktree=mp.use_worktree,
4410 submodules=mp.submodules,
4411 archive=mp.archive,
4412 partial_clone=mp.partial_clone,
4413 clone_filter=mp.clone_filter,
4414 partial_clone_exclude=mp.partial_clone_exclude,
4415 clone_bundle=mp.clone_bundle,
4416 git_lfs=mp.git_lfs,
4417 use_superproject=mp.use_superproject,
LaMont Jones55ee3042022-04-06 17:10:21 +00004418 verbose=verbose,
4419 current_branch_only=current_branch_only,
4420 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00004421 depth=mp.depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004422 git_event_log=git_event_log,
4423 manifest_name=spec.manifestName,
Gavin Makea2e3302023-03-11 06:46:20 +00004424 this_manifest_only=True,
LaMont Jones55ee3042022-04-06 17:10:21 +00004425 outer_manifest=False,
Jason Chang17833322023-05-23 13:06:55 -07004426 clone_filter_for_depth=mp.clone_filter_for_depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004427 )
LaMont Jones409407a2022-04-05 21:21:56 +00004428
Gavin Makea2e3302023-03-11 06:46:20 +00004429 def Sync(
4430 self,
4431 _kwargs_only=(),
4432 manifest_url="",
4433 manifest_branch=None,
4434 standalone_manifest=False,
4435 groups="",
4436 mirror=False,
4437 reference="",
4438 dissociate=False,
4439 worktree=False,
4440 submodules=False,
4441 archive=False,
4442 partial_clone=None,
4443 depth=None,
4444 clone_filter="blob:none",
4445 partial_clone_exclude=None,
4446 clone_bundle=None,
4447 git_lfs=None,
4448 use_superproject=None,
4449 verbose=False,
4450 current_branch_only=False,
4451 git_event_log=None,
4452 platform="",
4453 manifest_name="default.xml",
4454 tags="",
4455 this_manifest_only=False,
4456 outer_manifest=True,
Jason Chang17833322023-05-23 13:06:55 -07004457 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00004458 ):
4459 """Sync the manifest and all submanifests.
LaMont Jones409407a2022-04-05 21:21:56 +00004460
Gavin Makea2e3302023-03-11 06:46:20 +00004461 Args:
4462 manifest_url: a string, the URL of the manifest project.
4463 manifest_branch: a string, the manifest branch to use.
4464 standalone_manifest: a boolean, whether to store the manifest as a
4465 static file.
4466 groups: a string, restricts the checkout to projects with the
4467 specified groups.
4468 mirror: a boolean, whether to create a mirror of the remote
4469 repository.
4470 reference: a string, location of a repo instance to use as a
4471 reference.
4472 dissociate: a boolean, whether to dissociate from reference mirrors
4473 after clone.
4474 worktree: a boolean, whether to use git-worktree to manage projects.
4475 submodules: a boolean, whether sync submodules associated with the
4476 manifest project.
4477 archive: a boolean, whether to checkout each project as an archive.
4478 See git-archive.
4479 partial_clone: a boolean, whether to perform a partial clone.
4480 depth: an int, how deep of a shallow clone to create.
4481 clone_filter: a string, filter to use with partial_clone.
4482 partial_clone_exclude : a string, comma-delimeted list of project
4483 names to exclude from partial clone.
4484 clone_bundle: a boolean, whether to enable /clone.bundle on
4485 HTTP/HTTPS.
4486 git_lfs: a boolean, whether to enable git LFS support.
4487 use_superproject: a boolean, whether to use the manifest
4488 superproject to sync projects.
4489 verbose: a boolean, whether to show all output, rather than only
4490 errors.
4491 current_branch_only: a boolean, whether to only fetch the current
4492 manifest branch from the server.
4493 platform: a string, restrict the checkout to projects with the
4494 specified platform group.
4495 git_event_log: an EventLog, for git tracing.
4496 tags: a boolean, whether to fetch tags.
4497 manifest_name: a string, the name of the manifest file to use.
4498 this_manifest_only: a boolean, whether to only operate on the
4499 current sub manifest.
4500 outer_manifest: a boolean, whether to start at the outermost
4501 manifest.
Jason Chang17833322023-05-23 13:06:55 -07004502 clone_filter_for_depth: a string, when specified replaces shallow
4503 clones with partial.
LaMont Jones9b03f152022-03-29 23:01:18 +00004504
Gavin Makea2e3302023-03-11 06:46:20 +00004505 Returns:
4506 a boolean, whether the sync was successful.
4507 """
4508 assert _kwargs_only == (), "Sync only accepts keyword arguments."
LaMont Jones9b03f152022-03-29 23:01:18 +00004509
Gavin Makea2e3302023-03-11 06:46:20 +00004510 groups = groups or self.manifest.GetDefaultGroupsStr(
4511 with_platform=False
4512 )
4513 platform = platform or "auto"
4514 git_event_log = git_event_log or EventLog()
4515 if outer_manifest and self.manifest.is_submanifest:
4516 # In a multi-manifest checkout, use the outer manifest unless we are
4517 # told not to.
4518 return self.client.outer_manifest.manifestProject.Sync(
4519 manifest_url=manifest_url,
4520 manifest_branch=manifest_branch,
4521 standalone_manifest=standalone_manifest,
4522 groups=groups,
4523 platform=platform,
4524 mirror=mirror,
4525 dissociate=dissociate,
4526 reference=reference,
4527 worktree=worktree,
4528 submodules=submodules,
4529 archive=archive,
4530 partial_clone=partial_clone,
4531 clone_filter=clone_filter,
4532 partial_clone_exclude=partial_clone_exclude,
4533 clone_bundle=clone_bundle,
4534 git_lfs=git_lfs,
4535 use_superproject=use_superproject,
4536 verbose=verbose,
4537 current_branch_only=current_branch_only,
4538 tags=tags,
4539 depth=depth,
4540 git_event_log=git_event_log,
4541 manifest_name=manifest_name,
4542 this_manifest_only=this_manifest_only,
4543 outer_manifest=False,
4544 )
LaMont Jones9b03f152022-03-29 23:01:18 +00004545
Gavin Makea2e3302023-03-11 06:46:20 +00004546 # If repo has already been initialized, we take -u with the absence of
4547 # --standalone-manifest to mean "transition to a standard repo set up",
4548 # which necessitates starting fresh.
4549 # If --standalone-manifest is set, we always tear everything down and
4550 # start anew.
4551 if self.Exists:
4552 was_standalone_manifest = self.config.GetString(
4553 "manifest.standalone"
4554 )
4555 if was_standalone_manifest and not manifest_url:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004556 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004557 "fatal: repo was initialized with a standlone manifest, "
4558 "cannot be re-initialized without --manifest-url/-u"
4559 )
4560 return False
4561
4562 if standalone_manifest or (
4563 was_standalone_manifest and manifest_url
4564 ):
4565 self.config.ClearCache()
4566 if self.gitdir and os.path.exists(self.gitdir):
4567 platform_utils.rmtree(self.gitdir)
4568 if self.worktree and os.path.exists(self.worktree):
4569 platform_utils.rmtree(self.worktree)
4570
4571 is_new = not self.Exists
4572 if is_new:
4573 if not manifest_url:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004574 logger.error("fatal: manifest url is required.")
Gavin Makea2e3302023-03-11 06:46:20 +00004575 return False
4576
4577 if verbose:
4578 print(
4579 "Downloading manifest from %s"
4580 % (GitConfig.ForUser().UrlInsteadOf(manifest_url),),
4581 file=sys.stderr,
4582 )
4583
4584 # The manifest project object doesn't keep track of the path on the
4585 # server where this git is located, so let's save that here.
4586 mirrored_manifest_git = None
4587 if reference:
4588 manifest_git_path = urllib.parse.urlparse(manifest_url).path[1:]
4589 mirrored_manifest_git = os.path.join(
4590 reference, manifest_git_path
4591 )
4592 if not mirrored_manifest_git.endswith(".git"):
4593 mirrored_manifest_git += ".git"
4594 if not os.path.exists(mirrored_manifest_git):
4595 mirrored_manifest_git = os.path.join(
4596 reference, ".repo/manifests.git"
4597 )
4598
4599 self._InitGitDir(mirror_git=mirrored_manifest_git)
4600
4601 # If standalone_manifest is set, mark the project as "standalone" --
4602 # we'll still do much of the manifests.git set up, but will avoid actual
4603 # syncs to a remote.
4604 if standalone_manifest:
4605 self.config.SetString("manifest.standalone", manifest_url)
4606 elif not manifest_url and not manifest_branch:
4607 # If -u is set and --standalone-manifest is not, then we're not in
4608 # standalone mode. Otherwise, use config to infer what we were in
4609 # the last init.
4610 standalone_manifest = bool(
4611 self.config.GetString("manifest.standalone")
4612 )
4613 if not standalone_manifest:
4614 self.config.SetString("manifest.standalone", None)
4615
4616 self._ConfigureDepth(depth)
4617
4618 # Set the remote URL before the remote branch as we might need it below.
4619 if manifest_url:
4620 r = self.GetRemote()
4621 r.url = manifest_url
4622 r.ResetFetch()
4623 r.Save()
4624
4625 if not standalone_manifest:
4626 if manifest_branch:
4627 if manifest_branch == "HEAD":
4628 manifest_branch = self.ResolveRemoteHead()
4629 if manifest_branch is None:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004630 logger.error("fatal: unable to resolve HEAD")
Gavin Makea2e3302023-03-11 06:46:20 +00004631 return False
4632 self.revisionExpr = manifest_branch
4633 else:
4634 if is_new:
4635 default_branch = self.ResolveRemoteHead()
4636 if default_branch is None:
4637 # If the remote doesn't have HEAD configured, default to
4638 # master.
4639 default_branch = "refs/heads/master"
4640 self.revisionExpr = default_branch
4641 else:
4642 self.PreSync()
4643
4644 groups = re.split(r"[,\s]+", groups or "")
4645 all_platforms = ["linux", "darwin", "windows"]
4646 platformize = lambda x: "platform-" + x
4647 if platform == "auto":
4648 if not mirror and not self.mirror:
4649 groups.append(platformize(self._platform_name))
4650 elif platform == "all":
4651 groups.extend(map(platformize, all_platforms))
4652 elif platform in all_platforms:
4653 groups.append(platformize(platform))
4654 elif platform != "none":
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004655 logger.error("fatal: invalid platform flag", file=sys.stderr)
Gavin Makea2e3302023-03-11 06:46:20 +00004656 return False
4657 self.config.SetString("manifest.platform", platform)
4658
4659 groups = [x for x in groups if x]
4660 groupstr = ",".join(groups)
4661 if (
4662 platform == "auto"
4663 and groupstr == self.manifest.GetDefaultGroupsStr()
4664 ):
4665 groupstr = None
4666 self.config.SetString("manifest.groups", groupstr)
4667
4668 if reference:
4669 self.config.SetString("repo.reference", reference)
4670
4671 if dissociate:
4672 self.config.SetBoolean("repo.dissociate", dissociate)
4673
4674 if worktree:
4675 if mirror:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004676 logger.error("fatal: --mirror and --worktree are incompatible")
Gavin Makea2e3302023-03-11 06:46:20 +00004677 return False
4678 if submodules:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004679 logger.error(
4680 "fatal: --submodules and --worktree are incompatible"
Gavin Makea2e3302023-03-11 06:46:20 +00004681 )
4682 return False
4683 self.config.SetBoolean("repo.worktree", worktree)
4684 if is_new:
4685 self.use_git_worktrees = True
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004686 logger.warning("warning: --worktree is experimental!")
Gavin Makea2e3302023-03-11 06:46:20 +00004687
4688 if archive:
4689 if is_new:
4690 self.config.SetBoolean("repo.archive", archive)
4691 else:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004692 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004693 "fatal: --archive is only supported when initializing a "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004694 "new workspace."
Gavin Makea2e3302023-03-11 06:46:20 +00004695 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004696 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004697 "Either delete the .repo folder in this workspace, or "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004698 "initialize in another location."
Gavin Makea2e3302023-03-11 06:46:20 +00004699 )
4700 return False
4701
4702 if mirror:
4703 if is_new:
4704 self.config.SetBoolean("repo.mirror", mirror)
4705 else:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004706 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004707 "fatal: --mirror is only supported when initializing a new "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004708 "workspace."
Gavin Makea2e3302023-03-11 06:46:20 +00004709 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004710 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004711 "Either delete the .repo folder in this workspace, or "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004712 "initialize in another location."
Gavin Makea2e3302023-03-11 06:46:20 +00004713 )
4714 return False
4715
4716 if partial_clone is not None:
4717 if mirror:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004718 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004719 "fatal: --mirror and --partial-clone are mutually "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004720 "exclusive"
Gavin Makea2e3302023-03-11 06:46:20 +00004721 )
4722 return False
4723 self.config.SetBoolean("repo.partialclone", partial_clone)
4724 if clone_filter:
4725 self.config.SetString("repo.clonefilter", clone_filter)
4726 elif self.partial_clone:
4727 clone_filter = self.clone_filter
4728 else:
4729 clone_filter = None
4730
4731 if partial_clone_exclude is not None:
4732 self.config.SetString(
4733 "repo.partialcloneexclude", partial_clone_exclude
4734 )
4735
4736 if clone_bundle is None:
4737 clone_bundle = False if partial_clone else True
4738 else:
4739 self.config.SetBoolean("repo.clonebundle", clone_bundle)
4740
4741 if submodules:
4742 self.config.SetBoolean("repo.submodules", submodules)
4743
4744 if git_lfs is not None:
4745 if git_lfs:
4746 git_require((2, 17, 0), fail=True, msg="Git LFS support")
4747
4748 self.config.SetBoolean("repo.git-lfs", git_lfs)
4749 if not is_new:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004750 logger.warning(
Gavin Makea2e3302023-03-11 06:46:20 +00004751 "warning: Changing --git-lfs settings will only affect new "
4752 "project checkouts.\n"
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004753 " Existing projects will require manual updates.\n"
Gavin Makea2e3302023-03-11 06:46:20 +00004754 )
4755
Jason Chang17833322023-05-23 13:06:55 -07004756 if clone_filter_for_depth is not None:
4757 self.ConfigureCloneFilterForDepth(clone_filter_for_depth)
4758
Gavin Makea2e3302023-03-11 06:46:20 +00004759 if use_superproject is not None:
4760 self.config.SetBoolean("repo.superproject", use_superproject)
4761
4762 if not standalone_manifest:
4763 success = self.Sync_NetworkHalf(
4764 is_new=is_new,
4765 quiet=not verbose,
4766 verbose=verbose,
4767 clone_bundle=clone_bundle,
4768 current_branch_only=current_branch_only,
4769 tags=tags,
4770 submodules=submodules,
4771 clone_filter=clone_filter,
4772 partial_clone_exclude=self.manifest.PartialCloneExclude,
Jason Chang17833322023-05-23 13:06:55 -07004773 clone_filter_for_depth=self.manifest.CloneFilterForDepth,
Gavin Makea2e3302023-03-11 06:46:20 +00004774 ).success
4775 if not success:
4776 r = self.GetRemote()
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004777 logger.error("fatal: cannot obtain manifest %s", r.url)
Gavin Makea2e3302023-03-11 06:46:20 +00004778
4779 # Better delete the manifest git dir if we created it; otherwise
4780 # next time (when user fixes problems) we won't go through the
4781 # "is_new" logic.
4782 if is_new:
4783 platform_utils.rmtree(self.gitdir)
4784 return False
4785
4786 if manifest_branch:
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08004787 self.MetaBranchSwitch(submodules=submodules, verbose=verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00004788
4789 syncbuf = SyncBuffer(self.config)
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08004790 self.Sync_LocalHalf(syncbuf, submodules=submodules, verbose=verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00004791 syncbuf.Finish()
4792
4793 if is_new or self.CurrentBranch is None:
Jason Chang1a3612f2023-08-08 14:12:53 -07004794 try:
4795 self.StartBranch("default")
4796 except GitError as e:
4797 msg = str(e)
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004798 logger.error(
4799 "fatal: cannot create default in manifest %s", msg
Gavin Makea2e3302023-03-11 06:46:20 +00004800 )
4801 return False
4802
4803 if not manifest_name:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004804 logger.error("fatal: manifest name (-m) is required.")
Gavin Makea2e3302023-03-11 06:46:20 +00004805 return False
4806
4807 elif is_new:
4808 # This is a new standalone manifest.
4809 manifest_name = "default.xml"
4810 manifest_data = fetch.fetch_file(manifest_url, verbose=verbose)
4811 dest = os.path.join(self.worktree, manifest_name)
4812 os.makedirs(os.path.dirname(dest), exist_ok=True)
4813 with open(dest, "wb") as f:
4814 f.write(manifest_data)
4815
4816 try:
4817 self.manifest.Link(manifest_name)
4818 except ManifestParseError as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004819 logger.error("fatal: manifest '%s' not available", manifest_name)
4820 logger.error("fatal: %s", e)
Gavin Makea2e3302023-03-11 06:46:20 +00004821 return False
4822
4823 if not this_manifest_only:
4824 for submanifest in self.manifest.submanifests.values():
4825 spec = submanifest.ToSubmanifestSpec()
4826 submanifest.repo_client.manifestProject.Sync(
4827 manifest_url=spec.manifestUrl,
4828 manifest_branch=spec.revision,
4829 standalone_manifest=standalone_manifest,
4830 groups=self.manifest_groups,
4831 platform=platform,
4832 mirror=mirror,
4833 dissociate=dissociate,
4834 reference=reference,
4835 worktree=worktree,
4836 submodules=submodules,
4837 archive=archive,
4838 partial_clone=partial_clone,
4839 clone_filter=clone_filter,
4840 partial_clone_exclude=partial_clone_exclude,
4841 clone_bundle=clone_bundle,
4842 git_lfs=git_lfs,
4843 use_superproject=use_superproject,
4844 verbose=verbose,
4845 current_branch_only=current_branch_only,
4846 tags=tags,
4847 depth=depth,
4848 git_event_log=git_event_log,
4849 manifest_name=spec.manifestName,
4850 this_manifest_only=False,
4851 outer_manifest=False,
4852 )
4853
4854 # Lastly, if the manifest has a <superproject> then have the
4855 # superproject sync it (if it will be used).
4856 if git_superproject.UseSuperproject(use_superproject, self.manifest):
4857 sync_result = self.manifest.superproject.Sync(git_event_log)
4858 if not sync_result.success:
4859 submanifest = ""
4860 if self.manifest.path_prefix:
4861 submanifest = f"for {self.manifest.path_prefix} "
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004862 logger.warning(
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004863 "warning: git update of superproject %s failed, "
Gavin Makea2e3302023-03-11 06:46:20 +00004864 "repo sync will not use superproject to fetch source; "
4865 "while this error is not fatal, and you can continue to "
4866 "run repo sync, please run repo init with the "
4867 "--no-use-superproject option to stop seeing this warning",
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004868 submanifest,
Gavin Makea2e3302023-03-11 06:46:20 +00004869 )
4870 if sync_result.fatal and use_superproject is not None:
4871 return False
4872
4873 return True
4874
Jason Chang17833322023-05-23 13:06:55 -07004875 def ConfigureCloneFilterForDepth(self, clone_filter_for_depth):
4876 """Configure clone filter to replace shallow clones.
4877
4878 Args:
4879 clone_filter_for_depth: a string or None, e.g. 'blob:none' will
4880 disable shallow clones and replace with partial clone. None will
4881 enable shallow clones.
4882 """
4883 self.config.SetString(
4884 "repo.clonefilterfordepth", clone_filter_for_depth
4885 )
4886
Gavin Makea2e3302023-03-11 06:46:20 +00004887 def _ConfigureDepth(self, depth):
4888 """Configure the depth we'll sync down.
4889
4890 Args:
4891 depth: an int, how deep of a partial clone to create.
4892 """
4893 # Opt.depth will be non-None if user actually passed --depth to repo
4894 # init.
4895 if depth is not None:
4896 if depth > 0:
4897 # Positive values will set the depth.
4898 depth = str(depth)
4899 else:
4900 # Negative numbers will clear the depth; passing None to
4901 # SetString will do that.
4902 depth = None
4903
4904 # We store the depth in the main manifest project.
4905 self.config.SetString("repo.depth", depth)