blob: 069cc712d18bf7ea2e022b195d115e079b9c757b [file] [log] [blame]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001# Copyright (C) 2008 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
Shawn O. Pearce438ee1c2008-11-03 09:59:36 -080015import errno
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070016import filecmp
Wink Saville4c426ef2015-06-03 08:05:17 -070017import glob
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070018import os
LaMont Jonesd82be3e2022-04-05 19:30:46 +000019import platform
Shawn O. Pearcec325dc32011-10-03 08:30:24 -070020import random
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070021import re
22import shutil
23import stat
Shawn O. Pearce5e7127d2012-08-02 14:57:37 -070024import subprocess
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070025import sys
Julien Campergue335f5ef2013-10-16 11:02:35 +020026import tarfile
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +080027import tempfile
Shawn O. Pearcec325dc32011-10-03 08:30:24 -070028import time
Mike Frysinger64477332023-08-21 21:20:32 -040029from typing import List, NamedTuple
Mike Frysingeracf63b22019-06-13 02:24:21 -040030import urllib.parse
Shawn O. Pearcedf5ee522011-10-11 14:05:21 -070031
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070032from color import Coloring
Mike Frysinger64477332023-08-21 21:20:32 -040033from error import DownloadError
34from error import GitError
35from error import ManifestInvalidPathError
36from error import ManifestInvalidRevisionError
37from error import ManifestParseError
38from error import NoManifestException
39from error import RepoError
40from error import UploadError
LaMont Jones0de4fc32022-04-21 17:18:35 +000041import fetch
Mike Frysinger64477332023-08-21 21:20:32 -040042from git_command import git_require
43from git_command import GitCommand
44from git_config import GetSchemeFromUrl
45from git_config import GetUrlCookieFile
46from git_config import GitConfig
Mike Frysinger64477332023-08-21 21:20:32 -040047from git_config import IsId
48from git_refs import GitRefs
49from git_refs import HEAD
50from git_refs import R_HEADS
51from git_refs import R_M
52from git_refs import R_PUB
53from git_refs import R_TAGS
54from git_refs import R_WORKTREE_M
LaMont Jonesff6b1da2022-06-01 21:03:34 +000055import git_superproject
LaMont Jones55ee3042022-04-06 17:10:21 +000056from git_trace2_event_log import EventLog
Renaud Paquayd5cec5e2016-11-01 11:24:03 -070057import platform_utils
Mike Frysinger70d861f2019-08-26 15:22:36 -040058import progress
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +000059from repo_logging import RepoLogger
Joanna Wanga6c52f52022-11-03 16:51:19 -040060from repo_trace import Trace
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070061
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -070062
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +000063logger = RepoLogger(__file__)
64
65
LaMont Jones1eddca82022-09-01 15:15:04 +000066class SyncNetworkHalfResult(NamedTuple):
Gavin Makea2e3302023-03-11 06:46:20 +000067 """Sync_NetworkHalf return value."""
68
Gavin Makea2e3302023-03-11 06:46:20 +000069 # Did we query the remote? False when optimized_fetch is True and we have
70 # the commit already present.
71 remote_fetched: bool
Jason Chang32b59562023-07-14 16:45:35 -070072 # Error from SyncNetworkHalf
73 error: Exception = None
74
75 @property
76 def success(self) -> bool:
77 return not self.error
78
79
80class SyncNetworkHalfError(RepoError):
81 """Failure trying to sync."""
82
83
84class DeleteWorktreeError(RepoError):
85 """Failure to delete worktree."""
86
87 def __init__(
88 self, *args, aggregate_errors: List[Exception] = None, **kwargs
89 ) -> None:
90 super().__init__(*args, **kwargs)
91 self.aggregate_errors = aggregate_errors or []
92
93
94class DeleteDirtyWorktreeError(DeleteWorktreeError):
95 """Failure to delete worktree due to uncommitted changes."""
LaMont Jones1eddca82022-09-01 15:15:04 +000096
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +010097
George Engelbrecht9bc283e2020-04-02 12:36:09 -060098# Maximum sleep time allowed during retries.
99MAXIMUM_RETRY_SLEEP_SEC = 3600.0
100# +-10% random jitter is added to each Fetches retry sleep duration.
101RETRY_JITTER_PERCENT = 0.1
102
LaMont Jonesfa8d9392022-11-02 22:01:29 +0000103# Whether to use alternates. Switching back and forth is *NOT* supported.
Mike Frysinger1d00a7e2021-12-21 00:40:31 -0500104# TODO(vapier): Remove knob once behavior is verified.
Gavin Makea2e3302023-03-11 06:46:20 +0000105_ALTERNATES = os.environ.get("REPO_USE_ALTERNATES") == "1"
George Engelbrecht9bc283e2020-04-02 12:36:09 -0600106
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +0100107
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700108def _lwrite(path, content):
Gavin Makea2e3302023-03-11 06:46:20 +0000109 lock = "%s.lock" % path
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700110
Gavin Makea2e3302023-03-11 06:46:20 +0000111 # Maintain Unix line endings on all OS's to match git behavior.
112 with open(lock, "w", newline="\n") as fd:
113 fd.write(content)
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700114
Gavin Makea2e3302023-03-11 06:46:20 +0000115 try:
116 platform_utils.rename(lock, path)
117 except OSError:
118 platform_utils.remove(lock)
119 raise
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700120
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700121
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700122def not_rev(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000123 return "^" + r
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700124
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700125
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800126def sq(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000127 return "'" + r.replace("'", "'''") + "'"
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -0800128
David Pursehouse819827a2020-02-12 15:20:19 +0900129
Jonathan Nieder93719792015-03-17 11:29:58 -0700130_project_hook_list = None
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700131
132
Jonathan Nieder93719792015-03-17 11:29:58 -0700133def _ProjectHooks():
Gavin Makea2e3302023-03-11 06:46:20 +0000134 """List the hooks present in the 'hooks' directory.
Jonathan Nieder93719792015-03-17 11:29:58 -0700135
Gavin Makea2e3302023-03-11 06:46:20 +0000136 These hooks are project hooks and are copied to the '.git/hooks' directory
137 of all subprojects.
Jonathan Nieder93719792015-03-17 11:29:58 -0700138
Gavin Makea2e3302023-03-11 06:46:20 +0000139 This function caches the list of hooks (based on the contents of the
140 'repo/hooks' directory) on the first call.
Jonathan Nieder93719792015-03-17 11:29:58 -0700141
Gavin Makea2e3302023-03-11 06:46:20 +0000142 Returns:
143 A list of absolute paths to all of the files in the hooks directory.
144 """
145 global _project_hook_list
146 if _project_hook_list is None:
147 d = platform_utils.realpath(os.path.abspath(os.path.dirname(__file__)))
148 d = os.path.join(d, "hooks")
149 _project_hook_list = [
150 os.path.join(d, x) for x in platform_utils.listdir(d)
151 ]
152 return _project_hook_list
Jonathan Nieder93719792015-03-17 11:29:58 -0700153
154
Mike Frysingerd4aee652023-10-19 05:13:32 -0400155class DownloadedChange:
Gavin Makea2e3302023-03-11 06:46:20 +0000156 _commit_cache = None
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700157
Gavin Makea2e3302023-03-11 06:46:20 +0000158 def __init__(self, project, base, change_id, ps_id, commit):
159 self.project = project
160 self.base = base
161 self.change_id = change_id
162 self.ps_id = ps_id
163 self.commit = commit
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700164
Gavin Makea2e3302023-03-11 06:46:20 +0000165 @property
166 def commits(self):
167 if self._commit_cache is None:
168 self._commit_cache = self.project.bare_git.rev_list(
169 "--abbrev=8",
170 "--abbrev-commit",
171 "--pretty=oneline",
172 "--reverse",
173 "--date-order",
174 not_rev(self.base),
175 self.commit,
176 "--",
177 )
178 return self._commit_cache
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700179
180
Mike Frysingerd4aee652023-10-19 05:13:32 -0400181class ReviewableBranch:
Gavin Makea2e3302023-03-11 06:46:20 +0000182 _commit_cache = None
183 _base_exists = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700184
Gavin Makea2e3302023-03-11 06:46:20 +0000185 def __init__(self, project, branch, base):
186 self.project = project
187 self.branch = branch
188 self.base = base
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700189
Gavin Makea2e3302023-03-11 06:46:20 +0000190 @property
191 def name(self):
192 return self.branch.name
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700193
Gavin Makea2e3302023-03-11 06:46:20 +0000194 @property
195 def commits(self):
196 if self._commit_cache is None:
197 args = (
198 "--abbrev=8",
199 "--abbrev-commit",
200 "--pretty=oneline",
201 "--reverse",
202 "--date-order",
203 not_rev(self.base),
204 R_HEADS + self.name,
205 "--",
206 )
207 try:
Jason Chang87058c62023-09-27 11:34:43 -0700208 self._commit_cache = self.project.bare_git.rev_list(
209 *args, log_as_error=self.base_exists
210 )
Gavin Makea2e3302023-03-11 06:46:20 +0000211 except GitError:
212 # We weren't able to probe the commits for this branch. Was it
213 # tracking a branch that no longer exists? If so, return no
214 # commits. Otherwise, rethrow the error as we don't know what's
215 # going on.
216 if self.base_exists:
217 raise
Mike Frysinger6da17752019-09-11 18:43:17 -0400218
Gavin Makea2e3302023-03-11 06:46:20 +0000219 self._commit_cache = []
Mike Frysinger6da17752019-09-11 18:43:17 -0400220
Gavin Makea2e3302023-03-11 06:46:20 +0000221 return self._commit_cache
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700222
Gavin Makea2e3302023-03-11 06:46:20 +0000223 @property
224 def unabbrev_commits(self):
225 r = dict()
226 for commit in self.project.bare_git.rev_list(
227 not_rev(self.base), R_HEADS + self.name, "--"
228 ):
229 r[commit[0:8]] = commit
230 return r
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800231
Gavin Makea2e3302023-03-11 06:46:20 +0000232 @property
233 def date(self):
234 return self.project.bare_git.log(
235 "--pretty=format:%cd", "-n", "1", R_HEADS + self.name, "--"
236 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700237
Gavin Makea2e3302023-03-11 06:46:20 +0000238 @property
239 def base_exists(self):
240 """Whether the branch we're tracking exists.
Mike Frysinger6da17752019-09-11 18:43:17 -0400241
Gavin Makea2e3302023-03-11 06:46:20 +0000242 Normally it should, but sometimes branches we track can get deleted.
243 """
244 if self._base_exists is None:
245 try:
246 self.project.bare_git.rev_parse("--verify", not_rev(self.base))
247 # If we're still here, the base branch exists.
248 self._base_exists = True
249 except GitError:
250 # If we failed to verify, the base branch doesn't exist.
251 self._base_exists = False
Mike Frysinger6da17752019-09-11 18:43:17 -0400252
Gavin Makea2e3302023-03-11 06:46:20 +0000253 return self._base_exists
Mike Frysinger6da17752019-09-11 18:43:17 -0400254
Gavin Makea2e3302023-03-11 06:46:20 +0000255 def UploadForReview(
256 self,
257 people,
258 dryrun=False,
259 auto_topic=False,
260 hashtags=(),
261 labels=(),
262 private=False,
263 notify=None,
264 wip=False,
265 ready=False,
266 dest_branch=None,
267 validate_certs=True,
268 push_options=None,
269 ):
270 self.project.UploadForReview(
271 branch=self.name,
272 people=people,
273 dryrun=dryrun,
274 auto_topic=auto_topic,
275 hashtags=hashtags,
276 labels=labels,
277 private=private,
278 notify=notify,
279 wip=wip,
280 ready=ready,
281 dest_branch=dest_branch,
282 validate_certs=validate_certs,
283 push_options=push_options,
284 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700285
Gavin Makea2e3302023-03-11 06:46:20 +0000286 def GetPublishedRefs(self):
287 refs = {}
288 output = self.project.bare_git.ls_remote(
289 self.branch.remote.SshReviewUrl(self.project.UserEmail),
290 "refs/changes/*",
291 )
292 for line in output.split("\n"):
293 try:
294 (sha, ref) = line.split()
295 refs[sha] = ref
296 except ValueError:
297 pass
Ficus Kirkpatrickbc7ef672009-05-04 12:45:11 -0700298
Gavin Makea2e3302023-03-11 06:46:20 +0000299 return refs
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700300
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700301
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700302class StatusColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000303 def __init__(self, config):
304 super().__init__(config, "status")
305 self.project = self.printer("header", attr="bold")
306 self.branch = self.printer("header", attr="bold")
307 self.nobranch = self.printer("nobranch", fg="red")
308 self.important = self.printer("important", fg="red")
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700309
Gavin Makea2e3302023-03-11 06:46:20 +0000310 self.added = self.printer("added", fg="green")
311 self.changed = self.printer("changed", fg="red")
312 self.untracked = self.printer("untracked", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700313
314
315class DiffColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000316 def __init__(self, config):
317 super().__init__(config, "diff")
318 self.project = self.printer("header", attr="bold")
319 self.fail = self.printer("fail", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700320
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700321
Mike Frysingerd4aee652023-10-19 05:13:32 -0400322class Annotation:
Gavin Makea2e3302023-03-11 06:46:20 +0000323 def __init__(self, name, value, keep):
324 self.name = name
325 self.value = value
326 self.keep = keep
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700327
Gavin Makea2e3302023-03-11 06:46:20 +0000328 def __eq__(self, other):
329 if not isinstance(other, Annotation):
330 return False
331 return self.__dict__ == other.__dict__
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700332
Gavin Makea2e3302023-03-11 06:46:20 +0000333 def __lt__(self, other):
334 # This exists just so that lists of Annotation objects can be sorted,
335 # for use in comparisons.
336 if not isinstance(other, Annotation):
337 raise ValueError("comparison is not between two Annotation objects")
338 if self.name == other.name:
339 if self.value == other.value:
340 return self.keep < other.keep
341 return self.value < other.value
342 return self.name < other.name
Jack Neus6ea0cae2021-07-20 20:52:33 +0000343
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700344
Mike Frysingere6a202f2019-08-02 15:57:57 -0400345def _SafeExpandPath(base, subpath, skipfinal=False):
Gavin Makea2e3302023-03-11 06:46:20 +0000346 """Make sure |subpath| is completely safe under |base|.
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700347
Gavin Makea2e3302023-03-11 06:46:20 +0000348 We make sure no intermediate symlinks are traversed, and that the final path
349 is not a special file (e.g. not a socket or fifo).
Mike Frysingere6a202f2019-08-02 15:57:57 -0400350
Gavin Makea2e3302023-03-11 06:46:20 +0000351 NB: We rely on a number of paths already being filtered out while parsing
352 the manifest. See the validation logic in manifest_xml.py for more details.
353 """
354 # Split up the path by its components. We can't use os.path.sep exclusively
355 # as some platforms (like Windows) will convert / to \ and that bypasses all
356 # our constructed logic here. Especially since manifest authors only use
357 # / in their paths.
358 resep = re.compile(r"[/%s]" % re.escape(os.path.sep))
359 components = resep.split(subpath)
360 if skipfinal:
361 # Whether the caller handles the final component itself.
362 finalpart = components.pop()
Mike Frysingere6a202f2019-08-02 15:57:57 -0400363
Gavin Makea2e3302023-03-11 06:46:20 +0000364 path = base
365 for part in components:
366 if part in {".", ".."}:
367 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400368 f'{subpath}: "{part}" not allowed in paths'
Gavin Makea2e3302023-03-11 06:46:20 +0000369 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400370
Gavin Makea2e3302023-03-11 06:46:20 +0000371 path = os.path.join(path, part)
372 if platform_utils.islink(path):
373 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400374 f"{path}: traversing symlinks not allow"
Gavin Makea2e3302023-03-11 06:46:20 +0000375 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400376
Gavin Makea2e3302023-03-11 06:46:20 +0000377 if os.path.exists(path):
378 if not os.path.isfile(path) and not platform_utils.isdir(path):
379 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400380 f"{path}: only regular files & directories allowed"
Gavin Makea2e3302023-03-11 06:46:20 +0000381 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400382
Gavin Makea2e3302023-03-11 06:46:20 +0000383 if skipfinal:
384 path = os.path.join(path, finalpart)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400385
Gavin Makea2e3302023-03-11 06:46:20 +0000386 return path
Mike Frysingere6a202f2019-08-02 15:57:57 -0400387
388
Mike Frysingerd4aee652023-10-19 05:13:32 -0400389class _CopyFile:
Gavin Makea2e3302023-03-11 06:46:20 +0000390 """Container for <copyfile> manifest element."""
Mike Frysingere6a202f2019-08-02 15:57:57 -0400391
Gavin Makea2e3302023-03-11 06:46:20 +0000392 def __init__(self, git_worktree, src, topdir, dest):
393 """Register a <copyfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400394
Gavin Makea2e3302023-03-11 06:46:20 +0000395 Args:
396 git_worktree: Absolute path to the git project checkout.
397 src: Relative path under |git_worktree| of file to read.
398 topdir: Absolute path to the top of the repo client checkout.
399 dest: Relative path under |topdir| of file to write.
400 """
401 self.git_worktree = git_worktree
402 self.topdir = topdir
403 self.src = src
404 self.dest = dest
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700405
Gavin Makea2e3302023-03-11 06:46:20 +0000406 def _Copy(self):
407 src = _SafeExpandPath(self.git_worktree, self.src)
408 dest = _SafeExpandPath(self.topdir, self.dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400409
Gavin Makea2e3302023-03-11 06:46:20 +0000410 if platform_utils.isdir(src):
411 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400412 f"{self.src}: copying from directory not supported"
Gavin Makea2e3302023-03-11 06:46:20 +0000413 )
414 if platform_utils.isdir(dest):
415 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400416 f"{self.dest}: copying to directory not allowed"
Gavin Makea2e3302023-03-11 06:46:20 +0000417 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400418
Gavin Makea2e3302023-03-11 06:46:20 +0000419 # Copy file if it does not exist or is out of date.
420 if not os.path.exists(dest) or not filecmp.cmp(src, dest):
421 try:
422 # Remove existing file first, since it might be read-only.
423 if os.path.exists(dest):
424 platform_utils.remove(dest)
425 else:
426 dest_dir = os.path.dirname(dest)
427 if not platform_utils.isdir(dest_dir):
428 os.makedirs(dest_dir)
429 shutil.copy(src, dest)
430 # Make the file read-only.
431 mode = os.stat(dest)[stat.ST_MODE]
432 mode = mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
433 os.chmod(dest, mode)
Jason R. Coombsae824fb2023-10-20 23:32:40 +0545434 except OSError:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +0000435 logger.error("error: Cannot copy file %s to %s", src, dest)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700436
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700437
Mike Frysingerd4aee652023-10-19 05:13:32 -0400438class _LinkFile:
Gavin Makea2e3302023-03-11 06:46:20 +0000439 """Container for <linkfile> manifest element."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700440
Gavin Makea2e3302023-03-11 06:46:20 +0000441 def __init__(self, git_worktree, src, topdir, dest):
442 """Register a <linkfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400443
Gavin Makea2e3302023-03-11 06:46:20 +0000444 Args:
445 git_worktree: Absolute path to the git project checkout.
446 src: Target of symlink relative to path under |git_worktree|.
447 topdir: Absolute path to the top of the repo client checkout.
448 dest: Relative path under |topdir| of symlink to create.
449 """
450 self.git_worktree = git_worktree
451 self.topdir = topdir
452 self.src = src
453 self.dest = dest
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500454
Gavin Makea2e3302023-03-11 06:46:20 +0000455 def __linkIt(self, relSrc, absDest):
456 # Link file if it does not exist or is out of date.
457 if not platform_utils.islink(absDest) or (
458 platform_utils.readlink(absDest) != relSrc
459 ):
460 try:
461 # Remove existing file first, since it might be read-only.
462 if os.path.lexists(absDest):
463 platform_utils.remove(absDest)
464 else:
465 dest_dir = os.path.dirname(absDest)
466 if not platform_utils.isdir(dest_dir):
467 os.makedirs(dest_dir)
468 platform_utils.symlink(relSrc, absDest)
Jason R. Coombsae824fb2023-10-20 23:32:40 +0545469 except OSError:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +0000470 logger.error(
471 "error: Cannot link file %s to %s", relSrc, absDest
472 )
Gavin Makea2e3302023-03-11 06:46:20 +0000473
474 def _Link(self):
475 """Link the self.src & self.dest paths.
476
477 Handles wild cards on the src linking all of the files in the source in
478 to the destination directory.
479 """
480 # Some people use src="." to create stable links to projects. Let's
481 # allow that but reject all other uses of "." to keep things simple.
482 if self.src == ".":
483 src = self.git_worktree
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500484 else:
Gavin Makea2e3302023-03-11 06:46:20 +0000485 src = _SafeExpandPath(self.git_worktree, self.src)
Wink Saville4c426ef2015-06-03 08:05:17 -0700486
Gavin Makea2e3302023-03-11 06:46:20 +0000487 if not glob.has_magic(src):
488 # Entity does not contain a wild card so just a simple one to one
489 # link operation.
490 dest = _SafeExpandPath(self.topdir, self.dest, skipfinal=True)
491 # dest & src are absolute paths at this point. Make sure the target
492 # of the symlink is relative in the context of the repo client
493 # checkout.
494 relpath = os.path.relpath(src, os.path.dirname(dest))
495 self.__linkIt(relpath, dest)
496 else:
497 dest = _SafeExpandPath(self.topdir, self.dest)
498 # Entity contains a wild card.
499 if os.path.exists(dest) and not platform_utils.isdir(dest):
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +0000500 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +0000501 "Link error: src with wildcard, %s must be a directory",
502 dest,
503 )
504 else:
505 for absSrcFile in glob.glob(src):
506 # Create a releative path from source dir to destination
507 # dir.
508 absSrcDir = os.path.dirname(absSrcFile)
509 relSrcDir = os.path.relpath(absSrcDir, dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400510
Gavin Makea2e3302023-03-11 06:46:20 +0000511 # Get the source file name.
512 srcFile = os.path.basename(absSrcFile)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400513
Gavin Makea2e3302023-03-11 06:46:20 +0000514 # Now form the final full paths to srcFile. They will be
515 # absolute for the desintaiton and relative for the source.
516 absDest = os.path.join(dest, srcFile)
517 relSrc = os.path.join(relSrcDir, srcFile)
518 self.__linkIt(relSrc, absDest)
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500519
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700520
Mike Frysingerd4aee652023-10-19 05:13:32 -0400521class RemoteSpec:
Gavin Makea2e3302023-03-11 06:46:20 +0000522 def __init__(
523 self,
524 name,
525 url=None,
526 pushUrl=None,
527 review=None,
528 revision=None,
529 orig_name=None,
530 fetchUrl=None,
531 ):
532 self.name = name
533 self.url = url
534 self.pushUrl = pushUrl
535 self.review = review
536 self.revision = revision
537 self.orig_name = orig_name
538 self.fetchUrl = fetchUrl
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700539
Ian Kasprzak0286e312021-02-05 10:06:18 -0800540
Mike Frysingerd4aee652023-10-19 05:13:32 -0400541class Project:
Gavin Makea2e3302023-03-11 06:46:20 +0000542 # These objects can be shared between several working trees.
543 @property
544 def shareable_dirs(self):
545 """Return the shareable directories"""
546 if self.UseAlternates:
547 return ["hooks", "rr-cache"]
548 else:
549 return ["hooks", "objects", "rr-cache"]
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700550
Gavin Makea2e3302023-03-11 06:46:20 +0000551 def __init__(
552 self,
553 manifest,
554 name,
555 remote,
556 gitdir,
557 objdir,
558 worktree,
559 relpath,
560 revisionExpr,
561 revisionId,
562 rebase=True,
563 groups=None,
564 sync_c=False,
565 sync_s=False,
566 sync_tags=True,
567 clone_depth=None,
568 upstream=None,
569 parent=None,
570 use_git_worktrees=False,
571 is_derived=False,
572 dest_branch=None,
573 optimized_fetch=False,
574 retry_fetches=0,
575 old_revision=None,
576 ):
577 """Init a Project object.
Doug Anderson3ba5f952011-04-07 12:51:04 -0700578
579 Args:
Gavin Makea2e3302023-03-11 06:46:20 +0000580 manifest: The XmlManifest object.
581 name: The `name` attribute of manifest.xml's project element.
582 remote: RemoteSpec object specifying its remote's properties.
583 gitdir: Absolute path of git directory.
584 objdir: Absolute path of directory to store git objects.
585 worktree: Absolute path of git working tree.
586 relpath: Relative path of git working tree to repo's top directory.
587 revisionExpr: The `revision` attribute of manifest.xml's project
588 element.
589 revisionId: git commit id for checking out.
590 rebase: The `rebase` attribute of manifest.xml's project element.
591 groups: The `groups` attribute of manifest.xml's project element.
592 sync_c: The `sync-c` attribute of manifest.xml's project element.
593 sync_s: The `sync-s` attribute of manifest.xml's project element.
594 sync_tags: The `sync-tags` attribute of manifest.xml's project
595 element.
596 upstream: The `upstream` attribute of manifest.xml's project
597 element.
598 parent: The parent Project object.
599 use_git_worktrees: Whether to use `git worktree` for this project.
600 is_derived: False if the project was explicitly defined in the
601 manifest; True if the project is a discovered submodule.
602 dest_branch: The branch to which to push changes for review by
603 default.
604 optimized_fetch: If True, when a project is set to a sha1 revision,
605 only fetch from the remote if the sha1 is not present locally.
606 retry_fetches: Retry remote fetches n times upon receiving transient
607 error with exponential backoff and jitter.
608 old_revision: saved git commit id for open GITC projects.
609 """
610 self.client = self.manifest = manifest
611 self.name = name
612 self.remote = remote
613 self.UpdatePaths(relpath, worktree, gitdir, objdir)
614 self.SetRevision(revisionExpr, revisionId=revisionId)
615
616 self.rebase = rebase
617 self.groups = groups
618 self.sync_c = sync_c
619 self.sync_s = sync_s
620 self.sync_tags = sync_tags
621 self.clone_depth = clone_depth
622 self.upstream = upstream
623 self.parent = parent
624 # NB: Do not use this setting in __init__ to change behavior so that the
625 # manifest.git checkout can inspect & change it after instantiating.
626 # See the XmlManifest init code for more info.
627 self.use_git_worktrees = use_git_worktrees
628 self.is_derived = is_derived
629 self.optimized_fetch = optimized_fetch
630 self.retry_fetches = max(0, retry_fetches)
631 self.subprojects = []
632
633 self.snapshots = {}
634 self.copyfiles = []
635 self.linkfiles = []
636 self.annotations = []
637 self.dest_branch = dest_branch
638 self.old_revision = old_revision
639
640 # This will be filled in if a project is later identified to be the
641 # project containing repo hooks.
642 self.enabled_repo_hooks = []
643
644 def RelPath(self, local=True):
645 """Return the path for the project relative to a manifest.
646
647 Args:
648 local: a boolean, if True, the path is relative to the local
649 (sub)manifest. If false, the path is relative to the outermost
650 manifest.
651 """
652 if local:
653 return self.relpath
654 return os.path.join(self.manifest.path_prefix, self.relpath)
655
656 def SetRevision(self, revisionExpr, revisionId=None):
657 """Set revisionId based on revision expression and id"""
658 self.revisionExpr = revisionExpr
659 if revisionId is None and revisionExpr and IsId(revisionExpr):
660 self.revisionId = self.revisionExpr
661 else:
662 self.revisionId = revisionId
663
664 def UpdatePaths(self, relpath, worktree, gitdir, objdir):
665 """Update paths used by this project"""
666 self.gitdir = gitdir.replace("\\", "/")
667 self.objdir = objdir.replace("\\", "/")
668 if worktree:
669 self.worktree = os.path.normpath(worktree).replace("\\", "/")
670 else:
671 self.worktree = None
672 self.relpath = relpath
673
674 self.config = GitConfig.ForRepository(
675 gitdir=self.gitdir, defaults=self.manifest.globalConfig
676 )
677
678 if self.worktree:
679 self.work_git = self._GitGetByExec(
680 self, bare=False, gitdir=self.gitdir
681 )
682 else:
683 self.work_git = None
684 self.bare_git = self._GitGetByExec(self, bare=True, gitdir=self.gitdir)
685 self.bare_ref = GitRefs(self.gitdir)
686 self.bare_objdir = self._GitGetByExec(
687 self, bare=True, gitdir=self.objdir
688 )
689
690 @property
691 def UseAlternates(self):
692 """Whether git alternates are in use.
693
694 This will be removed once migration to alternates is complete.
695 """
696 return _ALTERNATES or self.manifest.is_multimanifest
697
698 @property
699 def Derived(self):
700 return self.is_derived
701
702 @property
703 def Exists(self):
704 return platform_utils.isdir(self.gitdir) and platform_utils.isdir(
705 self.objdir
706 )
707
708 @property
709 def CurrentBranch(self):
710 """Obtain the name of the currently checked out branch.
711
712 The branch name omits the 'refs/heads/' prefix.
713 None is returned if the project is on a detached HEAD, or if the
714 work_git is otheriwse inaccessible (e.g. an incomplete sync).
715 """
716 try:
717 b = self.work_git.GetHead()
718 except NoManifestException:
719 # If the local checkout is in a bad state, don't barf. Let the
720 # callers process this like the head is unreadable.
721 return None
722 if b.startswith(R_HEADS):
723 return b[len(R_HEADS) :]
724 return None
725
726 def IsRebaseInProgress(self):
727 return (
728 os.path.exists(self.work_git.GetDotgitPath("rebase-apply"))
729 or os.path.exists(self.work_git.GetDotgitPath("rebase-merge"))
730 or os.path.exists(os.path.join(self.worktree, ".dotest"))
731 )
732
733 def IsDirty(self, consider_untracked=True):
734 """Is the working directory modified in some way?"""
735 self.work_git.update_index(
736 "-q", "--unmerged", "--ignore-missing", "--refresh"
737 )
738 if self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD):
739 return True
740 if self.work_git.DiffZ("diff-files"):
741 return True
742 if consider_untracked and self.UntrackedFiles():
743 return True
744 return False
745
746 _userident_name = None
747 _userident_email = None
748
749 @property
750 def UserName(self):
751 """Obtain the user's personal name."""
752 if self._userident_name is None:
753 self._LoadUserIdentity()
754 return self._userident_name
755
756 @property
757 def UserEmail(self):
758 """Obtain the user's email address. This is very likely
759 to be their Gerrit login.
760 """
761 if self._userident_email is None:
762 self._LoadUserIdentity()
763 return self._userident_email
764
765 def _LoadUserIdentity(self):
766 u = self.bare_git.var("GIT_COMMITTER_IDENT")
767 m = re.compile("^(.*) <([^>]*)> ").match(u)
768 if m:
769 self._userident_name = m.group(1)
770 self._userident_email = m.group(2)
771 else:
772 self._userident_name = ""
773 self._userident_email = ""
774
775 def GetRemote(self, name=None):
776 """Get the configuration for a single remote.
777
778 Defaults to the current project's remote.
779 """
780 if name is None:
781 name = self.remote.name
782 return self.config.GetRemote(name)
783
784 def GetBranch(self, name):
785 """Get the configuration for a single branch."""
786 return self.config.GetBranch(name)
787
788 def GetBranches(self):
789 """Get all existing local branches."""
790 current = self.CurrentBranch
791 all_refs = self._allrefs
792 heads = {}
793
794 for name, ref_id in all_refs.items():
795 if name.startswith(R_HEADS):
796 name = name[len(R_HEADS) :]
797 b = self.GetBranch(name)
798 b.current = name == current
799 b.published = None
800 b.revision = ref_id
801 heads[name] = b
802
803 for name, ref_id in all_refs.items():
804 if name.startswith(R_PUB):
805 name = name[len(R_PUB) :]
806 b = heads.get(name)
807 if b:
808 b.published = ref_id
809
810 return heads
811
812 def MatchesGroups(self, manifest_groups):
813 """Returns true if the manifest groups specified at init should cause
814 this project to be synced.
815 Prefixing a manifest group with "-" inverts the meaning of a group.
816 All projects are implicitly labelled with "all".
817
818 labels are resolved in order. In the example case of
819 project_groups: "all,group1,group2"
820 manifest_groups: "-group1,group2"
821 the project will be matched.
822
823 The special manifest group "default" will match any project that
824 does not have the special project group "notdefault"
825 """
826 default_groups = self.manifest.default_groups or ["default"]
827 expanded_manifest_groups = manifest_groups or default_groups
828 expanded_project_groups = ["all"] + (self.groups or [])
829 if "notdefault" not in expanded_project_groups:
830 expanded_project_groups += ["default"]
831
832 matched = False
833 for group in expanded_manifest_groups:
834 if group.startswith("-") and group[1:] in expanded_project_groups:
835 matched = False
836 elif group in expanded_project_groups:
837 matched = True
838
839 return matched
840
841 def UncommitedFiles(self, get_all=True):
842 """Returns a list of strings, uncommitted files in the git tree.
843
844 Args:
845 get_all: a boolean, if True - get information about all different
846 uncommitted files. If False - return as soon as any kind of
847 uncommitted files is detected.
848 """
849 details = []
850 self.work_git.update_index(
851 "-q", "--unmerged", "--ignore-missing", "--refresh"
852 )
853 if self.IsRebaseInProgress():
854 details.append("rebase in progress")
855 if not get_all:
856 return details
857
858 changes = self.work_git.DiffZ("diff-index", "--cached", HEAD).keys()
859 if changes:
860 details.extend(changes)
861 if not get_all:
862 return details
863
864 changes = self.work_git.DiffZ("diff-files").keys()
865 if changes:
866 details.extend(changes)
867 if not get_all:
868 return details
869
870 changes = self.UntrackedFiles()
871 if changes:
872 details.extend(changes)
873
874 return details
875
876 def UntrackedFiles(self):
877 """Returns a list of strings, untracked files in the git tree."""
878 return self.work_git.LsOthers()
879
880 def HasChanges(self):
881 """Returns true if there are uncommitted changes."""
882 return bool(self.UncommitedFiles(get_all=False))
883
884 def PrintWorkTreeStatus(self, output_redir=None, quiet=False, local=False):
885 """Prints the status of the repository to stdout.
886
887 Args:
888 output_redir: If specified, redirect the output to this object.
889 quiet: If True then only print the project name. Do not print
890 the modified files, branch name, etc.
891 local: a boolean, if True, the path is relative to the local
892 (sub)manifest. If false, the path is relative to the outermost
893 manifest.
894 """
895 if not platform_utils.isdir(self.worktree):
896 if output_redir is None:
897 output_redir = sys.stdout
898 print(file=output_redir)
899 print("project %s/" % self.RelPath(local), file=output_redir)
900 print(' missing (run "repo sync")', file=output_redir)
901 return
902
903 self.work_git.update_index(
904 "-q", "--unmerged", "--ignore-missing", "--refresh"
905 )
906 rb = self.IsRebaseInProgress()
907 di = self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD)
908 df = self.work_git.DiffZ("diff-files")
909 do = self.work_git.LsOthers()
910 if not rb and not di and not df and not do and not self.CurrentBranch:
911 return "CLEAN"
912
913 out = StatusColoring(self.config)
914 if output_redir is not None:
915 out.redirect(output_redir)
916 out.project("project %-40s", self.RelPath(local) + "/ ")
917
918 if quiet:
919 out.nl()
920 return "DIRTY"
921
922 branch = self.CurrentBranch
923 if branch is None:
924 out.nobranch("(*** NO BRANCH ***)")
925 else:
926 out.branch("branch %s", branch)
927 out.nl()
928
929 if rb:
930 out.important("prior sync failed; rebase still in progress")
931 out.nl()
932
933 paths = list()
934 paths.extend(di.keys())
935 paths.extend(df.keys())
936 paths.extend(do)
937
938 for p in sorted(set(paths)):
939 try:
940 i = di[p]
941 except KeyError:
942 i = None
943
944 try:
945 f = df[p]
946 except KeyError:
947 f = None
948
949 if i:
950 i_status = i.status.upper()
951 else:
952 i_status = "-"
953
954 if f:
955 f_status = f.status.lower()
956 else:
957 f_status = "-"
958
959 if i and i.src_path:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400960 line = (
961 f" {i_status}{f_status}\t{i.src_path} => {p} ({i.level}%)"
Gavin Makea2e3302023-03-11 06:46:20 +0000962 )
963 else:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400964 line = f" {i_status}{f_status}\t{p}"
Gavin Makea2e3302023-03-11 06:46:20 +0000965
966 if i and not f:
967 out.added("%s", line)
968 elif (i and f) or (not i and f):
969 out.changed("%s", line)
970 elif not i and not f:
971 out.untracked("%s", line)
972 else:
973 out.write("%s", line)
974 out.nl()
975
976 return "DIRTY"
977
978 def PrintWorkTreeDiff(
979 self, absolute_paths=False, output_redir=None, local=False
980 ):
981 """Prints the status of the repository to stdout."""
982 out = DiffColoring(self.config)
983 if output_redir:
984 out.redirect(output_redir)
985 cmd = ["diff"]
986 if out.is_on:
987 cmd.append("--color")
988 cmd.append(HEAD)
989 if absolute_paths:
990 cmd.append("--src-prefix=a/%s/" % self.RelPath(local))
991 cmd.append("--dst-prefix=b/%s/" % self.RelPath(local))
992 cmd.append("--")
993 try:
994 p = GitCommand(self, cmd, capture_stdout=True, capture_stderr=True)
995 p.Wait()
996 except GitError as e:
997 out.nl()
998 out.project("project %s/" % self.RelPath(local))
999 out.nl()
1000 out.fail("%s", str(e))
1001 out.nl()
1002 return False
1003 if p.stdout:
1004 out.nl()
1005 out.project("project %s/" % self.RelPath(local))
1006 out.nl()
1007 out.write("%s", p.stdout)
1008 return p.Wait() == 0
1009
1010 def WasPublished(self, branch, all_refs=None):
1011 """Was the branch published (uploaded) for code review?
1012 If so, returns the SHA-1 hash of the last published
1013 state for the branch.
1014 """
1015 key = R_PUB + branch
1016 if all_refs is None:
1017 try:
1018 return self.bare_git.rev_parse(key)
1019 except GitError:
1020 return None
1021 else:
1022 try:
1023 return all_refs[key]
1024 except KeyError:
1025 return None
1026
1027 def CleanPublishedCache(self, all_refs=None):
1028 """Prunes any stale published refs."""
1029 if all_refs is None:
1030 all_refs = self._allrefs
1031 heads = set()
1032 canrm = {}
1033 for name, ref_id in all_refs.items():
1034 if name.startswith(R_HEADS):
1035 heads.add(name)
1036 elif name.startswith(R_PUB):
1037 canrm[name] = ref_id
1038
1039 for name, ref_id in canrm.items():
1040 n = name[len(R_PUB) :]
1041 if R_HEADS + n not in heads:
1042 self.bare_git.DeleteRef(name, ref_id)
1043
1044 def GetUploadableBranches(self, selected_branch=None):
1045 """List any branches which can be uploaded for review."""
1046 heads = {}
1047 pubed = {}
1048
1049 for name, ref_id in self._allrefs.items():
1050 if name.startswith(R_HEADS):
1051 heads[name[len(R_HEADS) :]] = ref_id
1052 elif name.startswith(R_PUB):
1053 pubed[name[len(R_PUB) :]] = ref_id
1054
1055 ready = []
1056 for branch, ref_id in heads.items():
1057 if branch in pubed and pubed[branch] == ref_id:
1058 continue
1059 if selected_branch and branch != selected_branch:
1060 continue
1061
1062 rb = self.GetUploadableBranch(branch)
1063 if rb:
1064 ready.append(rb)
1065 return ready
1066
1067 def GetUploadableBranch(self, branch_name):
1068 """Get a single uploadable branch, or None."""
1069 branch = self.GetBranch(branch_name)
1070 base = branch.LocalMerge
1071 if branch.LocalMerge:
1072 rb = ReviewableBranch(self, branch, base)
1073 if rb.commits:
1074 return rb
1075 return None
1076
1077 def UploadForReview(
1078 self,
1079 branch=None,
1080 people=([], []),
1081 dryrun=False,
1082 auto_topic=False,
1083 hashtags=(),
1084 labels=(),
1085 private=False,
1086 notify=None,
1087 wip=False,
1088 ready=False,
1089 dest_branch=None,
1090 validate_certs=True,
1091 push_options=None,
1092 ):
1093 """Uploads the named branch for code review."""
1094 if branch is None:
1095 branch = self.CurrentBranch
1096 if branch is None:
Jason Chang32b59562023-07-14 16:45:35 -07001097 raise GitError("not currently on a branch", project=self.name)
Gavin Makea2e3302023-03-11 06:46:20 +00001098
1099 branch = self.GetBranch(branch)
1100 if not branch.LocalMerge:
Jason Chang32b59562023-07-14 16:45:35 -07001101 raise GitError(
1102 "branch %s does not track a remote" % branch.name,
1103 project=self.name,
1104 )
Gavin Makea2e3302023-03-11 06:46:20 +00001105 if not branch.remote.review:
Jason Chang32b59562023-07-14 16:45:35 -07001106 raise GitError(
1107 "remote %s has no review url" % branch.remote.name,
1108 project=self.name,
1109 )
Gavin Makea2e3302023-03-11 06:46:20 +00001110
1111 # Basic validity check on label syntax.
1112 for label in labels:
1113 if not re.match(r"^.+[+-][0-9]+$", label):
1114 raise UploadError(
1115 f'invalid label syntax "{label}": labels use forms like '
Jason Chang5a3a5f72023-08-17 11:36:41 -07001116 "CodeReview+1 or Verified-1",
1117 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00001118 )
1119
1120 if dest_branch is None:
1121 dest_branch = self.dest_branch
1122 if dest_branch is None:
1123 dest_branch = branch.merge
1124 if not dest_branch.startswith(R_HEADS):
1125 dest_branch = R_HEADS + dest_branch
1126
1127 if not branch.remote.projectname:
1128 branch.remote.projectname = self.name
1129 branch.remote.Save()
1130
1131 url = branch.remote.ReviewUrl(self.UserEmail, validate_certs)
1132 if url is None:
Jason Chang5a3a5f72023-08-17 11:36:41 -07001133 raise UploadError("review not configured", project=self.name)
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +00001134 cmd = ["push", "--progress"]
Gavin Makea2e3302023-03-11 06:46:20 +00001135 if dryrun:
1136 cmd.append("-n")
1137
1138 if url.startswith("ssh://"):
1139 cmd.append("--receive-pack=gerrit receive-pack")
1140
Aravind Vasudevan99ebf622023-04-04 23:44:37 +00001141 # This stops git from pushing all reachable annotated tags when
1142 # push.followTags is configured. Gerrit does not accept any tags
1143 # pushed to a CL.
1144 if git_require((1, 8, 3)):
1145 cmd.append("--no-follow-tags")
1146
Gavin Makea2e3302023-03-11 06:46:20 +00001147 for push_option in push_options or []:
1148 cmd.append("-o")
1149 cmd.append(push_option)
1150
1151 cmd.append(url)
1152
1153 if dest_branch.startswith(R_HEADS):
1154 dest_branch = dest_branch[len(R_HEADS) :]
1155
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001156 ref_spec = f"{R_HEADS + branch.name}:refs/for/{dest_branch}"
Gavin Makea2e3302023-03-11 06:46:20 +00001157 opts = []
1158 if auto_topic:
1159 opts += ["topic=" + branch.name]
1160 opts += ["t=%s" % p for p in hashtags]
1161 # NB: No need to encode labels as they've been validated above.
1162 opts += ["l=%s" % p for p in labels]
1163
1164 opts += ["r=%s" % p for p in people[0]]
1165 opts += ["cc=%s" % p for p in people[1]]
1166 if notify:
1167 opts += ["notify=" + notify]
1168 if private:
1169 opts += ["private"]
1170 if wip:
1171 opts += ["wip"]
1172 if ready:
1173 opts += ["ready"]
1174 if opts:
1175 ref_spec = ref_spec + "%" + ",".join(opts)
1176 cmd.append(ref_spec)
1177
Jason Chang1e9f7b92023-08-25 10:31:04 -07001178 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001179
1180 if not dryrun:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001181 msg = f"posted to {branch.remote.review} for {dest_branch}"
Gavin Makea2e3302023-03-11 06:46:20 +00001182 self.bare_git.UpdateRef(
1183 R_PUB + branch.name, R_HEADS + branch.name, message=msg
1184 )
1185
1186 def _ExtractArchive(self, tarpath, path=None):
1187 """Extract the given tar on its current location
1188
1189 Args:
1190 tarpath: The path to the actual tar file
1191
1192 """
1193 try:
1194 with tarfile.open(tarpath, "r") as tar:
1195 tar.extractall(path=path)
1196 return True
Jason R. Coombsae824fb2023-10-20 23:32:40 +05451197 except (OSError, tarfile.TarError) as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001198 logger.error("error: Cannot extract archive %s: %s", tarpath, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001199 return False
1200
1201 def Sync_NetworkHalf(
1202 self,
1203 quiet=False,
1204 verbose=False,
1205 output_redir=None,
1206 is_new=None,
1207 current_branch_only=None,
1208 force_sync=False,
1209 clone_bundle=True,
1210 tags=None,
1211 archive=False,
1212 optimized_fetch=False,
1213 retry_fetches=0,
1214 prune=False,
1215 submodules=False,
1216 ssh_proxy=None,
1217 clone_filter=None,
1218 partial_clone_exclude=set(),
Jason Chang17833322023-05-23 13:06:55 -07001219 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001220 ):
1221 """Perform only the network IO portion of the sync process.
1222 Local working directory/branch state is not affected.
1223 """
1224 if archive and not isinstance(self, MetaProject):
1225 if self.remote.url.startswith(("http://", "https://")):
Jason Chang32b59562023-07-14 16:45:35 -07001226 msg_template = (
1227 "%s: Cannot fetch archives from http/https remotes."
Gavin Makea2e3302023-03-11 06:46:20 +00001228 )
Jason Chang32b59562023-07-14 16:45:35 -07001229 msg_args = self.name
1230 msg = msg_template % msg_args
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001231 logger.error(msg_template, msg_args)
Jason Chang32b59562023-07-14 16:45:35 -07001232 return SyncNetworkHalfResult(
1233 False, SyncNetworkHalfError(msg, project=self.name)
1234 )
Gavin Makea2e3302023-03-11 06:46:20 +00001235
1236 name = self.relpath.replace("\\", "/")
1237 name = name.replace("/", "_")
1238 tarpath = "%s.tar" % name
1239 topdir = self.manifest.topdir
1240
1241 try:
1242 self._FetchArchive(tarpath, cwd=topdir)
1243 except GitError as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001244 logger.error("error: %s", e)
Jason Chang32b59562023-07-14 16:45:35 -07001245 return SyncNetworkHalfResult(False, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001246
1247 # From now on, we only need absolute tarpath.
1248 tarpath = os.path.join(topdir, tarpath)
1249
1250 if not self._ExtractArchive(tarpath, path=topdir):
Jason Chang32b59562023-07-14 16:45:35 -07001251 return SyncNetworkHalfResult(
1252 True,
1253 SyncNetworkHalfError(
1254 f"Unable to Extract Archive {tarpath}",
1255 project=self.name,
1256 ),
1257 )
Gavin Makea2e3302023-03-11 06:46:20 +00001258 try:
1259 platform_utils.remove(tarpath)
1260 except OSError as e:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00001261 logger.warning("warn: Cannot remove archive %s: %s", tarpath, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001262 self._CopyAndLinkFiles()
Jason Chang32b59562023-07-14 16:45:35 -07001263 return SyncNetworkHalfResult(True)
Gavin Makea2e3302023-03-11 06:46:20 +00001264
1265 # If the shared object dir already exists, don't try to rebootstrap with
1266 # a clone bundle download. We should have the majority of objects
1267 # already.
1268 if clone_bundle and os.path.exists(self.objdir):
1269 clone_bundle = False
1270
1271 if self.name in partial_clone_exclude:
1272 clone_bundle = True
1273 clone_filter = None
1274
1275 if is_new is None:
1276 is_new = not self.Exists
1277 if is_new:
1278 self._InitGitDir(force_sync=force_sync, quiet=quiet)
1279 else:
1280 self._UpdateHooks(quiet=quiet)
1281 self._InitRemote()
1282
1283 if self.UseAlternates:
1284 # If gitdir/objects is a symlink, migrate it from the old layout.
1285 gitdir_objects = os.path.join(self.gitdir, "objects")
1286 if platform_utils.islink(gitdir_objects):
1287 platform_utils.remove(gitdir_objects, missing_ok=True)
1288 gitdir_alt = os.path.join(self.gitdir, "objects/info/alternates")
1289 if not os.path.exists(gitdir_alt):
1290 os.makedirs(os.path.dirname(gitdir_alt), exist_ok=True)
1291 _lwrite(
1292 gitdir_alt,
1293 os.path.join(
1294 os.path.relpath(self.objdir, gitdir_objects), "objects"
1295 )
1296 + "\n",
1297 )
1298
1299 if is_new:
1300 alt = os.path.join(self.objdir, "objects/info/alternates")
1301 try:
1302 with open(alt) as fd:
1303 # This works for both absolute and relative alternate
1304 # directories.
1305 alt_dir = os.path.join(
1306 self.objdir, "objects", fd.readline().rstrip()
1307 )
Jason R. Coombsae824fb2023-10-20 23:32:40 +05451308 except OSError:
Gavin Makea2e3302023-03-11 06:46:20 +00001309 alt_dir = None
1310 else:
1311 alt_dir = None
1312
1313 if (
1314 clone_bundle
1315 and alt_dir is None
1316 and self._ApplyCloneBundle(
1317 initial=is_new, quiet=quiet, verbose=verbose
1318 )
1319 ):
1320 is_new = False
1321
1322 if current_branch_only is None:
1323 if self.sync_c:
1324 current_branch_only = True
1325 elif not self.manifest._loaded:
1326 # Manifest cannot check defaults until it syncs.
1327 current_branch_only = False
1328 elif self.manifest.default.sync_c:
1329 current_branch_only = True
1330
1331 if tags is None:
1332 tags = self.sync_tags
1333
1334 if self.clone_depth:
1335 depth = self.clone_depth
1336 else:
1337 depth = self.manifest.manifestProject.depth
1338
Jason Chang17833322023-05-23 13:06:55 -07001339 if depth and clone_filter_for_depth:
1340 depth = None
1341 clone_filter = clone_filter_for_depth
1342
Gavin Makea2e3302023-03-11 06:46:20 +00001343 # See if we can skip the network fetch entirely.
1344 remote_fetched = False
1345 if not (
1346 optimized_fetch
Sylvain56a5a012023-09-11 13:38:00 +02001347 and IsId(self.revisionExpr)
1348 and self._CheckForImmutableRevision()
Gavin Makea2e3302023-03-11 06:46:20 +00001349 ):
1350 remote_fetched = True
Jason Chang32b59562023-07-14 16:45:35 -07001351 try:
1352 if not self._RemoteFetch(
1353 initial=is_new,
1354 quiet=quiet,
1355 verbose=verbose,
1356 output_redir=output_redir,
1357 alt_dir=alt_dir,
1358 current_branch_only=current_branch_only,
1359 tags=tags,
1360 prune=prune,
1361 depth=depth,
1362 submodules=submodules,
1363 force_sync=force_sync,
1364 ssh_proxy=ssh_proxy,
1365 clone_filter=clone_filter,
1366 retry_fetches=retry_fetches,
1367 ):
1368 return SyncNetworkHalfResult(
1369 remote_fetched,
1370 SyncNetworkHalfError(
1371 f"Unable to remote fetch project {self.name}",
1372 project=self.name,
1373 ),
1374 )
1375 except RepoError as e:
1376 return SyncNetworkHalfResult(
1377 remote_fetched,
1378 e,
1379 )
Gavin Makea2e3302023-03-11 06:46:20 +00001380
1381 mp = self.manifest.manifestProject
1382 dissociate = mp.dissociate
1383 if dissociate:
1384 alternates_file = os.path.join(
1385 self.objdir, "objects/info/alternates"
1386 )
1387 if os.path.exists(alternates_file):
1388 cmd = ["repack", "-a", "-d"]
1389 p = GitCommand(
1390 self,
1391 cmd,
1392 bare=True,
1393 capture_stdout=bool(output_redir),
1394 merge_output=bool(output_redir),
1395 )
1396 if p.stdout and output_redir:
1397 output_redir.write(p.stdout)
1398 if p.Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07001399 return SyncNetworkHalfResult(
1400 remote_fetched,
1401 GitError(
1402 "Unable to repack alternates", project=self.name
1403 ),
1404 )
Gavin Makea2e3302023-03-11 06:46:20 +00001405 platform_utils.remove(alternates_file)
1406
1407 if self.worktree:
1408 self._InitMRef()
1409 else:
1410 self._InitMirrorHead()
1411 platform_utils.remove(
1412 os.path.join(self.gitdir, "FETCH_HEAD"), missing_ok=True
1413 )
Jason Chang32b59562023-07-14 16:45:35 -07001414 return SyncNetworkHalfResult(remote_fetched)
Gavin Makea2e3302023-03-11 06:46:20 +00001415
1416 def PostRepoUpgrade(self):
1417 self._InitHooks()
1418
1419 def _CopyAndLinkFiles(self):
1420 if self.client.isGitcClient:
1421 return
1422 for copyfile in self.copyfiles:
1423 copyfile._Copy()
1424 for linkfile in self.linkfiles:
1425 linkfile._Link()
1426
1427 def GetCommitRevisionId(self):
1428 """Get revisionId of a commit.
1429
1430 Use this method instead of GetRevisionId to get the id of the commit
1431 rather than the id of the current git object (for example, a tag)
1432
1433 """
Sylvaine9cb3912023-09-10 23:35:01 +02001434 if self.revisionId:
1435 return self.revisionId
Gavin Makea2e3302023-03-11 06:46:20 +00001436 if not self.revisionExpr.startswith(R_TAGS):
1437 return self.GetRevisionId(self._allrefs)
1438
1439 try:
1440 return self.bare_git.rev_list(self.revisionExpr, "-1")[0]
1441 except GitError:
1442 raise ManifestInvalidRevisionError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001443 f"revision {self.revisionExpr} in {self.name} not found"
Gavin Makea2e3302023-03-11 06:46:20 +00001444 )
1445
1446 def GetRevisionId(self, all_refs=None):
1447 if self.revisionId:
1448 return self.revisionId
1449
1450 rem = self.GetRemote()
1451 rev = rem.ToLocal(self.revisionExpr)
1452
1453 if all_refs is not None and rev in all_refs:
1454 return all_refs[rev]
1455
1456 try:
1457 return self.bare_git.rev_parse("--verify", "%s^0" % rev)
1458 except GitError:
1459 raise ManifestInvalidRevisionError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001460 f"revision {self.revisionExpr} in {self.name} not found"
Gavin Makea2e3302023-03-11 06:46:20 +00001461 )
1462
1463 def SetRevisionId(self, revisionId):
1464 if self.revisionExpr:
1465 self.upstream = self.revisionExpr
1466
1467 self.revisionId = revisionId
1468
Jason Chang32b59562023-07-14 16:45:35 -07001469 def Sync_LocalHalf(
1470 self, syncbuf, force_sync=False, submodules=False, errors=None
1471 ):
Gavin Makea2e3302023-03-11 06:46:20 +00001472 """Perform only the local IO portion of the sync process.
1473
1474 Network access is not required.
1475 """
Jason Chang32b59562023-07-14 16:45:35 -07001476 if errors is None:
1477 errors = []
1478
1479 def fail(error: Exception):
1480 errors.append(error)
1481 syncbuf.fail(self, error)
1482
Gavin Makea2e3302023-03-11 06:46:20 +00001483 if not os.path.exists(self.gitdir):
Jason Chang32b59562023-07-14 16:45:35 -07001484 fail(
1485 LocalSyncFail(
1486 "Cannot checkout %s due to missing network sync; Run "
1487 "`repo sync -n %s` first." % (self.name, self.name),
1488 project=self.name,
1489 )
Gavin Makea2e3302023-03-11 06:46:20 +00001490 )
1491 return
1492
1493 self._InitWorkTree(force_sync=force_sync, submodules=submodules)
1494 all_refs = self.bare_ref.all
1495 self.CleanPublishedCache(all_refs)
1496 revid = self.GetRevisionId(all_refs)
1497
1498 # Special case the root of the repo client checkout. Make sure it
1499 # doesn't contain files being checked out to dirs we don't allow.
1500 if self.relpath == ".":
1501 PROTECTED_PATHS = {".repo"}
1502 paths = set(
1503 self.work_git.ls_tree("-z", "--name-only", "--", revid).split(
1504 "\0"
1505 )
1506 )
1507 bad_paths = paths & PROTECTED_PATHS
1508 if bad_paths:
Jason Chang32b59562023-07-14 16:45:35 -07001509 fail(
1510 LocalSyncFail(
1511 "Refusing to checkout project that writes to protected "
1512 "paths: %s" % (", ".join(bad_paths),),
1513 project=self.name,
1514 )
Gavin Makea2e3302023-03-11 06:46:20 +00001515 )
1516 return
1517
1518 def _doff():
1519 self._FastForward(revid)
1520 self._CopyAndLinkFiles()
1521
1522 def _dosubmodules():
1523 self._SyncSubmodules(quiet=True)
1524
1525 head = self.work_git.GetHead()
1526 if head.startswith(R_HEADS):
1527 branch = head[len(R_HEADS) :]
1528 try:
1529 head = all_refs[head]
1530 except KeyError:
1531 head = None
1532 else:
1533 branch = None
1534
1535 if branch is None or syncbuf.detach_head:
1536 # Currently on a detached HEAD. The user is assumed to
1537 # not have any local modifications worth worrying about.
1538 if self.IsRebaseInProgress():
Jason Chang32b59562023-07-14 16:45:35 -07001539 fail(_PriorSyncFailedError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001540 return
1541
1542 if head == revid:
1543 # No changes; don't do anything further.
1544 # Except if the head needs to be detached.
1545 if not syncbuf.detach_head:
1546 # The copy/linkfile config may have changed.
1547 self._CopyAndLinkFiles()
1548 return
1549 else:
1550 lost = self._revlist(not_rev(revid), HEAD)
1551 if lost:
1552 syncbuf.info(self, "discarding %d commits", len(lost))
1553
1554 try:
1555 self._Checkout(revid, quiet=True)
1556 if submodules:
1557 self._SyncSubmodules(quiet=True)
1558 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001559 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001560 return
1561 self._CopyAndLinkFiles()
1562 return
1563
1564 if head == revid:
1565 # No changes; don't do anything further.
1566 #
1567 # The copy/linkfile config may have changed.
1568 self._CopyAndLinkFiles()
1569 return
1570
1571 branch = self.GetBranch(branch)
1572
1573 if not branch.LocalMerge:
1574 # The current branch has no tracking configuration.
1575 # Jump off it to a detached HEAD.
1576 syncbuf.info(
1577 self, "leaving %s; does not track upstream", branch.name
1578 )
1579 try:
1580 self._Checkout(revid, quiet=True)
1581 if submodules:
1582 self._SyncSubmodules(quiet=True)
1583 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001584 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001585 return
1586 self._CopyAndLinkFiles()
1587 return
1588
1589 upstream_gain = self._revlist(not_rev(HEAD), revid)
1590
1591 # See if we can perform a fast forward merge. This can happen if our
1592 # branch isn't in the exact same state as we last published.
1593 try:
Jason Chang87058c62023-09-27 11:34:43 -07001594 self.work_git.merge_base(
1595 "--is-ancestor", HEAD, revid, log_as_error=False
1596 )
Gavin Makea2e3302023-03-11 06:46:20 +00001597 # Skip the published logic.
1598 pub = False
1599 except GitError:
1600 pub = self.WasPublished(branch.name, all_refs)
1601
1602 if pub:
1603 not_merged = self._revlist(not_rev(revid), pub)
1604 if not_merged:
1605 if upstream_gain:
1606 # The user has published this branch and some of those
1607 # commits are not yet merged upstream. We do not want
1608 # to rewrite the published commits so we punt.
Jason Chang32b59562023-07-14 16:45:35 -07001609 fail(
1610 LocalSyncFail(
1611 "branch %s is published (but not merged) and is "
1612 "now %d commits behind"
1613 % (branch.name, len(upstream_gain)),
1614 project=self.name,
1615 )
Gavin Makea2e3302023-03-11 06:46:20 +00001616 )
1617 return
1618 elif pub == head:
1619 # All published commits are merged, and thus we are a
1620 # strict subset. We can fast-forward safely.
1621 syncbuf.later1(self, _doff)
1622 if submodules:
1623 syncbuf.later1(self, _dosubmodules)
1624 return
1625
1626 # Examine the local commits not in the remote. Find the
1627 # last one attributed to this user, if any.
1628 local_changes = self._revlist(not_rev(revid), HEAD, format="%H %ce")
1629 last_mine = None
1630 cnt_mine = 0
1631 for commit in local_changes:
1632 commit_id, committer_email = commit.split(" ", 1)
1633 if committer_email == self.UserEmail:
1634 last_mine = commit_id
1635 cnt_mine += 1
1636
1637 if not upstream_gain and cnt_mine == len(local_changes):
1638 # The copy/linkfile config may have changed.
1639 self._CopyAndLinkFiles()
1640 return
1641
1642 if self.IsDirty(consider_untracked=False):
Jason Chang32b59562023-07-14 16:45:35 -07001643 fail(_DirtyError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001644 return
1645
1646 # If the upstream switched on us, warn the user.
1647 if branch.merge != self.revisionExpr:
1648 if branch.merge and self.revisionExpr:
1649 syncbuf.info(
1650 self,
1651 "manifest switched %s...%s",
1652 branch.merge,
1653 self.revisionExpr,
1654 )
1655 elif branch.merge:
1656 syncbuf.info(self, "manifest no longer tracks %s", branch.merge)
1657
1658 if cnt_mine < len(local_changes):
1659 # Upstream rebased. Not everything in HEAD was created by this user.
1660 syncbuf.info(
1661 self,
1662 "discarding %d commits removed from upstream",
1663 len(local_changes) - cnt_mine,
1664 )
1665
1666 branch.remote = self.GetRemote()
Sylvain56a5a012023-09-11 13:38:00 +02001667 if not IsId(self.revisionExpr):
Gavin Makea2e3302023-03-11 06:46:20 +00001668 # In case of manifest sync the revisionExpr might be a SHA1.
1669 branch.merge = self.revisionExpr
1670 if not branch.merge.startswith("refs/"):
1671 branch.merge = R_HEADS + branch.merge
1672 branch.Save()
1673
1674 if cnt_mine > 0 and self.rebase:
1675
1676 def _docopyandlink():
1677 self._CopyAndLinkFiles()
1678
1679 def _dorebase():
1680 self._Rebase(upstream="%s^1" % last_mine, onto=revid)
1681
1682 syncbuf.later2(self, _dorebase)
1683 if submodules:
1684 syncbuf.later2(self, _dosubmodules)
1685 syncbuf.later2(self, _docopyandlink)
1686 elif local_changes:
1687 try:
1688 self._ResetHard(revid)
1689 if submodules:
1690 self._SyncSubmodules(quiet=True)
1691 self._CopyAndLinkFiles()
1692 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001693 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001694 return
1695 else:
1696 syncbuf.later1(self, _doff)
1697 if submodules:
1698 syncbuf.later1(self, _dosubmodules)
1699
1700 def AddCopyFile(self, src, dest, topdir):
1701 """Mark |src| for copying to |dest| (relative to |topdir|).
1702
1703 No filesystem changes occur here. Actual copying happens later on.
1704
1705 Paths should have basic validation run on them before being queued.
1706 Further checking will be handled when the actual copy happens.
1707 """
1708 self.copyfiles.append(_CopyFile(self.worktree, src, topdir, dest))
1709
1710 def AddLinkFile(self, src, dest, topdir):
1711 """Mark |dest| to create a symlink (relative to |topdir|) pointing to
1712 |src|.
1713
1714 No filesystem changes occur here. Actual linking happens later on.
1715
1716 Paths should have basic validation run on them before being queued.
1717 Further checking will be handled when the actual link happens.
1718 """
1719 self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest))
1720
1721 def AddAnnotation(self, name, value, keep):
1722 self.annotations.append(Annotation(name, value, keep))
1723
1724 def DownloadPatchSet(self, change_id, patch_id):
1725 """Download a single patch set of a single change to FETCH_HEAD."""
1726 remote = self.GetRemote()
1727
1728 cmd = ["fetch", remote.name]
1729 cmd.append(
1730 "refs/changes/%2.2d/%d/%d" % (change_id % 100, change_id, patch_id)
1731 )
Jason Chang1a3612f2023-08-08 14:12:53 -07001732 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001733 return DownloadedChange(
1734 self,
1735 self.GetRevisionId(),
1736 change_id,
1737 patch_id,
1738 self.bare_git.rev_parse("FETCH_HEAD"),
1739 )
1740
1741 def DeleteWorktree(self, quiet=False, force=False):
1742 """Delete the source checkout and any other housekeeping tasks.
1743
1744 This currently leaves behind the internal .repo/ cache state. This
1745 helps when switching branches or manifest changes get reverted as we
1746 don't have to redownload all the git objects. But we should do some GC
1747 at some point.
1748
1749 Args:
1750 quiet: Whether to hide normal messages.
1751 force: Always delete tree even if dirty.
Doug Anderson3ba5f952011-04-07 12:51:04 -07001752
1753 Returns:
Gavin Makea2e3302023-03-11 06:46:20 +00001754 True if the worktree was completely cleaned out.
1755 """
1756 if self.IsDirty():
1757 if force:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00001758 logger.warning(
Gavin Makea2e3302023-03-11 06:46:20 +00001759 "warning: %s: Removing dirty project: uncommitted changes "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001760 "lost.",
1761 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001762 )
1763 else:
Jason Chang32b59562023-07-14 16:45:35 -07001764 msg = (
1765 "error: %s: Cannot remove project: uncommitted"
1766 "changes are present.\n" % self.RelPath(local=False)
Gavin Makea2e3302023-03-11 06:46:20 +00001767 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001768 logger.error(msg)
Jason Chang32b59562023-07-14 16:45:35 -07001769 raise DeleteDirtyWorktreeError(msg, project=self)
Wink Saville02d79452009-04-10 13:01:24 -07001770
Gavin Makea2e3302023-03-11 06:46:20 +00001771 if not quiet:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001772 print(f"{self.RelPath(local=False)}: Deleting obsolete checkout.")
Wink Saville02d79452009-04-10 13:01:24 -07001773
Gavin Makea2e3302023-03-11 06:46:20 +00001774 # Unlock and delink from the main worktree. We don't use git's worktree
1775 # remove because it will recursively delete projects -- we handle that
1776 # ourselves below. https://crbug.com/git/48
1777 if self.use_git_worktrees:
1778 needle = platform_utils.realpath(self.gitdir)
1779 # Find the git worktree commondir under .repo/worktrees/.
1780 output = self.bare_git.worktree("list", "--porcelain").splitlines()[
1781 0
1782 ]
1783 assert output.startswith("worktree "), output
1784 commondir = output[9:]
1785 # Walk each of the git worktrees to see where they point.
1786 configs = os.path.join(commondir, "worktrees")
1787 for name in os.listdir(configs):
1788 gitdir = os.path.join(configs, name, "gitdir")
1789 with open(gitdir) as fp:
1790 relpath = fp.read().strip()
1791 # Resolve the checkout path and see if it matches this project.
1792 fullpath = platform_utils.realpath(
1793 os.path.join(configs, name, relpath)
1794 )
1795 if fullpath == needle:
1796 platform_utils.rmtree(os.path.join(configs, name))
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001797
Gavin Makea2e3302023-03-11 06:46:20 +00001798 # Delete the .git directory first, so we're less likely to have a
1799 # partially working git repository around. There shouldn't be any git
1800 # projects here, so rmtree works.
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001801
Gavin Makea2e3302023-03-11 06:46:20 +00001802 # Try to remove plain files first in case of git worktrees. If this
1803 # fails for any reason, we'll fall back to rmtree, and that'll display
1804 # errors if it can't remove things either.
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001805 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001806 platform_utils.remove(self.gitdir)
1807 except OSError:
1808 pass
1809 try:
1810 platform_utils.rmtree(self.gitdir)
1811 except OSError as e:
1812 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001813 logger.error("error: %s: %s", self.gitdir, e)
1814 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00001815 "error: %s: Failed to delete obsolete checkout; remove "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001816 "manually, then run `repo sync -l`.",
1817 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001818 )
Jason Chang32b59562023-07-14 16:45:35 -07001819 raise DeleteWorktreeError(aggregate_errors=[e])
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001820
Gavin Makea2e3302023-03-11 06:46:20 +00001821 # Delete everything under the worktree, except for directories that
1822 # contain another git project.
1823 dirs_to_remove = []
1824 failed = False
Jason Chang32b59562023-07-14 16:45:35 -07001825 errors = []
Gavin Makea2e3302023-03-11 06:46:20 +00001826 for root, dirs, files in platform_utils.walk(self.worktree):
1827 for f in files:
1828 path = os.path.join(root, f)
1829 try:
1830 platform_utils.remove(path)
1831 except OSError as e:
1832 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001833 logger.error("error: %s: Failed to remove: %s", path, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001834 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001835 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001836 dirs[:] = [
1837 d
1838 for d in dirs
1839 if not os.path.lexists(os.path.join(root, d, ".git"))
1840 ]
1841 dirs_to_remove += [
1842 os.path.join(root, d)
1843 for d in dirs
1844 if os.path.join(root, d) not in dirs_to_remove
1845 ]
1846 for d in reversed(dirs_to_remove):
1847 if platform_utils.islink(d):
1848 try:
1849 platform_utils.remove(d)
1850 except OSError as e:
1851 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001852 logger.error("error: %s: Failed to remove: %s", d, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001853 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001854 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001855 elif not platform_utils.listdir(d):
1856 try:
1857 platform_utils.rmdir(d)
1858 except OSError as e:
1859 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001860 logger.error("error: %s: Failed to remove: %s", d, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001861 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001862 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001863 if failed:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001864 logger.error(
1865 "error: %s: Failed to delete obsolete checkout.",
1866 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001867 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001868 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00001869 " Remove manually, then run `repo sync -l`.",
Gavin Makea2e3302023-03-11 06:46:20 +00001870 )
Jason Chang32b59562023-07-14 16:45:35 -07001871 raise DeleteWorktreeError(aggregate_errors=errors)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07001872
Gavin Makea2e3302023-03-11 06:46:20 +00001873 # Try deleting parent dirs if they are empty.
1874 path = self.worktree
1875 while path != self.manifest.topdir:
1876 try:
1877 platform_utils.rmdir(path)
1878 except OSError as e:
1879 if e.errno != errno.ENOENT:
1880 break
1881 path = os.path.dirname(path)
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001882
Gavin Makea2e3302023-03-11 06:46:20 +00001883 return True
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001884
Gavin Makea2e3302023-03-11 06:46:20 +00001885 def StartBranch(self, name, branch_merge="", revision=None):
1886 """Create a new branch off the manifest's revision."""
1887 if not branch_merge:
1888 branch_merge = self.revisionExpr
1889 head = self.work_git.GetHead()
1890 if head == (R_HEADS + name):
1891 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001892
David Pursehouse8a68ff92012-09-24 12:15:13 +09001893 all_refs = self.bare_ref.all
Gavin Makea2e3302023-03-11 06:46:20 +00001894 if R_HEADS + name in all_refs:
Jason Chang1a3612f2023-08-08 14:12:53 -07001895 GitCommand(
1896 self, ["checkout", "-q", name, "--"], verify_command=True
1897 ).Wait()
1898 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001899
Gavin Makea2e3302023-03-11 06:46:20 +00001900 branch = self.GetBranch(name)
1901 branch.remote = self.GetRemote()
1902 branch.merge = branch_merge
Sylvain56a5a012023-09-11 13:38:00 +02001903 if not branch.merge.startswith("refs/") and not IsId(branch_merge):
Gavin Makea2e3302023-03-11 06:46:20 +00001904 branch.merge = R_HEADS + branch_merge
Shawn O. Pearce88443382010-10-08 10:02:09 +02001905
Gavin Makea2e3302023-03-11 06:46:20 +00001906 if revision is None:
1907 revid = self.GetRevisionId(all_refs)
Shawn O. Pearce88443382010-10-08 10:02:09 +02001908 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001909 revid = self.work_git.rev_parse(revision)
Brian Harring14a66742012-09-28 20:21:57 -07001910
Gavin Makea2e3302023-03-11 06:46:20 +00001911 if head.startswith(R_HEADS):
Kevin Degiabaa7f32014-11-12 11:27:45 -07001912 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001913 head = all_refs[head]
1914 except KeyError:
1915 head = None
1916 if revid and head and revid == head:
1917 ref = R_HEADS + name
1918 self.work_git.update_ref(ref, revid)
1919 self.work_git.symbolic_ref(HEAD, ref)
1920 branch.Save()
1921 return True
Kevin Degib1a07b82015-07-27 13:33:43 -06001922
Jason Chang1a3612f2023-08-08 14:12:53 -07001923 GitCommand(
1924 self,
1925 ["checkout", "-q", "-b", branch.name, revid],
1926 verify_command=True,
1927 ).Wait()
1928 branch.Save()
1929 return True
Kevin Degi384b3c52014-10-16 16:02:58 -06001930
Gavin Makea2e3302023-03-11 06:46:20 +00001931 def CheckoutBranch(self, name):
1932 """Checkout a local topic branch.
Shawn O. Pearce2816d4f2009-03-03 17:53:18 -08001933
Gavin Makea2e3302023-03-11 06:46:20 +00001934 Args:
1935 name: The name of the branch to checkout.
Shawn O. Pearce88443382010-10-08 10:02:09 +02001936
Gavin Makea2e3302023-03-11 06:46:20 +00001937 Returns:
Jason Chang1a3612f2023-08-08 14:12:53 -07001938 True if the checkout succeeded; False if the
1939 branch doesn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00001940 """
1941 rev = R_HEADS + name
1942 head = self.work_git.GetHead()
1943 if head == rev:
1944 # Already on the branch.
1945 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001946
Gavin Makea2e3302023-03-11 06:46:20 +00001947 all_refs = self.bare_ref.all
1948 try:
1949 revid = all_refs[rev]
1950 except KeyError:
1951 # Branch does not exist in this project.
Jason Chang1a3612f2023-08-08 14:12:53 -07001952 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001953
Gavin Makea2e3302023-03-11 06:46:20 +00001954 if head.startswith(R_HEADS):
1955 try:
1956 head = all_refs[head]
1957 except KeyError:
1958 head = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001959
Gavin Makea2e3302023-03-11 06:46:20 +00001960 if head == revid:
1961 # Same revision; just update HEAD to point to the new
1962 # target branch, but otherwise take no other action.
1963 _lwrite(
1964 self.work_git.GetDotgitPath(subpath=HEAD),
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001965 f"ref: {R_HEADS}{name}\n",
Gavin Makea2e3302023-03-11 06:46:20 +00001966 )
1967 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05001968
Jason Chang1a3612f2023-08-08 14:12:53 -07001969 GitCommand(
1970 self,
1971 ["checkout", name, "--"],
1972 capture_stdout=True,
1973 capture_stderr=True,
1974 verify_command=True,
1975 ).Wait()
1976 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05001977
Gavin Makea2e3302023-03-11 06:46:20 +00001978 def AbandonBranch(self, name):
1979 """Destroy a local topic branch.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07001980
Gavin Makea2e3302023-03-11 06:46:20 +00001981 Args:
1982 name: The name of the branch to abandon.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07001983
Gavin Makea2e3302023-03-11 06:46:20 +00001984 Returns:
Jason Changf9aacd42023-08-03 14:38:00 -07001985 True if the abandon succeeded; Raises GitCommandError if it didn't;
1986 None if the branch didn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00001987 """
1988 rev = R_HEADS + name
1989 all_refs = self.bare_ref.all
1990 if rev not in all_refs:
1991 # Doesn't exist
1992 return None
1993
1994 head = self.work_git.GetHead()
1995 if head == rev:
1996 # We can't destroy the branch while we are sitting
1997 # on it. Switch to a detached HEAD.
1998 head = all_refs[head]
1999
2000 revid = self.GetRevisionId(all_refs)
2001 if head == revid:
2002 _lwrite(
2003 self.work_git.GetDotgitPath(subpath=HEAD), "%s\n" % revid
2004 )
2005 else:
2006 self._Checkout(revid, quiet=True)
Jason Changf9aacd42023-08-03 14:38:00 -07002007 GitCommand(
2008 self,
2009 ["branch", "-D", name],
2010 capture_stdout=True,
2011 capture_stderr=True,
2012 verify_command=True,
2013 ).Wait()
2014 return True
Gavin Makea2e3302023-03-11 06:46:20 +00002015
2016 def PruneHeads(self):
2017 """Prune any topic branches already merged into upstream."""
2018 cb = self.CurrentBranch
2019 kill = []
2020 left = self._allrefs
2021 for name in left.keys():
2022 if name.startswith(R_HEADS):
2023 name = name[len(R_HEADS) :]
2024 if cb is None or name != cb:
2025 kill.append(name)
2026
2027 # Minor optimization: If there's nothing to prune, then don't try to
2028 # read any project state.
2029 if not kill and not cb:
2030 return []
2031
2032 rev = self.GetRevisionId(left)
2033 if (
2034 cb is not None
2035 and not self._revlist(HEAD + "..." + rev)
2036 and not self.IsDirty(consider_untracked=False)
2037 ):
2038 self.work_git.DetachHead(HEAD)
2039 kill.append(cb)
2040
2041 if kill:
2042 old = self.bare_git.GetHead()
2043
2044 try:
2045 self.bare_git.DetachHead(rev)
2046
2047 b = ["branch", "-d"]
2048 b.extend(kill)
2049 b = GitCommand(
2050 self, b, bare=True, capture_stdout=True, capture_stderr=True
2051 )
2052 b.Wait()
2053 finally:
Sylvain56a5a012023-09-11 13:38:00 +02002054 if IsId(old):
Gavin Makea2e3302023-03-11 06:46:20 +00002055 self.bare_git.DetachHead(old)
2056 else:
2057 self.bare_git.SetHead(old)
2058 left = self._allrefs
2059
2060 for branch in kill:
2061 if (R_HEADS + branch) not in left:
2062 self.CleanPublishedCache()
2063 break
2064
2065 if cb and cb not in kill:
2066 kill.append(cb)
2067 kill.sort()
2068
2069 kept = []
2070 for branch in kill:
2071 if R_HEADS + branch in left:
2072 branch = self.GetBranch(branch)
2073 base = branch.LocalMerge
2074 if not base:
2075 base = rev
2076 kept.append(ReviewableBranch(self, branch, base))
2077 return kept
2078
2079 def GetRegisteredSubprojects(self):
2080 result = []
2081
2082 def rec(subprojects):
2083 if not subprojects:
2084 return
2085 result.extend(subprojects)
2086 for p in subprojects:
2087 rec(p.subprojects)
2088
2089 rec(self.subprojects)
2090 return result
2091
2092 def _GetSubmodules(self):
2093 # Unfortunately we cannot call `git submodule status --recursive` here
2094 # because the working tree might not exist yet, and it cannot be used
2095 # without a working tree in its current implementation.
2096
2097 def get_submodules(gitdir, rev):
2098 # Parse .gitmodules for submodule sub_paths and sub_urls.
2099 sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
2100 if not sub_paths:
2101 return []
2102 # Run `git ls-tree` to read SHAs of submodule object, which happen
2103 # to be revision of submodule repository.
2104 sub_revs = git_ls_tree(gitdir, rev, sub_paths)
2105 submodules = []
2106 for sub_path, sub_url in zip(sub_paths, sub_urls):
2107 try:
2108 sub_rev = sub_revs[sub_path]
2109 except KeyError:
2110 # Ignore non-exist submodules.
2111 continue
2112 submodules.append((sub_rev, sub_path, sub_url))
2113 return submodules
2114
2115 re_path = re.compile(r"^submodule\.(.+)\.path=(.*)$")
2116 re_url = re.compile(r"^submodule\.(.+)\.url=(.*)$")
2117
2118 def parse_gitmodules(gitdir, rev):
2119 cmd = ["cat-file", "blob", "%s:.gitmodules" % rev]
2120 try:
2121 p = GitCommand(
2122 None,
2123 cmd,
2124 capture_stdout=True,
2125 capture_stderr=True,
2126 bare=True,
2127 gitdir=gitdir,
2128 )
2129 except GitError:
2130 return [], []
2131 if p.Wait() != 0:
2132 return [], []
2133
2134 gitmodules_lines = []
2135 fd, temp_gitmodules_path = tempfile.mkstemp()
2136 try:
2137 os.write(fd, p.stdout.encode("utf-8"))
2138 os.close(fd)
2139 cmd = ["config", "--file", temp_gitmodules_path, "--list"]
2140 p = GitCommand(
2141 None,
2142 cmd,
2143 capture_stdout=True,
2144 capture_stderr=True,
2145 bare=True,
2146 gitdir=gitdir,
2147 )
2148 if p.Wait() != 0:
2149 return [], []
2150 gitmodules_lines = p.stdout.split("\n")
2151 except GitError:
2152 return [], []
2153 finally:
2154 platform_utils.remove(temp_gitmodules_path)
2155
2156 names = set()
2157 paths = {}
2158 urls = {}
2159 for line in gitmodules_lines:
2160 if not line:
2161 continue
2162 m = re_path.match(line)
2163 if m:
2164 names.add(m.group(1))
2165 paths[m.group(1)] = m.group(2)
2166 continue
2167 m = re_url.match(line)
2168 if m:
2169 names.add(m.group(1))
2170 urls[m.group(1)] = m.group(2)
2171 continue
2172 names = sorted(names)
2173 return (
2174 [paths.get(name, "") for name in names],
2175 [urls.get(name, "") for name in names],
2176 )
2177
2178 def git_ls_tree(gitdir, rev, paths):
2179 cmd = ["ls-tree", rev, "--"]
2180 cmd.extend(paths)
2181 try:
2182 p = GitCommand(
2183 None,
2184 cmd,
2185 capture_stdout=True,
2186 capture_stderr=True,
2187 bare=True,
2188 gitdir=gitdir,
2189 )
2190 except GitError:
2191 return []
2192 if p.Wait() != 0:
2193 return []
2194 objects = {}
2195 for line in p.stdout.split("\n"):
2196 if not line.strip():
2197 continue
2198 object_rev, object_path = line.split()[2:4]
2199 objects[object_path] = object_rev
2200 return objects
2201
2202 try:
2203 rev = self.GetRevisionId()
2204 except GitError:
2205 return []
2206 return get_submodules(self.gitdir, rev)
2207
2208 def GetDerivedSubprojects(self):
2209 result = []
2210 if not self.Exists:
2211 # If git repo does not exist yet, querying its submodules will
2212 # mess up its states; so return here.
2213 return result
2214 for rev, path, url in self._GetSubmodules():
2215 name = self.manifest.GetSubprojectName(self, path)
2216 (
2217 relpath,
2218 worktree,
2219 gitdir,
2220 objdir,
2221 ) = self.manifest.GetSubprojectPaths(self, name, path)
2222 project = self.manifest.paths.get(relpath)
2223 if project:
2224 result.extend(project.GetDerivedSubprojects())
2225 continue
2226
2227 if url.startswith(".."):
2228 url = urllib.parse.urljoin("%s/" % self.remote.url, url)
2229 remote = RemoteSpec(
2230 self.remote.name,
2231 url=url,
2232 pushUrl=self.remote.pushUrl,
2233 review=self.remote.review,
2234 revision=self.remote.revision,
2235 )
2236 subproject = Project(
2237 manifest=self.manifest,
2238 name=name,
2239 remote=remote,
2240 gitdir=gitdir,
2241 objdir=objdir,
2242 worktree=worktree,
2243 relpath=relpath,
2244 revisionExpr=rev,
2245 revisionId=rev,
2246 rebase=self.rebase,
2247 groups=self.groups,
2248 sync_c=self.sync_c,
2249 sync_s=self.sync_s,
2250 sync_tags=self.sync_tags,
2251 parent=self,
2252 is_derived=True,
2253 )
2254 result.append(subproject)
2255 result.extend(subproject.GetDerivedSubprojects())
2256 return result
2257
2258 def EnableRepositoryExtension(self, key, value="true", version=1):
2259 """Enable git repository extension |key| with |value|.
2260
2261 Args:
2262 key: The extension to enabled. Omit the "extensions." prefix.
2263 value: The value to use for the extension.
2264 version: The minimum git repository version needed.
2265 """
2266 # Make sure the git repo version is new enough already.
2267 found_version = self.config.GetInt("core.repositoryFormatVersion")
2268 if found_version is None:
2269 found_version = 0
2270 if found_version < version:
2271 self.config.SetString("core.repositoryFormatVersion", str(version))
2272
2273 # Enable the extension!
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002274 self.config.SetString(f"extensions.{key}", value)
Gavin Makea2e3302023-03-11 06:46:20 +00002275
2276 def ResolveRemoteHead(self, name=None):
2277 """Find out what the default branch (HEAD) points to.
2278
2279 Normally this points to refs/heads/master, but projects are moving to
2280 main. Support whatever the server uses rather than hardcoding "master"
2281 ourselves.
2282 """
2283 if name is None:
2284 name = self.remote.name
2285
2286 # The output will look like (NB: tabs are separators):
2287 # ref: refs/heads/master HEAD
2288 # 5f6803b100bb3cd0f534e96e88c91373e8ed1c44 HEAD
2289 output = self.bare_git.ls_remote(
2290 "-q", "--symref", "--exit-code", name, "HEAD"
2291 )
2292
2293 for line in output.splitlines():
2294 lhs, rhs = line.split("\t", 1)
2295 if rhs == "HEAD" and lhs.startswith("ref:"):
2296 return lhs[4:].strip()
2297
2298 return None
2299
2300 def _CheckForImmutableRevision(self):
2301 try:
2302 # if revision (sha or tag) is not present then following function
2303 # throws an error.
2304 self.bare_git.rev_list(
Jason Chang87058c62023-09-27 11:34:43 -07002305 "-1",
2306 "--missing=allow-any",
2307 "%s^0" % self.revisionExpr,
2308 "--",
2309 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002310 )
2311 if self.upstream:
2312 rev = self.GetRemote().ToLocal(self.upstream)
2313 self.bare_git.rev_list(
Jason Chang87058c62023-09-27 11:34:43 -07002314 "-1",
2315 "--missing=allow-any",
2316 "%s^0" % rev,
2317 "--",
2318 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002319 )
2320 self.bare_git.merge_base(
Jason Chang87058c62023-09-27 11:34:43 -07002321 "--is-ancestor",
2322 self.revisionExpr,
2323 rev,
2324 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002325 )
2326 return True
2327 except GitError:
2328 # There is no such persistent revision. We have to fetch it.
2329 return False
2330
2331 def _FetchArchive(self, tarpath, cwd=None):
2332 cmd = ["archive", "-v", "-o", tarpath]
2333 cmd.append("--remote=%s" % self.remote.url)
2334 cmd.append("--prefix=%s/" % self.RelPath(local=False))
2335 cmd.append(self.revisionExpr)
2336
2337 command = GitCommand(
Jason Chang32b59562023-07-14 16:45:35 -07002338 self,
2339 cmd,
2340 cwd=cwd,
2341 capture_stdout=True,
2342 capture_stderr=True,
2343 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00002344 )
Jason Chang32b59562023-07-14 16:45:35 -07002345 command.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00002346
2347 def _RemoteFetch(
2348 self,
2349 name=None,
2350 current_branch_only=False,
2351 initial=False,
2352 quiet=False,
2353 verbose=False,
2354 output_redir=None,
2355 alt_dir=None,
2356 tags=True,
2357 prune=False,
2358 depth=None,
2359 submodules=False,
2360 ssh_proxy=None,
2361 force_sync=False,
2362 clone_filter=None,
2363 retry_fetches=2,
2364 retry_sleep_initial_sec=4.0,
2365 retry_exp_factor=2.0,
Jason Chang32b59562023-07-14 16:45:35 -07002366 ) -> bool:
Gavin Makea2e3302023-03-11 06:46:20 +00002367 tag_name = None
2368 # The depth should not be used when fetching to a mirror because
2369 # it will result in a shallow repository that cannot be cloned or
2370 # fetched from.
2371 # The repo project should also never be synced with partial depth.
2372 if self.manifest.IsMirror or self.relpath == ".repo/repo":
2373 depth = None
2374
2375 if depth:
2376 current_branch_only = True
2377
Sylvain56a5a012023-09-11 13:38:00 +02002378 is_sha1 = bool(IsId(self.revisionExpr))
Gavin Makea2e3302023-03-11 06:46:20 +00002379
2380 if current_branch_only:
2381 if self.revisionExpr.startswith(R_TAGS):
2382 # This is a tag and its commit id should never change.
2383 tag_name = self.revisionExpr[len(R_TAGS) :]
2384 elif self.upstream and self.upstream.startswith(R_TAGS):
2385 # This is a tag and its commit id should never change.
2386 tag_name = self.upstream[len(R_TAGS) :]
2387
2388 if is_sha1 or tag_name is not None:
2389 if self._CheckForImmutableRevision():
2390 if verbose:
2391 print(
2392 "Skipped fetching project %s (already have "
2393 "persistent ref)" % self.name
2394 )
2395 return True
2396 if is_sha1 and not depth:
2397 # When syncing a specific commit and --depth is not set:
2398 # * if upstream is explicitly specified and is not a sha1, fetch
2399 # only upstream as users expect only upstream to be fetch.
2400 # Note: The commit might not be in upstream in which case the
2401 # sync will fail.
2402 # * otherwise, fetch all branches to make sure we end up with
2403 # the specific commit.
2404 if self.upstream:
Sylvain56a5a012023-09-11 13:38:00 +02002405 current_branch_only = not IsId(self.upstream)
Gavin Makea2e3302023-03-11 06:46:20 +00002406 else:
2407 current_branch_only = False
2408
2409 if not name:
2410 name = self.remote.name
2411
2412 remote = self.GetRemote(name)
2413 if not remote.PreConnectFetch(ssh_proxy):
2414 ssh_proxy = None
2415
2416 if initial:
2417 if alt_dir and "objects" == os.path.basename(alt_dir):
2418 ref_dir = os.path.dirname(alt_dir)
2419 packed_refs = os.path.join(self.gitdir, "packed-refs")
2420
2421 all_refs = self.bare_ref.all
2422 ids = set(all_refs.values())
2423 tmp = set()
2424
2425 for r, ref_id in GitRefs(ref_dir).all.items():
2426 if r not in all_refs:
2427 if r.startswith(R_TAGS) or remote.WritesTo(r):
2428 all_refs[r] = ref_id
2429 ids.add(ref_id)
2430 continue
2431
2432 if ref_id in ids:
2433 continue
2434
2435 r = "refs/_alt/%s" % ref_id
2436 all_refs[r] = ref_id
2437 ids.add(ref_id)
2438 tmp.add(r)
2439
2440 tmp_packed_lines = []
2441 old_packed_lines = []
2442
2443 for r in sorted(all_refs):
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002444 line = f"{all_refs[r]} {r}\n"
Gavin Makea2e3302023-03-11 06:46:20 +00002445 tmp_packed_lines.append(line)
2446 if r not in tmp:
2447 old_packed_lines.append(line)
2448
2449 tmp_packed = "".join(tmp_packed_lines)
2450 old_packed = "".join(old_packed_lines)
2451 _lwrite(packed_refs, tmp_packed)
2452 else:
2453 alt_dir = None
2454
2455 cmd = ["fetch"]
2456
2457 if clone_filter:
2458 git_require((2, 19, 0), fail=True, msg="partial clones")
2459 cmd.append("--filter=%s" % clone_filter)
2460 self.EnableRepositoryExtension("partialclone", self.remote.name)
2461
2462 if depth:
2463 cmd.append("--depth=%s" % depth)
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002464 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002465 # If this repo has shallow objects, then we don't know which refs
2466 # have shallow objects or not. Tell git to unshallow all fetched
2467 # refs. Don't do this with projects that don't have shallow
2468 # objects, since it is less efficient.
2469 if os.path.exists(os.path.join(self.gitdir, "shallow")):
2470 cmd.append("--depth=2147483647")
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002471
Gavin Makea2e3302023-03-11 06:46:20 +00002472 if not verbose:
2473 cmd.append("--quiet")
2474 if not quiet and sys.stdout.isatty():
2475 cmd.append("--progress")
2476 if not self.worktree:
2477 cmd.append("--update-head-ok")
2478 cmd.append(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002479
Gavin Makea2e3302023-03-11 06:46:20 +00002480 if force_sync:
2481 cmd.append("--force")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002482
Gavin Makea2e3302023-03-11 06:46:20 +00002483 if prune:
2484 cmd.append("--prune")
Mike Frysinger29626b42021-05-01 09:37:13 -04002485
Gavin Makea2e3302023-03-11 06:46:20 +00002486 # Always pass something for --recurse-submodules, git with GIT_DIR
2487 # behaves incorrectly when not given `--recurse-submodules=no`.
2488 # (b/218891912)
2489 cmd.append(
2490 f'--recurse-submodules={"on-demand" if submodules else "no"}'
2491 )
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002492
Gavin Makea2e3302023-03-11 06:46:20 +00002493 spec = []
2494 if not current_branch_only:
2495 # Fetch whole repo.
2496 spec.append(
2497 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2498 )
2499 elif tag_name is not None:
2500 spec.append("tag")
2501 spec.append(tag_name)
Remy Böhmer1469c282020-12-15 18:49:02 +01002502
Gavin Makea2e3302023-03-11 06:46:20 +00002503 if self.manifest.IsMirror and not current_branch_only:
2504 branch = None
Remy Böhmer1469c282020-12-15 18:49:02 +01002505 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002506 branch = self.revisionExpr
2507 if (
2508 not self.manifest.IsMirror
2509 and is_sha1
2510 and depth
2511 and git_require((1, 8, 3))
2512 ):
2513 # Shallow checkout of a specific commit, fetch from that commit and
2514 # not the heads only as the commit might be deeper in the history.
2515 spec.append(branch)
2516 if self.upstream:
2517 spec.append(self.upstream)
David James8d201162013-10-11 17:03:19 -07002518 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002519 if is_sha1:
2520 branch = self.upstream
2521 if branch is not None and branch.strip():
2522 if not branch.startswith("refs/"):
2523 branch = R_HEADS + branch
2524 spec.append(str(("+%s:" % branch) + remote.ToLocal(branch)))
David James8d201162013-10-11 17:03:19 -07002525
Gavin Makea2e3302023-03-11 06:46:20 +00002526 # If mirroring repo and we cannot deduce the tag or branch to fetch,
2527 # fetch whole repo.
2528 if self.manifest.IsMirror and not spec:
2529 spec.append(
2530 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2531 )
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002532
Gavin Makea2e3302023-03-11 06:46:20 +00002533 # If using depth then we should not get all the tags since they may
2534 # be outside of the depth.
2535 if not tags or depth:
2536 cmd.append("--no-tags")
2537 else:
2538 cmd.append("--tags")
2539 spec.append(str(("+refs/tags/*:") + remote.ToLocal("refs/tags/*")))
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002540
Gavin Makea2e3302023-03-11 06:46:20 +00002541 cmd.extend(spec)
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002542
Gavin Makea2e3302023-03-11 06:46:20 +00002543 # At least one retry minimum due to git remote prune.
2544 retry_fetches = max(retry_fetches, 2)
2545 retry_cur_sleep = retry_sleep_initial_sec
2546 ok = prune_tried = False
2547 for try_n in range(retry_fetches):
Jason Chang32b59562023-07-14 16:45:35 -07002548 verify_command = try_n == retry_fetches - 1
Gavin Makea2e3302023-03-11 06:46:20 +00002549 gitcmd = GitCommand(
2550 self,
2551 cmd,
2552 bare=True,
2553 objdir=os.path.join(self.objdir, "objects"),
2554 ssh_proxy=ssh_proxy,
2555 merge_output=True,
2556 capture_stdout=quiet or bool(output_redir),
Jason Chang32b59562023-07-14 16:45:35 -07002557 verify_command=verify_command,
Gavin Makea2e3302023-03-11 06:46:20 +00002558 )
2559 if gitcmd.stdout and not quiet and output_redir:
2560 output_redir.write(gitcmd.stdout)
2561 ret = gitcmd.Wait()
2562 if ret == 0:
2563 ok = True
2564 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002565
Gavin Makea2e3302023-03-11 06:46:20 +00002566 # Retry later due to HTTP 429 Too Many Requests.
2567 elif (
2568 gitcmd.stdout
2569 and "error:" in gitcmd.stdout
2570 and "HTTP 429" in gitcmd.stdout
2571 ):
2572 # Fallthru to sleep+retry logic at the bottom.
2573 pass
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002574
Gavin Makea2e3302023-03-11 06:46:20 +00002575 # Try to prune remote branches once in case there are conflicts.
2576 # For example, if the remote had refs/heads/upstream, but deleted
2577 # that and now has refs/heads/upstream/foo.
2578 elif (
2579 gitcmd.stdout
2580 and "error:" in gitcmd.stdout
2581 and "git remote prune" in gitcmd.stdout
2582 and not prune_tried
2583 ):
2584 prune_tried = True
2585 prunecmd = GitCommand(
2586 self,
2587 ["remote", "prune", name],
2588 bare=True,
2589 ssh_proxy=ssh_proxy,
2590 )
2591 ret = prunecmd.Wait()
2592 if ret:
2593 break
2594 print(
2595 "retrying fetch after pruning remote branches",
2596 file=output_redir,
2597 )
2598 # Continue right away so we don't sleep as we shouldn't need to.
2599 continue
2600 elif current_branch_only and is_sha1 and ret == 128:
2601 # Exit code 128 means "couldn't find the ref you asked for"; if
2602 # we're in sha1 mode, we just tried sync'ing from the upstream
2603 # field; it doesn't exist, thus abort the optimization attempt
2604 # and do a full sync.
2605 break
2606 elif ret < 0:
2607 # Git died with a signal, exit immediately.
2608 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002609
Gavin Makea2e3302023-03-11 06:46:20 +00002610 # Figure out how long to sleep before the next attempt, if there is
2611 # one.
2612 if not verbose and gitcmd.stdout:
2613 print(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002614 f"\n{self.name}:\n{gitcmd.stdout}",
Gavin Makea2e3302023-03-11 06:46:20 +00002615 end="",
2616 file=output_redir,
2617 )
2618 if try_n < retry_fetches - 1:
2619 print(
2620 "%s: sleeping %s seconds before retrying"
2621 % (self.name, retry_cur_sleep),
2622 file=output_redir,
2623 )
2624 time.sleep(retry_cur_sleep)
2625 retry_cur_sleep = min(
2626 retry_exp_factor * retry_cur_sleep, MAXIMUM_RETRY_SLEEP_SEC
2627 )
2628 retry_cur_sleep *= 1 - random.uniform(
2629 -RETRY_JITTER_PERCENT, RETRY_JITTER_PERCENT
2630 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002631
Gavin Makea2e3302023-03-11 06:46:20 +00002632 if initial:
2633 if alt_dir:
2634 if old_packed != "":
2635 _lwrite(packed_refs, old_packed)
2636 else:
2637 platform_utils.remove(packed_refs)
2638 self.bare_git.pack_refs("--all", "--prune")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002639
Gavin Makea2e3302023-03-11 06:46:20 +00002640 if is_sha1 and current_branch_only:
2641 # We just synced the upstream given branch; verify we
2642 # got what we wanted, else trigger a second run of all
2643 # refs.
2644 if not self._CheckForImmutableRevision():
2645 # Sync the current branch only with depth set to None.
2646 # We always pass depth=None down to avoid infinite recursion.
2647 return self._RemoteFetch(
2648 name=name,
2649 quiet=quiet,
2650 verbose=verbose,
2651 output_redir=output_redir,
2652 current_branch_only=current_branch_only and depth,
2653 initial=False,
2654 alt_dir=alt_dir,
Nasser Grainawiaafed292023-05-24 12:51:03 -06002655 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00002656 depth=None,
2657 ssh_proxy=ssh_proxy,
2658 clone_filter=clone_filter,
2659 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002660
Gavin Makea2e3302023-03-11 06:46:20 +00002661 return ok
Mike Frysingerf4545122019-11-11 04:34:16 -05002662
Gavin Makea2e3302023-03-11 06:46:20 +00002663 def _ApplyCloneBundle(self, initial=False, quiet=False, verbose=False):
2664 if initial and (
2665 self.manifest.manifestProject.depth or self.clone_depth
2666 ):
2667 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002668
Gavin Makea2e3302023-03-11 06:46:20 +00002669 remote = self.GetRemote()
2670 bundle_url = remote.url + "/clone.bundle"
2671 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
2672 if GetSchemeFromUrl(bundle_url) not in (
2673 "http",
2674 "https",
2675 "persistent-http",
2676 "persistent-https",
2677 ):
2678 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06002679
Gavin Makea2e3302023-03-11 06:46:20 +00002680 bundle_dst = os.path.join(self.gitdir, "clone.bundle")
2681 bundle_tmp = os.path.join(self.gitdir, "clone.bundle.tmp")
2682
2683 exist_dst = os.path.exists(bundle_dst)
2684 exist_tmp = os.path.exists(bundle_tmp)
2685
2686 if not initial and not exist_dst and not exist_tmp:
2687 return False
2688
2689 if not exist_dst:
2690 exist_dst = self._FetchBundle(
2691 bundle_url, bundle_tmp, bundle_dst, quiet, verbose
2692 )
2693 if not exist_dst:
2694 return False
2695
2696 cmd = ["fetch"]
2697 if not verbose:
2698 cmd.append("--quiet")
2699 if not quiet and sys.stdout.isatty():
2700 cmd.append("--progress")
2701 if not self.worktree:
2702 cmd.append("--update-head-ok")
2703 cmd.append(bundle_dst)
2704 for f in remote.fetch:
2705 cmd.append(str(f))
2706 cmd.append("+refs/tags/*:refs/tags/*")
2707
2708 ok = (
2709 GitCommand(
2710 self,
2711 cmd,
2712 bare=True,
2713 objdir=os.path.join(self.objdir, "objects"),
2714 ).Wait()
2715 == 0
2716 )
2717 platform_utils.remove(bundle_dst, missing_ok=True)
2718 platform_utils.remove(bundle_tmp, missing_ok=True)
2719 return ok
2720
2721 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
2722 platform_utils.remove(dstPath, missing_ok=True)
2723
2724 cmd = ["curl", "--fail", "--output", tmpPath, "--netrc", "--location"]
2725 if quiet:
2726 cmd += ["--silent", "--show-error"]
2727 if os.path.exists(tmpPath):
2728 size = os.stat(tmpPath).st_size
2729 if size >= 1024:
2730 cmd += ["--continue-at", "%d" % (size,)]
2731 else:
2732 platform_utils.remove(tmpPath)
2733 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
2734 if cookiefile:
2735 cmd += ["--cookie", cookiefile]
2736 if proxy:
2737 cmd += ["--proxy", proxy]
2738 elif "http_proxy" in os.environ and "darwin" == sys.platform:
2739 cmd += ["--proxy", os.environ["http_proxy"]]
2740 if srcUrl.startswith("persistent-https"):
2741 srcUrl = "http" + srcUrl[len("persistent-https") :]
2742 elif srcUrl.startswith("persistent-http"):
2743 srcUrl = "http" + srcUrl[len("persistent-http") :]
2744 cmd += [srcUrl]
2745
2746 proc = None
2747 with Trace("Fetching bundle: %s", " ".join(cmd)):
2748 if verbose:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002749 print(f"{self.name}: Downloading bundle: {srcUrl}")
Gavin Makea2e3302023-03-11 06:46:20 +00002750 stdout = None if verbose else subprocess.PIPE
2751 stderr = None if verbose else subprocess.STDOUT
2752 try:
2753 proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
2754 except OSError:
2755 return False
2756
2757 (output, _) = proc.communicate()
2758 curlret = proc.returncode
2759
2760 if curlret == 22:
2761 # From curl man page:
2762 # 22: HTTP page not retrieved. The requested url was not found
2763 # or returned another error with the HTTP error code being 400
2764 # or above. This return code only appears if -f, --fail is used.
2765 if verbose:
2766 print(
2767 "%s: Unable to retrieve clone.bundle; ignoring."
2768 % self.name
2769 )
2770 if output:
2771 print("Curl output:\n%s" % output)
2772 return False
2773 elif curlret and not verbose and output:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002774 logger.error("%s", output)
Gavin Makea2e3302023-03-11 06:46:20 +00002775
2776 if os.path.exists(tmpPath):
2777 if curlret == 0 and self._IsValidBundle(tmpPath, quiet):
2778 platform_utils.rename(tmpPath, dstPath)
2779 return True
2780 else:
2781 platform_utils.remove(tmpPath)
2782 return False
2783 else:
2784 return False
2785
2786 def _IsValidBundle(self, path, quiet):
2787 try:
2788 with open(path, "rb") as f:
2789 if f.read(16) == b"# v2 git bundle\n":
2790 return True
2791 else:
2792 if not quiet:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002793 logger.error("Invalid clone.bundle file; ignoring.")
Gavin Makea2e3302023-03-11 06:46:20 +00002794 return False
2795 except OSError:
2796 return False
2797
2798 def _Checkout(self, rev, quiet=False):
2799 cmd = ["checkout"]
2800 if quiet:
2801 cmd.append("-q")
2802 cmd.append(rev)
2803 cmd.append("--")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002804 if GitCommand(self, cmd).Wait() != 0:
Gavin Makea2e3302023-03-11 06:46:20 +00002805 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002806 raise GitError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002807 f"{self.name} checkout {rev} ", project=self.name
Jason Chang32b59562023-07-14 16:45:35 -07002808 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002809
Gavin Makea2e3302023-03-11 06:46:20 +00002810 def _CherryPick(self, rev, ffonly=False, record_origin=False):
2811 cmd = ["cherry-pick"]
2812 if ffonly:
2813 cmd.append("--ff")
2814 if record_origin:
2815 cmd.append("-x")
2816 cmd.append(rev)
2817 cmd.append("--")
2818 if GitCommand(self, cmd).Wait() != 0:
2819 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002820 raise GitError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002821 f"{self.name} cherry-pick {rev} ", project=self.name
Jason Chang32b59562023-07-14 16:45:35 -07002822 )
Victor Boivie0960b5b2010-11-26 13:42:13 +01002823
Gavin Makea2e3302023-03-11 06:46:20 +00002824 def _LsRemote(self, refs):
2825 cmd = ["ls-remote", self.remote.name, refs]
2826 p = GitCommand(self, cmd, capture_stdout=True)
2827 if p.Wait() == 0:
2828 return p.stdout
2829 return None
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002830
Gavin Makea2e3302023-03-11 06:46:20 +00002831 def _Revert(self, rev):
2832 cmd = ["revert"]
2833 cmd.append("--no-edit")
2834 cmd.append(rev)
2835 cmd.append("--")
2836 if GitCommand(self, cmd).Wait() != 0:
2837 if self._allrefs:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002838 raise GitError(f"{self.name} revert {rev} ", project=self.name)
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002839
Gavin Makea2e3302023-03-11 06:46:20 +00002840 def _ResetHard(self, rev, quiet=True):
2841 cmd = ["reset", "--hard"]
2842 if quiet:
2843 cmd.append("-q")
2844 cmd.append(rev)
2845 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002846 raise GitError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002847 f"{self.name} reset --hard {rev} ", project=self.name
Jason Chang32b59562023-07-14 16:45:35 -07002848 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002849
Gavin Makea2e3302023-03-11 06:46:20 +00002850 def _SyncSubmodules(self, quiet=True):
2851 cmd = ["submodule", "update", "--init", "--recursive"]
2852 if quiet:
2853 cmd.append("-q")
2854 if GitCommand(self, cmd).Wait() != 0:
2855 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07002856 "%s submodule update --init --recursive " % self.name,
2857 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00002858 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002859
Gavin Makea2e3302023-03-11 06:46:20 +00002860 def _Rebase(self, upstream, onto=None):
2861 cmd = ["rebase"]
2862 if onto is not None:
2863 cmd.extend(["--onto", onto])
2864 cmd.append(upstream)
2865 if GitCommand(self, cmd).Wait() != 0:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002866 raise GitError(f"{self.name} rebase {upstream} ", project=self.name)
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002867
Gavin Makea2e3302023-03-11 06:46:20 +00002868 def _FastForward(self, head, ffonly=False):
2869 cmd = ["merge", "--no-stat", head]
2870 if ffonly:
2871 cmd.append("--ff-only")
2872 if GitCommand(self, cmd).Wait() != 0:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002873 raise GitError(f"{self.name} merge {head} ", project=self.name)
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002874
Gavin Makea2e3302023-03-11 06:46:20 +00002875 def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False):
2876 init_git_dir = not os.path.exists(self.gitdir)
2877 init_obj_dir = not os.path.exists(self.objdir)
2878 try:
2879 # Initialize the bare repository, which contains all of the objects.
2880 if init_obj_dir:
2881 os.makedirs(self.objdir)
2882 self.bare_objdir.init()
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002883
Gavin Makea2e3302023-03-11 06:46:20 +00002884 self._UpdateHooks(quiet=quiet)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002885
Gavin Makea2e3302023-03-11 06:46:20 +00002886 if self.use_git_worktrees:
2887 # Enable per-worktree config file support if possible. This
2888 # is more a nice-to-have feature for users rather than a
2889 # hard requirement.
2890 if git_require((2, 20, 0)):
2891 self.EnableRepositoryExtension("worktreeConfig")
Renaud Paquay788e9622017-01-27 11:41:12 -08002892
Gavin Makea2e3302023-03-11 06:46:20 +00002893 # If we have a separate directory to hold refs, initialize it as
2894 # well.
2895 if self.objdir != self.gitdir:
2896 if init_git_dir:
2897 os.makedirs(self.gitdir)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002898
Gavin Makea2e3302023-03-11 06:46:20 +00002899 if init_obj_dir or init_git_dir:
2900 self._ReferenceGitDir(
2901 self.objdir, self.gitdir, copy_all=True
2902 )
2903 try:
2904 self._CheckDirReference(self.objdir, self.gitdir)
2905 except GitError as e:
2906 if force_sync:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002907 logger.error(
2908 "Retrying clone after deleting %s", self.gitdir
Gavin Makea2e3302023-03-11 06:46:20 +00002909 )
2910 try:
2911 platform_utils.rmtree(
2912 platform_utils.realpath(self.gitdir)
2913 )
2914 if self.worktree and os.path.exists(
2915 platform_utils.realpath(self.worktree)
2916 ):
2917 platform_utils.rmtree(
2918 platform_utils.realpath(self.worktree)
2919 )
2920 return self._InitGitDir(
2921 mirror_git=mirror_git,
2922 force_sync=False,
2923 quiet=quiet,
2924 )
2925 except Exception:
2926 raise e
2927 raise e
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002928
Gavin Makea2e3302023-03-11 06:46:20 +00002929 if init_git_dir:
2930 mp = self.manifest.manifestProject
2931 ref_dir = mp.reference or ""
Julien Camperguedd654222014-01-09 16:21:37 +01002932
Gavin Makea2e3302023-03-11 06:46:20 +00002933 def _expanded_ref_dirs():
2934 """Iterate through possible git reference dir paths."""
2935 name = self.name + ".git"
2936 yield mirror_git or os.path.join(ref_dir, name)
2937 for prefix in "", self.remote.name:
2938 yield os.path.join(
2939 ref_dir, ".repo", "project-objects", prefix, name
2940 )
2941 yield os.path.join(
2942 ref_dir, ".repo", "worktrees", prefix, name
2943 )
2944
2945 if ref_dir or mirror_git:
2946 found_ref_dir = None
2947 for path in _expanded_ref_dirs():
2948 if os.path.exists(path):
2949 found_ref_dir = path
2950 break
2951 ref_dir = found_ref_dir
2952
2953 if ref_dir:
2954 if not os.path.isabs(ref_dir):
2955 # The alternate directory is relative to the object
2956 # database.
2957 ref_dir = os.path.relpath(
2958 ref_dir, os.path.join(self.objdir, "objects")
2959 )
2960 _lwrite(
2961 os.path.join(
2962 self.objdir, "objects/info/alternates"
2963 ),
2964 os.path.join(ref_dir, "objects") + "\n",
2965 )
2966
2967 m = self.manifest.manifestProject.config
2968 for key in ["user.name", "user.email"]:
2969 if m.Has(key, include_defaults=False):
2970 self.config.SetString(key, m.GetString(key))
2971 if not self.manifest.EnableGitLfs:
2972 self.config.SetString(
2973 "filter.lfs.smudge", "git-lfs smudge --skip -- %f"
2974 )
2975 self.config.SetString(
2976 "filter.lfs.process", "git-lfs filter-process --skip"
2977 )
2978 self.config.SetBoolean(
2979 "core.bare", True if self.manifest.IsMirror else None
2980 )
Josip Sokcevic9267d582023-10-19 14:46:11 -07002981
2982 if not init_obj_dir:
2983 # The project might be shared (obj_dir already initialized), but
2984 # such information is not available here. Instead of passing it,
2985 # set it as shared, and rely to be unset down the execution
2986 # path.
2987 if git_require((2, 7, 0)):
2988 self.EnableRepositoryExtension("preciousObjects")
2989 else:
2990 self.config.SetString("gc.pruneExpire", "never")
2991
Gavin Makea2e3302023-03-11 06:46:20 +00002992 except Exception:
2993 if init_obj_dir and os.path.exists(self.objdir):
2994 platform_utils.rmtree(self.objdir)
2995 if init_git_dir and os.path.exists(self.gitdir):
2996 platform_utils.rmtree(self.gitdir)
2997 raise
2998
2999 def _UpdateHooks(self, quiet=False):
3000 if os.path.exists(self.objdir):
3001 self._InitHooks(quiet=quiet)
3002
3003 def _InitHooks(self, quiet=False):
3004 hooks = platform_utils.realpath(os.path.join(self.objdir, "hooks"))
3005 if not os.path.exists(hooks):
3006 os.makedirs(hooks)
3007
3008 # Delete sample hooks. They're noise.
3009 for hook in glob.glob(os.path.join(hooks, "*.sample")):
3010 try:
3011 platform_utils.remove(hook, missing_ok=True)
3012 except PermissionError:
3013 pass
3014
3015 for stock_hook in _ProjectHooks():
3016 name = os.path.basename(stock_hook)
3017
3018 if (
3019 name in ("commit-msg",)
3020 and not self.remote.review
3021 and self is not self.manifest.manifestProject
3022 ):
3023 # Don't install a Gerrit Code Review hook if this
3024 # project does not appear to use it for reviews.
3025 #
3026 # Since the manifest project is one of those, but also
3027 # managed through gerrit, it's excluded.
3028 continue
3029
3030 dst = os.path.join(hooks, name)
3031 if platform_utils.islink(dst):
3032 continue
3033 if os.path.exists(dst):
3034 # If the files are the same, we'll leave it alone. We create
3035 # symlinks below by default but fallback to hardlinks if the OS
3036 # blocks them. So if we're here, it's probably because we made a
3037 # hardlink below.
3038 if not filecmp.cmp(stock_hook, dst, shallow=False):
3039 if not quiet:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00003040 logger.warning(
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00003041 "warn: %s: Not replacing locally modified %s hook",
Gavin Makea2e3302023-03-11 06:46:20 +00003042 self.RelPath(local=False),
3043 name,
3044 )
3045 continue
3046 try:
3047 platform_utils.symlink(
3048 os.path.relpath(stock_hook, os.path.dirname(dst)), dst
3049 )
3050 except OSError as e:
3051 if e.errno == errno.EPERM:
3052 try:
3053 os.link(stock_hook, dst)
3054 except OSError:
Jason Chang32b59562023-07-14 16:45:35 -07003055 raise GitError(
3056 self._get_symlink_error_message(), project=self.name
3057 )
Gavin Makea2e3302023-03-11 06:46:20 +00003058 else:
3059 raise
3060
3061 def _InitRemote(self):
3062 if self.remote.url:
3063 remote = self.GetRemote()
3064 remote.url = self.remote.url
3065 remote.pushUrl = self.remote.pushUrl
3066 remote.review = self.remote.review
3067 remote.projectname = self.name
3068
3069 if self.worktree:
3070 remote.ResetFetch(mirror=False)
3071 else:
3072 remote.ResetFetch(mirror=True)
3073 remote.Save()
3074
3075 def _InitMRef(self):
3076 """Initialize the pseudo m/<manifest branch> ref."""
3077 if self.manifest.branch:
3078 if self.use_git_worktrees:
3079 # Set up the m/ space to point to the worktree-specific ref
3080 # space. We'll update the worktree-specific ref space on each
3081 # checkout.
3082 ref = R_M + self.manifest.branch
3083 if not self.bare_ref.symref(ref):
3084 self.bare_git.symbolic_ref(
3085 "-m",
3086 "redirecting to worktree scope",
3087 ref,
3088 R_WORKTREE_M + self.manifest.branch,
3089 )
3090
3091 # We can't update this ref with git worktrees until it exists.
3092 # We'll wait until the initial checkout to set it.
3093 if not os.path.exists(self.worktree):
3094 return
3095
3096 base = R_WORKTREE_M
3097 active_git = self.work_git
3098
3099 self._InitAnyMRef(HEAD, self.bare_git, detach=True)
3100 else:
3101 base = R_M
3102 active_git = self.bare_git
3103
3104 self._InitAnyMRef(base + self.manifest.branch, active_git)
3105
3106 def _InitMirrorHead(self):
3107 self._InitAnyMRef(HEAD, self.bare_git)
3108
3109 def _InitAnyMRef(self, ref, active_git, detach=False):
3110 """Initialize |ref| in |active_git| to the value in the manifest.
3111
3112 This points |ref| to the <project> setting in the manifest.
3113
3114 Args:
3115 ref: The branch to update.
3116 active_git: The git repository to make updates in.
3117 detach: Whether to update target of symbolic refs, or overwrite the
3118 ref directly (and thus make it non-symbolic).
3119 """
3120 cur = self.bare_ref.symref(ref)
3121
3122 if self.revisionId:
3123 if cur != "" or self.bare_ref.get(ref) != self.revisionId:
3124 msg = "manifest set to %s" % self.revisionId
3125 dst = self.revisionId + "^0"
3126 active_git.UpdateRef(ref, dst, message=msg, detach=True)
Julien Camperguedd654222014-01-09 16:21:37 +01003127 else:
Gavin Makea2e3302023-03-11 06:46:20 +00003128 remote = self.GetRemote()
3129 dst = remote.ToLocal(self.revisionExpr)
3130 if cur != dst:
3131 msg = "manifest set to %s" % self.revisionExpr
3132 if detach:
3133 active_git.UpdateRef(ref, dst, message=msg, detach=True)
3134 else:
3135 active_git.symbolic_ref("-m", msg, ref, dst)
Julien Camperguedd654222014-01-09 16:21:37 +01003136
Gavin Makea2e3302023-03-11 06:46:20 +00003137 def _CheckDirReference(self, srcdir, destdir):
3138 # Git worktrees don't use symlinks to share at all.
3139 if self.use_git_worktrees:
3140 return
Julien Camperguedd654222014-01-09 16:21:37 +01003141
Gavin Makea2e3302023-03-11 06:46:20 +00003142 for name in self.shareable_dirs:
3143 # Try to self-heal a bit in simple cases.
3144 dst_path = os.path.join(destdir, name)
3145 src_path = os.path.join(srcdir, name)
Julien Camperguedd654222014-01-09 16:21:37 +01003146
Gavin Makea2e3302023-03-11 06:46:20 +00003147 dst = platform_utils.realpath(dst_path)
3148 if os.path.lexists(dst):
3149 src = platform_utils.realpath(src_path)
3150 # Fail if the links are pointing to the wrong place.
3151 if src != dst:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00003152 logger.error(
3153 "error: %s is different in %s vs %s",
3154 name,
3155 destdir,
3156 srcdir,
3157 )
Gavin Makea2e3302023-03-11 06:46:20 +00003158 raise GitError(
3159 "--force-sync not enabled; cannot overwrite a local "
3160 "work tree. If you're comfortable with the "
3161 "possibility of losing the work tree's git metadata,"
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003162 " use "
3163 f"`repo sync --force-sync {self.RelPath(local=False)}` "
3164 "to proceed.",
Jason Chang32b59562023-07-14 16:45:35 -07003165 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003166 )
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003167
Gavin Makea2e3302023-03-11 06:46:20 +00003168 def _ReferenceGitDir(self, gitdir, dotgit, copy_all):
3169 """Update |dotgit| to reference |gitdir|, using symlinks where possible.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003170
Gavin Makea2e3302023-03-11 06:46:20 +00003171 Args:
3172 gitdir: The bare git repository. Must already be initialized.
3173 dotgit: The repository you would like to initialize.
3174 copy_all: If true, copy all remaining files from |gitdir| ->
3175 |dotgit|. This saves you the effort of initializing |dotgit|
3176 yourself.
3177 """
3178 symlink_dirs = self.shareable_dirs[:]
3179 to_symlink = symlink_dirs
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003180
Gavin Makea2e3302023-03-11 06:46:20 +00003181 to_copy = []
3182 if copy_all:
3183 to_copy = platform_utils.listdir(gitdir)
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003184
Gavin Makea2e3302023-03-11 06:46:20 +00003185 dotgit = platform_utils.realpath(dotgit)
3186 for name in set(to_copy).union(to_symlink):
3187 try:
3188 src = platform_utils.realpath(os.path.join(gitdir, name))
3189 dst = os.path.join(dotgit, name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003190
Gavin Makea2e3302023-03-11 06:46:20 +00003191 if os.path.lexists(dst):
3192 continue
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003193
Gavin Makea2e3302023-03-11 06:46:20 +00003194 # If the source dir doesn't exist, create an empty dir.
3195 if name in symlink_dirs and not os.path.lexists(src):
3196 os.makedirs(src)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003197
Gavin Makea2e3302023-03-11 06:46:20 +00003198 if name in to_symlink:
3199 platform_utils.symlink(
3200 os.path.relpath(src, os.path.dirname(dst)), dst
3201 )
3202 elif copy_all and not platform_utils.islink(dst):
3203 if platform_utils.isdir(src):
3204 shutil.copytree(src, dst)
3205 elif os.path.isfile(src):
3206 shutil.copy(src, dst)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003207
Gavin Makea2e3302023-03-11 06:46:20 +00003208 except OSError as e:
3209 if e.errno == errno.EPERM:
3210 raise DownloadError(self._get_symlink_error_message())
3211 else:
3212 raise
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003213
Gavin Makea2e3302023-03-11 06:46:20 +00003214 def _InitGitWorktree(self):
3215 """Init the project using git worktrees."""
3216 self.bare_git.worktree("prune")
3217 self.bare_git.worktree(
3218 "add",
3219 "-ff",
3220 "--checkout",
3221 "--detach",
3222 "--lock",
3223 self.worktree,
3224 self.GetRevisionId(),
3225 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003226
Gavin Makea2e3302023-03-11 06:46:20 +00003227 # Rewrite the internal state files to use relative paths between the
3228 # checkouts & worktrees.
3229 dotgit = os.path.join(self.worktree, ".git")
Jason R. Coombs034950b2023-10-20 23:32:02 +05453230 with open(dotgit) as fp:
Gavin Makea2e3302023-03-11 06:46:20 +00003231 # Figure out the checkout->worktree path.
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003232 setting = fp.read()
Gavin Makea2e3302023-03-11 06:46:20 +00003233 assert setting.startswith("gitdir:")
3234 git_worktree_path = setting.split(":", 1)[1].strip()
3235 # Some platforms (e.g. Windows) won't let us update dotgit in situ
3236 # because of file permissions. Delete it and recreate it from scratch
3237 # to avoid.
3238 platform_utils.remove(dotgit)
3239 # Use relative path from checkout->worktree & maintain Unix line endings
3240 # on all OS's to match git behavior.
3241 with open(dotgit, "w", newline="\n") as fp:
3242 print(
3243 "gitdir:",
3244 os.path.relpath(git_worktree_path, self.worktree),
3245 file=fp,
3246 )
3247 # Use relative path from worktree->checkout & maintain Unix line endings
3248 # on all OS's to match git behavior.
3249 with open(
3250 os.path.join(git_worktree_path, "gitdir"), "w", newline="\n"
3251 ) as fp:
3252 print(os.path.relpath(dotgit, git_worktree_path), file=fp)
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003253
Gavin Makea2e3302023-03-11 06:46:20 +00003254 self._InitMRef()
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003255
Gavin Makea2e3302023-03-11 06:46:20 +00003256 def _InitWorkTree(self, force_sync=False, submodules=False):
3257 """Setup the worktree .git path.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003258
Gavin Makea2e3302023-03-11 06:46:20 +00003259 This is the user-visible path like src/foo/.git/.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003260
Gavin Makea2e3302023-03-11 06:46:20 +00003261 With non-git-worktrees, this will be a symlink to the .repo/projects/
3262 path. With git-worktrees, this will be a .git file using "gitdir: ..."
3263 syntax.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003264
Gavin Makea2e3302023-03-11 06:46:20 +00003265 Older checkouts had .git/ directories. If we see that, migrate it.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003266
Gavin Makea2e3302023-03-11 06:46:20 +00003267 This also handles changes in the manifest. Maybe this project was
3268 backed by "foo/bar" on the server, but now it's "new/foo/bar". We have
3269 to update the path we point to under .repo/projects/ to match.
3270 """
3271 dotgit = os.path.join(self.worktree, ".git")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003272
Gavin Makea2e3302023-03-11 06:46:20 +00003273 # If using an old layout style (a directory), migrate it.
3274 if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
Jason Chang32b59562023-07-14 16:45:35 -07003275 self._MigrateOldWorkTreeGitDir(dotgit, project=self.name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003276
Gavin Makea2e3302023-03-11 06:46:20 +00003277 init_dotgit = not os.path.exists(dotgit)
3278 if self.use_git_worktrees:
3279 if init_dotgit:
3280 self._InitGitWorktree()
3281 self._CopyAndLinkFiles()
3282 else:
3283 if not init_dotgit:
3284 # See if the project has changed.
3285 if platform_utils.realpath(
3286 self.gitdir
3287 ) != platform_utils.realpath(dotgit):
3288 platform_utils.remove(dotgit)
Doug Anderson37282b42011-03-04 11:54:18 -08003289
Gavin Makea2e3302023-03-11 06:46:20 +00003290 if init_dotgit or not os.path.exists(dotgit):
3291 os.makedirs(self.worktree, exist_ok=True)
3292 platform_utils.symlink(
3293 os.path.relpath(self.gitdir, self.worktree), dotgit
3294 )
Doug Anderson37282b42011-03-04 11:54:18 -08003295
Gavin Makea2e3302023-03-11 06:46:20 +00003296 if init_dotgit:
3297 _lwrite(
3298 os.path.join(dotgit, HEAD), "%s\n" % self.GetRevisionId()
3299 )
Doug Anderson37282b42011-03-04 11:54:18 -08003300
Gavin Makea2e3302023-03-11 06:46:20 +00003301 # Finish checking out the worktree.
3302 cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
3303 if GitCommand(self, cmd).Wait() != 0:
3304 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07003305 "Cannot initialize work tree for " + self.name,
3306 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003307 )
Doug Anderson37282b42011-03-04 11:54:18 -08003308
Gavin Makea2e3302023-03-11 06:46:20 +00003309 if submodules:
3310 self._SyncSubmodules(quiet=True)
3311 self._CopyAndLinkFiles()
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003312
Gavin Makea2e3302023-03-11 06:46:20 +00003313 @classmethod
Jason Chang32b59562023-07-14 16:45:35 -07003314 def _MigrateOldWorkTreeGitDir(cls, dotgit, project=None):
Gavin Makea2e3302023-03-11 06:46:20 +00003315 """Migrate the old worktree .git/ dir style to a symlink.
3316
3317 This logic specifically only uses state from |dotgit| to figure out
3318 where to move content and not |self|. This way if the backing project
3319 also changed places, we only do the .git/ dir to .git symlink migration
3320 here. The path updates will happen independently.
3321 """
3322 # Figure out where in .repo/projects/ it's pointing to.
3323 if not os.path.islink(os.path.join(dotgit, "refs")):
Jason Chang32b59562023-07-14 16:45:35 -07003324 raise GitError(
3325 f"{dotgit}: unsupported checkout state", project=project
3326 )
Gavin Makea2e3302023-03-11 06:46:20 +00003327 gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, "refs")))
3328
3329 # Remove known symlink paths that exist in .repo/projects/.
3330 KNOWN_LINKS = {
3331 "config",
3332 "description",
3333 "hooks",
3334 "info",
3335 "logs",
3336 "objects",
3337 "packed-refs",
3338 "refs",
3339 "rr-cache",
3340 "shallow",
3341 "svn",
3342 }
3343 # Paths that we know will be in both, but are safe to clobber in
3344 # .repo/projects/.
3345 SAFE_TO_CLOBBER = {
3346 "COMMIT_EDITMSG",
3347 "FETCH_HEAD",
3348 "HEAD",
3349 "gc.log",
3350 "gitk.cache",
3351 "index",
3352 "ORIG_HEAD",
3353 }
3354
3355 # First see if we'd succeed before starting the migration.
3356 unknown_paths = []
3357 for name in platform_utils.listdir(dotgit):
3358 # Ignore all temporary/backup names. These are common with vim &
3359 # emacs.
3360 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3361 continue
3362
3363 dotgit_path = os.path.join(dotgit, name)
3364 if name in KNOWN_LINKS:
3365 if not platform_utils.islink(dotgit_path):
3366 unknown_paths.append(f"{dotgit_path}: should be a symlink")
3367 else:
3368 gitdir_path = os.path.join(gitdir, name)
3369 if name not in SAFE_TO_CLOBBER and os.path.exists(gitdir_path):
3370 unknown_paths.append(
3371 f"{dotgit_path}: unknown file; please file a bug"
3372 )
3373 if unknown_paths:
Jason Chang32b59562023-07-14 16:45:35 -07003374 raise GitError(
3375 "Aborting migration: " + "\n".join(unknown_paths),
3376 project=project,
3377 )
Gavin Makea2e3302023-03-11 06:46:20 +00003378
3379 # Now walk the paths and sync the .git/ to .repo/projects/.
3380 for name in platform_utils.listdir(dotgit):
3381 dotgit_path = os.path.join(dotgit, name)
3382
3383 # Ignore all temporary/backup names. These are common with vim &
3384 # emacs.
3385 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3386 platform_utils.remove(dotgit_path)
3387 elif name in KNOWN_LINKS:
3388 platform_utils.remove(dotgit_path)
3389 else:
3390 gitdir_path = os.path.join(gitdir, name)
3391 platform_utils.remove(gitdir_path, missing_ok=True)
3392 platform_utils.rename(dotgit_path, gitdir_path)
3393
3394 # Now that the dir should be empty, clear it out, and symlink it over.
3395 platform_utils.rmdir(dotgit)
3396 platform_utils.symlink(
Jason R. Coombs47944bb2023-09-29 12:42:22 -04003397 os.path.relpath(gitdir, os.path.dirname(os.path.realpath(dotgit))),
3398 dotgit,
Gavin Makea2e3302023-03-11 06:46:20 +00003399 )
3400
3401 def _get_symlink_error_message(self):
3402 if platform_utils.isWindows():
3403 return (
3404 "Unable to create symbolic link. Please re-run the command as "
3405 "Administrator, or see "
3406 "https://github.com/git-for-windows/git/wiki/Symbolic-Links "
3407 "for other options."
3408 )
3409 return "filesystem must support symlinks"
3410
3411 def _revlist(self, *args, **kw):
3412 a = []
3413 a.extend(args)
3414 a.append("--")
3415 return self.work_git.rev_list(*a, **kw)
3416
3417 @property
3418 def _allrefs(self):
3419 return self.bare_ref.all
3420
3421 def _getLogs(
3422 self, rev1, rev2, oneline=False, color=True, pretty_format=None
3423 ):
3424 """Get logs between two revisions of this project."""
3425 comp = ".."
3426 if rev1:
3427 revs = [rev1]
3428 if rev2:
3429 revs.extend([comp, rev2])
3430 cmd = ["log", "".join(revs)]
3431 out = DiffColoring(self.config)
3432 if out.is_on and color:
3433 cmd.append("--color")
3434 if pretty_format is not None:
3435 cmd.append("--pretty=format:%s" % pretty_format)
3436 if oneline:
3437 cmd.append("--oneline")
3438
3439 try:
3440 log = GitCommand(
3441 self, cmd, capture_stdout=True, capture_stderr=True
3442 )
3443 if log.Wait() == 0:
3444 return log.stdout
3445 except GitError:
3446 # worktree may not exist if groups changed for example. In that
3447 # case, try in gitdir instead.
3448 if not os.path.exists(self.worktree):
3449 return self.bare_git.log(*cmd[1:])
3450 else:
3451 raise
3452 return None
3453
3454 def getAddedAndRemovedLogs(
3455 self, toProject, oneline=False, color=True, pretty_format=None
3456 ):
3457 """Get the list of logs from this revision to given revisionId"""
3458 logs = {}
3459 selfId = self.GetRevisionId(self._allrefs)
3460 toId = toProject.GetRevisionId(toProject._allrefs)
3461
3462 logs["added"] = self._getLogs(
3463 selfId,
3464 toId,
3465 oneline=oneline,
3466 color=color,
3467 pretty_format=pretty_format,
3468 )
3469 logs["removed"] = self._getLogs(
3470 toId,
3471 selfId,
3472 oneline=oneline,
3473 color=color,
3474 pretty_format=pretty_format,
3475 )
3476 return logs
3477
Mike Frysingerd4aee652023-10-19 05:13:32 -04003478 class _GitGetByExec:
Gavin Makea2e3302023-03-11 06:46:20 +00003479 def __init__(self, project, bare, gitdir):
3480 self._project = project
3481 self._bare = bare
3482 self._gitdir = gitdir
3483
3484 # __getstate__ and __setstate__ are required for pickling because
3485 # __getattr__ exists.
3486 def __getstate__(self):
3487 return (self._project, self._bare, self._gitdir)
3488
3489 def __setstate__(self, state):
3490 self._project, self._bare, self._gitdir = state
3491
3492 def LsOthers(self):
3493 p = GitCommand(
3494 self._project,
3495 ["ls-files", "-z", "--others", "--exclude-standard"],
3496 bare=False,
3497 gitdir=self._gitdir,
3498 capture_stdout=True,
3499 capture_stderr=True,
3500 )
3501 if p.Wait() == 0:
3502 out = p.stdout
3503 if out:
3504 # Backslash is not anomalous.
3505 return out[:-1].split("\0")
3506 return []
3507
3508 def DiffZ(self, name, *args):
3509 cmd = [name]
3510 cmd.append("-z")
3511 cmd.append("--ignore-submodules")
3512 cmd.extend(args)
3513 p = GitCommand(
3514 self._project,
3515 cmd,
3516 gitdir=self._gitdir,
3517 bare=False,
3518 capture_stdout=True,
3519 capture_stderr=True,
3520 )
3521 p.Wait()
3522 r = {}
3523 out = p.stdout
3524 if out:
3525 out = iter(out[:-1].split("\0"))
3526 while out:
3527 try:
3528 info = next(out)
3529 path = next(out)
3530 except StopIteration:
3531 break
3532
Mike Frysingerd4aee652023-10-19 05:13:32 -04003533 class _Info:
Gavin Makea2e3302023-03-11 06:46:20 +00003534 def __init__(self, path, omode, nmode, oid, nid, state):
3535 self.path = path
3536 self.src_path = None
3537 self.old_mode = omode
3538 self.new_mode = nmode
3539 self.old_id = oid
3540 self.new_id = nid
3541
3542 if len(state) == 1:
3543 self.status = state
3544 self.level = None
3545 else:
3546 self.status = state[:1]
3547 self.level = state[1:]
3548 while self.level.startswith("0"):
3549 self.level = self.level[1:]
3550
3551 info = info[1:].split(" ")
3552 info = _Info(path, *info)
3553 if info.status in ("R", "C"):
3554 info.src_path = info.path
3555 info.path = next(out)
3556 r[info.path] = info
3557 return r
3558
3559 def GetDotgitPath(self, subpath=None):
3560 """Return the full path to the .git dir.
3561
3562 As a convenience, append |subpath| if provided.
3563 """
3564 if self._bare:
3565 dotgit = self._gitdir
3566 else:
3567 dotgit = os.path.join(self._project.worktree, ".git")
3568 if os.path.isfile(dotgit):
3569 # Git worktrees use a "gitdir:" syntax to point to the
3570 # scratch space.
3571 with open(dotgit) as fp:
3572 setting = fp.read()
3573 assert setting.startswith("gitdir:")
3574 gitdir = setting.split(":", 1)[1].strip()
3575 dotgit = os.path.normpath(
3576 os.path.join(self._project.worktree, gitdir)
3577 )
3578
3579 return dotgit if subpath is None else os.path.join(dotgit, subpath)
3580
3581 def GetHead(self):
3582 """Return the ref that HEAD points to."""
3583 path = self.GetDotgitPath(subpath=HEAD)
3584 try:
3585 with open(path) as fd:
3586 line = fd.readline()
Jason R. Coombsae824fb2023-10-20 23:32:40 +05453587 except OSError as e:
Gavin Makea2e3302023-03-11 06:46:20 +00003588 raise NoManifestException(path, str(e))
3589 try:
3590 line = line.decode()
3591 except AttributeError:
3592 pass
3593 if line.startswith("ref: "):
3594 return line[5:-1]
3595 return line[:-1]
3596
3597 def SetHead(self, ref, message=None):
3598 cmdv = []
3599 if message is not None:
3600 cmdv.extend(["-m", message])
3601 cmdv.append(HEAD)
3602 cmdv.append(ref)
3603 self.symbolic_ref(*cmdv)
3604
3605 def DetachHead(self, new, message=None):
3606 cmdv = ["--no-deref"]
3607 if message is not None:
3608 cmdv.extend(["-m", message])
3609 cmdv.append(HEAD)
3610 cmdv.append(new)
3611 self.update_ref(*cmdv)
3612
3613 def UpdateRef(self, name, new, old=None, message=None, detach=False):
3614 cmdv = []
3615 if message is not None:
3616 cmdv.extend(["-m", message])
3617 if detach:
3618 cmdv.append("--no-deref")
3619 cmdv.append(name)
3620 cmdv.append(new)
3621 if old is not None:
3622 cmdv.append(old)
3623 self.update_ref(*cmdv)
3624
3625 def DeleteRef(self, name, old=None):
3626 if not old:
3627 old = self.rev_parse(name)
3628 self.update_ref("-d", name, old)
3629 self._project.bare_ref.deleted(name)
3630
Jason Chang87058c62023-09-27 11:34:43 -07003631 def rev_list(self, *args, log_as_error=True, **kw):
Gavin Makea2e3302023-03-11 06:46:20 +00003632 if "format" in kw:
3633 cmdv = ["log", "--pretty=format:%s" % kw["format"]]
3634 else:
3635 cmdv = ["rev-list"]
3636 cmdv.extend(args)
3637 p = GitCommand(
3638 self._project,
3639 cmdv,
3640 bare=self._bare,
3641 gitdir=self._gitdir,
3642 capture_stdout=True,
3643 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003644 verify_command=True,
Jason Chang87058c62023-09-27 11:34:43 -07003645 log_as_error=log_as_error,
Gavin Makea2e3302023-03-11 06:46:20 +00003646 )
Jason Chang32b59562023-07-14 16:45:35 -07003647 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003648 return p.stdout.splitlines()
3649
3650 def __getattr__(self, name):
3651 """Allow arbitrary git commands using pythonic syntax.
3652
3653 This allows you to do things like:
3654 git_obj.rev_parse('HEAD')
3655
3656 Since we don't have a 'rev_parse' method defined, the __getattr__
3657 will run. We'll replace the '_' with a '-' and try to run a git
3658 command. Any other positional arguments will be passed to the git
3659 command, and the following keyword arguments are supported:
3660 config: An optional dict of git config options to be passed with
3661 '-c'.
3662
3663 Args:
3664 name: The name of the git command to call. Any '_' characters
3665 will be replaced with '-'.
3666
3667 Returns:
3668 A callable object that will try to call git with the named
3669 command.
3670 """
3671 name = name.replace("_", "-")
3672
Jason Chang87058c62023-09-27 11:34:43 -07003673 def runner(*args, log_as_error=True, **kwargs):
Gavin Makea2e3302023-03-11 06:46:20 +00003674 cmdv = []
3675 config = kwargs.pop("config", None)
3676 for k in kwargs:
3677 raise TypeError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003678 f"{name}() got an unexpected keyword argument {k!r}"
Gavin Makea2e3302023-03-11 06:46:20 +00003679 )
3680 if config is not None:
3681 for k, v in config.items():
3682 cmdv.append("-c")
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003683 cmdv.append(f"{k}={v}")
Gavin Makea2e3302023-03-11 06:46:20 +00003684 cmdv.append(name)
3685 cmdv.extend(args)
3686 p = GitCommand(
3687 self._project,
3688 cmdv,
3689 bare=self._bare,
3690 gitdir=self._gitdir,
3691 capture_stdout=True,
3692 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003693 verify_command=True,
Jason Chang87058c62023-09-27 11:34:43 -07003694 log_as_error=log_as_error,
Gavin Makea2e3302023-03-11 06:46:20 +00003695 )
Jason Chang32b59562023-07-14 16:45:35 -07003696 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003697 r = p.stdout
3698 if r.endswith("\n") and r.index("\n") == len(r) - 1:
3699 return r[:-1]
3700 return r
3701
3702 return runner
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003703
3704
Jason Chang32b59562023-07-14 16:45:35 -07003705class LocalSyncFail(RepoError):
3706 """Default error when there is an Sync_LocalHalf error."""
3707
3708
3709class _PriorSyncFailedError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003710 def __str__(self):
3711 return "prior sync failed; rebase still in progress"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003712
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003713
Jason Chang32b59562023-07-14 16:45:35 -07003714class _DirtyError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003715 def __str__(self):
3716 return "contains uncommitted changes"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003717
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003718
Mike Frysingerd4aee652023-10-19 05:13:32 -04003719class _InfoMessage:
Gavin Makea2e3302023-03-11 06:46:20 +00003720 def __init__(self, project, text):
3721 self.project = project
3722 self.text = text
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003723
Gavin Makea2e3302023-03-11 06:46:20 +00003724 def Print(self, syncbuf):
3725 syncbuf.out.info(
3726 "%s/: %s", self.project.RelPath(local=False), self.text
3727 )
3728 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003729
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003730
Mike Frysingerd4aee652023-10-19 05:13:32 -04003731class _Failure:
Gavin Makea2e3302023-03-11 06:46:20 +00003732 def __init__(self, project, why):
3733 self.project = project
3734 self.why = why
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003735
Gavin Makea2e3302023-03-11 06:46:20 +00003736 def Print(self, syncbuf):
3737 syncbuf.out.fail(
3738 "error: %s/: %s", self.project.RelPath(local=False), str(self.why)
3739 )
3740 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003741
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003742
Mike Frysingerd4aee652023-10-19 05:13:32 -04003743class _Later:
Gavin Makea2e3302023-03-11 06:46:20 +00003744 def __init__(self, project, action):
3745 self.project = project
3746 self.action = action
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003747
Gavin Makea2e3302023-03-11 06:46:20 +00003748 def Run(self, syncbuf):
3749 out = syncbuf.out
3750 out.project("project %s/", self.project.RelPath(local=False))
3751 out.nl()
3752 try:
3753 self.action()
3754 out.nl()
3755 return True
3756 except GitError:
3757 out.nl()
3758 return False
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003759
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003760
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003761class _SyncColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +00003762 def __init__(self, config):
3763 super().__init__(config, "reposync")
3764 self.project = self.printer("header", attr="bold")
3765 self.info = self.printer("info")
3766 self.fail = self.printer("fail", fg="red")
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003767
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003768
Mike Frysingerd4aee652023-10-19 05:13:32 -04003769class SyncBuffer:
Gavin Makea2e3302023-03-11 06:46:20 +00003770 def __init__(self, config, detach_head=False):
3771 self._messages = []
3772 self._failures = []
3773 self._later_queue1 = []
3774 self._later_queue2 = []
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003775
Gavin Makea2e3302023-03-11 06:46:20 +00003776 self.out = _SyncColoring(config)
3777 self.out.redirect(sys.stderr)
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003778
Gavin Makea2e3302023-03-11 06:46:20 +00003779 self.detach_head = detach_head
3780 self.clean = True
3781 self.recent_clean = True
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003782
Gavin Makea2e3302023-03-11 06:46:20 +00003783 def info(self, project, fmt, *args):
3784 self._messages.append(_InfoMessage(project, fmt % args))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003785
Gavin Makea2e3302023-03-11 06:46:20 +00003786 def fail(self, project, err=None):
3787 self._failures.append(_Failure(project, err))
David Rileye0684ad2017-04-05 00:02:59 -07003788 self._MarkUnclean()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003789
Gavin Makea2e3302023-03-11 06:46:20 +00003790 def later1(self, project, what):
3791 self._later_queue1.append(_Later(project, what))
Mike Frysinger70d861f2019-08-26 15:22:36 -04003792
Gavin Makea2e3302023-03-11 06:46:20 +00003793 def later2(self, project, what):
3794 self._later_queue2.append(_Later(project, what))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003795
Gavin Makea2e3302023-03-11 06:46:20 +00003796 def Finish(self):
3797 self._PrintMessages()
3798 self._RunLater()
3799 self._PrintMessages()
3800 return self.clean
3801
3802 def Recently(self):
3803 recent_clean = self.recent_clean
3804 self.recent_clean = True
3805 return recent_clean
3806
3807 def _MarkUnclean(self):
3808 self.clean = False
3809 self.recent_clean = False
3810
3811 def _RunLater(self):
3812 for q in ["_later_queue1", "_later_queue2"]:
3813 if not self._RunQueue(q):
3814 return
3815
3816 def _RunQueue(self, queue):
3817 for m in getattr(self, queue):
3818 if not m.Run(self):
3819 self._MarkUnclean()
3820 return False
3821 setattr(self, queue, [])
3822 return True
3823
3824 def _PrintMessages(self):
3825 if self._messages or self._failures:
3826 if os.isatty(2):
3827 self.out.write(progress.CSI_ERASE_LINE)
3828 self.out.write("\r")
3829
3830 for m in self._messages:
3831 m.Print(self)
3832 for m in self._failures:
3833 m.Print(self)
3834
3835 self._messages = []
3836 self._failures = []
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003837
3838
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003839class MetaProject(Project):
Gavin Makea2e3302023-03-11 06:46:20 +00003840 """A special project housed under .repo."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003841
Gavin Makea2e3302023-03-11 06:46:20 +00003842 def __init__(self, manifest, name, gitdir, worktree):
3843 Project.__init__(
3844 self,
3845 manifest=manifest,
3846 name=name,
3847 gitdir=gitdir,
3848 objdir=gitdir,
3849 worktree=worktree,
3850 remote=RemoteSpec("origin"),
3851 relpath=".repo/%s" % name,
3852 revisionExpr="refs/heads/master",
3853 revisionId=None,
3854 groups=None,
3855 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003856
Gavin Makea2e3302023-03-11 06:46:20 +00003857 def PreSync(self):
3858 if self.Exists:
3859 cb = self.CurrentBranch
3860 if cb:
3861 base = self.GetBranch(cb).merge
3862 if base:
3863 self.revisionExpr = base
3864 self.revisionId = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003865
Gavin Makea2e3302023-03-11 06:46:20 +00003866 @property
3867 def HasChanges(self):
3868 """Has the remote received new commits not yet checked out?"""
3869 if not self.remote or not self.revisionExpr:
3870 return False
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003871
Gavin Makea2e3302023-03-11 06:46:20 +00003872 all_refs = self.bare_ref.all
3873 revid = self.GetRevisionId(all_refs)
3874 head = self.work_git.GetHead()
3875 if head.startswith(R_HEADS):
3876 try:
3877 head = all_refs[head]
3878 except KeyError:
3879 head = None
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003880
Gavin Makea2e3302023-03-11 06:46:20 +00003881 if revid == head:
3882 return False
3883 elif self._revlist(not_rev(HEAD), revid):
3884 return True
3885 return False
LaMont Jones9b72cf22022-03-29 21:54:22 +00003886
3887
3888class RepoProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003889 """The MetaProject for repo itself."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003890
Gavin Makea2e3302023-03-11 06:46:20 +00003891 @property
3892 def LastFetch(self):
3893 try:
3894 fh = os.path.join(self.gitdir, "FETCH_HEAD")
3895 return os.path.getmtime(fh)
3896 except OSError:
3897 return 0
LaMont Jones9b72cf22022-03-29 21:54:22 +00003898
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +01003899
LaMont Jones9b72cf22022-03-29 21:54:22 +00003900class ManifestProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003901 """The MetaProject for manifests."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003902
Gavin Makea2e3302023-03-11 06:46:20 +00003903 def MetaBranchSwitch(self, submodules=False):
3904 """Prepare for manifest branch switch."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003905
Gavin Makea2e3302023-03-11 06:46:20 +00003906 # detach and delete manifest branch, allowing a new
3907 # branch to take over
3908 syncbuf = SyncBuffer(self.config, detach_head=True)
3909 self.Sync_LocalHalf(syncbuf, submodules=submodules)
3910 syncbuf.Finish()
LaMont Jones9b72cf22022-03-29 21:54:22 +00003911
Gavin Makea2e3302023-03-11 06:46:20 +00003912 return (
3913 GitCommand(
3914 self,
3915 ["update-ref", "-d", "refs/heads/default"],
3916 capture_stdout=True,
3917 capture_stderr=True,
3918 ).Wait()
3919 == 0
3920 )
LaMont Jones9b03f152022-03-29 23:01:18 +00003921
Gavin Makea2e3302023-03-11 06:46:20 +00003922 @property
3923 def standalone_manifest_url(self):
3924 """The URL of the standalone manifest, or None."""
3925 return self.config.GetString("manifest.standalone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003926
Gavin Makea2e3302023-03-11 06:46:20 +00003927 @property
3928 def manifest_groups(self):
3929 """The manifest groups string."""
3930 return self.config.GetString("manifest.groups")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003931
Gavin Makea2e3302023-03-11 06:46:20 +00003932 @property
3933 def reference(self):
3934 """The --reference for this manifest."""
3935 return self.config.GetString("repo.reference")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003936
Gavin Makea2e3302023-03-11 06:46:20 +00003937 @property
3938 def dissociate(self):
3939 """Whether to dissociate."""
3940 return self.config.GetBoolean("repo.dissociate")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003941
Gavin Makea2e3302023-03-11 06:46:20 +00003942 @property
3943 def archive(self):
3944 """Whether we use archive."""
3945 return self.config.GetBoolean("repo.archive")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003946
Gavin Makea2e3302023-03-11 06:46:20 +00003947 @property
3948 def mirror(self):
3949 """Whether we use mirror."""
3950 return self.config.GetBoolean("repo.mirror")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003951
Gavin Makea2e3302023-03-11 06:46:20 +00003952 @property
3953 def use_worktree(self):
3954 """Whether we use worktree."""
3955 return self.config.GetBoolean("repo.worktree")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003956
Gavin Makea2e3302023-03-11 06:46:20 +00003957 @property
3958 def clone_bundle(self):
3959 """Whether we use clone_bundle."""
3960 return self.config.GetBoolean("repo.clonebundle")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003961
Gavin Makea2e3302023-03-11 06:46:20 +00003962 @property
3963 def submodules(self):
3964 """Whether we use submodules."""
3965 return self.config.GetBoolean("repo.submodules")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003966
Gavin Makea2e3302023-03-11 06:46:20 +00003967 @property
3968 def git_lfs(self):
3969 """Whether we use git_lfs."""
3970 return self.config.GetBoolean("repo.git-lfs")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003971
Gavin Makea2e3302023-03-11 06:46:20 +00003972 @property
3973 def use_superproject(self):
3974 """Whether we use superproject."""
3975 return self.config.GetBoolean("repo.superproject")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003976
Gavin Makea2e3302023-03-11 06:46:20 +00003977 @property
3978 def partial_clone(self):
3979 """Whether this is a partial clone."""
3980 return self.config.GetBoolean("repo.partialclone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003981
Gavin Makea2e3302023-03-11 06:46:20 +00003982 @property
3983 def depth(self):
3984 """Partial clone depth."""
Roberto Vladimir Prado Carranza3d58d212023-09-13 10:27:26 +02003985 return self.config.GetInt("repo.depth")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003986
Gavin Makea2e3302023-03-11 06:46:20 +00003987 @property
3988 def clone_filter(self):
3989 """The clone filter."""
3990 return self.config.GetString("repo.clonefilter")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003991
Gavin Makea2e3302023-03-11 06:46:20 +00003992 @property
3993 def partial_clone_exclude(self):
3994 """Partial clone exclude string"""
3995 return self.config.GetString("repo.partialcloneexclude")
LaMont Jones4ada0432022-04-14 15:10:43 +00003996
Gavin Makea2e3302023-03-11 06:46:20 +00003997 @property
Jason Chang17833322023-05-23 13:06:55 -07003998 def clone_filter_for_depth(self):
3999 """Replace shallow clone with partial clone."""
4000 return self.config.GetString("repo.clonefilterfordepth")
4001
4002 @property
Gavin Makea2e3302023-03-11 06:46:20 +00004003 def manifest_platform(self):
4004 """The --platform argument from `repo init`."""
4005 return self.config.GetString("manifest.platform")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004006
Gavin Makea2e3302023-03-11 06:46:20 +00004007 @property
4008 def _platform_name(self):
4009 """Return the name of the platform."""
4010 return platform.system().lower()
LaMont Jones9b03f152022-03-29 23:01:18 +00004011
Gavin Makea2e3302023-03-11 06:46:20 +00004012 def SyncWithPossibleInit(
4013 self,
4014 submanifest,
4015 verbose=False,
4016 current_branch_only=False,
4017 tags="",
4018 git_event_log=None,
4019 ):
4020 """Sync a manifestProject, possibly for the first time.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004021
Gavin Makea2e3302023-03-11 06:46:20 +00004022 Call Sync() with arguments from the most recent `repo init`. If this is
4023 a new sub manifest, then inherit options from the parent's
4024 manifestProject.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004025
Gavin Makea2e3302023-03-11 06:46:20 +00004026 This is used by subcmds.Sync() to do an initial download of new sub
4027 manifests.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004028
Gavin Makea2e3302023-03-11 06:46:20 +00004029 Args:
4030 submanifest: an XmlSubmanifest, the submanifest to re-sync.
4031 verbose: a boolean, whether to show all output, rather than only
4032 errors.
4033 current_branch_only: a boolean, whether to only fetch the current
4034 manifest branch from the server.
4035 tags: a boolean, whether to fetch tags.
4036 git_event_log: an EventLog, for git tracing.
4037 """
4038 # TODO(lamontjones): when refactoring sync (and init?) consider how to
4039 # better get the init options that we should use for new submanifests
4040 # that are added when syncing an existing workspace.
4041 git_event_log = git_event_log or EventLog()
LaMont Jonesb90a4222022-04-14 15:00:09 +00004042 spec = submanifest.ToSubmanifestSpec()
Gavin Makea2e3302023-03-11 06:46:20 +00004043 # Use the init options from the existing manifestProject, or the parent
4044 # if it doesn't exist.
4045 #
4046 # Today, we only support changing manifest_groups on the sub-manifest,
4047 # with no supported-for-the-user way to change the other arguments from
4048 # those specified by the outermost manifest.
4049 #
4050 # TODO(lamontjones): determine which of these should come from the
4051 # outermost manifest and which should come from the parent manifest.
4052 mp = self if self.Exists else submanifest.parent.manifestProject
4053 return self.Sync(
LaMont Jones55ee3042022-04-06 17:10:21 +00004054 manifest_url=spec.manifestUrl,
4055 manifest_branch=spec.revision,
Gavin Makea2e3302023-03-11 06:46:20 +00004056 standalone_manifest=mp.standalone_manifest_url,
4057 groups=mp.manifest_groups,
4058 platform=mp.manifest_platform,
4059 mirror=mp.mirror,
4060 dissociate=mp.dissociate,
4061 reference=mp.reference,
4062 worktree=mp.use_worktree,
4063 submodules=mp.submodules,
4064 archive=mp.archive,
4065 partial_clone=mp.partial_clone,
4066 clone_filter=mp.clone_filter,
4067 partial_clone_exclude=mp.partial_clone_exclude,
4068 clone_bundle=mp.clone_bundle,
4069 git_lfs=mp.git_lfs,
4070 use_superproject=mp.use_superproject,
LaMont Jones55ee3042022-04-06 17:10:21 +00004071 verbose=verbose,
4072 current_branch_only=current_branch_only,
4073 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00004074 depth=mp.depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004075 git_event_log=git_event_log,
4076 manifest_name=spec.manifestName,
Gavin Makea2e3302023-03-11 06:46:20 +00004077 this_manifest_only=True,
LaMont Jones55ee3042022-04-06 17:10:21 +00004078 outer_manifest=False,
Jason Chang17833322023-05-23 13:06:55 -07004079 clone_filter_for_depth=mp.clone_filter_for_depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004080 )
LaMont Jones409407a2022-04-05 21:21:56 +00004081
Gavin Makea2e3302023-03-11 06:46:20 +00004082 def Sync(
4083 self,
4084 _kwargs_only=(),
4085 manifest_url="",
4086 manifest_branch=None,
4087 standalone_manifest=False,
4088 groups="",
4089 mirror=False,
4090 reference="",
4091 dissociate=False,
4092 worktree=False,
4093 submodules=False,
4094 archive=False,
4095 partial_clone=None,
4096 depth=None,
4097 clone_filter="blob:none",
4098 partial_clone_exclude=None,
4099 clone_bundle=None,
4100 git_lfs=None,
4101 use_superproject=None,
4102 verbose=False,
4103 current_branch_only=False,
4104 git_event_log=None,
4105 platform="",
4106 manifest_name="default.xml",
4107 tags="",
4108 this_manifest_only=False,
4109 outer_manifest=True,
Jason Chang17833322023-05-23 13:06:55 -07004110 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00004111 ):
4112 """Sync the manifest and all submanifests.
LaMont Jones409407a2022-04-05 21:21:56 +00004113
Gavin Makea2e3302023-03-11 06:46:20 +00004114 Args:
4115 manifest_url: a string, the URL of the manifest project.
4116 manifest_branch: a string, the manifest branch to use.
4117 standalone_manifest: a boolean, whether to store the manifest as a
4118 static file.
4119 groups: a string, restricts the checkout to projects with the
4120 specified groups.
4121 mirror: a boolean, whether to create a mirror of the remote
4122 repository.
4123 reference: a string, location of a repo instance to use as a
4124 reference.
4125 dissociate: a boolean, whether to dissociate from reference mirrors
4126 after clone.
4127 worktree: a boolean, whether to use git-worktree to manage projects.
4128 submodules: a boolean, whether sync submodules associated with the
4129 manifest project.
4130 archive: a boolean, whether to checkout each project as an archive.
4131 See git-archive.
4132 partial_clone: a boolean, whether to perform a partial clone.
4133 depth: an int, how deep of a shallow clone to create.
4134 clone_filter: a string, filter to use with partial_clone.
4135 partial_clone_exclude : a string, comma-delimeted list of project
4136 names to exclude from partial clone.
4137 clone_bundle: a boolean, whether to enable /clone.bundle on
4138 HTTP/HTTPS.
4139 git_lfs: a boolean, whether to enable git LFS support.
4140 use_superproject: a boolean, whether to use the manifest
4141 superproject to sync projects.
4142 verbose: a boolean, whether to show all output, rather than only
4143 errors.
4144 current_branch_only: a boolean, whether to only fetch the current
4145 manifest branch from the server.
4146 platform: a string, restrict the checkout to projects with the
4147 specified platform group.
4148 git_event_log: an EventLog, for git tracing.
4149 tags: a boolean, whether to fetch tags.
4150 manifest_name: a string, the name of the manifest file to use.
4151 this_manifest_only: a boolean, whether to only operate on the
4152 current sub manifest.
4153 outer_manifest: a boolean, whether to start at the outermost
4154 manifest.
Jason Chang17833322023-05-23 13:06:55 -07004155 clone_filter_for_depth: a string, when specified replaces shallow
4156 clones with partial.
LaMont Jones9b03f152022-03-29 23:01:18 +00004157
Gavin Makea2e3302023-03-11 06:46:20 +00004158 Returns:
4159 a boolean, whether the sync was successful.
4160 """
4161 assert _kwargs_only == (), "Sync only accepts keyword arguments."
LaMont Jones9b03f152022-03-29 23:01:18 +00004162
Gavin Makea2e3302023-03-11 06:46:20 +00004163 groups = groups or self.manifest.GetDefaultGroupsStr(
4164 with_platform=False
4165 )
4166 platform = platform or "auto"
4167 git_event_log = git_event_log or EventLog()
4168 if outer_manifest and self.manifest.is_submanifest:
4169 # In a multi-manifest checkout, use the outer manifest unless we are
4170 # told not to.
4171 return self.client.outer_manifest.manifestProject.Sync(
4172 manifest_url=manifest_url,
4173 manifest_branch=manifest_branch,
4174 standalone_manifest=standalone_manifest,
4175 groups=groups,
4176 platform=platform,
4177 mirror=mirror,
4178 dissociate=dissociate,
4179 reference=reference,
4180 worktree=worktree,
4181 submodules=submodules,
4182 archive=archive,
4183 partial_clone=partial_clone,
4184 clone_filter=clone_filter,
4185 partial_clone_exclude=partial_clone_exclude,
4186 clone_bundle=clone_bundle,
4187 git_lfs=git_lfs,
4188 use_superproject=use_superproject,
4189 verbose=verbose,
4190 current_branch_only=current_branch_only,
4191 tags=tags,
4192 depth=depth,
4193 git_event_log=git_event_log,
4194 manifest_name=manifest_name,
4195 this_manifest_only=this_manifest_only,
4196 outer_manifest=False,
4197 )
LaMont Jones9b03f152022-03-29 23:01:18 +00004198
Gavin Makea2e3302023-03-11 06:46:20 +00004199 # If repo has already been initialized, we take -u with the absence of
4200 # --standalone-manifest to mean "transition to a standard repo set up",
4201 # which necessitates starting fresh.
4202 # If --standalone-manifest is set, we always tear everything down and
4203 # start anew.
4204 if self.Exists:
4205 was_standalone_manifest = self.config.GetString(
4206 "manifest.standalone"
4207 )
4208 if was_standalone_manifest and not manifest_url:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004209 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004210 "fatal: repo was initialized with a standlone manifest, "
4211 "cannot be re-initialized without --manifest-url/-u"
4212 )
4213 return False
4214
4215 if standalone_manifest or (
4216 was_standalone_manifest and manifest_url
4217 ):
4218 self.config.ClearCache()
4219 if self.gitdir and os.path.exists(self.gitdir):
4220 platform_utils.rmtree(self.gitdir)
4221 if self.worktree and os.path.exists(self.worktree):
4222 platform_utils.rmtree(self.worktree)
4223
4224 is_new = not self.Exists
4225 if is_new:
4226 if not manifest_url:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004227 logger.error("fatal: manifest url is required.")
Gavin Makea2e3302023-03-11 06:46:20 +00004228 return False
4229
4230 if verbose:
4231 print(
4232 "Downloading manifest from %s"
4233 % (GitConfig.ForUser().UrlInsteadOf(manifest_url),),
4234 file=sys.stderr,
4235 )
4236
4237 # The manifest project object doesn't keep track of the path on the
4238 # server where this git is located, so let's save that here.
4239 mirrored_manifest_git = None
4240 if reference:
4241 manifest_git_path = urllib.parse.urlparse(manifest_url).path[1:]
4242 mirrored_manifest_git = os.path.join(
4243 reference, manifest_git_path
4244 )
4245 if not mirrored_manifest_git.endswith(".git"):
4246 mirrored_manifest_git += ".git"
4247 if not os.path.exists(mirrored_manifest_git):
4248 mirrored_manifest_git = os.path.join(
4249 reference, ".repo/manifests.git"
4250 )
4251
4252 self._InitGitDir(mirror_git=mirrored_manifest_git)
4253
4254 # If standalone_manifest is set, mark the project as "standalone" --
4255 # we'll still do much of the manifests.git set up, but will avoid actual
4256 # syncs to a remote.
4257 if standalone_manifest:
4258 self.config.SetString("manifest.standalone", manifest_url)
4259 elif not manifest_url and not manifest_branch:
4260 # If -u is set and --standalone-manifest is not, then we're not in
4261 # standalone mode. Otherwise, use config to infer what we were in
4262 # the last init.
4263 standalone_manifest = bool(
4264 self.config.GetString("manifest.standalone")
4265 )
4266 if not standalone_manifest:
4267 self.config.SetString("manifest.standalone", None)
4268
4269 self._ConfigureDepth(depth)
4270
4271 # Set the remote URL before the remote branch as we might need it below.
4272 if manifest_url:
4273 r = self.GetRemote()
4274 r.url = manifest_url
4275 r.ResetFetch()
4276 r.Save()
4277
4278 if not standalone_manifest:
4279 if manifest_branch:
4280 if manifest_branch == "HEAD":
4281 manifest_branch = self.ResolveRemoteHead()
4282 if manifest_branch is None:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004283 logger.error("fatal: unable to resolve HEAD")
Gavin Makea2e3302023-03-11 06:46:20 +00004284 return False
4285 self.revisionExpr = manifest_branch
4286 else:
4287 if is_new:
4288 default_branch = self.ResolveRemoteHead()
4289 if default_branch is None:
4290 # If the remote doesn't have HEAD configured, default to
4291 # master.
4292 default_branch = "refs/heads/master"
4293 self.revisionExpr = default_branch
4294 else:
4295 self.PreSync()
4296
4297 groups = re.split(r"[,\s]+", groups or "")
4298 all_platforms = ["linux", "darwin", "windows"]
4299 platformize = lambda x: "platform-" + x
4300 if platform == "auto":
4301 if not mirror and not self.mirror:
4302 groups.append(platformize(self._platform_name))
4303 elif platform == "all":
4304 groups.extend(map(platformize, all_platforms))
4305 elif platform in all_platforms:
4306 groups.append(platformize(platform))
4307 elif platform != "none":
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004308 logger.error("fatal: invalid platform flag", file=sys.stderr)
Gavin Makea2e3302023-03-11 06:46:20 +00004309 return False
4310 self.config.SetString("manifest.platform", platform)
4311
4312 groups = [x for x in groups if x]
4313 groupstr = ",".join(groups)
4314 if (
4315 platform == "auto"
4316 and groupstr == self.manifest.GetDefaultGroupsStr()
4317 ):
4318 groupstr = None
4319 self.config.SetString("manifest.groups", groupstr)
4320
4321 if reference:
4322 self.config.SetString("repo.reference", reference)
4323
4324 if dissociate:
4325 self.config.SetBoolean("repo.dissociate", dissociate)
4326
4327 if worktree:
4328 if mirror:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004329 logger.error("fatal: --mirror and --worktree are incompatible")
Gavin Makea2e3302023-03-11 06:46:20 +00004330 return False
4331 if submodules:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004332 logger.error(
4333 "fatal: --submodules and --worktree are incompatible"
Gavin Makea2e3302023-03-11 06:46:20 +00004334 )
4335 return False
4336 self.config.SetBoolean("repo.worktree", worktree)
4337 if is_new:
4338 self.use_git_worktrees = True
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004339 logger.warning("warning: --worktree is experimental!")
Gavin Makea2e3302023-03-11 06:46:20 +00004340
4341 if archive:
4342 if is_new:
4343 self.config.SetBoolean("repo.archive", archive)
4344 else:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004345 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004346 "fatal: --archive is only supported when initializing a "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004347 "new workspace."
Gavin Makea2e3302023-03-11 06:46:20 +00004348 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004349 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004350 "Either delete the .repo folder in this workspace, or "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004351 "initialize in another location."
Gavin Makea2e3302023-03-11 06:46:20 +00004352 )
4353 return False
4354
4355 if mirror:
4356 if is_new:
4357 self.config.SetBoolean("repo.mirror", mirror)
4358 else:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004359 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004360 "fatal: --mirror is only supported when initializing a new "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004361 "workspace."
Gavin Makea2e3302023-03-11 06:46:20 +00004362 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004363 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004364 "Either delete the .repo folder in this workspace, or "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004365 "initialize in another location."
Gavin Makea2e3302023-03-11 06:46:20 +00004366 )
4367 return False
4368
4369 if partial_clone is not None:
4370 if mirror:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004371 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004372 "fatal: --mirror and --partial-clone are mutually "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004373 "exclusive"
Gavin Makea2e3302023-03-11 06:46:20 +00004374 )
4375 return False
4376 self.config.SetBoolean("repo.partialclone", partial_clone)
4377 if clone_filter:
4378 self.config.SetString("repo.clonefilter", clone_filter)
4379 elif self.partial_clone:
4380 clone_filter = self.clone_filter
4381 else:
4382 clone_filter = None
4383
4384 if partial_clone_exclude is not None:
4385 self.config.SetString(
4386 "repo.partialcloneexclude", partial_clone_exclude
4387 )
4388
4389 if clone_bundle is None:
4390 clone_bundle = False if partial_clone else True
4391 else:
4392 self.config.SetBoolean("repo.clonebundle", clone_bundle)
4393
4394 if submodules:
4395 self.config.SetBoolean("repo.submodules", submodules)
4396
4397 if git_lfs is not None:
4398 if git_lfs:
4399 git_require((2, 17, 0), fail=True, msg="Git LFS support")
4400
4401 self.config.SetBoolean("repo.git-lfs", git_lfs)
4402 if not is_new:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004403 logger.warning(
Gavin Makea2e3302023-03-11 06:46:20 +00004404 "warning: Changing --git-lfs settings will only affect new "
4405 "project checkouts.\n"
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004406 " Existing projects will require manual updates.\n"
Gavin Makea2e3302023-03-11 06:46:20 +00004407 )
4408
Jason Chang17833322023-05-23 13:06:55 -07004409 if clone_filter_for_depth is not None:
4410 self.ConfigureCloneFilterForDepth(clone_filter_for_depth)
4411
Gavin Makea2e3302023-03-11 06:46:20 +00004412 if use_superproject is not None:
4413 self.config.SetBoolean("repo.superproject", use_superproject)
4414
4415 if not standalone_manifest:
4416 success = self.Sync_NetworkHalf(
4417 is_new=is_new,
4418 quiet=not verbose,
4419 verbose=verbose,
4420 clone_bundle=clone_bundle,
4421 current_branch_only=current_branch_only,
4422 tags=tags,
4423 submodules=submodules,
4424 clone_filter=clone_filter,
4425 partial_clone_exclude=self.manifest.PartialCloneExclude,
Jason Chang17833322023-05-23 13:06:55 -07004426 clone_filter_for_depth=self.manifest.CloneFilterForDepth,
Gavin Makea2e3302023-03-11 06:46:20 +00004427 ).success
4428 if not success:
4429 r = self.GetRemote()
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004430 logger.error("fatal: cannot obtain manifest %s", r.url)
Gavin Makea2e3302023-03-11 06:46:20 +00004431
4432 # Better delete the manifest git dir if we created it; otherwise
4433 # next time (when user fixes problems) we won't go through the
4434 # "is_new" logic.
4435 if is_new:
4436 platform_utils.rmtree(self.gitdir)
4437 return False
4438
4439 if manifest_branch:
4440 self.MetaBranchSwitch(submodules=submodules)
4441
4442 syncbuf = SyncBuffer(self.config)
4443 self.Sync_LocalHalf(syncbuf, submodules=submodules)
4444 syncbuf.Finish()
4445
4446 if is_new or self.CurrentBranch is None:
Jason Chang1a3612f2023-08-08 14:12:53 -07004447 try:
4448 self.StartBranch("default")
4449 except GitError as e:
4450 msg = str(e)
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004451 logger.error(
4452 "fatal: cannot create default in manifest %s", msg
Gavin Makea2e3302023-03-11 06:46:20 +00004453 )
4454 return False
4455
4456 if not manifest_name:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004457 logger.error("fatal: manifest name (-m) is required.")
Gavin Makea2e3302023-03-11 06:46:20 +00004458 return False
4459
4460 elif is_new:
4461 # This is a new standalone manifest.
4462 manifest_name = "default.xml"
4463 manifest_data = fetch.fetch_file(manifest_url, verbose=verbose)
4464 dest = os.path.join(self.worktree, manifest_name)
4465 os.makedirs(os.path.dirname(dest), exist_ok=True)
4466 with open(dest, "wb") as f:
4467 f.write(manifest_data)
4468
4469 try:
4470 self.manifest.Link(manifest_name)
4471 except ManifestParseError as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004472 logger.error("fatal: manifest '%s' not available", manifest_name)
4473 logger.error("fatal: %s", e)
Gavin Makea2e3302023-03-11 06:46:20 +00004474 return False
4475
4476 if not this_manifest_only:
4477 for submanifest in self.manifest.submanifests.values():
4478 spec = submanifest.ToSubmanifestSpec()
4479 submanifest.repo_client.manifestProject.Sync(
4480 manifest_url=spec.manifestUrl,
4481 manifest_branch=spec.revision,
4482 standalone_manifest=standalone_manifest,
4483 groups=self.manifest_groups,
4484 platform=platform,
4485 mirror=mirror,
4486 dissociate=dissociate,
4487 reference=reference,
4488 worktree=worktree,
4489 submodules=submodules,
4490 archive=archive,
4491 partial_clone=partial_clone,
4492 clone_filter=clone_filter,
4493 partial_clone_exclude=partial_clone_exclude,
4494 clone_bundle=clone_bundle,
4495 git_lfs=git_lfs,
4496 use_superproject=use_superproject,
4497 verbose=verbose,
4498 current_branch_only=current_branch_only,
4499 tags=tags,
4500 depth=depth,
4501 git_event_log=git_event_log,
4502 manifest_name=spec.manifestName,
4503 this_manifest_only=False,
4504 outer_manifest=False,
4505 )
4506
4507 # Lastly, if the manifest has a <superproject> then have the
4508 # superproject sync it (if it will be used).
4509 if git_superproject.UseSuperproject(use_superproject, self.manifest):
4510 sync_result = self.manifest.superproject.Sync(git_event_log)
4511 if not sync_result.success:
4512 submanifest = ""
4513 if self.manifest.path_prefix:
4514 submanifest = f"for {self.manifest.path_prefix} "
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004515 logger.warning(
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004516 "warning: git update of superproject %s failed, "
Gavin Makea2e3302023-03-11 06:46:20 +00004517 "repo sync will not use superproject to fetch source; "
4518 "while this error is not fatal, and you can continue to "
4519 "run repo sync, please run repo init with the "
4520 "--no-use-superproject option to stop seeing this warning",
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004521 submanifest,
Gavin Makea2e3302023-03-11 06:46:20 +00004522 )
4523 if sync_result.fatal and use_superproject is not None:
4524 return False
4525
4526 return True
4527
Jason Chang17833322023-05-23 13:06:55 -07004528 def ConfigureCloneFilterForDepth(self, clone_filter_for_depth):
4529 """Configure clone filter to replace shallow clones.
4530
4531 Args:
4532 clone_filter_for_depth: a string or None, e.g. 'blob:none' will
4533 disable shallow clones and replace with partial clone. None will
4534 enable shallow clones.
4535 """
4536 self.config.SetString(
4537 "repo.clonefilterfordepth", clone_filter_for_depth
4538 )
4539
Gavin Makea2e3302023-03-11 06:46:20 +00004540 def _ConfigureDepth(self, depth):
4541 """Configure the depth we'll sync down.
4542
4543 Args:
4544 depth: an int, how deep of a partial clone to create.
4545 """
4546 # Opt.depth will be non-None if user actually passed --depth to repo
4547 # init.
4548 if depth is not None:
4549 if depth > 0:
4550 # Positive values will set the depth.
4551 depth = str(depth)
4552 else:
4553 # Negative numbers will clear the depth; passing None to
4554 # SetString will do that.
4555 depth = None
4556
4557 # We store the depth in the main manifest project.
4558 self.config.SetString("repo.depth", depth)