blob: 40ca116da8223826b5dda7419794217eeb34f2eb [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
Sergiy Belozorov96edb9b2024-03-04 19:48:52 +010024import string
Shawn O. Pearce5e7127d2012-08-02 14:57:37 -070025import subprocess
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070026import sys
Julien Campergue335f5ef2013-10-16 11:02:35 +020027import tarfile
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +080028import tempfile
Shawn O. Pearcec325dc32011-10-03 08:30:24 -070029import time
Mike Frysinger64477332023-08-21 21:20:32 -040030from typing import List, NamedTuple
Mike Frysingeracf63b22019-06-13 02:24:21 -040031import urllib.parse
Shawn O. Pearcedf5ee522011-10-11 14:05:21 -070032
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070033from color import Coloring
Mike Frysinger64477332023-08-21 21:20:32 -040034from error import DownloadError
35from error import GitError
36from error import ManifestInvalidPathError
37from error import ManifestInvalidRevisionError
38from error import ManifestParseError
39from error import NoManifestException
40from error import RepoError
41from error import UploadError
LaMont Jones0de4fc32022-04-21 17:18:35 +000042import fetch
Mike Frysinger64477332023-08-21 21:20:32 -040043from git_command import git_require
44from git_command import GitCommand
45from git_config import GetSchemeFromUrl
46from git_config import GetUrlCookieFile
47from git_config import GitConfig
Mike Frysinger64477332023-08-21 21:20:32 -040048from git_config import IsId
49from git_refs import GitRefs
50from git_refs import HEAD
51from git_refs import R_HEADS
52from git_refs import R_M
53from git_refs import R_PUB
54from git_refs import R_TAGS
55from git_refs import R_WORKTREE_M
LaMont Jonesff6b1da2022-06-01 21:03:34 +000056import git_superproject
LaMont Jones55ee3042022-04-06 17:10:21 +000057from git_trace2_event_log import EventLog
Renaud Paquayd5cec5e2016-11-01 11:24:03 -070058import platform_utils
Mike Frysinger70d861f2019-08-26 15:22:36 -040059import progress
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +000060from repo_logging import RepoLogger
Joanna Wanga6c52f52022-11-03 16:51:19 -040061from repo_trace import Trace
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070062
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -070063
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +000064logger = RepoLogger(__file__)
65
66
LaMont Jones1eddca82022-09-01 15:15:04 +000067class SyncNetworkHalfResult(NamedTuple):
Gavin Makea2e3302023-03-11 06:46:20 +000068 """Sync_NetworkHalf return value."""
69
Gavin Makea2e3302023-03-11 06:46:20 +000070 # Did we query the remote? False when optimized_fetch is True and we have
71 # the commit already present.
72 remote_fetched: bool
Jason Chang32b59562023-07-14 16:45:35 -070073 # Error from SyncNetworkHalf
74 error: Exception = None
75
76 @property
77 def success(self) -> bool:
78 return not self.error
79
80
81class SyncNetworkHalfError(RepoError):
82 """Failure trying to sync."""
83
84
85class DeleteWorktreeError(RepoError):
86 """Failure to delete worktree."""
87
88 def __init__(
89 self, *args, aggregate_errors: List[Exception] = None, **kwargs
90 ) -> None:
91 super().__init__(*args, **kwargs)
92 self.aggregate_errors = aggregate_errors or []
93
94
95class DeleteDirtyWorktreeError(DeleteWorktreeError):
96 """Failure to delete worktree due to uncommitted changes."""
LaMont Jones1eddca82022-09-01 15:15:04 +000097
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +010098
George Engelbrecht9bc283e2020-04-02 12:36:09 -060099# Maximum sleep time allowed during retries.
100MAXIMUM_RETRY_SLEEP_SEC = 3600.0
101# +-10% random jitter is added to each Fetches retry sleep duration.
102RETRY_JITTER_PERCENT = 0.1
103
LaMont Jonesfa8d9392022-11-02 22:01:29 +0000104# Whether to use alternates. Switching back and forth is *NOT* supported.
Mike Frysinger1d00a7e2021-12-21 00:40:31 -0500105# TODO(vapier): Remove knob once behavior is verified.
Gavin Makea2e3302023-03-11 06:46:20 +0000106_ALTERNATES = os.environ.get("REPO_USE_ALTERNATES") == "1"
George Engelbrecht9bc283e2020-04-02 12:36:09 -0600107
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +0100108
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700109def _lwrite(path, content):
Gavin Makea2e3302023-03-11 06:46:20 +0000110 lock = "%s.lock" % path
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700111
Gavin Makea2e3302023-03-11 06:46:20 +0000112 # Maintain Unix line endings on all OS's to match git behavior.
113 with open(lock, "w", newline="\n") as fd:
114 fd.write(content)
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700115
Gavin Makea2e3302023-03-11 06:46:20 +0000116 try:
117 platform_utils.rename(lock, path)
118 except OSError:
119 platform_utils.remove(lock)
120 raise
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700121
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700122
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700123def not_rev(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000124 return "^" + r
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700125
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700126
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800127def sq(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000128 return "'" + r.replace("'", "'''") + "'"
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -0800129
David Pursehouse819827a2020-02-12 15:20:19 +0900130
Jonathan Nieder93719792015-03-17 11:29:58 -0700131_project_hook_list = None
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700132
133
Jonathan Nieder93719792015-03-17 11:29:58 -0700134def _ProjectHooks():
Gavin Makea2e3302023-03-11 06:46:20 +0000135 """List the hooks present in the 'hooks' directory.
Jonathan Nieder93719792015-03-17 11:29:58 -0700136
Gavin Makea2e3302023-03-11 06:46:20 +0000137 These hooks are project hooks and are copied to the '.git/hooks' directory
138 of all subprojects.
Jonathan Nieder93719792015-03-17 11:29:58 -0700139
Gavin Makea2e3302023-03-11 06:46:20 +0000140 This function caches the list of hooks (based on the contents of the
141 'repo/hooks' directory) on the first call.
Jonathan Nieder93719792015-03-17 11:29:58 -0700142
Gavin Makea2e3302023-03-11 06:46:20 +0000143 Returns:
144 A list of absolute paths to all of the files in the hooks directory.
145 """
146 global _project_hook_list
147 if _project_hook_list is None:
148 d = platform_utils.realpath(os.path.abspath(os.path.dirname(__file__)))
149 d = os.path.join(d, "hooks")
150 _project_hook_list = [
151 os.path.join(d, x) for x in platform_utils.listdir(d)
152 ]
153 return _project_hook_list
Jonathan Nieder93719792015-03-17 11:29:58 -0700154
155
Mike Frysingerd4aee652023-10-19 05:13:32 -0400156class DownloadedChange:
Gavin Makea2e3302023-03-11 06:46:20 +0000157 _commit_cache = None
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700158
Gavin Makea2e3302023-03-11 06:46:20 +0000159 def __init__(self, project, base, change_id, ps_id, commit):
160 self.project = project
161 self.base = base
162 self.change_id = change_id
163 self.ps_id = ps_id
164 self.commit = commit
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700165
Gavin Makea2e3302023-03-11 06:46:20 +0000166 @property
167 def commits(self):
168 if self._commit_cache is None:
169 self._commit_cache = self.project.bare_git.rev_list(
170 "--abbrev=8",
171 "--abbrev-commit",
172 "--pretty=oneline",
173 "--reverse",
174 "--date-order",
175 not_rev(self.base),
176 self.commit,
177 "--",
178 )
179 return self._commit_cache
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700180
181
Mike Frysingerd4aee652023-10-19 05:13:32 -0400182class ReviewableBranch:
Gavin Makea2e3302023-03-11 06:46:20 +0000183 _commit_cache = None
184 _base_exists = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700185
Gavin Makea2e3302023-03-11 06:46:20 +0000186 def __init__(self, project, branch, base):
187 self.project = project
188 self.branch = branch
189 self.base = base
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700190
Gavin Makea2e3302023-03-11 06:46:20 +0000191 @property
192 def name(self):
193 return self.branch.name
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700194
Gavin Makea2e3302023-03-11 06:46:20 +0000195 @property
196 def commits(self):
197 if self._commit_cache is None:
198 args = (
199 "--abbrev=8",
200 "--abbrev-commit",
201 "--pretty=oneline",
202 "--reverse",
203 "--date-order",
204 not_rev(self.base),
205 R_HEADS + self.name,
206 "--",
207 )
208 try:
Jason Chang87058c62023-09-27 11:34:43 -0700209 self._commit_cache = self.project.bare_git.rev_list(
210 *args, log_as_error=self.base_exists
211 )
Gavin Makea2e3302023-03-11 06:46:20 +0000212 except GitError:
213 # We weren't able to probe the commits for this branch. Was it
214 # tracking a branch that no longer exists? If so, return no
215 # commits. Otherwise, rethrow the error as we don't know what's
216 # going on.
217 if self.base_exists:
218 raise
Mike Frysinger6da17752019-09-11 18:43:17 -0400219
Gavin Makea2e3302023-03-11 06:46:20 +0000220 self._commit_cache = []
Mike Frysinger6da17752019-09-11 18:43:17 -0400221
Gavin Makea2e3302023-03-11 06:46:20 +0000222 return self._commit_cache
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700223
Gavin Makea2e3302023-03-11 06:46:20 +0000224 @property
225 def unabbrev_commits(self):
226 r = dict()
227 for commit in self.project.bare_git.rev_list(
228 not_rev(self.base), R_HEADS + self.name, "--"
229 ):
230 r[commit[0:8]] = commit
231 return r
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800232
Gavin Makea2e3302023-03-11 06:46:20 +0000233 @property
234 def date(self):
235 return self.project.bare_git.log(
236 "--pretty=format:%cd", "-n", "1", R_HEADS + self.name, "--"
237 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700238
Gavin Makea2e3302023-03-11 06:46:20 +0000239 @property
240 def base_exists(self):
241 """Whether the branch we're tracking exists.
Mike Frysinger6da17752019-09-11 18:43:17 -0400242
Gavin Makea2e3302023-03-11 06:46:20 +0000243 Normally it should, but sometimes branches we track can get deleted.
244 """
245 if self._base_exists is None:
246 try:
247 self.project.bare_git.rev_parse("--verify", not_rev(self.base))
248 # If we're still here, the base branch exists.
249 self._base_exists = True
250 except GitError:
251 # If we failed to verify, the base branch doesn't exist.
252 self._base_exists = False
Mike Frysinger6da17752019-09-11 18:43:17 -0400253
Gavin Makea2e3302023-03-11 06:46:20 +0000254 return self._base_exists
Mike Frysinger6da17752019-09-11 18:43:17 -0400255
Gavin Makea2e3302023-03-11 06:46:20 +0000256 def UploadForReview(
257 self,
258 people,
259 dryrun=False,
260 auto_topic=False,
261 hashtags=(),
262 labels=(),
263 private=False,
264 notify=None,
265 wip=False,
266 ready=False,
267 dest_branch=None,
268 validate_certs=True,
269 push_options=None,
Sergiy Belozorov96edb9b2024-03-04 19:48:52 +0100270 patchset_description=None,
Gavin Makea2e3302023-03-11 06:46:20 +0000271 ):
272 self.project.UploadForReview(
273 branch=self.name,
274 people=people,
275 dryrun=dryrun,
276 auto_topic=auto_topic,
277 hashtags=hashtags,
278 labels=labels,
279 private=private,
280 notify=notify,
281 wip=wip,
282 ready=ready,
283 dest_branch=dest_branch,
284 validate_certs=validate_certs,
285 push_options=push_options,
Sergiy Belozorov96edb9b2024-03-04 19:48:52 +0100286 patchset_description=patchset_description,
Gavin Makea2e3302023-03-11 06:46:20 +0000287 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700288
Gavin Makea2e3302023-03-11 06:46:20 +0000289 def GetPublishedRefs(self):
290 refs = {}
291 output = self.project.bare_git.ls_remote(
292 self.branch.remote.SshReviewUrl(self.project.UserEmail),
293 "refs/changes/*",
294 )
295 for line in output.split("\n"):
296 try:
297 (sha, ref) = line.split()
298 refs[sha] = ref
299 except ValueError:
300 pass
Ficus Kirkpatrickbc7ef672009-05-04 12:45:11 -0700301
Gavin Makea2e3302023-03-11 06:46:20 +0000302 return refs
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700303
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700304
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700305class StatusColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000306 def __init__(self, config):
307 super().__init__(config, "status")
308 self.project = self.printer("header", attr="bold")
309 self.branch = self.printer("header", attr="bold")
310 self.nobranch = self.printer("nobranch", fg="red")
311 self.important = self.printer("important", fg="red")
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700312
Gavin Makea2e3302023-03-11 06:46:20 +0000313 self.added = self.printer("added", fg="green")
314 self.changed = self.printer("changed", fg="red")
315 self.untracked = self.printer("untracked", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700316
317
318class DiffColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000319 def __init__(self, config):
320 super().__init__(config, "diff")
321 self.project = self.printer("header", attr="bold")
322 self.fail = self.printer("fail", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700323
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700324
Mike Frysingerd4aee652023-10-19 05:13:32 -0400325class Annotation:
Gavin Makea2e3302023-03-11 06:46:20 +0000326 def __init__(self, name, value, keep):
327 self.name = name
328 self.value = value
329 self.keep = keep
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700330
Gavin Makea2e3302023-03-11 06:46:20 +0000331 def __eq__(self, other):
332 if not isinstance(other, Annotation):
333 return False
334 return self.__dict__ == other.__dict__
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700335
Gavin Makea2e3302023-03-11 06:46:20 +0000336 def __lt__(self, other):
337 # This exists just so that lists of Annotation objects can be sorted,
338 # for use in comparisons.
339 if not isinstance(other, Annotation):
340 raise ValueError("comparison is not between two Annotation objects")
341 if self.name == other.name:
342 if self.value == other.value:
343 return self.keep < other.keep
344 return self.value < other.value
345 return self.name < other.name
Jack Neus6ea0cae2021-07-20 20:52:33 +0000346
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700347
Mike Frysingere6a202f2019-08-02 15:57:57 -0400348def _SafeExpandPath(base, subpath, skipfinal=False):
Gavin Makea2e3302023-03-11 06:46:20 +0000349 """Make sure |subpath| is completely safe under |base|.
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700350
Gavin Makea2e3302023-03-11 06:46:20 +0000351 We make sure no intermediate symlinks are traversed, and that the final path
352 is not a special file (e.g. not a socket or fifo).
Mike Frysingere6a202f2019-08-02 15:57:57 -0400353
Gavin Makea2e3302023-03-11 06:46:20 +0000354 NB: We rely on a number of paths already being filtered out while parsing
355 the manifest. See the validation logic in manifest_xml.py for more details.
356 """
357 # Split up the path by its components. We can't use os.path.sep exclusively
358 # as some platforms (like Windows) will convert / to \ and that bypasses all
359 # our constructed logic here. Especially since manifest authors only use
360 # / in their paths.
361 resep = re.compile(r"[/%s]" % re.escape(os.path.sep))
362 components = resep.split(subpath)
363 if skipfinal:
364 # Whether the caller handles the final component itself.
365 finalpart = components.pop()
Mike Frysingere6a202f2019-08-02 15:57:57 -0400366
Gavin Makea2e3302023-03-11 06:46:20 +0000367 path = base
368 for part in components:
369 if part in {".", ".."}:
370 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400371 f'{subpath}: "{part}" not allowed in paths'
Gavin Makea2e3302023-03-11 06:46:20 +0000372 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400373
Gavin Makea2e3302023-03-11 06:46:20 +0000374 path = os.path.join(path, part)
375 if platform_utils.islink(path):
376 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400377 f"{path}: traversing symlinks not allow"
Gavin Makea2e3302023-03-11 06:46:20 +0000378 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400379
Gavin Makea2e3302023-03-11 06:46:20 +0000380 if os.path.exists(path):
381 if not os.path.isfile(path) and not platform_utils.isdir(path):
382 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400383 f"{path}: only regular files & directories allowed"
Gavin Makea2e3302023-03-11 06:46:20 +0000384 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400385
Gavin Makea2e3302023-03-11 06:46:20 +0000386 if skipfinal:
387 path = os.path.join(path, finalpart)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400388
Gavin Makea2e3302023-03-11 06:46:20 +0000389 return path
Mike Frysingere6a202f2019-08-02 15:57:57 -0400390
391
Mike Frysingerd4aee652023-10-19 05:13:32 -0400392class _CopyFile:
Gavin Makea2e3302023-03-11 06:46:20 +0000393 """Container for <copyfile> manifest element."""
Mike Frysingere6a202f2019-08-02 15:57:57 -0400394
Gavin Makea2e3302023-03-11 06:46:20 +0000395 def __init__(self, git_worktree, src, topdir, dest):
396 """Register a <copyfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400397
Gavin Makea2e3302023-03-11 06:46:20 +0000398 Args:
399 git_worktree: Absolute path to the git project checkout.
400 src: Relative path under |git_worktree| of file to read.
401 topdir: Absolute path to the top of the repo client checkout.
402 dest: Relative path under |topdir| of file to write.
403 """
404 self.git_worktree = git_worktree
405 self.topdir = topdir
406 self.src = src
407 self.dest = dest
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700408
Gavin Makea2e3302023-03-11 06:46:20 +0000409 def _Copy(self):
410 src = _SafeExpandPath(self.git_worktree, self.src)
411 dest = _SafeExpandPath(self.topdir, self.dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400412
Gavin Makea2e3302023-03-11 06:46:20 +0000413 if platform_utils.isdir(src):
414 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400415 f"{self.src}: copying from directory not supported"
Gavin Makea2e3302023-03-11 06:46:20 +0000416 )
417 if platform_utils.isdir(dest):
418 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400419 f"{self.dest}: copying to directory not allowed"
Gavin Makea2e3302023-03-11 06:46:20 +0000420 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400421
Gavin Makea2e3302023-03-11 06:46:20 +0000422 # Copy file if it does not exist or is out of date.
423 if not os.path.exists(dest) or not filecmp.cmp(src, dest):
424 try:
425 # Remove existing file first, since it might be read-only.
426 if os.path.exists(dest):
427 platform_utils.remove(dest)
428 else:
429 dest_dir = os.path.dirname(dest)
430 if not platform_utils.isdir(dest_dir):
431 os.makedirs(dest_dir)
432 shutil.copy(src, dest)
433 # Make the file read-only.
434 mode = os.stat(dest)[stat.ST_MODE]
435 mode = mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
436 os.chmod(dest, mode)
Jason R. Coombsae824fb2023-10-20 23:32:40 +0545437 except OSError:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +0000438 logger.error("error: Cannot copy file %s to %s", src, dest)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700439
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700440
Mike Frysingerd4aee652023-10-19 05:13:32 -0400441class _LinkFile:
Gavin Makea2e3302023-03-11 06:46:20 +0000442 """Container for <linkfile> manifest element."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700443
Gavin Makea2e3302023-03-11 06:46:20 +0000444 def __init__(self, git_worktree, src, topdir, dest):
445 """Register a <linkfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400446
Gavin Makea2e3302023-03-11 06:46:20 +0000447 Args:
448 git_worktree: Absolute path to the git project checkout.
449 src: Target of symlink relative to path under |git_worktree|.
450 topdir: Absolute path to the top of the repo client checkout.
451 dest: Relative path under |topdir| of symlink to create.
452 """
453 self.git_worktree = git_worktree
454 self.topdir = topdir
455 self.src = src
456 self.dest = dest
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500457
Gavin Makea2e3302023-03-11 06:46:20 +0000458 def __linkIt(self, relSrc, absDest):
459 # Link file if it does not exist or is out of date.
460 if not platform_utils.islink(absDest) or (
461 platform_utils.readlink(absDest) != relSrc
462 ):
463 try:
464 # Remove existing file first, since it might be read-only.
465 if os.path.lexists(absDest):
466 platform_utils.remove(absDest)
467 else:
468 dest_dir = os.path.dirname(absDest)
469 if not platform_utils.isdir(dest_dir):
470 os.makedirs(dest_dir)
471 platform_utils.symlink(relSrc, absDest)
Jason R. Coombsae824fb2023-10-20 23:32:40 +0545472 except OSError:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +0000473 logger.error(
474 "error: Cannot link file %s to %s", relSrc, absDest
475 )
Gavin Makea2e3302023-03-11 06:46:20 +0000476
477 def _Link(self):
478 """Link the self.src & self.dest paths.
479
480 Handles wild cards on the src linking all of the files in the source in
481 to the destination directory.
482 """
483 # Some people use src="." to create stable links to projects. Let's
484 # allow that but reject all other uses of "." to keep things simple.
485 if self.src == ".":
486 src = self.git_worktree
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500487 else:
Gavin Makea2e3302023-03-11 06:46:20 +0000488 src = _SafeExpandPath(self.git_worktree, self.src)
Wink Saville4c426ef2015-06-03 08:05:17 -0700489
Gavin Makea2e3302023-03-11 06:46:20 +0000490 if not glob.has_magic(src):
491 # Entity does not contain a wild card so just a simple one to one
492 # link operation.
493 dest = _SafeExpandPath(self.topdir, self.dest, skipfinal=True)
494 # dest & src are absolute paths at this point. Make sure the target
495 # of the symlink is relative in the context of the repo client
496 # checkout.
497 relpath = os.path.relpath(src, os.path.dirname(dest))
498 self.__linkIt(relpath, dest)
499 else:
500 dest = _SafeExpandPath(self.topdir, self.dest)
501 # Entity contains a wild card.
502 if os.path.exists(dest) and not platform_utils.isdir(dest):
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +0000503 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +0000504 "Link error: src with wildcard, %s must be a directory",
505 dest,
506 )
507 else:
508 for absSrcFile in glob.glob(src):
509 # Create a releative path from source dir to destination
510 # dir.
511 absSrcDir = os.path.dirname(absSrcFile)
512 relSrcDir = os.path.relpath(absSrcDir, dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400513
Gavin Makea2e3302023-03-11 06:46:20 +0000514 # Get the source file name.
515 srcFile = os.path.basename(absSrcFile)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400516
Gavin Makea2e3302023-03-11 06:46:20 +0000517 # Now form the final full paths to srcFile. They will be
518 # absolute for the desintaiton and relative for the source.
519 absDest = os.path.join(dest, srcFile)
520 relSrc = os.path.join(relSrcDir, srcFile)
521 self.__linkIt(relSrc, absDest)
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500522
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700523
Mike Frysingerd4aee652023-10-19 05:13:32 -0400524class RemoteSpec:
Gavin Makea2e3302023-03-11 06:46:20 +0000525 def __init__(
526 self,
527 name,
528 url=None,
529 pushUrl=None,
530 review=None,
531 revision=None,
532 orig_name=None,
533 fetchUrl=None,
534 ):
535 self.name = name
536 self.url = url
537 self.pushUrl = pushUrl
538 self.review = review
539 self.revision = revision
540 self.orig_name = orig_name
541 self.fetchUrl = fetchUrl
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700542
Ian Kasprzak0286e312021-02-05 10:06:18 -0800543
Mike Frysingerd4aee652023-10-19 05:13:32 -0400544class Project:
Gavin Makea2e3302023-03-11 06:46:20 +0000545 # These objects can be shared between several working trees.
546 @property
547 def shareable_dirs(self):
548 """Return the shareable directories"""
549 if self.UseAlternates:
550 return ["hooks", "rr-cache"]
551 else:
552 return ["hooks", "objects", "rr-cache"]
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700553
Gavin Makea2e3302023-03-11 06:46:20 +0000554 def __init__(
555 self,
556 manifest,
557 name,
558 remote,
559 gitdir,
560 objdir,
561 worktree,
562 relpath,
563 revisionExpr,
564 revisionId,
565 rebase=True,
566 groups=None,
567 sync_c=False,
568 sync_s=False,
569 sync_tags=True,
570 clone_depth=None,
571 upstream=None,
572 parent=None,
573 use_git_worktrees=False,
574 is_derived=False,
575 dest_branch=None,
576 optimized_fetch=False,
577 retry_fetches=0,
578 old_revision=None,
579 ):
580 """Init a Project object.
Doug Anderson3ba5f952011-04-07 12:51:04 -0700581
582 Args:
Gavin Makea2e3302023-03-11 06:46:20 +0000583 manifest: The XmlManifest object.
584 name: The `name` attribute of manifest.xml's project element.
585 remote: RemoteSpec object specifying its remote's properties.
586 gitdir: Absolute path of git directory.
587 objdir: Absolute path of directory to store git objects.
588 worktree: Absolute path of git working tree.
589 relpath: Relative path of git working tree to repo's top directory.
590 revisionExpr: The `revision` attribute of manifest.xml's project
591 element.
592 revisionId: git commit id for checking out.
593 rebase: The `rebase` attribute of manifest.xml's project element.
594 groups: The `groups` attribute of manifest.xml's project element.
595 sync_c: The `sync-c` attribute of manifest.xml's project element.
596 sync_s: The `sync-s` attribute of manifest.xml's project element.
597 sync_tags: The `sync-tags` attribute of manifest.xml's project
598 element.
599 upstream: The `upstream` attribute of manifest.xml's project
600 element.
601 parent: The parent Project object.
602 use_git_worktrees: Whether to use `git worktree` for this project.
603 is_derived: False if the project was explicitly defined in the
604 manifest; True if the project is a discovered submodule.
605 dest_branch: The branch to which to push changes for review by
606 default.
607 optimized_fetch: If True, when a project is set to a sha1 revision,
608 only fetch from the remote if the sha1 is not present locally.
609 retry_fetches: Retry remote fetches n times upon receiving transient
610 error with exponential backoff and jitter.
611 old_revision: saved git commit id for open GITC projects.
612 """
613 self.client = self.manifest = manifest
614 self.name = name
615 self.remote = remote
616 self.UpdatePaths(relpath, worktree, gitdir, objdir)
617 self.SetRevision(revisionExpr, revisionId=revisionId)
618
619 self.rebase = rebase
620 self.groups = groups
621 self.sync_c = sync_c
622 self.sync_s = sync_s
623 self.sync_tags = sync_tags
624 self.clone_depth = clone_depth
625 self.upstream = upstream
626 self.parent = parent
627 # NB: Do not use this setting in __init__ to change behavior so that the
628 # manifest.git checkout can inspect & change it after instantiating.
629 # See the XmlManifest init code for more info.
630 self.use_git_worktrees = use_git_worktrees
631 self.is_derived = is_derived
632 self.optimized_fetch = optimized_fetch
633 self.retry_fetches = max(0, retry_fetches)
634 self.subprojects = []
635
636 self.snapshots = {}
637 self.copyfiles = []
638 self.linkfiles = []
639 self.annotations = []
640 self.dest_branch = dest_branch
641 self.old_revision = old_revision
642
643 # This will be filled in if a project is later identified to be the
644 # project containing repo hooks.
645 self.enabled_repo_hooks = []
646
647 def RelPath(self, local=True):
648 """Return the path for the project relative to a manifest.
649
650 Args:
651 local: a boolean, if True, the path is relative to the local
652 (sub)manifest. If false, the path is relative to the outermost
653 manifest.
654 """
655 if local:
656 return self.relpath
657 return os.path.join(self.manifest.path_prefix, self.relpath)
658
659 def SetRevision(self, revisionExpr, revisionId=None):
660 """Set revisionId based on revision expression and id"""
661 self.revisionExpr = revisionExpr
662 if revisionId is None and revisionExpr and IsId(revisionExpr):
663 self.revisionId = self.revisionExpr
664 else:
665 self.revisionId = revisionId
666
667 def UpdatePaths(self, relpath, worktree, gitdir, objdir):
668 """Update paths used by this project"""
669 self.gitdir = gitdir.replace("\\", "/")
670 self.objdir = objdir.replace("\\", "/")
671 if worktree:
672 self.worktree = os.path.normpath(worktree).replace("\\", "/")
673 else:
674 self.worktree = None
675 self.relpath = relpath
676
677 self.config = GitConfig.ForRepository(
678 gitdir=self.gitdir, defaults=self.manifest.globalConfig
679 )
680
681 if self.worktree:
682 self.work_git = self._GitGetByExec(
683 self, bare=False, gitdir=self.gitdir
684 )
685 else:
686 self.work_git = None
687 self.bare_git = self._GitGetByExec(self, bare=True, gitdir=self.gitdir)
688 self.bare_ref = GitRefs(self.gitdir)
689 self.bare_objdir = self._GitGetByExec(
690 self, bare=True, gitdir=self.objdir
691 )
692
693 @property
694 def UseAlternates(self):
695 """Whether git alternates are in use.
696
697 This will be removed once migration to alternates is complete.
698 """
699 return _ALTERNATES or self.manifest.is_multimanifest
700
701 @property
702 def Derived(self):
703 return self.is_derived
704
705 @property
706 def Exists(self):
707 return platform_utils.isdir(self.gitdir) and platform_utils.isdir(
708 self.objdir
709 )
710
711 @property
712 def CurrentBranch(self):
713 """Obtain the name of the currently checked out branch.
714
715 The branch name omits the 'refs/heads/' prefix.
716 None is returned if the project is on a detached HEAD, or if the
717 work_git is otheriwse inaccessible (e.g. an incomplete sync).
718 """
719 try:
720 b = self.work_git.GetHead()
721 except NoManifestException:
722 # If the local checkout is in a bad state, don't barf. Let the
723 # callers process this like the head is unreadable.
724 return None
725 if b.startswith(R_HEADS):
726 return b[len(R_HEADS) :]
727 return None
728
729 def IsRebaseInProgress(self):
730 return (
731 os.path.exists(self.work_git.GetDotgitPath("rebase-apply"))
732 or os.path.exists(self.work_git.GetDotgitPath("rebase-merge"))
733 or os.path.exists(os.path.join(self.worktree, ".dotest"))
734 )
735
736 def IsDirty(self, consider_untracked=True):
737 """Is the working directory modified in some way?"""
738 self.work_git.update_index(
739 "-q", "--unmerged", "--ignore-missing", "--refresh"
740 )
741 if self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD):
742 return True
743 if self.work_git.DiffZ("diff-files"):
744 return True
745 if consider_untracked and self.UntrackedFiles():
746 return True
747 return False
748
749 _userident_name = None
750 _userident_email = None
751
752 @property
753 def UserName(self):
754 """Obtain the user's personal name."""
755 if self._userident_name is None:
756 self._LoadUserIdentity()
757 return self._userident_name
758
759 @property
760 def UserEmail(self):
761 """Obtain the user's email address. This is very likely
762 to be their Gerrit login.
763 """
764 if self._userident_email is None:
765 self._LoadUserIdentity()
766 return self._userident_email
767
768 def _LoadUserIdentity(self):
769 u = self.bare_git.var("GIT_COMMITTER_IDENT")
770 m = re.compile("^(.*) <([^>]*)> ").match(u)
771 if m:
772 self._userident_name = m.group(1)
773 self._userident_email = m.group(2)
774 else:
775 self._userident_name = ""
776 self._userident_email = ""
777
778 def GetRemote(self, name=None):
779 """Get the configuration for a single remote.
780
781 Defaults to the current project's remote.
782 """
783 if name is None:
784 name = self.remote.name
785 return self.config.GetRemote(name)
786
787 def GetBranch(self, name):
788 """Get the configuration for a single branch."""
789 return self.config.GetBranch(name)
790
791 def GetBranches(self):
792 """Get all existing local branches."""
793 current = self.CurrentBranch
794 all_refs = self._allrefs
795 heads = {}
796
797 for name, ref_id in all_refs.items():
798 if name.startswith(R_HEADS):
799 name = name[len(R_HEADS) :]
800 b = self.GetBranch(name)
801 b.current = name == current
802 b.published = None
803 b.revision = ref_id
804 heads[name] = b
805
806 for name, ref_id in all_refs.items():
807 if name.startswith(R_PUB):
808 name = name[len(R_PUB) :]
809 b = heads.get(name)
810 if b:
811 b.published = ref_id
812
813 return heads
814
815 def MatchesGroups(self, manifest_groups):
816 """Returns true if the manifest groups specified at init should cause
817 this project to be synced.
818 Prefixing a manifest group with "-" inverts the meaning of a group.
819 All projects are implicitly labelled with "all".
820
821 labels are resolved in order. In the example case of
822 project_groups: "all,group1,group2"
823 manifest_groups: "-group1,group2"
824 the project will be matched.
825
826 The special manifest group "default" will match any project that
827 does not have the special project group "notdefault"
828 """
829 default_groups = self.manifest.default_groups or ["default"]
830 expanded_manifest_groups = manifest_groups or default_groups
831 expanded_project_groups = ["all"] + (self.groups or [])
832 if "notdefault" not in expanded_project_groups:
833 expanded_project_groups += ["default"]
834
835 matched = False
836 for group in expanded_manifest_groups:
837 if group.startswith("-") and group[1:] in expanded_project_groups:
838 matched = False
839 elif group in expanded_project_groups:
840 matched = True
841
842 return matched
843
844 def UncommitedFiles(self, get_all=True):
845 """Returns a list of strings, uncommitted files in the git tree.
846
847 Args:
848 get_all: a boolean, if True - get information about all different
849 uncommitted files. If False - return as soon as any kind of
850 uncommitted files is detected.
851 """
852 details = []
853 self.work_git.update_index(
854 "-q", "--unmerged", "--ignore-missing", "--refresh"
855 )
856 if self.IsRebaseInProgress():
857 details.append("rebase in progress")
858 if not get_all:
859 return details
860
861 changes = self.work_git.DiffZ("diff-index", "--cached", HEAD).keys()
862 if changes:
863 details.extend(changes)
864 if not get_all:
865 return details
866
867 changes = self.work_git.DiffZ("diff-files").keys()
868 if changes:
869 details.extend(changes)
870 if not get_all:
871 return details
872
873 changes = self.UntrackedFiles()
874 if changes:
875 details.extend(changes)
876
877 return details
878
879 def UntrackedFiles(self):
880 """Returns a list of strings, untracked files in the git tree."""
881 return self.work_git.LsOthers()
882
883 def HasChanges(self):
884 """Returns true if there are uncommitted changes."""
885 return bool(self.UncommitedFiles(get_all=False))
886
887 def PrintWorkTreeStatus(self, output_redir=None, quiet=False, local=False):
888 """Prints the status of the repository to stdout.
889
890 Args:
891 output_redir: If specified, redirect the output to this object.
892 quiet: If True then only print the project name. Do not print
893 the modified files, branch name, etc.
894 local: a boolean, if True, the path is relative to the local
895 (sub)manifest. If false, the path is relative to the outermost
896 manifest.
897 """
898 if not platform_utils.isdir(self.worktree):
899 if output_redir is None:
900 output_redir = sys.stdout
901 print(file=output_redir)
902 print("project %s/" % self.RelPath(local), file=output_redir)
903 print(' missing (run "repo sync")', file=output_redir)
904 return
905
906 self.work_git.update_index(
907 "-q", "--unmerged", "--ignore-missing", "--refresh"
908 )
909 rb = self.IsRebaseInProgress()
910 di = self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD)
911 df = self.work_git.DiffZ("diff-files")
912 do = self.work_git.LsOthers()
913 if not rb and not di and not df and not do and not self.CurrentBranch:
914 return "CLEAN"
915
916 out = StatusColoring(self.config)
917 if output_redir is not None:
918 out.redirect(output_redir)
919 out.project("project %-40s", self.RelPath(local) + "/ ")
920
921 if quiet:
922 out.nl()
923 return "DIRTY"
924
925 branch = self.CurrentBranch
926 if branch is None:
927 out.nobranch("(*** NO BRANCH ***)")
928 else:
929 out.branch("branch %s", branch)
930 out.nl()
931
932 if rb:
933 out.important("prior sync failed; rebase still in progress")
934 out.nl()
935
936 paths = list()
937 paths.extend(di.keys())
938 paths.extend(df.keys())
939 paths.extend(do)
940
941 for p in sorted(set(paths)):
942 try:
943 i = di[p]
944 except KeyError:
945 i = None
946
947 try:
948 f = df[p]
949 except KeyError:
950 f = None
951
952 if i:
953 i_status = i.status.upper()
954 else:
955 i_status = "-"
956
957 if f:
958 f_status = f.status.lower()
959 else:
960 f_status = "-"
961
962 if i and i.src_path:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400963 line = (
964 f" {i_status}{f_status}\t{i.src_path} => {p} ({i.level}%)"
Gavin Makea2e3302023-03-11 06:46:20 +0000965 )
966 else:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400967 line = f" {i_status}{f_status}\t{p}"
Gavin Makea2e3302023-03-11 06:46:20 +0000968
969 if i and not f:
970 out.added("%s", line)
971 elif (i and f) or (not i and f):
972 out.changed("%s", line)
973 elif not i and not f:
974 out.untracked("%s", line)
975 else:
976 out.write("%s", line)
977 out.nl()
978
979 return "DIRTY"
980
981 def PrintWorkTreeDiff(
982 self, absolute_paths=False, output_redir=None, local=False
983 ):
984 """Prints the status of the repository to stdout."""
985 out = DiffColoring(self.config)
986 if output_redir:
987 out.redirect(output_redir)
988 cmd = ["diff"]
989 if out.is_on:
990 cmd.append("--color")
991 cmd.append(HEAD)
992 if absolute_paths:
993 cmd.append("--src-prefix=a/%s/" % self.RelPath(local))
994 cmd.append("--dst-prefix=b/%s/" % self.RelPath(local))
995 cmd.append("--")
996 try:
997 p = GitCommand(self, cmd, capture_stdout=True, capture_stderr=True)
998 p.Wait()
999 except GitError as e:
1000 out.nl()
1001 out.project("project %s/" % self.RelPath(local))
1002 out.nl()
1003 out.fail("%s", str(e))
1004 out.nl()
1005 return False
1006 if p.stdout:
1007 out.nl()
1008 out.project("project %s/" % self.RelPath(local))
1009 out.nl()
1010 out.write("%s", p.stdout)
1011 return p.Wait() == 0
1012
1013 def WasPublished(self, branch, all_refs=None):
1014 """Was the branch published (uploaded) for code review?
1015 If so, returns the SHA-1 hash of the last published
1016 state for the branch.
1017 """
1018 key = R_PUB + branch
1019 if all_refs is None:
1020 try:
1021 return self.bare_git.rev_parse(key)
1022 except GitError:
1023 return None
1024 else:
1025 try:
1026 return all_refs[key]
1027 except KeyError:
1028 return None
1029
1030 def CleanPublishedCache(self, all_refs=None):
1031 """Prunes any stale published refs."""
1032 if all_refs is None:
1033 all_refs = self._allrefs
1034 heads = set()
1035 canrm = {}
1036 for name, ref_id in all_refs.items():
1037 if name.startswith(R_HEADS):
1038 heads.add(name)
1039 elif name.startswith(R_PUB):
1040 canrm[name] = ref_id
1041
1042 for name, ref_id in canrm.items():
1043 n = name[len(R_PUB) :]
1044 if R_HEADS + n not in heads:
1045 self.bare_git.DeleteRef(name, ref_id)
1046
1047 def GetUploadableBranches(self, selected_branch=None):
1048 """List any branches which can be uploaded for review."""
1049 heads = {}
1050 pubed = {}
1051
1052 for name, ref_id in self._allrefs.items():
1053 if name.startswith(R_HEADS):
1054 heads[name[len(R_HEADS) :]] = ref_id
1055 elif name.startswith(R_PUB):
1056 pubed[name[len(R_PUB) :]] = ref_id
1057
1058 ready = []
1059 for branch, ref_id in heads.items():
1060 if branch in pubed and pubed[branch] == ref_id:
1061 continue
1062 if selected_branch and branch != selected_branch:
1063 continue
1064
1065 rb = self.GetUploadableBranch(branch)
1066 if rb:
1067 ready.append(rb)
1068 return ready
1069
1070 def GetUploadableBranch(self, branch_name):
1071 """Get a single uploadable branch, or None."""
1072 branch = self.GetBranch(branch_name)
1073 base = branch.LocalMerge
1074 if branch.LocalMerge:
1075 rb = ReviewableBranch(self, branch, base)
1076 if rb.commits:
1077 return rb
1078 return None
1079
1080 def UploadForReview(
1081 self,
1082 branch=None,
1083 people=([], []),
1084 dryrun=False,
1085 auto_topic=False,
1086 hashtags=(),
1087 labels=(),
1088 private=False,
1089 notify=None,
1090 wip=False,
1091 ready=False,
1092 dest_branch=None,
1093 validate_certs=True,
1094 push_options=None,
Sergiy Belozorov96edb9b2024-03-04 19:48:52 +01001095 patchset_description=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001096 ):
1097 """Uploads the named branch for code review."""
1098 if branch is None:
1099 branch = self.CurrentBranch
1100 if branch is None:
Jason Chang32b59562023-07-14 16:45:35 -07001101 raise GitError("not currently on a branch", project=self.name)
Gavin Makea2e3302023-03-11 06:46:20 +00001102
1103 branch = self.GetBranch(branch)
1104 if not branch.LocalMerge:
Jason Chang32b59562023-07-14 16:45:35 -07001105 raise GitError(
1106 "branch %s does not track a remote" % branch.name,
1107 project=self.name,
1108 )
Gavin Makea2e3302023-03-11 06:46:20 +00001109 if not branch.remote.review:
Jason Chang32b59562023-07-14 16:45:35 -07001110 raise GitError(
1111 "remote %s has no review url" % branch.remote.name,
1112 project=self.name,
1113 )
Gavin Makea2e3302023-03-11 06:46:20 +00001114
1115 # Basic validity check on label syntax.
1116 for label in labels:
1117 if not re.match(r"^.+[+-][0-9]+$", label):
1118 raise UploadError(
1119 f'invalid label syntax "{label}": labels use forms like '
Jason Chang5a3a5f72023-08-17 11:36:41 -07001120 "CodeReview+1 or Verified-1",
1121 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00001122 )
1123
1124 if dest_branch is None:
1125 dest_branch = self.dest_branch
1126 if dest_branch is None:
1127 dest_branch = branch.merge
1128 if not dest_branch.startswith(R_HEADS):
1129 dest_branch = R_HEADS + dest_branch
1130
1131 if not branch.remote.projectname:
1132 branch.remote.projectname = self.name
1133 branch.remote.Save()
1134
1135 url = branch.remote.ReviewUrl(self.UserEmail, validate_certs)
1136 if url is None:
Jason Chang5a3a5f72023-08-17 11:36:41 -07001137 raise UploadError("review not configured", project=self.name)
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +00001138 cmd = ["push", "--progress"]
Gavin Makea2e3302023-03-11 06:46:20 +00001139 if dryrun:
1140 cmd.append("-n")
1141
1142 if url.startswith("ssh://"):
1143 cmd.append("--receive-pack=gerrit receive-pack")
1144
Aravind Vasudevan99ebf622023-04-04 23:44:37 +00001145 # This stops git from pushing all reachable annotated tags when
1146 # push.followTags is configured. Gerrit does not accept any tags
1147 # pushed to a CL.
1148 if git_require((1, 8, 3)):
1149 cmd.append("--no-follow-tags")
1150
Gavin Makea2e3302023-03-11 06:46:20 +00001151 for push_option in push_options or []:
1152 cmd.append("-o")
1153 cmd.append(push_option)
1154
1155 cmd.append(url)
1156
1157 if dest_branch.startswith(R_HEADS):
1158 dest_branch = dest_branch[len(R_HEADS) :]
1159
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001160 ref_spec = f"{R_HEADS + branch.name}:refs/for/{dest_branch}"
Gavin Makea2e3302023-03-11 06:46:20 +00001161 opts = []
1162 if auto_topic:
1163 opts += ["topic=" + branch.name]
1164 opts += ["t=%s" % p for p in hashtags]
1165 # NB: No need to encode labels as they've been validated above.
1166 opts += ["l=%s" % p for p in labels]
1167
1168 opts += ["r=%s" % p for p in people[0]]
1169 opts += ["cc=%s" % p for p in people[1]]
1170 if notify:
1171 opts += ["notify=" + notify]
1172 if private:
1173 opts += ["private"]
1174 if wip:
1175 opts += ["wip"]
1176 if ready:
1177 opts += ["ready"]
Sergiy Belozorov96edb9b2024-03-04 19:48:52 +01001178 if patchset_description:
1179 opts += [
1180 f"m={self._encode_patchset_description(patchset_description)}"
1181 ]
Gavin Makea2e3302023-03-11 06:46:20 +00001182 if opts:
1183 ref_spec = ref_spec + "%" + ",".join(opts)
1184 cmd.append(ref_spec)
1185
Jason Chang1e9f7b92023-08-25 10:31:04 -07001186 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001187
1188 if not dryrun:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001189 msg = f"posted to {branch.remote.review} for {dest_branch}"
Gavin Makea2e3302023-03-11 06:46:20 +00001190 self.bare_git.UpdateRef(
1191 R_PUB + branch.name, R_HEADS + branch.name, message=msg
1192 )
1193
Sergiy Belozorov96edb9b2024-03-04 19:48:52 +01001194 @staticmethod
1195 def _encode_patchset_description(original):
1196 """Applies percent-encoding for strings sent as patchset description.
1197
1198 The encoding used is based on but stricter than URL encoding (Section
1199 2.1 of RFC 3986). The only non-escaped characters are alphanumerics, and
1200 'SPACE' (U+0020) can be represented as 'LOW LINE' (U+005F) or
1201 'PLUS SIGN' (U+002B).
1202
1203 For more information, see the Gerrit docs here:
1204 https://gerrit-review.googlesource.com/Documentation/user-upload.html#patch_set_description
1205 """
1206 SAFE = {ord(x) for x in string.ascii_letters + string.digits}
1207
1208 def _enc(b):
1209 if b in SAFE:
1210 return chr(b)
1211 elif b == ord(" "):
1212 return "_"
1213 else:
1214 return f"%{b:02x}"
1215
1216 return "".join(_enc(x) for x in original.encode("utf-8"))
1217
Gavin Makea2e3302023-03-11 06:46:20 +00001218 def _ExtractArchive(self, tarpath, path=None):
1219 """Extract the given tar on its current location
1220
1221 Args:
1222 tarpath: The path to the actual tar file
1223
1224 """
1225 try:
1226 with tarfile.open(tarpath, "r") as tar:
1227 tar.extractall(path=path)
1228 return True
Jason R. Coombsae824fb2023-10-20 23:32:40 +05451229 except (OSError, tarfile.TarError) as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001230 logger.error("error: Cannot extract archive %s: %s", tarpath, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001231 return False
1232
1233 def Sync_NetworkHalf(
1234 self,
1235 quiet=False,
1236 verbose=False,
1237 output_redir=None,
1238 is_new=None,
1239 current_branch_only=None,
1240 force_sync=False,
1241 clone_bundle=True,
1242 tags=None,
1243 archive=False,
1244 optimized_fetch=False,
1245 retry_fetches=0,
1246 prune=False,
1247 submodules=False,
1248 ssh_proxy=None,
1249 clone_filter=None,
1250 partial_clone_exclude=set(),
Jason Chang17833322023-05-23 13:06:55 -07001251 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001252 ):
1253 """Perform only the network IO portion of the sync process.
1254 Local working directory/branch state is not affected.
1255 """
1256 if archive and not isinstance(self, MetaProject):
1257 if self.remote.url.startswith(("http://", "https://")):
Jason Chang32b59562023-07-14 16:45:35 -07001258 msg_template = (
1259 "%s: Cannot fetch archives from http/https remotes."
Gavin Makea2e3302023-03-11 06:46:20 +00001260 )
Jason Chang32b59562023-07-14 16:45:35 -07001261 msg_args = self.name
1262 msg = msg_template % msg_args
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001263 logger.error(msg_template, msg_args)
Jason Chang32b59562023-07-14 16:45:35 -07001264 return SyncNetworkHalfResult(
1265 False, SyncNetworkHalfError(msg, project=self.name)
1266 )
Gavin Makea2e3302023-03-11 06:46:20 +00001267
1268 name = self.relpath.replace("\\", "/")
1269 name = name.replace("/", "_")
1270 tarpath = "%s.tar" % name
1271 topdir = self.manifest.topdir
1272
1273 try:
1274 self._FetchArchive(tarpath, cwd=topdir)
1275 except GitError as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001276 logger.error("error: %s", e)
Jason Chang32b59562023-07-14 16:45:35 -07001277 return SyncNetworkHalfResult(False, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001278
1279 # From now on, we only need absolute tarpath.
1280 tarpath = os.path.join(topdir, tarpath)
1281
1282 if not self._ExtractArchive(tarpath, path=topdir):
Jason Chang32b59562023-07-14 16:45:35 -07001283 return SyncNetworkHalfResult(
1284 True,
1285 SyncNetworkHalfError(
1286 f"Unable to Extract Archive {tarpath}",
1287 project=self.name,
1288 ),
1289 )
Gavin Makea2e3302023-03-11 06:46:20 +00001290 try:
1291 platform_utils.remove(tarpath)
1292 except OSError as e:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00001293 logger.warning("warn: Cannot remove archive %s: %s", tarpath, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001294 self._CopyAndLinkFiles()
Jason Chang32b59562023-07-14 16:45:35 -07001295 return SyncNetworkHalfResult(True)
Gavin Makea2e3302023-03-11 06:46:20 +00001296
1297 # If the shared object dir already exists, don't try to rebootstrap with
1298 # a clone bundle download. We should have the majority of objects
1299 # already.
1300 if clone_bundle and os.path.exists(self.objdir):
1301 clone_bundle = False
1302
1303 if self.name in partial_clone_exclude:
1304 clone_bundle = True
1305 clone_filter = None
1306
1307 if is_new is None:
1308 is_new = not self.Exists
1309 if is_new:
1310 self._InitGitDir(force_sync=force_sync, quiet=quiet)
1311 else:
Josip Sokcevic9b57aa02023-12-01 23:01:52 +00001312 try:
1313 # At this point, it's possible that gitdir points to an old
1314 # objdir (e.g. name changed, but objdir exists). Check
1315 # references to ensure that's not the case. See
1316 # https://issues.gerritcodereview.com/40013418 for more
1317 # details.
1318 self._CheckDirReference(self.objdir, self.gitdir)
1319
1320 self._UpdateHooks(quiet=quiet)
1321 except GitError as e:
1322 if not force_sync:
1323 raise e
1324 # Let _InitGitDir fix the issue, force_sync is always True here.
1325 self._InitGitDir(force_sync=True, quiet=quiet)
Gavin Makea2e3302023-03-11 06:46:20 +00001326 self._InitRemote()
1327
1328 if self.UseAlternates:
1329 # If gitdir/objects is a symlink, migrate it from the old layout.
1330 gitdir_objects = os.path.join(self.gitdir, "objects")
1331 if platform_utils.islink(gitdir_objects):
1332 platform_utils.remove(gitdir_objects, missing_ok=True)
1333 gitdir_alt = os.path.join(self.gitdir, "objects/info/alternates")
1334 if not os.path.exists(gitdir_alt):
1335 os.makedirs(os.path.dirname(gitdir_alt), exist_ok=True)
1336 _lwrite(
1337 gitdir_alt,
1338 os.path.join(
1339 os.path.relpath(self.objdir, gitdir_objects), "objects"
1340 )
1341 + "\n",
1342 )
1343
1344 if is_new:
1345 alt = os.path.join(self.objdir, "objects/info/alternates")
1346 try:
1347 with open(alt) as fd:
1348 # This works for both absolute and relative alternate
1349 # directories.
1350 alt_dir = os.path.join(
1351 self.objdir, "objects", fd.readline().rstrip()
1352 )
Jason R. Coombsae824fb2023-10-20 23:32:40 +05451353 except OSError:
Gavin Makea2e3302023-03-11 06:46:20 +00001354 alt_dir = None
1355 else:
1356 alt_dir = None
1357
1358 if (
1359 clone_bundle
1360 and alt_dir is None
1361 and self._ApplyCloneBundle(
1362 initial=is_new, quiet=quiet, verbose=verbose
1363 )
1364 ):
1365 is_new = False
1366
1367 if current_branch_only is None:
1368 if self.sync_c:
1369 current_branch_only = True
1370 elif not self.manifest._loaded:
1371 # Manifest cannot check defaults until it syncs.
1372 current_branch_only = False
1373 elif self.manifest.default.sync_c:
1374 current_branch_only = True
1375
1376 if tags is None:
1377 tags = self.sync_tags
1378
1379 if self.clone_depth:
1380 depth = self.clone_depth
1381 else:
1382 depth = self.manifest.manifestProject.depth
1383
Jason Chang17833322023-05-23 13:06:55 -07001384 if depth and clone_filter_for_depth:
1385 depth = None
1386 clone_filter = clone_filter_for_depth
1387
Gavin Makea2e3302023-03-11 06:46:20 +00001388 # See if we can skip the network fetch entirely.
1389 remote_fetched = False
1390 if not (
1391 optimized_fetch
Sylvain56a5a012023-09-11 13:38:00 +02001392 and IsId(self.revisionExpr)
1393 and self._CheckForImmutableRevision()
Gavin Makea2e3302023-03-11 06:46:20 +00001394 ):
1395 remote_fetched = True
Jason Chang32b59562023-07-14 16:45:35 -07001396 try:
1397 if not self._RemoteFetch(
1398 initial=is_new,
1399 quiet=quiet,
1400 verbose=verbose,
1401 output_redir=output_redir,
1402 alt_dir=alt_dir,
1403 current_branch_only=current_branch_only,
1404 tags=tags,
1405 prune=prune,
1406 depth=depth,
1407 submodules=submodules,
1408 force_sync=force_sync,
1409 ssh_proxy=ssh_proxy,
1410 clone_filter=clone_filter,
1411 retry_fetches=retry_fetches,
1412 ):
1413 return SyncNetworkHalfResult(
1414 remote_fetched,
1415 SyncNetworkHalfError(
1416 f"Unable to remote fetch project {self.name}",
1417 project=self.name,
1418 ),
1419 )
1420 except RepoError as e:
1421 return SyncNetworkHalfResult(
1422 remote_fetched,
1423 e,
1424 )
Gavin Makea2e3302023-03-11 06:46:20 +00001425
1426 mp = self.manifest.manifestProject
1427 dissociate = mp.dissociate
1428 if dissociate:
1429 alternates_file = os.path.join(
1430 self.objdir, "objects/info/alternates"
1431 )
1432 if os.path.exists(alternates_file):
1433 cmd = ["repack", "-a", "-d"]
1434 p = GitCommand(
1435 self,
1436 cmd,
1437 bare=True,
1438 capture_stdout=bool(output_redir),
1439 merge_output=bool(output_redir),
1440 )
1441 if p.stdout and output_redir:
1442 output_redir.write(p.stdout)
1443 if p.Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07001444 return SyncNetworkHalfResult(
1445 remote_fetched,
1446 GitError(
1447 "Unable to repack alternates", project=self.name
1448 ),
1449 )
Gavin Makea2e3302023-03-11 06:46:20 +00001450 platform_utils.remove(alternates_file)
1451
1452 if self.worktree:
1453 self._InitMRef()
1454 else:
1455 self._InitMirrorHead()
1456 platform_utils.remove(
1457 os.path.join(self.gitdir, "FETCH_HEAD"), missing_ok=True
1458 )
Jason Chang32b59562023-07-14 16:45:35 -07001459 return SyncNetworkHalfResult(remote_fetched)
Gavin Makea2e3302023-03-11 06:46:20 +00001460
1461 def PostRepoUpgrade(self):
1462 self._InitHooks()
1463
1464 def _CopyAndLinkFiles(self):
1465 if self.client.isGitcClient:
1466 return
1467 for copyfile in self.copyfiles:
1468 copyfile._Copy()
1469 for linkfile in self.linkfiles:
1470 linkfile._Link()
1471
1472 def GetCommitRevisionId(self):
1473 """Get revisionId of a commit.
1474
1475 Use this method instead of GetRevisionId to get the id of the commit
1476 rather than the id of the current git object (for example, a tag)
1477
1478 """
Sylvaine9cb3912023-09-10 23:35:01 +02001479 if self.revisionId:
1480 return self.revisionId
Gavin Makea2e3302023-03-11 06:46:20 +00001481 if not self.revisionExpr.startswith(R_TAGS):
1482 return self.GetRevisionId(self._allrefs)
1483
1484 try:
1485 return self.bare_git.rev_list(self.revisionExpr, "-1")[0]
1486 except GitError:
1487 raise ManifestInvalidRevisionError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001488 f"revision {self.revisionExpr} in {self.name} not found"
Gavin Makea2e3302023-03-11 06:46:20 +00001489 )
1490
1491 def GetRevisionId(self, all_refs=None):
1492 if self.revisionId:
1493 return self.revisionId
1494
1495 rem = self.GetRemote()
1496 rev = rem.ToLocal(self.revisionExpr)
1497
1498 if all_refs is not None and rev in all_refs:
1499 return all_refs[rev]
1500
1501 try:
1502 return self.bare_git.rev_parse("--verify", "%s^0" % rev)
1503 except GitError:
1504 raise ManifestInvalidRevisionError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001505 f"revision {self.revisionExpr} in {self.name} not found"
Gavin Makea2e3302023-03-11 06:46:20 +00001506 )
1507
1508 def SetRevisionId(self, revisionId):
1509 if self.revisionExpr:
1510 self.upstream = self.revisionExpr
1511
1512 self.revisionId = revisionId
1513
Jason Chang32b59562023-07-14 16:45:35 -07001514 def Sync_LocalHalf(
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001515 self,
1516 syncbuf,
1517 force_sync=False,
1518 submodules=False,
1519 errors=None,
1520 verbose=False,
Jason Chang32b59562023-07-14 16:45:35 -07001521 ):
Gavin Makea2e3302023-03-11 06:46:20 +00001522 """Perform only the local IO portion of the sync process.
1523
1524 Network access is not required.
1525 """
Jason Chang32b59562023-07-14 16:45:35 -07001526 if errors is None:
1527 errors = []
1528
1529 def fail(error: Exception):
1530 errors.append(error)
1531 syncbuf.fail(self, error)
1532
Gavin Makea2e3302023-03-11 06:46:20 +00001533 if not os.path.exists(self.gitdir):
Jason Chang32b59562023-07-14 16:45:35 -07001534 fail(
1535 LocalSyncFail(
1536 "Cannot checkout %s due to missing network sync; Run "
1537 "`repo sync -n %s` first." % (self.name, self.name),
1538 project=self.name,
1539 )
Gavin Makea2e3302023-03-11 06:46:20 +00001540 )
1541 return
1542
1543 self._InitWorkTree(force_sync=force_sync, submodules=submodules)
1544 all_refs = self.bare_ref.all
1545 self.CleanPublishedCache(all_refs)
1546 revid = self.GetRevisionId(all_refs)
1547
1548 # Special case the root of the repo client checkout. Make sure it
1549 # doesn't contain files being checked out to dirs we don't allow.
1550 if self.relpath == ".":
1551 PROTECTED_PATHS = {".repo"}
1552 paths = set(
1553 self.work_git.ls_tree("-z", "--name-only", "--", revid).split(
1554 "\0"
1555 )
1556 )
1557 bad_paths = paths & PROTECTED_PATHS
1558 if bad_paths:
Jason Chang32b59562023-07-14 16:45:35 -07001559 fail(
1560 LocalSyncFail(
1561 "Refusing to checkout project that writes to protected "
1562 "paths: %s" % (", ".join(bad_paths),),
1563 project=self.name,
1564 )
Gavin Makea2e3302023-03-11 06:46:20 +00001565 )
1566 return
1567
1568 def _doff():
1569 self._FastForward(revid)
1570 self._CopyAndLinkFiles()
1571
1572 def _dosubmodules():
1573 self._SyncSubmodules(quiet=True)
1574
1575 head = self.work_git.GetHead()
1576 if head.startswith(R_HEADS):
1577 branch = head[len(R_HEADS) :]
1578 try:
1579 head = all_refs[head]
1580 except KeyError:
1581 head = None
1582 else:
1583 branch = None
1584
1585 if branch is None or syncbuf.detach_head:
1586 # Currently on a detached HEAD. The user is assumed to
1587 # not have any local modifications worth worrying about.
1588 if self.IsRebaseInProgress():
Jason Chang32b59562023-07-14 16:45:35 -07001589 fail(_PriorSyncFailedError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001590 return
1591
1592 if head == revid:
1593 # No changes; don't do anything further.
1594 # Except if the head needs to be detached.
1595 if not syncbuf.detach_head:
1596 # The copy/linkfile config may have changed.
1597 self._CopyAndLinkFiles()
1598 return
1599 else:
1600 lost = self._revlist(not_rev(revid), HEAD)
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001601 if lost and verbose:
Gavin Makea2e3302023-03-11 06:46:20 +00001602 syncbuf.info(self, "discarding %d commits", len(lost))
1603
1604 try:
1605 self._Checkout(revid, quiet=True)
1606 if submodules:
1607 self._SyncSubmodules(quiet=True)
1608 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001609 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001610 return
1611 self._CopyAndLinkFiles()
1612 return
1613
1614 if head == revid:
1615 # No changes; don't do anything further.
1616 #
1617 # The copy/linkfile config may have changed.
1618 self._CopyAndLinkFiles()
1619 return
1620
1621 branch = self.GetBranch(branch)
1622
1623 if not branch.LocalMerge:
1624 # The current branch has no tracking configuration.
1625 # Jump off it to a detached HEAD.
1626 syncbuf.info(
1627 self, "leaving %s; does not track upstream", branch.name
1628 )
1629 try:
1630 self._Checkout(revid, quiet=True)
1631 if submodules:
1632 self._SyncSubmodules(quiet=True)
1633 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001634 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001635 return
1636 self._CopyAndLinkFiles()
1637 return
1638
1639 upstream_gain = self._revlist(not_rev(HEAD), revid)
1640
1641 # See if we can perform a fast forward merge. This can happen if our
1642 # branch isn't in the exact same state as we last published.
1643 try:
Jason Chang87058c62023-09-27 11:34:43 -07001644 self.work_git.merge_base(
1645 "--is-ancestor", HEAD, revid, log_as_error=False
1646 )
Gavin Makea2e3302023-03-11 06:46:20 +00001647 # Skip the published logic.
1648 pub = False
1649 except GitError:
1650 pub = self.WasPublished(branch.name, all_refs)
1651
1652 if pub:
1653 not_merged = self._revlist(not_rev(revid), pub)
1654 if not_merged:
1655 if upstream_gain:
1656 # The user has published this branch and some of those
1657 # commits are not yet merged upstream. We do not want
1658 # to rewrite the published commits so we punt.
Jason Chang32b59562023-07-14 16:45:35 -07001659 fail(
1660 LocalSyncFail(
1661 "branch %s is published (but not merged) and is "
1662 "now %d commits behind"
1663 % (branch.name, len(upstream_gain)),
1664 project=self.name,
1665 )
Gavin Makea2e3302023-03-11 06:46:20 +00001666 )
1667 return
1668 elif pub == head:
1669 # All published commits are merged, and thus we are a
1670 # strict subset. We can fast-forward safely.
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001671 syncbuf.later1(self, _doff, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001672 if submodules:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001673 syncbuf.later1(self, _dosubmodules, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001674 return
1675
1676 # Examine the local commits not in the remote. Find the
1677 # last one attributed to this user, if any.
1678 local_changes = self._revlist(not_rev(revid), HEAD, format="%H %ce")
1679 last_mine = None
1680 cnt_mine = 0
1681 for commit in local_changes:
1682 commit_id, committer_email = commit.split(" ", 1)
1683 if committer_email == self.UserEmail:
1684 last_mine = commit_id
1685 cnt_mine += 1
1686
1687 if not upstream_gain and cnt_mine == len(local_changes):
1688 # The copy/linkfile config may have changed.
1689 self._CopyAndLinkFiles()
1690 return
1691
1692 if self.IsDirty(consider_untracked=False):
Jason Chang32b59562023-07-14 16:45:35 -07001693 fail(_DirtyError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001694 return
1695
1696 # If the upstream switched on us, warn the user.
1697 if branch.merge != self.revisionExpr:
1698 if branch.merge and self.revisionExpr:
1699 syncbuf.info(
1700 self,
1701 "manifest switched %s...%s",
1702 branch.merge,
1703 self.revisionExpr,
1704 )
1705 elif branch.merge:
1706 syncbuf.info(self, "manifest no longer tracks %s", branch.merge)
1707
1708 if cnt_mine < len(local_changes):
1709 # Upstream rebased. Not everything in HEAD was created by this user.
1710 syncbuf.info(
1711 self,
1712 "discarding %d commits removed from upstream",
1713 len(local_changes) - cnt_mine,
1714 )
1715
1716 branch.remote = self.GetRemote()
Sylvain56a5a012023-09-11 13:38:00 +02001717 if not IsId(self.revisionExpr):
Gavin Makea2e3302023-03-11 06:46:20 +00001718 # In case of manifest sync the revisionExpr might be a SHA1.
1719 branch.merge = self.revisionExpr
1720 if not branch.merge.startswith("refs/"):
1721 branch.merge = R_HEADS + branch.merge
1722 branch.Save()
1723
1724 if cnt_mine > 0 and self.rebase:
1725
1726 def _docopyandlink():
1727 self._CopyAndLinkFiles()
1728
1729 def _dorebase():
1730 self._Rebase(upstream="%s^1" % last_mine, onto=revid)
1731
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001732 syncbuf.later2(self, _dorebase, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001733 if submodules:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001734 syncbuf.later2(self, _dosubmodules, not verbose)
1735 syncbuf.later2(self, _docopyandlink, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001736 elif local_changes:
1737 try:
1738 self._ResetHard(revid)
1739 if submodules:
1740 self._SyncSubmodules(quiet=True)
1741 self._CopyAndLinkFiles()
1742 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001743 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001744 return
1745 else:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001746 syncbuf.later1(self, _doff, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001747 if submodules:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001748 syncbuf.later1(self, _dosubmodules, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001749
1750 def AddCopyFile(self, src, dest, topdir):
1751 """Mark |src| for copying to |dest| (relative to |topdir|).
1752
1753 No filesystem changes occur here. Actual copying happens later on.
1754
1755 Paths should have basic validation run on them before being queued.
1756 Further checking will be handled when the actual copy happens.
1757 """
1758 self.copyfiles.append(_CopyFile(self.worktree, src, topdir, dest))
1759
1760 def AddLinkFile(self, src, dest, topdir):
1761 """Mark |dest| to create a symlink (relative to |topdir|) pointing to
1762 |src|.
1763
1764 No filesystem changes occur here. Actual linking happens later on.
1765
1766 Paths should have basic validation run on them before being queued.
1767 Further checking will be handled when the actual link happens.
1768 """
1769 self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest))
1770
1771 def AddAnnotation(self, name, value, keep):
1772 self.annotations.append(Annotation(name, value, keep))
1773
1774 def DownloadPatchSet(self, change_id, patch_id):
1775 """Download a single patch set of a single change to FETCH_HEAD."""
1776 remote = self.GetRemote()
1777
1778 cmd = ["fetch", remote.name]
1779 cmd.append(
1780 "refs/changes/%2.2d/%d/%d" % (change_id % 100, change_id, patch_id)
1781 )
Jason Chang1a3612f2023-08-08 14:12:53 -07001782 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001783 return DownloadedChange(
1784 self,
1785 self.GetRevisionId(),
1786 change_id,
1787 patch_id,
1788 self.bare_git.rev_parse("FETCH_HEAD"),
1789 )
1790
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001791 def DeleteWorktree(self, verbose=False, force=False):
Gavin Makea2e3302023-03-11 06:46:20 +00001792 """Delete the source checkout and any other housekeeping tasks.
1793
1794 This currently leaves behind the internal .repo/ cache state. This
1795 helps when switching branches or manifest changes get reverted as we
1796 don't have to redownload all the git objects. But we should do some GC
1797 at some point.
1798
1799 Args:
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001800 verbose: Whether to show verbose messages.
Gavin Makea2e3302023-03-11 06:46:20 +00001801 force: Always delete tree even if dirty.
Doug Anderson3ba5f952011-04-07 12:51:04 -07001802
1803 Returns:
Gavin Makea2e3302023-03-11 06:46:20 +00001804 True if the worktree was completely cleaned out.
1805 """
1806 if self.IsDirty():
1807 if force:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00001808 logger.warning(
Gavin Makea2e3302023-03-11 06:46:20 +00001809 "warning: %s: Removing dirty project: uncommitted changes "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001810 "lost.",
1811 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001812 )
1813 else:
Jason Chang32b59562023-07-14 16:45:35 -07001814 msg = (
1815 "error: %s: Cannot remove project: uncommitted"
1816 "changes are present.\n" % self.RelPath(local=False)
Gavin Makea2e3302023-03-11 06:46:20 +00001817 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001818 logger.error(msg)
Jason Chang32b59562023-07-14 16:45:35 -07001819 raise DeleteDirtyWorktreeError(msg, project=self)
Wink Saville02d79452009-04-10 13:01:24 -07001820
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001821 if verbose:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001822 print(f"{self.RelPath(local=False)}: Deleting obsolete checkout.")
Wink Saville02d79452009-04-10 13:01:24 -07001823
Gavin Makea2e3302023-03-11 06:46:20 +00001824 # Unlock and delink from the main worktree. We don't use git's worktree
1825 # remove because it will recursively delete projects -- we handle that
1826 # ourselves below. https://crbug.com/git/48
1827 if self.use_git_worktrees:
1828 needle = platform_utils.realpath(self.gitdir)
1829 # Find the git worktree commondir under .repo/worktrees/.
1830 output = self.bare_git.worktree("list", "--porcelain").splitlines()[
1831 0
1832 ]
1833 assert output.startswith("worktree "), output
1834 commondir = output[9:]
1835 # Walk each of the git worktrees to see where they point.
1836 configs = os.path.join(commondir, "worktrees")
1837 for name in os.listdir(configs):
1838 gitdir = os.path.join(configs, name, "gitdir")
1839 with open(gitdir) as fp:
1840 relpath = fp.read().strip()
1841 # Resolve the checkout path and see if it matches this project.
1842 fullpath = platform_utils.realpath(
1843 os.path.join(configs, name, relpath)
1844 )
1845 if fullpath == needle:
1846 platform_utils.rmtree(os.path.join(configs, name))
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001847
Gavin Makea2e3302023-03-11 06:46:20 +00001848 # Delete the .git directory first, so we're less likely to have a
1849 # partially working git repository around. There shouldn't be any git
1850 # projects here, so rmtree works.
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001851
Gavin Makea2e3302023-03-11 06:46:20 +00001852 # Try to remove plain files first in case of git worktrees. If this
1853 # fails for any reason, we'll fall back to rmtree, and that'll display
1854 # errors if it can't remove things either.
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001855 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001856 platform_utils.remove(self.gitdir)
1857 except OSError:
1858 pass
1859 try:
1860 platform_utils.rmtree(self.gitdir)
1861 except OSError as e:
1862 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001863 logger.error("error: %s: %s", self.gitdir, e)
1864 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00001865 "error: %s: Failed to delete obsolete checkout; remove "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001866 "manually, then run `repo sync -l`.",
1867 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001868 )
Jason Chang32b59562023-07-14 16:45:35 -07001869 raise DeleteWorktreeError(aggregate_errors=[e])
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001870
Gavin Makea2e3302023-03-11 06:46:20 +00001871 # Delete everything under the worktree, except for directories that
1872 # contain another git project.
1873 dirs_to_remove = []
1874 failed = False
Jason Chang32b59562023-07-14 16:45:35 -07001875 errors = []
Gavin Makea2e3302023-03-11 06:46:20 +00001876 for root, dirs, files in platform_utils.walk(self.worktree):
1877 for f in files:
1878 path = os.path.join(root, f)
1879 try:
1880 platform_utils.remove(path)
1881 except OSError as e:
1882 if e.errno != errno.ENOENT:
Josip Sokcevic4217a822024-01-24 13:54:25 -08001883 logger.warning("%s: Failed to remove: %s", path, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001884 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001885 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001886 dirs[:] = [
1887 d
1888 for d in dirs
1889 if not os.path.lexists(os.path.join(root, d, ".git"))
1890 ]
1891 dirs_to_remove += [
1892 os.path.join(root, d)
1893 for d in dirs
1894 if os.path.join(root, d) not in dirs_to_remove
1895 ]
1896 for d in reversed(dirs_to_remove):
1897 if platform_utils.islink(d):
1898 try:
1899 platform_utils.remove(d)
1900 except OSError as e:
1901 if e.errno != errno.ENOENT:
Josip Sokcevic4217a822024-01-24 13:54:25 -08001902 logger.warning("%s: Failed to remove: %s", d, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001903 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001904 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001905 elif not platform_utils.listdir(d):
1906 try:
1907 platform_utils.rmdir(d)
1908 except OSError as e:
1909 if e.errno != errno.ENOENT:
Josip Sokcevic4217a822024-01-24 13:54:25 -08001910 logger.warning("%s: Failed to remove: %s", d, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001911 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001912 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001913 if failed:
Josip Sokcevic4217a822024-01-24 13:54:25 -08001914 rename_path = (
1915 f"{self.worktree}_repo_to_be_deleted_{int(time.time())}"
Gavin Makea2e3302023-03-11 06:46:20 +00001916 )
Josip Sokcevic4217a822024-01-24 13:54:25 -08001917 try:
1918 platform_utils.rename(self.worktree, rename_path)
1919 logger.warning(
1920 "warning: renamed %s to %s. You can delete it, but you "
1921 "might need elevated permissions (e.g. root)",
1922 self.worktree,
1923 rename_path,
1924 )
1925 # Rename successful! Clear the errors.
1926 errors = []
1927 except OSError:
1928 logger.error(
1929 "%s: Failed to delete obsolete checkout.\n",
1930 " Remove manually, then run `repo sync -l`.",
1931 self.RelPath(local=False),
1932 )
1933 raise DeleteWorktreeError(aggregate_errors=errors)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07001934
Gavin Makea2e3302023-03-11 06:46:20 +00001935 # Try deleting parent dirs if they are empty.
1936 path = self.worktree
1937 while path != self.manifest.topdir:
1938 try:
1939 platform_utils.rmdir(path)
1940 except OSError as e:
1941 if e.errno != errno.ENOENT:
1942 break
1943 path = os.path.dirname(path)
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001944
Gavin Makea2e3302023-03-11 06:46:20 +00001945 return True
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001946
Gavin Makea2e3302023-03-11 06:46:20 +00001947 def StartBranch(self, name, branch_merge="", revision=None):
1948 """Create a new branch off the manifest's revision."""
1949 if not branch_merge:
1950 branch_merge = self.revisionExpr
1951 head = self.work_git.GetHead()
1952 if head == (R_HEADS + name):
1953 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001954
David Pursehouse8a68ff92012-09-24 12:15:13 +09001955 all_refs = self.bare_ref.all
Gavin Makea2e3302023-03-11 06:46:20 +00001956 if R_HEADS + name in all_refs:
Jason Chang1a3612f2023-08-08 14:12:53 -07001957 GitCommand(
1958 self, ["checkout", "-q", name, "--"], verify_command=True
1959 ).Wait()
1960 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001961
Gavin Makea2e3302023-03-11 06:46:20 +00001962 branch = self.GetBranch(name)
1963 branch.remote = self.GetRemote()
1964 branch.merge = branch_merge
Sylvain56a5a012023-09-11 13:38:00 +02001965 if not branch.merge.startswith("refs/") and not IsId(branch_merge):
Gavin Makea2e3302023-03-11 06:46:20 +00001966 branch.merge = R_HEADS + branch_merge
Shawn O. Pearce88443382010-10-08 10:02:09 +02001967
Gavin Makea2e3302023-03-11 06:46:20 +00001968 if revision is None:
1969 revid = self.GetRevisionId(all_refs)
Shawn O. Pearce88443382010-10-08 10:02:09 +02001970 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001971 revid = self.work_git.rev_parse(revision)
Brian Harring14a66742012-09-28 20:21:57 -07001972
Gavin Makea2e3302023-03-11 06:46:20 +00001973 if head.startswith(R_HEADS):
Kevin Degiabaa7f32014-11-12 11:27:45 -07001974 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001975 head = all_refs[head]
1976 except KeyError:
1977 head = None
1978 if revid and head and revid == head:
1979 ref = R_HEADS + name
1980 self.work_git.update_ref(ref, revid)
1981 self.work_git.symbolic_ref(HEAD, ref)
1982 branch.Save()
1983 return True
Kevin Degib1a07b82015-07-27 13:33:43 -06001984
Jason Chang1a3612f2023-08-08 14:12:53 -07001985 GitCommand(
1986 self,
1987 ["checkout", "-q", "-b", branch.name, revid],
1988 verify_command=True,
1989 ).Wait()
1990 branch.Save()
1991 return True
Kevin Degi384b3c52014-10-16 16:02:58 -06001992
Gavin Makea2e3302023-03-11 06:46:20 +00001993 def CheckoutBranch(self, name):
1994 """Checkout a local topic branch.
Shawn O. Pearce2816d4f2009-03-03 17:53:18 -08001995
Gavin Makea2e3302023-03-11 06:46:20 +00001996 Args:
1997 name: The name of the branch to checkout.
Shawn O. Pearce88443382010-10-08 10:02:09 +02001998
Gavin Makea2e3302023-03-11 06:46:20 +00001999 Returns:
Jason Chang1a3612f2023-08-08 14:12:53 -07002000 True if the checkout succeeded; False if the
2001 branch doesn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00002002 """
2003 rev = R_HEADS + name
2004 head = self.work_git.GetHead()
2005 if head == rev:
2006 # Already on the branch.
2007 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02002008
Gavin Makea2e3302023-03-11 06:46:20 +00002009 all_refs = self.bare_ref.all
2010 try:
2011 revid = all_refs[rev]
2012 except KeyError:
2013 # Branch does not exist in this project.
Jason Chang1a3612f2023-08-08 14:12:53 -07002014 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002015
Gavin Makea2e3302023-03-11 06:46:20 +00002016 if head.startswith(R_HEADS):
2017 try:
2018 head = all_refs[head]
2019 except KeyError:
2020 head = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002021
Gavin Makea2e3302023-03-11 06:46:20 +00002022 if head == revid:
2023 # Same revision; just update HEAD to point to the new
2024 # target branch, but otherwise take no other action.
2025 _lwrite(
2026 self.work_git.GetDotgitPath(subpath=HEAD),
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002027 f"ref: {R_HEADS}{name}\n",
Gavin Makea2e3302023-03-11 06:46:20 +00002028 )
2029 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05002030
Jason Chang1a3612f2023-08-08 14:12:53 -07002031 GitCommand(
2032 self,
2033 ["checkout", name, "--"],
2034 capture_stdout=True,
2035 capture_stderr=True,
2036 verify_command=True,
2037 ).Wait()
2038 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05002039
Gavin Makea2e3302023-03-11 06:46:20 +00002040 def AbandonBranch(self, name):
2041 """Destroy a local topic branch.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002042
Gavin Makea2e3302023-03-11 06:46:20 +00002043 Args:
2044 name: The name of the branch to abandon.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002045
Gavin Makea2e3302023-03-11 06:46:20 +00002046 Returns:
Jason Changf9aacd42023-08-03 14:38:00 -07002047 True if the abandon succeeded; Raises GitCommandError if it didn't;
2048 None if the branch didn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00002049 """
2050 rev = R_HEADS + name
2051 all_refs = self.bare_ref.all
2052 if rev not in all_refs:
2053 # Doesn't exist
2054 return None
2055
2056 head = self.work_git.GetHead()
2057 if head == rev:
2058 # We can't destroy the branch while we are sitting
2059 # on it. Switch to a detached HEAD.
2060 head = all_refs[head]
2061
2062 revid = self.GetRevisionId(all_refs)
2063 if head == revid:
2064 _lwrite(
2065 self.work_git.GetDotgitPath(subpath=HEAD), "%s\n" % revid
2066 )
2067 else:
2068 self._Checkout(revid, quiet=True)
Jason Changf9aacd42023-08-03 14:38:00 -07002069 GitCommand(
2070 self,
2071 ["branch", "-D", name],
2072 capture_stdout=True,
2073 capture_stderr=True,
2074 verify_command=True,
2075 ).Wait()
2076 return True
Gavin Makea2e3302023-03-11 06:46:20 +00002077
2078 def PruneHeads(self):
2079 """Prune any topic branches already merged into upstream."""
2080 cb = self.CurrentBranch
2081 kill = []
2082 left = self._allrefs
2083 for name in left.keys():
2084 if name.startswith(R_HEADS):
2085 name = name[len(R_HEADS) :]
2086 if cb is None or name != cb:
2087 kill.append(name)
2088
2089 # Minor optimization: If there's nothing to prune, then don't try to
2090 # read any project state.
2091 if not kill and not cb:
2092 return []
2093
2094 rev = self.GetRevisionId(left)
2095 if (
2096 cb is not None
2097 and not self._revlist(HEAD + "..." + rev)
2098 and not self.IsDirty(consider_untracked=False)
2099 ):
2100 self.work_git.DetachHead(HEAD)
2101 kill.append(cb)
2102
2103 if kill:
2104 old = self.bare_git.GetHead()
2105
2106 try:
2107 self.bare_git.DetachHead(rev)
2108
2109 b = ["branch", "-d"]
2110 b.extend(kill)
2111 b = GitCommand(
2112 self, b, bare=True, capture_stdout=True, capture_stderr=True
2113 )
2114 b.Wait()
2115 finally:
Sylvain56a5a012023-09-11 13:38:00 +02002116 if IsId(old):
Gavin Makea2e3302023-03-11 06:46:20 +00002117 self.bare_git.DetachHead(old)
2118 else:
2119 self.bare_git.SetHead(old)
2120 left = self._allrefs
2121
2122 for branch in kill:
2123 if (R_HEADS + branch) not in left:
2124 self.CleanPublishedCache()
2125 break
2126
2127 if cb and cb not in kill:
2128 kill.append(cb)
2129 kill.sort()
2130
2131 kept = []
2132 for branch in kill:
2133 if R_HEADS + branch in left:
2134 branch = self.GetBranch(branch)
2135 base = branch.LocalMerge
2136 if not base:
2137 base = rev
2138 kept.append(ReviewableBranch(self, branch, base))
2139 return kept
2140
2141 def GetRegisteredSubprojects(self):
2142 result = []
2143
2144 def rec(subprojects):
2145 if not subprojects:
2146 return
2147 result.extend(subprojects)
2148 for p in subprojects:
2149 rec(p.subprojects)
2150
2151 rec(self.subprojects)
2152 return result
2153
2154 def _GetSubmodules(self):
2155 # Unfortunately we cannot call `git submodule status --recursive` here
2156 # because the working tree might not exist yet, and it cannot be used
2157 # without a working tree in its current implementation.
2158
2159 def get_submodules(gitdir, rev):
2160 # Parse .gitmodules for submodule sub_paths and sub_urls.
2161 sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
2162 if not sub_paths:
2163 return []
2164 # Run `git ls-tree` to read SHAs of submodule object, which happen
2165 # to be revision of submodule repository.
2166 sub_revs = git_ls_tree(gitdir, rev, sub_paths)
2167 submodules = []
2168 for sub_path, sub_url in zip(sub_paths, sub_urls):
2169 try:
2170 sub_rev = sub_revs[sub_path]
2171 except KeyError:
2172 # Ignore non-exist submodules.
2173 continue
2174 submodules.append((sub_rev, sub_path, sub_url))
2175 return submodules
2176
2177 re_path = re.compile(r"^submodule\.(.+)\.path=(.*)$")
2178 re_url = re.compile(r"^submodule\.(.+)\.url=(.*)$")
2179
2180 def parse_gitmodules(gitdir, rev):
2181 cmd = ["cat-file", "blob", "%s:.gitmodules" % rev]
2182 try:
2183 p = GitCommand(
2184 None,
2185 cmd,
2186 capture_stdout=True,
2187 capture_stderr=True,
2188 bare=True,
2189 gitdir=gitdir,
2190 )
2191 except GitError:
2192 return [], []
2193 if p.Wait() != 0:
2194 return [], []
2195
2196 gitmodules_lines = []
2197 fd, temp_gitmodules_path = tempfile.mkstemp()
2198 try:
2199 os.write(fd, p.stdout.encode("utf-8"))
2200 os.close(fd)
2201 cmd = ["config", "--file", temp_gitmodules_path, "--list"]
2202 p = GitCommand(
2203 None,
2204 cmd,
2205 capture_stdout=True,
2206 capture_stderr=True,
2207 bare=True,
2208 gitdir=gitdir,
2209 )
2210 if p.Wait() != 0:
2211 return [], []
2212 gitmodules_lines = p.stdout.split("\n")
2213 except GitError:
2214 return [], []
2215 finally:
2216 platform_utils.remove(temp_gitmodules_path)
2217
2218 names = set()
2219 paths = {}
2220 urls = {}
2221 for line in gitmodules_lines:
2222 if not line:
2223 continue
2224 m = re_path.match(line)
2225 if m:
2226 names.add(m.group(1))
2227 paths[m.group(1)] = m.group(2)
2228 continue
2229 m = re_url.match(line)
2230 if m:
2231 names.add(m.group(1))
2232 urls[m.group(1)] = m.group(2)
2233 continue
2234 names = sorted(names)
2235 return (
2236 [paths.get(name, "") for name in names],
2237 [urls.get(name, "") for name in names],
2238 )
2239
2240 def git_ls_tree(gitdir, rev, paths):
2241 cmd = ["ls-tree", rev, "--"]
2242 cmd.extend(paths)
2243 try:
2244 p = GitCommand(
2245 None,
2246 cmd,
2247 capture_stdout=True,
2248 capture_stderr=True,
2249 bare=True,
2250 gitdir=gitdir,
2251 )
2252 except GitError:
2253 return []
2254 if p.Wait() != 0:
2255 return []
2256 objects = {}
2257 for line in p.stdout.split("\n"):
2258 if not line.strip():
2259 continue
2260 object_rev, object_path = line.split()[2:4]
2261 objects[object_path] = object_rev
2262 return objects
2263
2264 try:
2265 rev = self.GetRevisionId()
2266 except GitError:
2267 return []
2268 return get_submodules(self.gitdir, rev)
2269
2270 def GetDerivedSubprojects(self):
2271 result = []
2272 if not self.Exists:
2273 # If git repo does not exist yet, querying its submodules will
2274 # mess up its states; so return here.
2275 return result
2276 for rev, path, url in self._GetSubmodules():
2277 name = self.manifest.GetSubprojectName(self, path)
2278 (
2279 relpath,
2280 worktree,
2281 gitdir,
2282 objdir,
2283 ) = self.manifest.GetSubprojectPaths(self, name, path)
2284 project = self.manifest.paths.get(relpath)
2285 if project:
2286 result.extend(project.GetDerivedSubprojects())
2287 continue
2288
2289 if url.startswith(".."):
2290 url = urllib.parse.urljoin("%s/" % self.remote.url, url)
2291 remote = RemoteSpec(
2292 self.remote.name,
2293 url=url,
2294 pushUrl=self.remote.pushUrl,
2295 review=self.remote.review,
2296 revision=self.remote.revision,
2297 )
2298 subproject = Project(
2299 manifest=self.manifest,
2300 name=name,
2301 remote=remote,
2302 gitdir=gitdir,
2303 objdir=objdir,
2304 worktree=worktree,
2305 relpath=relpath,
2306 revisionExpr=rev,
2307 revisionId=rev,
2308 rebase=self.rebase,
2309 groups=self.groups,
2310 sync_c=self.sync_c,
2311 sync_s=self.sync_s,
2312 sync_tags=self.sync_tags,
2313 parent=self,
2314 is_derived=True,
2315 )
2316 result.append(subproject)
2317 result.extend(subproject.GetDerivedSubprojects())
2318 return result
2319
2320 def EnableRepositoryExtension(self, key, value="true", version=1):
2321 """Enable git repository extension |key| with |value|.
2322
2323 Args:
2324 key: The extension to enabled. Omit the "extensions." prefix.
2325 value: The value to use for the extension.
2326 version: The minimum git repository version needed.
2327 """
2328 # Make sure the git repo version is new enough already.
2329 found_version = self.config.GetInt("core.repositoryFormatVersion")
2330 if found_version is None:
2331 found_version = 0
2332 if found_version < version:
2333 self.config.SetString("core.repositoryFormatVersion", str(version))
2334
2335 # Enable the extension!
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002336 self.config.SetString(f"extensions.{key}", value)
Gavin Makea2e3302023-03-11 06:46:20 +00002337
2338 def ResolveRemoteHead(self, name=None):
2339 """Find out what the default branch (HEAD) points to.
2340
2341 Normally this points to refs/heads/master, but projects are moving to
2342 main. Support whatever the server uses rather than hardcoding "master"
2343 ourselves.
2344 """
2345 if name is None:
2346 name = self.remote.name
2347
2348 # The output will look like (NB: tabs are separators):
2349 # ref: refs/heads/master HEAD
2350 # 5f6803b100bb3cd0f534e96e88c91373e8ed1c44 HEAD
2351 output = self.bare_git.ls_remote(
2352 "-q", "--symref", "--exit-code", name, "HEAD"
2353 )
2354
2355 for line in output.splitlines():
2356 lhs, rhs = line.split("\t", 1)
2357 if rhs == "HEAD" and lhs.startswith("ref:"):
2358 return lhs[4:].strip()
2359
2360 return None
2361
2362 def _CheckForImmutableRevision(self):
2363 try:
2364 # if revision (sha or tag) is not present then following function
2365 # throws an error.
2366 self.bare_git.rev_list(
Jason Chang87058c62023-09-27 11:34:43 -07002367 "-1",
2368 "--missing=allow-any",
2369 "%s^0" % self.revisionExpr,
2370 "--",
2371 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002372 )
2373 if self.upstream:
2374 rev = self.GetRemote().ToLocal(self.upstream)
2375 self.bare_git.rev_list(
Jason Chang87058c62023-09-27 11:34:43 -07002376 "-1",
2377 "--missing=allow-any",
2378 "%s^0" % rev,
2379 "--",
2380 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002381 )
2382 self.bare_git.merge_base(
Jason Chang87058c62023-09-27 11:34:43 -07002383 "--is-ancestor",
2384 self.revisionExpr,
2385 rev,
2386 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002387 )
2388 return True
2389 except GitError:
2390 # There is no such persistent revision. We have to fetch it.
2391 return False
2392
2393 def _FetchArchive(self, tarpath, cwd=None):
2394 cmd = ["archive", "-v", "-o", tarpath]
2395 cmd.append("--remote=%s" % self.remote.url)
2396 cmd.append("--prefix=%s/" % self.RelPath(local=False))
2397 cmd.append(self.revisionExpr)
2398
2399 command = GitCommand(
Jason Chang32b59562023-07-14 16:45:35 -07002400 self,
2401 cmd,
2402 cwd=cwd,
2403 capture_stdout=True,
2404 capture_stderr=True,
2405 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00002406 )
Jason Chang32b59562023-07-14 16:45:35 -07002407 command.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00002408
2409 def _RemoteFetch(
2410 self,
2411 name=None,
2412 current_branch_only=False,
2413 initial=False,
2414 quiet=False,
2415 verbose=False,
2416 output_redir=None,
2417 alt_dir=None,
2418 tags=True,
2419 prune=False,
2420 depth=None,
2421 submodules=False,
2422 ssh_proxy=None,
2423 force_sync=False,
2424 clone_filter=None,
2425 retry_fetches=2,
2426 retry_sleep_initial_sec=4.0,
2427 retry_exp_factor=2.0,
Jason Chang32b59562023-07-14 16:45:35 -07002428 ) -> bool:
Gavin Makea2e3302023-03-11 06:46:20 +00002429 tag_name = None
2430 # The depth should not be used when fetching to a mirror because
2431 # it will result in a shallow repository that cannot be cloned or
2432 # fetched from.
2433 # The repo project should also never be synced with partial depth.
2434 if self.manifest.IsMirror or self.relpath == ".repo/repo":
2435 depth = None
2436
2437 if depth:
2438 current_branch_only = True
2439
Sylvain56a5a012023-09-11 13:38:00 +02002440 is_sha1 = bool(IsId(self.revisionExpr))
Gavin Makea2e3302023-03-11 06:46:20 +00002441
2442 if current_branch_only:
2443 if self.revisionExpr.startswith(R_TAGS):
2444 # This is a tag and its commit id should never change.
2445 tag_name = self.revisionExpr[len(R_TAGS) :]
2446 elif self.upstream and self.upstream.startswith(R_TAGS):
2447 # This is a tag and its commit id should never change.
2448 tag_name = self.upstream[len(R_TAGS) :]
2449
2450 if is_sha1 or tag_name is not None:
2451 if self._CheckForImmutableRevision():
2452 if verbose:
2453 print(
2454 "Skipped fetching project %s (already have "
2455 "persistent ref)" % self.name
2456 )
2457 return True
2458 if is_sha1 and not depth:
2459 # When syncing a specific commit and --depth is not set:
2460 # * if upstream is explicitly specified and is not a sha1, fetch
2461 # only upstream as users expect only upstream to be fetch.
2462 # Note: The commit might not be in upstream in which case the
2463 # sync will fail.
2464 # * otherwise, fetch all branches to make sure we end up with
2465 # the specific commit.
2466 if self.upstream:
Sylvain56a5a012023-09-11 13:38:00 +02002467 current_branch_only = not IsId(self.upstream)
Gavin Makea2e3302023-03-11 06:46:20 +00002468 else:
2469 current_branch_only = False
2470
2471 if not name:
2472 name = self.remote.name
2473
2474 remote = self.GetRemote(name)
2475 if not remote.PreConnectFetch(ssh_proxy):
2476 ssh_proxy = None
2477
2478 if initial:
2479 if alt_dir and "objects" == os.path.basename(alt_dir):
2480 ref_dir = os.path.dirname(alt_dir)
2481 packed_refs = os.path.join(self.gitdir, "packed-refs")
2482
2483 all_refs = self.bare_ref.all
2484 ids = set(all_refs.values())
2485 tmp = set()
2486
2487 for r, ref_id in GitRefs(ref_dir).all.items():
2488 if r not in all_refs:
2489 if r.startswith(R_TAGS) or remote.WritesTo(r):
2490 all_refs[r] = ref_id
2491 ids.add(ref_id)
2492 continue
2493
2494 if ref_id in ids:
2495 continue
2496
2497 r = "refs/_alt/%s" % ref_id
2498 all_refs[r] = ref_id
2499 ids.add(ref_id)
2500 tmp.add(r)
2501
2502 tmp_packed_lines = []
2503 old_packed_lines = []
2504
2505 for r in sorted(all_refs):
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002506 line = f"{all_refs[r]} {r}\n"
Gavin Makea2e3302023-03-11 06:46:20 +00002507 tmp_packed_lines.append(line)
2508 if r not in tmp:
2509 old_packed_lines.append(line)
2510
2511 tmp_packed = "".join(tmp_packed_lines)
2512 old_packed = "".join(old_packed_lines)
2513 _lwrite(packed_refs, tmp_packed)
2514 else:
2515 alt_dir = None
2516
2517 cmd = ["fetch"]
2518
2519 if clone_filter:
2520 git_require((2, 19, 0), fail=True, msg="partial clones")
2521 cmd.append("--filter=%s" % clone_filter)
2522 self.EnableRepositoryExtension("partialclone", self.remote.name)
2523
2524 if depth:
2525 cmd.append("--depth=%s" % depth)
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002526 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002527 # If this repo has shallow objects, then we don't know which refs
2528 # have shallow objects or not. Tell git to unshallow all fetched
2529 # refs. Don't do this with projects that don't have shallow
2530 # objects, since it is less efficient.
2531 if os.path.exists(os.path.join(self.gitdir, "shallow")):
2532 cmd.append("--depth=2147483647")
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002533
Gavin Makea2e3302023-03-11 06:46:20 +00002534 if not verbose:
2535 cmd.append("--quiet")
2536 if not quiet and sys.stdout.isatty():
2537 cmd.append("--progress")
2538 if not self.worktree:
2539 cmd.append("--update-head-ok")
2540 cmd.append(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002541
Gavin Makea2e3302023-03-11 06:46:20 +00002542 if force_sync:
2543 cmd.append("--force")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002544
Gavin Makea2e3302023-03-11 06:46:20 +00002545 if prune:
2546 cmd.append("--prune")
Mike Frysinger29626b42021-05-01 09:37:13 -04002547
Gavin Makea2e3302023-03-11 06:46:20 +00002548 # Always pass something for --recurse-submodules, git with GIT_DIR
2549 # behaves incorrectly when not given `--recurse-submodules=no`.
2550 # (b/218891912)
2551 cmd.append(
2552 f'--recurse-submodules={"on-demand" if submodules else "no"}'
2553 )
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002554
Gavin Makea2e3302023-03-11 06:46:20 +00002555 spec = []
2556 if not current_branch_only:
2557 # Fetch whole repo.
2558 spec.append(
2559 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2560 )
2561 elif tag_name is not None:
2562 spec.append("tag")
2563 spec.append(tag_name)
Remy Böhmer1469c282020-12-15 18:49:02 +01002564
Gavin Makea2e3302023-03-11 06:46:20 +00002565 if self.manifest.IsMirror and not current_branch_only:
2566 branch = None
Remy Böhmer1469c282020-12-15 18:49:02 +01002567 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002568 branch = self.revisionExpr
2569 if (
2570 not self.manifest.IsMirror
2571 and is_sha1
2572 and depth
2573 and git_require((1, 8, 3))
2574 ):
2575 # Shallow checkout of a specific commit, fetch from that commit and
2576 # not the heads only as the commit might be deeper in the history.
2577 spec.append(branch)
2578 if self.upstream:
2579 spec.append(self.upstream)
David James8d201162013-10-11 17:03:19 -07002580 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002581 if is_sha1:
2582 branch = self.upstream
2583 if branch is not None and branch.strip():
2584 if not branch.startswith("refs/"):
2585 branch = R_HEADS + branch
2586 spec.append(str(("+%s:" % branch) + remote.ToLocal(branch)))
David James8d201162013-10-11 17:03:19 -07002587
Gavin Makea2e3302023-03-11 06:46:20 +00002588 # If mirroring repo and we cannot deduce the tag or branch to fetch,
2589 # fetch whole repo.
2590 if self.manifest.IsMirror and not spec:
2591 spec.append(
2592 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2593 )
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002594
Gavin Makea2e3302023-03-11 06:46:20 +00002595 # If using depth then we should not get all the tags since they may
2596 # be outside of the depth.
2597 if not tags or depth:
2598 cmd.append("--no-tags")
2599 else:
2600 cmd.append("--tags")
2601 spec.append(str(("+refs/tags/*:") + remote.ToLocal("refs/tags/*")))
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002602
Gavin Makea2e3302023-03-11 06:46:20 +00002603 cmd.extend(spec)
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002604
Gavin Makea2e3302023-03-11 06:46:20 +00002605 # At least one retry minimum due to git remote prune.
2606 retry_fetches = max(retry_fetches, 2)
2607 retry_cur_sleep = retry_sleep_initial_sec
2608 ok = prune_tried = False
2609 for try_n in range(retry_fetches):
Jason Chang32b59562023-07-14 16:45:35 -07002610 verify_command = try_n == retry_fetches - 1
Gavin Makea2e3302023-03-11 06:46:20 +00002611 gitcmd = GitCommand(
2612 self,
2613 cmd,
2614 bare=True,
2615 objdir=os.path.join(self.objdir, "objects"),
2616 ssh_proxy=ssh_proxy,
2617 merge_output=True,
2618 capture_stdout=quiet or bool(output_redir),
Jason Chang32b59562023-07-14 16:45:35 -07002619 verify_command=verify_command,
Gavin Makea2e3302023-03-11 06:46:20 +00002620 )
2621 if gitcmd.stdout and not quiet and output_redir:
2622 output_redir.write(gitcmd.stdout)
2623 ret = gitcmd.Wait()
2624 if ret == 0:
2625 ok = True
2626 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002627
Gavin Makea2e3302023-03-11 06:46:20 +00002628 # Retry later due to HTTP 429 Too Many Requests.
2629 elif (
2630 gitcmd.stdout
2631 and "error:" in gitcmd.stdout
2632 and "HTTP 429" in gitcmd.stdout
2633 ):
2634 # Fallthru to sleep+retry logic at the bottom.
2635 pass
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002636
Gavin Makea2e3302023-03-11 06:46:20 +00002637 # Try to prune remote branches once in case there are conflicts.
2638 # For example, if the remote had refs/heads/upstream, but deleted
2639 # that and now has refs/heads/upstream/foo.
2640 elif (
2641 gitcmd.stdout
2642 and "error:" in gitcmd.stdout
2643 and "git remote prune" in gitcmd.stdout
2644 and not prune_tried
2645 ):
2646 prune_tried = True
2647 prunecmd = GitCommand(
2648 self,
2649 ["remote", "prune", name],
2650 bare=True,
2651 ssh_proxy=ssh_proxy,
2652 )
2653 ret = prunecmd.Wait()
2654 if ret:
2655 break
2656 print(
2657 "retrying fetch after pruning remote branches",
2658 file=output_redir,
2659 )
2660 # Continue right away so we don't sleep as we shouldn't need to.
2661 continue
2662 elif current_branch_only and is_sha1 and ret == 128:
2663 # Exit code 128 means "couldn't find the ref you asked for"; if
2664 # we're in sha1 mode, we just tried sync'ing from the upstream
2665 # field; it doesn't exist, thus abort the optimization attempt
2666 # and do a full sync.
2667 break
2668 elif ret < 0:
2669 # Git died with a signal, exit immediately.
2670 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002671
Gavin Makea2e3302023-03-11 06:46:20 +00002672 # Figure out how long to sleep before the next attempt, if there is
2673 # one.
2674 if not verbose and gitcmd.stdout:
2675 print(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002676 f"\n{self.name}:\n{gitcmd.stdout}",
Gavin Makea2e3302023-03-11 06:46:20 +00002677 end="",
2678 file=output_redir,
2679 )
2680 if try_n < retry_fetches - 1:
2681 print(
2682 "%s: sleeping %s seconds before retrying"
2683 % (self.name, retry_cur_sleep),
2684 file=output_redir,
2685 )
2686 time.sleep(retry_cur_sleep)
2687 retry_cur_sleep = min(
2688 retry_exp_factor * retry_cur_sleep, MAXIMUM_RETRY_SLEEP_SEC
2689 )
2690 retry_cur_sleep *= 1 - random.uniform(
2691 -RETRY_JITTER_PERCENT, RETRY_JITTER_PERCENT
2692 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002693
Gavin Makea2e3302023-03-11 06:46:20 +00002694 if initial:
2695 if alt_dir:
2696 if old_packed != "":
2697 _lwrite(packed_refs, old_packed)
2698 else:
2699 platform_utils.remove(packed_refs)
2700 self.bare_git.pack_refs("--all", "--prune")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002701
Gavin Makea2e3302023-03-11 06:46:20 +00002702 if is_sha1 and current_branch_only:
2703 # We just synced the upstream given branch; verify we
2704 # got what we wanted, else trigger a second run of all
2705 # refs.
2706 if not self._CheckForImmutableRevision():
2707 # Sync the current branch only with depth set to None.
2708 # We always pass depth=None down to avoid infinite recursion.
2709 return self._RemoteFetch(
2710 name=name,
2711 quiet=quiet,
2712 verbose=verbose,
2713 output_redir=output_redir,
2714 current_branch_only=current_branch_only and depth,
2715 initial=False,
2716 alt_dir=alt_dir,
Nasser Grainawiaafed292023-05-24 12:51:03 -06002717 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00002718 depth=None,
2719 ssh_proxy=ssh_proxy,
2720 clone_filter=clone_filter,
2721 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002722
Gavin Makea2e3302023-03-11 06:46:20 +00002723 return ok
Mike Frysingerf4545122019-11-11 04:34:16 -05002724
Gavin Makea2e3302023-03-11 06:46:20 +00002725 def _ApplyCloneBundle(self, initial=False, quiet=False, verbose=False):
2726 if initial and (
2727 self.manifest.manifestProject.depth or self.clone_depth
2728 ):
2729 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002730
Gavin Makea2e3302023-03-11 06:46:20 +00002731 remote = self.GetRemote()
2732 bundle_url = remote.url + "/clone.bundle"
2733 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
2734 if GetSchemeFromUrl(bundle_url) not in (
2735 "http",
2736 "https",
2737 "persistent-http",
2738 "persistent-https",
2739 ):
2740 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06002741
Gavin Makea2e3302023-03-11 06:46:20 +00002742 bundle_dst = os.path.join(self.gitdir, "clone.bundle")
2743 bundle_tmp = os.path.join(self.gitdir, "clone.bundle.tmp")
2744
2745 exist_dst = os.path.exists(bundle_dst)
2746 exist_tmp = os.path.exists(bundle_tmp)
2747
2748 if not initial and not exist_dst and not exist_tmp:
2749 return False
2750
2751 if not exist_dst:
2752 exist_dst = self._FetchBundle(
2753 bundle_url, bundle_tmp, bundle_dst, quiet, verbose
2754 )
2755 if not exist_dst:
2756 return False
2757
2758 cmd = ["fetch"]
2759 if not verbose:
2760 cmd.append("--quiet")
2761 if not quiet and sys.stdout.isatty():
2762 cmd.append("--progress")
2763 if not self.worktree:
2764 cmd.append("--update-head-ok")
2765 cmd.append(bundle_dst)
2766 for f in remote.fetch:
2767 cmd.append(str(f))
2768 cmd.append("+refs/tags/*:refs/tags/*")
2769
2770 ok = (
2771 GitCommand(
2772 self,
2773 cmd,
2774 bare=True,
2775 objdir=os.path.join(self.objdir, "objects"),
2776 ).Wait()
2777 == 0
2778 )
2779 platform_utils.remove(bundle_dst, missing_ok=True)
2780 platform_utils.remove(bundle_tmp, missing_ok=True)
2781 return ok
2782
2783 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
2784 platform_utils.remove(dstPath, missing_ok=True)
2785
2786 cmd = ["curl", "--fail", "--output", tmpPath, "--netrc", "--location"]
2787 if quiet:
2788 cmd += ["--silent", "--show-error"]
2789 if os.path.exists(tmpPath):
2790 size = os.stat(tmpPath).st_size
2791 if size >= 1024:
2792 cmd += ["--continue-at", "%d" % (size,)]
2793 else:
2794 platform_utils.remove(tmpPath)
2795 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
2796 if cookiefile:
2797 cmd += ["--cookie", cookiefile]
2798 if proxy:
2799 cmd += ["--proxy", proxy]
2800 elif "http_proxy" in os.environ and "darwin" == sys.platform:
2801 cmd += ["--proxy", os.environ["http_proxy"]]
2802 if srcUrl.startswith("persistent-https"):
2803 srcUrl = "http" + srcUrl[len("persistent-https") :]
2804 elif srcUrl.startswith("persistent-http"):
2805 srcUrl = "http" + srcUrl[len("persistent-http") :]
2806 cmd += [srcUrl]
2807
2808 proc = None
2809 with Trace("Fetching bundle: %s", " ".join(cmd)):
2810 if verbose:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002811 print(f"{self.name}: Downloading bundle: {srcUrl}")
Gavin Makea2e3302023-03-11 06:46:20 +00002812 stdout = None if verbose else subprocess.PIPE
2813 stderr = None if verbose else subprocess.STDOUT
2814 try:
2815 proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
2816 except OSError:
2817 return False
2818
2819 (output, _) = proc.communicate()
2820 curlret = proc.returncode
2821
2822 if curlret == 22:
2823 # From curl man page:
2824 # 22: HTTP page not retrieved. The requested url was not found
2825 # or returned another error with the HTTP error code being 400
2826 # or above. This return code only appears if -f, --fail is used.
2827 if verbose:
2828 print(
2829 "%s: Unable to retrieve clone.bundle; ignoring."
2830 % self.name
2831 )
2832 if output:
2833 print("Curl output:\n%s" % output)
2834 return False
2835 elif curlret and not verbose and output:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002836 logger.error("%s", output)
Gavin Makea2e3302023-03-11 06:46:20 +00002837
2838 if os.path.exists(tmpPath):
2839 if curlret == 0 and self._IsValidBundle(tmpPath, quiet):
2840 platform_utils.rename(tmpPath, dstPath)
2841 return True
2842 else:
2843 platform_utils.remove(tmpPath)
2844 return False
2845 else:
2846 return False
2847
2848 def _IsValidBundle(self, path, quiet):
2849 try:
2850 with open(path, "rb") as f:
2851 if f.read(16) == b"# v2 git bundle\n":
2852 return True
2853 else:
2854 if not quiet:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002855 logger.error("Invalid clone.bundle file; ignoring.")
Gavin Makea2e3302023-03-11 06:46:20 +00002856 return False
2857 except OSError:
2858 return False
2859
2860 def _Checkout(self, rev, quiet=False):
2861 cmd = ["checkout"]
2862 if quiet:
2863 cmd.append("-q")
2864 cmd.append(rev)
2865 cmd.append("--")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002866 if GitCommand(self, cmd).Wait() != 0:
Gavin Makea2e3302023-03-11 06:46:20 +00002867 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002868 raise GitError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002869 f"{self.name} checkout {rev} ", project=self.name
Jason Chang32b59562023-07-14 16:45:35 -07002870 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002871
Gavin Makea2e3302023-03-11 06:46:20 +00002872 def _CherryPick(self, rev, ffonly=False, record_origin=False):
2873 cmd = ["cherry-pick"]
2874 if ffonly:
2875 cmd.append("--ff")
2876 if record_origin:
2877 cmd.append("-x")
2878 cmd.append(rev)
2879 cmd.append("--")
2880 if GitCommand(self, cmd).Wait() != 0:
2881 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002882 raise GitError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002883 f"{self.name} cherry-pick {rev} ", project=self.name
Jason Chang32b59562023-07-14 16:45:35 -07002884 )
Victor Boivie0960b5b2010-11-26 13:42:13 +01002885
Gavin Makea2e3302023-03-11 06:46:20 +00002886 def _LsRemote(self, refs):
2887 cmd = ["ls-remote", self.remote.name, refs]
2888 p = GitCommand(self, cmd, capture_stdout=True)
2889 if p.Wait() == 0:
2890 return p.stdout
2891 return None
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002892
Gavin Makea2e3302023-03-11 06:46:20 +00002893 def _Revert(self, rev):
2894 cmd = ["revert"]
2895 cmd.append("--no-edit")
2896 cmd.append(rev)
2897 cmd.append("--")
2898 if GitCommand(self, cmd).Wait() != 0:
2899 if self._allrefs:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002900 raise GitError(f"{self.name} revert {rev} ", project=self.name)
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002901
Gavin Makea2e3302023-03-11 06:46:20 +00002902 def _ResetHard(self, rev, quiet=True):
2903 cmd = ["reset", "--hard"]
2904 if quiet:
2905 cmd.append("-q")
2906 cmd.append(rev)
2907 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002908 raise GitError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002909 f"{self.name} reset --hard {rev} ", project=self.name
Jason Chang32b59562023-07-14 16:45:35 -07002910 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002911
Gavin Makea2e3302023-03-11 06:46:20 +00002912 def _SyncSubmodules(self, quiet=True):
2913 cmd = ["submodule", "update", "--init", "--recursive"]
2914 if quiet:
2915 cmd.append("-q")
2916 if GitCommand(self, cmd).Wait() != 0:
2917 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07002918 "%s submodule update --init --recursive " % self.name,
2919 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00002920 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002921
Gavin Makea2e3302023-03-11 06:46:20 +00002922 def _Rebase(self, upstream, onto=None):
2923 cmd = ["rebase"]
2924 if onto is not None:
2925 cmd.extend(["--onto", onto])
2926 cmd.append(upstream)
2927 if GitCommand(self, cmd).Wait() != 0:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002928 raise GitError(f"{self.name} rebase {upstream} ", project=self.name)
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002929
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08002930 def _FastForward(self, head, ffonly=False, quiet=True):
Gavin Makea2e3302023-03-11 06:46:20 +00002931 cmd = ["merge", "--no-stat", head]
2932 if ffonly:
2933 cmd.append("--ff-only")
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08002934 if quiet:
2935 cmd.append("-q")
Gavin Makea2e3302023-03-11 06:46:20 +00002936 if GitCommand(self, cmd).Wait() != 0:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002937 raise GitError(f"{self.name} merge {head} ", project=self.name)
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002938
Gavin Makea2e3302023-03-11 06:46:20 +00002939 def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False):
2940 init_git_dir = not os.path.exists(self.gitdir)
2941 init_obj_dir = not os.path.exists(self.objdir)
2942 try:
2943 # Initialize the bare repository, which contains all of the objects.
2944 if init_obj_dir:
2945 os.makedirs(self.objdir)
2946 self.bare_objdir.init()
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002947
Gavin Makea2e3302023-03-11 06:46:20 +00002948 self._UpdateHooks(quiet=quiet)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002949
Gavin Makea2e3302023-03-11 06:46:20 +00002950 if self.use_git_worktrees:
2951 # Enable per-worktree config file support if possible. This
2952 # is more a nice-to-have feature for users rather than a
2953 # hard requirement.
2954 if git_require((2, 20, 0)):
2955 self.EnableRepositoryExtension("worktreeConfig")
Renaud Paquay788e9622017-01-27 11:41:12 -08002956
Gavin Makea2e3302023-03-11 06:46:20 +00002957 # If we have a separate directory to hold refs, initialize it as
2958 # well.
2959 if self.objdir != self.gitdir:
2960 if init_git_dir:
2961 os.makedirs(self.gitdir)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002962
Gavin Makea2e3302023-03-11 06:46:20 +00002963 if init_obj_dir or init_git_dir:
2964 self._ReferenceGitDir(
2965 self.objdir, self.gitdir, copy_all=True
2966 )
2967 try:
2968 self._CheckDirReference(self.objdir, self.gitdir)
2969 except GitError as e:
2970 if force_sync:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002971 logger.error(
2972 "Retrying clone after deleting %s", self.gitdir
Gavin Makea2e3302023-03-11 06:46:20 +00002973 )
2974 try:
2975 platform_utils.rmtree(
2976 platform_utils.realpath(self.gitdir)
2977 )
2978 if self.worktree and os.path.exists(
2979 platform_utils.realpath(self.worktree)
2980 ):
2981 platform_utils.rmtree(
2982 platform_utils.realpath(self.worktree)
2983 )
2984 return self._InitGitDir(
2985 mirror_git=mirror_git,
2986 force_sync=False,
2987 quiet=quiet,
2988 )
2989 except Exception:
2990 raise e
2991 raise e
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002992
Gavin Makea2e3302023-03-11 06:46:20 +00002993 if init_git_dir:
2994 mp = self.manifest.manifestProject
2995 ref_dir = mp.reference or ""
Julien Camperguedd654222014-01-09 16:21:37 +01002996
Gavin Makea2e3302023-03-11 06:46:20 +00002997 def _expanded_ref_dirs():
2998 """Iterate through possible git reference dir paths."""
2999 name = self.name + ".git"
3000 yield mirror_git or os.path.join(ref_dir, name)
3001 for prefix in "", self.remote.name:
3002 yield os.path.join(
3003 ref_dir, ".repo", "project-objects", prefix, name
3004 )
3005 yield os.path.join(
3006 ref_dir, ".repo", "worktrees", prefix, name
3007 )
3008
3009 if ref_dir or mirror_git:
3010 found_ref_dir = None
3011 for path in _expanded_ref_dirs():
3012 if os.path.exists(path):
3013 found_ref_dir = path
3014 break
3015 ref_dir = found_ref_dir
3016
3017 if ref_dir:
3018 if not os.path.isabs(ref_dir):
3019 # The alternate directory is relative to the object
3020 # database.
3021 ref_dir = os.path.relpath(
3022 ref_dir, os.path.join(self.objdir, "objects")
3023 )
3024 _lwrite(
3025 os.path.join(
3026 self.objdir, "objects/info/alternates"
3027 ),
3028 os.path.join(ref_dir, "objects") + "\n",
3029 )
3030
3031 m = self.manifest.manifestProject.config
3032 for key in ["user.name", "user.email"]:
3033 if m.Has(key, include_defaults=False):
3034 self.config.SetString(key, m.GetString(key))
3035 if not self.manifest.EnableGitLfs:
3036 self.config.SetString(
3037 "filter.lfs.smudge", "git-lfs smudge --skip -- %f"
3038 )
3039 self.config.SetString(
3040 "filter.lfs.process", "git-lfs filter-process --skip"
3041 )
3042 self.config.SetBoolean(
3043 "core.bare", True if self.manifest.IsMirror else None
3044 )
Josip Sokcevic9267d582023-10-19 14:46:11 -07003045
3046 if not init_obj_dir:
3047 # The project might be shared (obj_dir already initialized), but
3048 # such information is not available here. Instead of passing it,
3049 # set it as shared, and rely to be unset down the execution
3050 # path.
3051 if git_require((2, 7, 0)):
3052 self.EnableRepositoryExtension("preciousObjects")
3053 else:
3054 self.config.SetString("gc.pruneExpire", "never")
3055
Gavin Makea2e3302023-03-11 06:46:20 +00003056 except Exception:
3057 if init_obj_dir and os.path.exists(self.objdir):
3058 platform_utils.rmtree(self.objdir)
3059 if init_git_dir and os.path.exists(self.gitdir):
3060 platform_utils.rmtree(self.gitdir)
3061 raise
3062
3063 def _UpdateHooks(self, quiet=False):
3064 if os.path.exists(self.objdir):
3065 self._InitHooks(quiet=quiet)
3066
3067 def _InitHooks(self, quiet=False):
3068 hooks = platform_utils.realpath(os.path.join(self.objdir, "hooks"))
3069 if not os.path.exists(hooks):
3070 os.makedirs(hooks)
3071
3072 # Delete sample hooks. They're noise.
3073 for hook in glob.glob(os.path.join(hooks, "*.sample")):
3074 try:
3075 platform_utils.remove(hook, missing_ok=True)
3076 except PermissionError:
3077 pass
3078
3079 for stock_hook in _ProjectHooks():
3080 name = os.path.basename(stock_hook)
3081
3082 if (
3083 name in ("commit-msg",)
3084 and not self.remote.review
3085 and self is not self.manifest.manifestProject
3086 ):
3087 # Don't install a Gerrit Code Review hook if this
3088 # project does not appear to use it for reviews.
3089 #
3090 # Since the manifest project is one of those, but also
3091 # managed through gerrit, it's excluded.
3092 continue
3093
3094 dst = os.path.join(hooks, name)
3095 if platform_utils.islink(dst):
3096 continue
3097 if os.path.exists(dst):
3098 # If the files are the same, we'll leave it alone. We create
3099 # symlinks below by default but fallback to hardlinks if the OS
3100 # blocks them. So if we're here, it's probably because we made a
3101 # hardlink below.
3102 if not filecmp.cmp(stock_hook, dst, shallow=False):
3103 if not quiet:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00003104 logger.warning(
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00003105 "warn: %s: Not replacing locally modified %s hook",
Gavin Makea2e3302023-03-11 06:46:20 +00003106 self.RelPath(local=False),
3107 name,
3108 )
3109 continue
3110 try:
3111 platform_utils.symlink(
3112 os.path.relpath(stock_hook, os.path.dirname(dst)), dst
3113 )
3114 except OSError as e:
3115 if e.errno == errno.EPERM:
3116 try:
3117 os.link(stock_hook, dst)
3118 except OSError:
Jason Chang32b59562023-07-14 16:45:35 -07003119 raise GitError(
3120 self._get_symlink_error_message(), project=self.name
3121 )
Gavin Makea2e3302023-03-11 06:46:20 +00003122 else:
3123 raise
3124
3125 def _InitRemote(self):
3126 if self.remote.url:
3127 remote = self.GetRemote()
3128 remote.url = self.remote.url
3129 remote.pushUrl = self.remote.pushUrl
3130 remote.review = self.remote.review
3131 remote.projectname = self.name
3132
3133 if self.worktree:
3134 remote.ResetFetch(mirror=False)
3135 else:
3136 remote.ResetFetch(mirror=True)
3137 remote.Save()
3138
3139 def _InitMRef(self):
3140 """Initialize the pseudo m/<manifest branch> ref."""
3141 if self.manifest.branch:
3142 if self.use_git_worktrees:
3143 # Set up the m/ space to point to the worktree-specific ref
3144 # space. We'll update the worktree-specific ref space on each
3145 # checkout.
3146 ref = R_M + self.manifest.branch
3147 if not self.bare_ref.symref(ref):
3148 self.bare_git.symbolic_ref(
3149 "-m",
3150 "redirecting to worktree scope",
3151 ref,
3152 R_WORKTREE_M + self.manifest.branch,
3153 )
3154
3155 # We can't update this ref with git worktrees until it exists.
3156 # We'll wait until the initial checkout to set it.
3157 if not os.path.exists(self.worktree):
3158 return
3159
3160 base = R_WORKTREE_M
3161 active_git = self.work_git
3162
3163 self._InitAnyMRef(HEAD, self.bare_git, detach=True)
3164 else:
3165 base = R_M
3166 active_git = self.bare_git
3167
3168 self._InitAnyMRef(base + self.manifest.branch, active_git)
3169
3170 def _InitMirrorHead(self):
3171 self._InitAnyMRef(HEAD, self.bare_git)
3172
3173 def _InitAnyMRef(self, ref, active_git, detach=False):
3174 """Initialize |ref| in |active_git| to the value in the manifest.
3175
3176 This points |ref| to the <project> setting in the manifest.
3177
3178 Args:
3179 ref: The branch to update.
3180 active_git: The git repository to make updates in.
3181 detach: Whether to update target of symbolic refs, or overwrite the
3182 ref directly (and thus make it non-symbolic).
3183 """
3184 cur = self.bare_ref.symref(ref)
3185
3186 if self.revisionId:
3187 if cur != "" or self.bare_ref.get(ref) != self.revisionId:
3188 msg = "manifest set to %s" % self.revisionId
3189 dst = self.revisionId + "^0"
3190 active_git.UpdateRef(ref, dst, message=msg, detach=True)
Julien Camperguedd654222014-01-09 16:21:37 +01003191 else:
Gavin Makea2e3302023-03-11 06:46:20 +00003192 remote = self.GetRemote()
3193 dst = remote.ToLocal(self.revisionExpr)
3194 if cur != dst:
3195 msg = "manifest set to %s" % self.revisionExpr
3196 if detach:
3197 active_git.UpdateRef(ref, dst, message=msg, detach=True)
3198 else:
3199 active_git.symbolic_ref("-m", msg, ref, dst)
Julien Camperguedd654222014-01-09 16:21:37 +01003200
Gavin Makea2e3302023-03-11 06:46:20 +00003201 def _CheckDirReference(self, srcdir, destdir):
3202 # Git worktrees don't use symlinks to share at all.
3203 if self.use_git_worktrees:
3204 return
Julien Camperguedd654222014-01-09 16:21:37 +01003205
Gavin Makea2e3302023-03-11 06:46:20 +00003206 for name in self.shareable_dirs:
3207 # Try to self-heal a bit in simple cases.
3208 dst_path = os.path.join(destdir, name)
3209 src_path = os.path.join(srcdir, name)
Julien Camperguedd654222014-01-09 16:21:37 +01003210
Gavin Makea2e3302023-03-11 06:46:20 +00003211 dst = platform_utils.realpath(dst_path)
3212 if os.path.lexists(dst):
3213 src = platform_utils.realpath(src_path)
3214 # Fail if the links are pointing to the wrong place.
3215 if src != dst:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00003216 logger.error(
3217 "error: %s is different in %s vs %s",
3218 name,
3219 destdir,
3220 srcdir,
3221 )
Gavin Makea2e3302023-03-11 06:46:20 +00003222 raise GitError(
3223 "--force-sync not enabled; cannot overwrite a local "
3224 "work tree. If you're comfortable with the "
3225 "possibility of losing the work tree's git metadata,"
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003226 " use "
3227 f"`repo sync --force-sync {self.RelPath(local=False)}` "
3228 "to proceed.",
Jason Chang32b59562023-07-14 16:45:35 -07003229 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003230 )
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003231
Gavin Makea2e3302023-03-11 06:46:20 +00003232 def _ReferenceGitDir(self, gitdir, dotgit, copy_all):
3233 """Update |dotgit| to reference |gitdir|, using symlinks where possible.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003234
Gavin Makea2e3302023-03-11 06:46:20 +00003235 Args:
3236 gitdir: The bare git repository. Must already be initialized.
3237 dotgit: The repository you would like to initialize.
3238 copy_all: If true, copy all remaining files from |gitdir| ->
3239 |dotgit|. This saves you the effort of initializing |dotgit|
3240 yourself.
3241 """
3242 symlink_dirs = self.shareable_dirs[:]
3243 to_symlink = symlink_dirs
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003244
Gavin Makea2e3302023-03-11 06:46:20 +00003245 to_copy = []
3246 if copy_all:
3247 to_copy = platform_utils.listdir(gitdir)
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003248
Gavin Makea2e3302023-03-11 06:46:20 +00003249 dotgit = platform_utils.realpath(dotgit)
3250 for name in set(to_copy).union(to_symlink):
3251 try:
3252 src = platform_utils.realpath(os.path.join(gitdir, name))
3253 dst = os.path.join(dotgit, name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003254
Gavin Makea2e3302023-03-11 06:46:20 +00003255 if os.path.lexists(dst):
3256 continue
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003257
Gavin Makea2e3302023-03-11 06:46:20 +00003258 # If the source dir doesn't exist, create an empty dir.
3259 if name in symlink_dirs and not os.path.lexists(src):
3260 os.makedirs(src)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003261
Gavin Makea2e3302023-03-11 06:46:20 +00003262 if name in to_symlink:
3263 platform_utils.symlink(
3264 os.path.relpath(src, os.path.dirname(dst)), dst
3265 )
3266 elif copy_all and not platform_utils.islink(dst):
3267 if platform_utils.isdir(src):
3268 shutil.copytree(src, dst)
3269 elif os.path.isfile(src):
3270 shutil.copy(src, dst)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003271
Gavin Makea2e3302023-03-11 06:46:20 +00003272 except OSError as e:
3273 if e.errno == errno.EPERM:
3274 raise DownloadError(self._get_symlink_error_message())
3275 else:
3276 raise
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003277
Gavin Makea2e3302023-03-11 06:46:20 +00003278 def _InitGitWorktree(self):
3279 """Init the project using git worktrees."""
3280 self.bare_git.worktree("prune")
3281 self.bare_git.worktree(
3282 "add",
3283 "-ff",
3284 "--checkout",
3285 "--detach",
3286 "--lock",
3287 self.worktree,
3288 self.GetRevisionId(),
3289 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003290
Gavin Makea2e3302023-03-11 06:46:20 +00003291 # Rewrite the internal state files to use relative paths between the
3292 # checkouts & worktrees.
3293 dotgit = os.path.join(self.worktree, ".git")
Jason R. Coombs034950b2023-10-20 23:32:02 +05453294 with open(dotgit) as fp:
Gavin Makea2e3302023-03-11 06:46:20 +00003295 # Figure out the checkout->worktree path.
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003296 setting = fp.read()
Gavin Makea2e3302023-03-11 06:46:20 +00003297 assert setting.startswith("gitdir:")
3298 git_worktree_path = setting.split(":", 1)[1].strip()
3299 # Some platforms (e.g. Windows) won't let us update dotgit in situ
3300 # because of file permissions. Delete it and recreate it from scratch
3301 # to avoid.
3302 platform_utils.remove(dotgit)
3303 # Use relative path from checkout->worktree & maintain Unix line endings
3304 # on all OS's to match git behavior.
3305 with open(dotgit, "w", newline="\n") as fp:
3306 print(
3307 "gitdir:",
3308 os.path.relpath(git_worktree_path, self.worktree),
3309 file=fp,
3310 )
3311 # Use relative path from worktree->checkout & maintain Unix line endings
3312 # on all OS's to match git behavior.
3313 with open(
3314 os.path.join(git_worktree_path, "gitdir"), "w", newline="\n"
3315 ) as fp:
3316 print(os.path.relpath(dotgit, git_worktree_path), file=fp)
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003317
Gavin Makea2e3302023-03-11 06:46:20 +00003318 self._InitMRef()
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003319
Gavin Makea2e3302023-03-11 06:46:20 +00003320 def _InitWorkTree(self, force_sync=False, submodules=False):
3321 """Setup the worktree .git path.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003322
Gavin Makea2e3302023-03-11 06:46:20 +00003323 This is the user-visible path like src/foo/.git/.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003324
Gavin Makea2e3302023-03-11 06:46:20 +00003325 With non-git-worktrees, this will be a symlink to the .repo/projects/
3326 path. With git-worktrees, this will be a .git file using "gitdir: ..."
3327 syntax.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003328
Gavin Makea2e3302023-03-11 06:46:20 +00003329 Older checkouts had .git/ directories. If we see that, migrate it.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003330
Gavin Makea2e3302023-03-11 06:46:20 +00003331 This also handles changes in the manifest. Maybe this project was
3332 backed by "foo/bar" on the server, but now it's "new/foo/bar". We have
3333 to update the path we point to under .repo/projects/ to match.
3334 """
3335 dotgit = os.path.join(self.worktree, ".git")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003336
Gavin Makea2e3302023-03-11 06:46:20 +00003337 # If using an old layout style (a directory), migrate it.
3338 if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
Jason Chang32b59562023-07-14 16:45:35 -07003339 self._MigrateOldWorkTreeGitDir(dotgit, project=self.name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003340
Gavin Makea2e3302023-03-11 06:46:20 +00003341 init_dotgit = not os.path.exists(dotgit)
3342 if self.use_git_worktrees:
3343 if init_dotgit:
3344 self._InitGitWorktree()
3345 self._CopyAndLinkFiles()
3346 else:
3347 if not init_dotgit:
3348 # See if the project has changed.
3349 if platform_utils.realpath(
3350 self.gitdir
3351 ) != platform_utils.realpath(dotgit):
3352 platform_utils.remove(dotgit)
Doug Anderson37282b42011-03-04 11:54:18 -08003353
Gavin Makea2e3302023-03-11 06:46:20 +00003354 if init_dotgit or not os.path.exists(dotgit):
3355 os.makedirs(self.worktree, exist_ok=True)
3356 platform_utils.symlink(
3357 os.path.relpath(self.gitdir, self.worktree), dotgit
3358 )
Doug Anderson37282b42011-03-04 11:54:18 -08003359
Gavin Makea2e3302023-03-11 06:46:20 +00003360 if init_dotgit:
3361 _lwrite(
3362 os.path.join(dotgit, HEAD), "%s\n" % self.GetRevisionId()
3363 )
Doug Anderson37282b42011-03-04 11:54:18 -08003364
Gavin Makea2e3302023-03-11 06:46:20 +00003365 # Finish checking out the worktree.
3366 cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
3367 if GitCommand(self, cmd).Wait() != 0:
3368 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07003369 "Cannot initialize work tree for " + self.name,
3370 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003371 )
Doug Anderson37282b42011-03-04 11:54:18 -08003372
Gavin Makea2e3302023-03-11 06:46:20 +00003373 if submodules:
3374 self._SyncSubmodules(quiet=True)
3375 self._CopyAndLinkFiles()
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003376
Gavin Makea2e3302023-03-11 06:46:20 +00003377 @classmethod
Jason Chang32b59562023-07-14 16:45:35 -07003378 def _MigrateOldWorkTreeGitDir(cls, dotgit, project=None):
Gavin Makea2e3302023-03-11 06:46:20 +00003379 """Migrate the old worktree .git/ dir style to a symlink.
3380
3381 This logic specifically only uses state from |dotgit| to figure out
3382 where to move content and not |self|. This way if the backing project
3383 also changed places, we only do the .git/ dir to .git symlink migration
3384 here. The path updates will happen independently.
3385 """
3386 # Figure out where in .repo/projects/ it's pointing to.
3387 if not os.path.islink(os.path.join(dotgit, "refs")):
Jason Chang32b59562023-07-14 16:45:35 -07003388 raise GitError(
3389 f"{dotgit}: unsupported checkout state", project=project
3390 )
Gavin Makea2e3302023-03-11 06:46:20 +00003391 gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, "refs")))
3392
3393 # Remove known symlink paths that exist in .repo/projects/.
3394 KNOWN_LINKS = {
3395 "config",
3396 "description",
3397 "hooks",
3398 "info",
3399 "logs",
3400 "objects",
3401 "packed-refs",
3402 "refs",
3403 "rr-cache",
3404 "shallow",
3405 "svn",
3406 }
3407 # Paths that we know will be in both, but are safe to clobber in
3408 # .repo/projects/.
3409 SAFE_TO_CLOBBER = {
3410 "COMMIT_EDITMSG",
3411 "FETCH_HEAD",
3412 "HEAD",
3413 "gc.log",
3414 "gitk.cache",
3415 "index",
3416 "ORIG_HEAD",
3417 }
3418
3419 # First see if we'd succeed before starting the migration.
3420 unknown_paths = []
3421 for name in platform_utils.listdir(dotgit):
3422 # Ignore all temporary/backup names. These are common with vim &
3423 # emacs.
3424 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3425 continue
3426
3427 dotgit_path = os.path.join(dotgit, name)
3428 if name in KNOWN_LINKS:
3429 if not platform_utils.islink(dotgit_path):
3430 unknown_paths.append(f"{dotgit_path}: should be a symlink")
3431 else:
3432 gitdir_path = os.path.join(gitdir, name)
3433 if name not in SAFE_TO_CLOBBER and os.path.exists(gitdir_path):
3434 unknown_paths.append(
3435 f"{dotgit_path}: unknown file; please file a bug"
3436 )
3437 if unknown_paths:
Jason Chang32b59562023-07-14 16:45:35 -07003438 raise GitError(
3439 "Aborting migration: " + "\n".join(unknown_paths),
3440 project=project,
3441 )
Gavin Makea2e3302023-03-11 06:46:20 +00003442
3443 # Now walk the paths and sync the .git/ to .repo/projects/.
3444 for name in platform_utils.listdir(dotgit):
3445 dotgit_path = os.path.join(dotgit, name)
3446
3447 # Ignore all temporary/backup names. These are common with vim &
3448 # emacs.
3449 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3450 platform_utils.remove(dotgit_path)
3451 elif name in KNOWN_LINKS:
3452 platform_utils.remove(dotgit_path)
3453 else:
3454 gitdir_path = os.path.join(gitdir, name)
3455 platform_utils.remove(gitdir_path, missing_ok=True)
3456 platform_utils.rename(dotgit_path, gitdir_path)
3457
3458 # Now that the dir should be empty, clear it out, and symlink it over.
3459 platform_utils.rmdir(dotgit)
3460 platform_utils.symlink(
Jason R. Coombs47944bb2023-09-29 12:42:22 -04003461 os.path.relpath(gitdir, os.path.dirname(os.path.realpath(dotgit))),
3462 dotgit,
Gavin Makea2e3302023-03-11 06:46:20 +00003463 )
3464
3465 def _get_symlink_error_message(self):
3466 if platform_utils.isWindows():
3467 return (
3468 "Unable to create symbolic link. Please re-run the command as "
3469 "Administrator, or see "
3470 "https://github.com/git-for-windows/git/wiki/Symbolic-Links "
3471 "for other options."
3472 )
3473 return "filesystem must support symlinks"
3474
3475 def _revlist(self, *args, **kw):
3476 a = []
3477 a.extend(args)
3478 a.append("--")
3479 return self.work_git.rev_list(*a, **kw)
3480
3481 @property
3482 def _allrefs(self):
3483 return self.bare_ref.all
3484
3485 def _getLogs(
3486 self, rev1, rev2, oneline=False, color=True, pretty_format=None
3487 ):
3488 """Get logs between two revisions of this project."""
3489 comp = ".."
3490 if rev1:
3491 revs = [rev1]
3492 if rev2:
3493 revs.extend([comp, rev2])
3494 cmd = ["log", "".join(revs)]
3495 out = DiffColoring(self.config)
3496 if out.is_on and color:
3497 cmd.append("--color")
3498 if pretty_format is not None:
3499 cmd.append("--pretty=format:%s" % pretty_format)
3500 if oneline:
3501 cmd.append("--oneline")
3502
3503 try:
3504 log = GitCommand(
3505 self, cmd, capture_stdout=True, capture_stderr=True
3506 )
3507 if log.Wait() == 0:
3508 return log.stdout
3509 except GitError:
3510 # worktree may not exist if groups changed for example. In that
3511 # case, try in gitdir instead.
3512 if not os.path.exists(self.worktree):
3513 return self.bare_git.log(*cmd[1:])
3514 else:
3515 raise
3516 return None
3517
3518 def getAddedAndRemovedLogs(
3519 self, toProject, oneline=False, color=True, pretty_format=None
3520 ):
3521 """Get the list of logs from this revision to given revisionId"""
3522 logs = {}
3523 selfId = self.GetRevisionId(self._allrefs)
3524 toId = toProject.GetRevisionId(toProject._allrefs)
3525
3526 logs["added"] = self._getLogs(
3527 selfId,
3528 toId,
3529 oneline=oneline,
3530 color=color,
3531 pretty_format=pretty_format,
3532 )
3533 logs["removed"] = self._getLogs(
3534 toId,
3535 selfId,
3536 oneline=oneline,
3537 color=color,
3538 pretty_format=pretty_format,
3539 )
3540 return logs
3541
Mike Frysingerd4aee652023-10-19 05:13:32 -04003542 class _GitGetByExec:
Gavin Makea2e3302023-03-11 06:46:20 +00003543 def __init__(self, project, bare, gitdir):
3544 self._project = project
3545 self._bare = bare
3546 self._gitdir = gitdir
3547
3548 # __getstate__ and __setstate__ are required for pickling because
3549 # __getattr__ exists.
3550 def __getstate__(self):
3551 return (self._project, self._bare, self._gitdir)
3552
3553 def __setstate__(self, state):
3554 self._project, self._bare, self._gitdir = state
3555
3556 def LsOthers(self):
3557 p = GitCommand(
3558 self._project,
3559 ["ls-files", "-z", "--others", "--exclude-standard"],
3560 bare=False,
3561 gitdir=self._gitdir,
3562 capture_stdout=True,
3563 capture_stderr=True,
3564 )
3565 if p.Wait() == 0:
3566 out = p.stdout
3567 if out:
3568 # Backslash is not anomalous.
3569 return out[:-1].split("\0")
3570 return []
3571
3572 def DiffZ(self, name, *args):
3573 cmd = [name]
3574 cmd.append("-z")
3575 cmd.append("--ignore-submodules")
3576 cmd.extend(args)
3577 p = GitCommand(
3578 self._project,
3579 cmd,
3580 gitdir=self._gitdir,
3581 bare=False,
3582 capture_stdout=True,
3583 capture_stderr=True,
3584 )
3585 p.Wait()
3586 r = {}
3587 out = p.stdout
3588 if out:
3589 out = iter(out[:-1].split("\0"))
3590 while out:
3591 try:
3592 info = next(out)
3593 path = next(out)
3594 except StopIteration:
3595 break
3596
Mike Frysingerd4aee652023-10-19 05:13:32 -04003597 class _Info:
Gavin Makea2e3302023-03-11 06:46:20 +00003598 def __init__(self, path, omode, nmode, oid, nid, state):
3599 self.path = path
3600 self.src_path = None
3601 self.old_mode = omode
3602 self.new_mode = nmode
3603 self.old_id = oid
3604 self.new_id = nid
3605
3606 if len(state) == 1:
3607 self.status = state
3608 self.level = None
3609 else:
3610 self.status = state[:1]
3611 self.level = state[1:]
3612 while self.level.startswith("0"):
3613 self.level = self.level[1:]
3614
3615 info = info[1:].split(" ")
3616 info = _Info(path, *info)
3617 if info.status in ("R", "C"):
3618 info.src_path = info.path
3619 info.path = next(out)
3620 r[info.path] = info
3621 return r
3622
3623 def GetDotgitPath(self, subpath=None):
3624 """Return the full path to the .git dir.
3625
3626 As a convenience, append |subpath| if provided.
3627 """
3628 if self._bare:
3629 dotgit = self._gitdir
3630 else:
3631 dotgit = os.path.join(self._project.worktree, ".git")
3632 if os.path.isfile(dotgit):
3633 # Git worktrees use a "gitdir:" syntax to point to the
3634 # scratch space.
3635 with open(dotgit) as fp:
3636 setting = fp.read()
3637 assert setting.startswith("gitdir:")
3638 gitdir = setting.split(":", 1)[1].strip()
3639 dotgit = os.path.normpath(
3640 os.path.join(self._project.worktree, gitdir)
3641 )
3642
3643 return dotgit if subpath is None else os.path.join(dotgit, subpath)
3644
3645 def GetHead(self):
3646 """Return the ref that HEAD points to."""
3647 path = self.GetDotgitPath(subpath=HEAD)
3648 try:
3649 with open(path) as fd:
3650 line = fd.readline()
Jason R. Coombsae824fb2023-10-20 23:32:40 +05453651 except OSError as e:
Gavin Makea2e3302023-03-11 06:46:20 +00003652 raise NoManifestException(path, str(e))
3653 try:
3654 line = line.decode()
3655 except AttributeError:
3656 pass
3657 if line.startswith("ref: "):
3658 return line[5:-1]
3659 return line[:-1]
3660
3661 def SetHead(self, ref, message=None):
3662 cmdv = []
3663 if message is not None:
3664 cmdv.extend(["-m", message])
3665 cmdv.append(HEAD)
3666 cmdv.append(ref)
3667 self.symbolic_ref(*cmdv)
3668
3669 def DetachHead(self, new, message=None):
3670 cmdv = ["--no-deref"]
3671 if message is not None:
3672 cmdv.extend(["-m", message])
3673 cmdv.append(HEAD)
3674 cmdv.append(new)
3675 self.update_ref(*cmdv)
3676
3677 def UpdateRef(self, name, new, old=None, message=None, detach=False):
3678 cmdv = []
3679 if message is not None:
3680 cmdv.extend(["-m", message])
3681 if detach:
3682 cmdv.append("--no-deref")
3683 cmdv.append(name)
3684 cmdv.append(new)
3685 if old is not None:
3686 cmdv.append(old)
3687 self.update_ref(*cmdv)
3688
3689 def DeleteRef(self, name, old=None):
3690 if not old:
3691 old = self.rev_parse(name)
3692 self.update_ref("-d", name, old)
3693 self._project.bare_ref.deleted(name)
3694
Jason Chang87058c62023-09-27 11:34:43 -07003695 def rev_list(self, *args, log_as_error=True, **kw):
Gavin Makea2e3302023-03-11 06:46:20 +00003696 if "format" in kw:
3697 cmdv = ["log", "--pretty=format:%s" % kw["format"]]
3698 else:
3699 cmdv = ["rev-list"]
3700 cmdv.extend(args)
3701 p = GitCommand(
3702 self._project,
3703 cmdv,
3704 bare=self._bare,
3705 gitdir=self._gitdir,
3706 capture_stdout=True,
3707 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003708 verify_command=True,
Jason Chang87058c62023-09-27 11:34:43 -07003709 log_as_error=log_as_error,
Gavin Makea2e3302023-03-11 06:46:20 +00003710 )
Jason Chang32b59562023-07-14 16:45:35 -07003711 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003712 return p.stdout.splitlines()
3713
3714 def __getattr__(self, name):
3715 """Allow arbitrary git commands using pythonic syntax.
3716
3717 This allows you to do things like:
3718 git_obj.rev_parse('HEAD')
3719
3720 Since we don't have a 'rev_parse' method defined, the __getattr__
3721 will run. We'll replace the '_' with a '-' and try to run a git
3722 command. Any other positional arguments will be passed to the git
3723 command, and the following keyword arguments are supported:
3724 config: An optional dict of git config options to be passed with
3725 '-c'.
3726
3727 Args:
3728 name: The name of the git command to call. Any '_' characters
3729 will be replaced with '-'.
3730
3731 Returns:
3732 A callable object that will try to call git with the named
3733 command.
3734 """
3735 name = name.replace("_", "-")
3736
Jason Chang87058c62023-09-27 11:34:43 -07003737 def runner(*args, log_as_error=True, **kwargs):
Gavin Makea2e3302023-03-11 06:46:20 +00003738 cmdv = []
3739 config = kwargs.pop("config", None)
3740 for k in kwargs:
3741 raise TypeError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003742 f"{name}() got an unexpected keyword argument {k!r}"
Gavin Makea2e3302023-03-11 06:46:20 +00003743 )
3744 if config is not None:
3745 for k, v in config.items():
3746 cmdv.append("-c")
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003747 cmdv.append(f"{k}={v}")
Gavin Makea2e3302023-03-11 06:46:20 +00003748 cmdv.append(name)
3749 cmdv.extend(args)
3750 p = GitCommand(
3751 self._project,
3752 cmdv,
3753 bare=self._bare,
3754 gitdir=self._gitdir,
3755 capture_stdout=True,
3756 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003757 verify_command=True,
Jason Chang87058c62023-09-27 11:34:43 -07003758 log_as_error=log_as_error,
Gavin Makea2e3302023-03-11 06:46:20 +00003759 )
Jason Chang32b59562023-07-14 16:45:35 -07003760 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003761 r = p.stdout
3762 if r.endswith("\n") and r.index("\n") == len(r) - 1:
3763 return r[:-1]
3764 return r
3765
3766 return runner
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003767
3768
Jason Chang32b59562023-07-14 16:45:35 -07003769class LocalSyncFail(RepoError):
3770 """Default error when there is an Sync_LocalHalf error."""
3771
3772
3773class _PriorSyncFailedError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003774 def __str__(self):
3775 return "prior sync failed; rebase still in progress"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003776
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003777
Jason Chang32b59562023-07-14 16:45:35 -07003778class _DirtyError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003779 def __str__(self):
3780 return "contains uncommitted changes"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003781
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003782
Mike Frysingerd4aee652023-10-19 05:13:32 -04003783class _InfoMessage:
Gavin Makea2e3302023-03-11 06:46:20 +00003784 def __init__(self, project, text):
3785 self.project = project
3786 self.text = text
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003787
Gavin Makea2e3302023-03-11 06:46:20 +00003788 def Print(self, syncbuf):
3789 syncbuf.out.info(
3790 "%s/: %s", self.project.RelPath(local=False), self.text
3791 )
3792 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003793
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003794
Mike Frysingerd4aee652023-10-19 05:13:32 -04003795class _Failure:
Gavin Makea2e3302023-03-11 06:46:20 +00003796 def __init__(self, project, why):
3797 self.project = project
3798 self.why = why
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003799
Gavin Makea2e3302023-03-11 06:46:20 +00003800 def Print(self, syncbuf):
3801 syncbuf.out.fail(
3802 "error: %s/: %s", self.project.RelPath(local=False), str(self.why)
3803 )
3804 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003805
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003806
Mike Frysingerd4aee652023-10-19 05:13:32 -04003807class _Later:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003808 def __init__(self, project, action, quiet):
Gavin Makea2e3302023-03-11 06:46:20 +00003809 self.project = project
3810 self.action = action
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003811 self.quiet = quiet
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003812
Gavin Makea2e3302023-03-11 06:46:20 +00003813 def Run(self, syncbuf):
3814 out = syncbuf.out
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003815 if not self.quiet:
3816 out.project("project %s/", self.project.RelPath(local=False))
3817 out.nl()
Gavin Makea2e3302023-03-11 06:46:20 +00003818 try:
3819 self.action()
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003820 if not self.quiet:
3821 out.nl()
Gavin Makea2e3302023-03-11 06:46:20 +00003822 return True
3823 except GitError:
3824 out.nl()
3825 return False
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003826
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003827
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003828class _SyncColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +00003829 def __init__(self, config):
3830 super().__init__(config, "reposync")
3831 self.project = self.printer("header", attr="bold")
3832 self.info = self.printer("info")
3833 self.fail = self.printer("fail", fg="red")
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003834
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003835
Mike Frysingerd4aee652023-10-19 05:13:32 -04003836class SyncBuffer:
Gavin Makea2e3302023-03-11 06:46:20 +00003837 def __init__(self, config, detach_head=False):
3838 self._messages = []
3839 self._failures = []
3840 self._later_queue1 = []
3841 self._later_queue2 = []
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003842
Gavin Makea2e3302023-03-11 06:46:20 +00003843 self.out = _SyncColoring(config)
3844 self.out.redirect(sys.stderr)
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003845
Gavin Makea2e3302023-03-11 06:46:20 +00003846 self.detach_head = detach_head
3847 self.clean = True
3848 self.recent_clean = True
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003849
Gavin Makea2e3302023-03-11 06:46:20 +00003850 def info(self, project, fmt, *args):
3851 self._messages.append(_InfoMessage(project, fmt % args))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003852
Gavin Makea2e3302023-03-11 06:46:20 +00003853 def fail(self, project, err=None):
3854 self._failures.append(_Failure(project, err))
David Rileye0684ad2017-04-05 00:02:59 -07003855 self._MarkUnclean()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003856
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003857 def later1(self, project, what, quiet):
3858 self._later_queue1.append(_Later(project, what, quiet))
Mike Frysinger70d861f2019-08-26 15:22:36 -04003859
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003860 def later2(self, project, what, quiet):
3861 self._later_queue2.append(_Later(project, what, quiet))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003862
Gavin Makea2e3302023-03-11 06:46:20 +00003863 def Finish(self):
3864 self._PrintMessages()
3865 self._RunLater()
3866 self._PrintMessages()
3867 return self.clean
3868
3869 def Recently(self):
3870 recent_clean = self.recent_clean
3871 self.recent_clean = True
3872 return recent_clean
3873
3874 def _MarkUnclean(self):
3875 self.clean = False
3876 self.recent_clean = False
3877
3878 def _RunLater(self):
3879 for q in ["_later_queue1", "_later_queue2"]:
3880 if not self._RunQueue(q):
3881 return
3882
3883 def _RunQueue(self, queue):
3884 for m in getattr(self, queue):
3885 if not m.Run(self):
3886 self._MarkUnclean()
3887 return False
3888 setattr(self, queue, [])
3889 return True
3890
3891 def _PrintMessages(self):
3892 if self._messages or self._failures:
3893 if os.isatty(2):
3894 self.out.write(progress.CSI_ERASE_LINE)
3895 self.out.write("\r")
3896
3897 for m in self._messages:
3898 m.Print(self)
3899 for m in self._failures:
3900 m.Print(self)
3901
3902 self._messages = []
3903 self._failures = []
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003904
3905
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003906class MetaProject(Project):
Gavin Makea2e3302023-03-11 06:46:20 +00003907 """A special project housed under .repo."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003908
Gavin Makea2e3302023-03-11 06:46:20 +00003909 def __init__(self, manifest, name, gitdir, worktree):
3910 Project.__init__(
3911 self,
3912 manifest=manifest,
3913 name=name,
3914 gitdir=gitdir,
3915 objdir=gitdir,
3916 worktree=worktree,
3917 remote=RemoteSpec("origin"),
3918 relpath=".repo/%s" % name,
3919 revisionExpr="refs/heads/master",
3920 revisionId=None,
3921 groups=None,
3922 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003923
Gavin Makea2e3302023-03-11 06:46:20 +00003924 def PreSync(self):
3925 if self.Exists:
3926 cb = self.CurrentBranch
3927 if cb:
3928 base = self.GetBranch(cb).merge
3929 if base:
3930 self.revisionExpr = base
3931 self.revisionId = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003932
Gavin Makea2e3302023-03-11 06:46:20 +00003933 @property
3934 def HasChanges(self):
3935 """Has the remote received new commits not yet checked out?"""
3936 if not self.remote or not self.revisionExpr:
3937 return False
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003938
Gavin Makea2e3302023-03-11 06:46:20 +00003939 all_refs = self.bare_ref.all
3940 revid = self.GetRevisionId(all_refs)
3941 head = self.work_git.GetHead()
3942 if head.startswith(R_HEADS):
3943 try:
3944 head = all_refs[head]
3945 except KeyError:
3946 head = None
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003947
Gavin Makea2e3302023-03-11 06:46:20 +00003948 if revid == head:
3949 return False
3950 elif self._revlist(not_rev(HEAD), revid):
3951 return True
3952 return False
LaMont Jones9b72cf22022-03-29 21:54:22 +00003953
3954
3955class RepoProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003956 """The MetaProject for repo itself."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003957
Gavin Makea2e3302023-03-11 06:46:20 +00003958 @property
3959 def LastFetch(self):
3960 try:
3961 fh = os.path.join(self.gitdir, "FETCH_HEAD")
3962 return os.path.getmtime(fh)
3963 except OSError:
3964 return 0
LaMont Jones9b72cf22022-03-29 21:54:22 +00003965
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +01003966
LaMont Jones9b72cf22022-03-29 21:54:22 +00003967class ManifestProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003968 """The MetaProject for manifests."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003969
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08003970 def MetaBranchSwitch(self, submodules=False, verbose=False):
Gavin Makea2e3302023-03-11 06:46:20 +00003971 """Prepare for manifest branch switch."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003972
Gavin Makea2e3302023-03-11 06:46:20 +00003973 # detach and delete manifest branch, allowing a new
3974 # branch to take over
3975 syncbuf = SyncBuffer(self.config, detach_head=True)
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08003976 self.Sync_LocalHalf(syncbuf, submodules=submodules, verbose=verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00003977 syncbuf.Finish()
LaMont Jones9b72cf22022-03-29 21:54:22 +00003978
Gavin Makea2e3302023-03-11 06:46:20 +00003979 return (
3980 GitCommand(
3981 self,
3982 ["update-ref", "-d", "refs/heads/default"],
3983 capture_stdout=True,
3984 capture_stderr=True,
3985 ).Wait()
3986 == 0
3987 )
LaMont Jones9b03f152022-03-29 23:01:18 +00003988
Gavin Makea2e3302023-03-11 06:46:20 +00003989 @property
3990 def standalone_manifest_url(self):
3991 """The URL of the standalone manifest, or None."""
3992 return self.config.GetString("manifest.standalone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003993
Gavin Makea2e3302023-03-11 06:46:20 +00003994 @property
3995 def manifest_groups(self):
3996 """The manifest groups string."""
3997 return self.config.GetString("manifest.groups")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003998
Gavin Makea2e3302023-03-11 06:46:20 +00003999 @property
4000 def reference(self):
4001 """The --reference for this manifest."""
4002 return self.config.GetString("repo.reference")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004003
Gavin Makea2e3302023-03-11 06:46:20 +00004004 @property
4005 def dissociate(self):
4006 """Whether to dissociate."""
4007 return self.config.GetBoolean("repo.dissociate")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004008
Gavin Makea2e3302023-03-11 06:46:20 +00004009 @property
4010 def archive(self):
4011 """Whether we use archive."""
4012 return self.config.GetBoolean("repo.archive")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004013
Gavin Makea2e3302023-03-11 06:46:20 +00004014 @property
4015 def mirror(self):
4016 """Whether we use mirror."""
4017 return self.config.GetBoolean("repo.mirror")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004018
Gavin Makea2e3302023-03-11 06:46:20 +00004019 @property
4020 def use_worktree(self):
4021 """Whether we use worktree."""
4022 return self.config.GetBoolean("repo.worktree")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004023
Gavin Makea2e3302023-03-11 06:46:20 +00004024 @property
4025 def clone_bundle(self):
4026 """Whether we use clone_bundle."""
4027 return self.config.GetBoolean("repo.clonebundle")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004028
Gavin Makea2e3302023-03-11 06:46:20 +00004029 @property
4030 def submodules(self):
4031 """Whether we use submodules."""
4032 return self.config.GetBoolean("repo.submodules")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004033
Gavin Makea2e3302023-03-11 06:46:20 +00004034 @property
4035 def git_lfs(self):
4036 """Whether we use git_lfs."""
4037 return self.config.GetBoolean("repo.git-lfs")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004038
Gavin Makea2e3302023-03-11 06:46:20 +00004039 @property
4040 def use_superproject(self):
4041 """Whether we use superproject."""
4042 return self.config.GetBoolean("repo.superproject")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004043
Gavin Makea2e3302023-03-11 06:46:20 +00004044 @property
4045 def partial_clone(self):
4046 """Whether this is a partial clone."""
4047 return self.config.GetBoolean("repo.partialclone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004048
Gavin Makea2e3302023-03-11 06:46:20 +00004049 @property
4050 def depth(self):
4051 """Partial clone depth."""
Roberto Vladimir Prado Carranza3d58d212023-09-13 10:27:26 +02004052 return self.config.GetInt("repo.depth")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004053
Gavin Makea2e3302023-03-11 06:46:20 +00004054 @property
4055 def clone_filter(self):
4056 """The clone filter."""
4057 return self.config.GetString("repo.clonefilter")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004058
Gavin Makea2e3302023-03-11 06:46:20 +00004059 @property
4060 def partial_clone_exclude(self):
4061 """Partial clone exclude string"""
4062 return self.config.GetString("repo.partialcloneexclude")
LaMont Jones4ada0432022-04-14 15:10:43 +00004063
Gavin Makea2e3302023-03-11 06:46:20 +00004064 @property
Jason Chang17833322023-05-23 13:06:55 -07004065 def clone_filter_for_depth(self):
4066 """Replace shallow clone with partial clone."""
4067 return self.config.GetString("repo.clonefilterfordepth")
4068
4069 @property
Gavin Makea2e3302023-03-11 06:46:20 +00004070 def manifest_platform(self):
4071 """The --platform argument from `repo init`."""
4072 return self.config.GetString("manifest.platform")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004073
Gavin Makea2e3302023-03-11 06:46:20 +00004074 @property
4075 def _platform_name(self):
4076 """Return the name of the platform."""
4077 return platform.system().lower()
LaMont Jones9b03f152022-03-29 23:01:18 +00004078
Gavin Makea2e3302023-03-11 06:46:20 +00004079 def SyncWithPossibleInit(
4080 self,
4081 submanifest,
4082 verbose=False,
4083 current_branch_only=False,
4084 tags="",
4085 git_event_log=None,
4086 ):
4087 """Sync a manifestProject, possibly for the first time.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004088
Gavin Makea2e3302023-03-11 06:46:20 +00004089 Call Sync() with arguments from the most recent `repo init`. If this is
4090 a new sub manifest, then inherit options from the parent's
4091 manifestProject.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004092
Gavin Makea2e3302023-03-11 06:46:20 +00004093 This is used by subcmds.Sync() to do an initial download of new sub
4094 manifests.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004095
Gavin Makea2e3302023-03-11 06:46:20 +00004096 Args:
4097 submanifest: an XmlSubmanifest, the submanifest to re-sync.
4098 verbose: a boolean, whether to show all output, rather than only
4099 errors.
4100 current_branch_only: a boolean, whether to only fetch the current
4101 manifest branch from the server.
4102 tags: a boolean, whether to fetch tags.
4103 git_event_log: an EventLog, for git tracing.
4104 """
4105 # TODO(lamontjones): when refactoring sync (and init?) consider how to
4106 # better get the init options that we should use for new submanifests
4107 # that are added when syncing an existing workspace.
4108 git_event_log = git_event_log or EventLog()
LaMont Jonesb90a4222022-04-14 15:00:09 +00004109 spec = submanifest.ToSubmanifestSpec()
Gavin Makea2e3302023-03-11 06:46:20 +00004110 # Use the init options from the existing manifestProject, or the parent
4111 # if it doesn't exist.
4112 #
4113 # Today, we only support changing manifest_groups on the sub-manifest,
4114 # with no supported-for-the-user way to change the other arguments from
4115 # those specified by the outermost manifest.
4116 #
4117 # TODO(lamontjones): determine which of these should come from the
4118 # outermost manifest and which should come from the parent manifest.
4119 mp = self if self.Exists else submanifest.parent.manifestProject
4120 return self.Sync(
LaMont Jones55ee3042022-04-06 17:10:21 +00004121 manifest_url=spec.manifestUrl,
4122 manifest_branch=spec.revision,
Gavin Makea2e3302023-03-11 06:46:20 +00004123 standalone_manifest=mp.standalone_manifest_url,
4124 groups=mp.manifest_groups,
4125 platform=mp.manifest_platform,
4126 mirror=mp.mirror,
4127 dissociate=mp.dissociate,
4128 reference=mp.reference,
4129 worktree=mp.use_worktree,
4130 submodules=mp.submodules,
4131 archive=mp.archive,
4132 partial_clone=mp.partial_clone,
4133 clone_filter=mp.clone_filter,
4134 partial_clone_exclude=mp.partial_clone_exclude,
4135 clone_bundle=mp.clone_bundle,
4136 git_lfs=mp.git_lfs,
4137 use_superproject=mp.use_superproject,
LaMont Jones55ee3042022-04-06 17:10:21 +00004138 verbose=verbose,
4139 current_branch_only=current_branch_only,
4140 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00004141 depth=mp.depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004142 git_event_log=git_event_log,
4143 manifest_name=spec.manifestName,
Gavin Makea2e3302023-03-11 06:46:20 +00004144 this_manifest_only=True,
LaMont Jones55ee3042022-04-06 17:10:21 +00004145 outer_manifest=False,
Jason Chang17833322023-05-23 13:06:55 -07004146 clone_filter_for_depth=mp.clone_filter_for_depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004147 )
LaMont Jones409407a2022-04-05 21:21:56 +00004148
Gavin Makea2e3302023-03-11 06:46:20 +00004149 def Sync(
4150 self,
4151 _kwargs_only=(),
4152 manifest_url="",
4153 manifest_branch=None,
4154 standalone_manifest=False,
4155 groups="",
4156 mirror=False,
4157 reference="",
4158 dissociate=False,
4159 worktree=False,
4160 submodules=False,
4161 archive=False,
4162 partial_clone=None,
4163 depth=None,
4164 clone_filter="blob:none",
4165 partial_clone_exclude=None,
4166 clone_bundle=None,
4167 git_lfs=None,
4168 use_superproject=None,
4169 verbose=False,
4170 current_branch_only=False,
4171 git_event_log=None,
4172 platform="",
4173 manifest_name="default.xml",
4174 tags="",
4175 this_manifest_only=False,
4176 outer_manifest=True,
Jason Chang17833322023-05-23 13:06:55 -07004177 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00004178 ):
4179 """Sync the manifest and all submanifests.
LaMont Jones409407a2022-04-05 21:21:56 +00004180
Gavin Makea2e3302023-03-11 06:46:20 +00004181 Args:
4182 manifest_url: a string, the URL of the manifest project.
4183 manifest_branch: a string, the manifest branch to use.
4184 standalone_manifest: a boolean, whether to store the manifest as a
4185 static file.
4186 groups: a string, restricts the checkout to projects with the
4187 specified groups.
4188 mirror: a boolean, whether to create a mirror of the remote
4189 repository.
4190 reference: a string, location of a repo instance to use as a
4191 reference.
4192 dissociate: a boolean, whether to dissociate from reference mirrors
4193 after clone.
4194 worktree: a boolean, whether to use git-worktree to manage projects.
4195 submodules: a boolean, whether sync submodules associated with the
4196 manifest project.
4197 archive: a boolean, whether to checkout each project as an archive.
4198 See git-archive.
4199 partial_clone: a boolean, whether to perform a partial clone.
4200 depth: an int, how deep of a shallow clone to create.
4201 clone_filter: a string, filter to use with partial_clone.
4202 partial_clone_exclude : a string, comma-delimeted list of project
4203 names to exclude from partial clone.
4204 clone_bundle: a boolean, whether to enable /clone.bundle on
4205 HTTP/HTTPS.
4206 git_lfs: a boolean, whether to enable git LFS support.
4207 use_superproject: a boolean, whether to use the manifest
4208 superproject to sync projects.
4209 verbose: a boolean, whether to show all output, rather than only
4210 errors.
4211 current_branch_only: a boolean, whether to only fetch the current
4212 manifest branch from the server.
4213 platform: a string, restrict the checkout to projects with the
4214 specified platform group.
4215 git_event_log: an EventLog, for git tracing.
4216 tags: a boolean, whether to fetch tags.
4217 manifest_name: a string, the name of the manifest file to use.
4218 this_manifest_only: a boolean, whether to only operate on the
4219 current sub manifest.
4220 outer_manifest: a boolean, whether to start at the outermost
4221 manifest.
Jason Chang17833322023-05-23 13:06:55 -07004222 clone_filter_for_depth: a string, when specified replaces shallow
4223 clones with partial.
LaMont Jones9b03f152022-03-29 23:01:18 +00004224
Gavin Makea2e3302023-03-11 06:46:20 +00004225 Returns:
4226 a boolean, whether the sync was successful.
4227 """
4228 assert _kwargs_only == (), "Sync only accepts keyword arguments."
LaMont Jones9b03f152022-03-29 23:01:18 +00004229
Gavin Makea2e3302023-03-11 06:46:20 +00004230 groups = groups or self.manifest.GetDefaultGroupsStr(
4231 with_platform=False
4232 )
4233 platform = platform or "auto"
4234 git_event_log = git_event_log or EventLog()
4235 if outer_manifest and self.manifest.is_submanifest:
4236 # In a multi-manifest checkout, use the outer manifest unless we are
4237 # told not to.
4238 return self.client.outer_manifest.manifestProject.Sync(
4239 manifest_url=manifest_url,
4240 manifest_branch=manifest_branch,
4241 standalone_manifest=standalone_manifest,
4242 groups=groups,
4243 platform=platform,
4244 mirror=mirror,
4245 dissociate=dissociate,
4246 reference=reference,
4247 worktree=worktree,
4248 submodules=submodules,
4249 archive=archive,
4250 partial_clone=partial_clone,
4251 clone_filter=clone_filter,
4252 partial_clone_exclude=partial_clone_exclude,
4253 clone_bundle=clone_bundle,
4254 git_lfs=git_lfs,
4255 use_superproject=use_superproject,
4256 verbose=verbose,
4257 current_branch_only=current_branch_only,
4258 tags=tags,
4259 depth=depth,
4260 git_event_log=git_event_log,
4261 manifest_name=manifest_name,
4262 this_manifest_only=this_manifest_only,
4263 outer_manifest=False,
4264 )
LaMont Jones9b03f152022-03-29 23:01:18 +00004265
Gavin Makea2e3302023-03-11 06:46:20 +00004266 # If repo has already been initialized, we take -u with the absence of
4267 # --standalone-manifest to mean "transition to a standard repo set up",
4268 # which necessitates starting fresh.
4269 # If --standalone-manifest is set, we always tear everything down and
4270 # start anew.
4271 if self.Exists:
4272 was_standalone_manifest = self.config.GetString(
4273 "manifest.standalone"
4274 )
4275 if was_standalone_manifest and not manifest_url:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004276 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004277 "fatal: repo was initialized with a standlone manifest, "
4278 "cannot be re-initialized without --manifest-url/-u"
4279 )
4280 return False
4281
4282 if standalone_manifest or (
4283 was_standalone_manifest and manifest_url
4284 ):
4285 self.config.ClearCache()
4286 if self.gitdir and os.path.exists(self.gitdir):
4287 platform_utils.rmtree(self.gitdir)
4288 if self.worktree and os.path.exists(self.worktree):
4289 platform_utils.rmtree(self.worktree)
4290
4291 is_new = not self.Exists
4292 if is_new:
4293 if not manifest_url:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004294 logger.error("fatal: manifest url is required.")
Gavin Makea2e3302023-03-11 06:46:20 +00004295 return False
4296
4297 if verbose:
4298 print(
4299 "Downloading manifest from %s"
4300 % (GitConfig.ForUser().UrlInsteadOf(manifest_url),),
4301 file=sys.stderr,
4302 )
4303
4304 # The manifest project object doesn't keep track of the path on the
4305 # server where this git is located, so let's save that here.
4306 mirrored_manifest_git = None
4307 if reference:
4308 manifest_git_path = urllib.parse.urlparse(manifest_url).path[1:]
4309 mirrored_manifest_git = os.path.join(
4310 reference, manifest_git_path
4311 )
4312 if not mirrored_manifest_git.endswith(".git"):
4313 mirrored_manifest_git += ".git"
4314 if not os.path.exists(mirrored_manifest_git):
4315 mirrored_manifest_git = os.path.join(
4316 reference, ".repo/manifests.git"
4317 )
4318
4319 self._InitGitDir(mirror_git=mirrored_manifest_git)
4320
4321 # If standalone_manifest is set, mark the project as "standalone" --
4322 # we'll still do much of the manifests.git set up, but will avoid actual
4323 # syncs to a remote.
4324 if standalone_manifest:
4325 self.config.SetString("manifest.standalone", manifest_url)
4326 elif not manifest_url and not manifest_branch:
4327 # If -u is set and --standalone-manifest is not, then we're not in
4328 # standalone mode. Otherwise, use config to infer what we were in
4329 # the last init.
4330 standalone_manifest = bool(
4331 self.config.GetString("manifest.standalone")
4332 )
4333 if not standalone_manifest:
4334 self.config.SetString("manifest.standalone", None)
4335
4336 self._ConfigureDepth(depth)
4337
4338 # Set the remote URL before the remote branch as we might need it below.
4339 if manifest_url:
4340 r = self.GetRemote()
4341 r.url = manifest_url
4342 r.ResetFetch()
4343 r.Save()
4344
4345 if not standalone_manifest:
4346 if manifest_branch:
4347 if manifest_branch == "HEAD":
4348 manifest_branch = self.ResolveRemoteHead()
4349 if manifest_branch is None:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004350 logger.error("fatal: unable to resolve HEAD")
Gavin Makea2e3302023-03-11 06:46:20 +00004351 return False
4352 self.revisionExpr = manifest_branch
4353 else:
4354 if is_new:
4355 default_branch = self.ResolveRemoteHead()
4356 if default_branch is None:
4357 # If the remote doesn't have HEAD configured, default to
4358 # master.
4359 default_branch = "refs/heads/master"
4360 self.revisionExpr = default_branch
4361 else:
4362 self.PreSync()
4363
4364 groups = re.split(r"[,\s]+", groups or "")
4365 all_platforms = ["linux", "darwin", "windows"]
4366 platformize = lambda x: "platform-" + x
4367 if platform == "auto":
4368 if not mirror and not self.mirror:
4369 groups.append(platformize(self._platform_name))
4370 elif platform == "all":
4371 groups.extend(map(platformize, all_platforms))
4372 elif platform in all_platforms:
4373 groups.append(platformize(platform))
4374 elif platform != "none":
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004375 logger.error("fatal: invalid platform flag", file=sys.stderr)
Gavin Makea2e3302023-03-11 06:46:20 +00004376 return False
4377 self.config.SetString("manifest.platform", platform)
4378
4379 groups = [x for x in groups if x]
4380 groupstr = ",".join(groups)
4381 if (
4382 platform == "auto"
4383 and groupstr == self.manifest.GetDefaultGroupsStr()
4384 ):
4385 groupstr = None
4386 self.config.SetString("manifest.groups", groupstr)
4387
4388 if reference:
4389 self.config.SetString("repo.reference", reference)
4390
4391 if dissociate:
4392 self.config.SetBoolean("repo.dissociate", dissociate)
4393
4394 if worktree:
4395 if mirror:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004396 logger.error("fatal: --mirror and --worktree are incompatible")
Gavin Makea2e3302023-03-11 06:46:20 +00004397 return False
4398 if submodules:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004399 logger.error(
4400 "fatal: --submodules and --worktree are incompatible"
Gavin Makea2e3302023-03-11 06:46:20 +00004401 )
4402 return False
4403 self.config.SetBoolean("repo.worktree", worktree)
4404 if is_new:
4405 self.use_git_worktrees = True
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004406 logger.warning("warning: --worktree is experimental!")
Gavin Makea2e3302023-03-11 06:46:20 +00004407
4408 if archive:
4409 if is_new:
4410 self.config.SetBoolean("repo.archive", archive)
4411 else:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004412 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004413 "fatal: --archive is only supported when initializing a "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004414 "new workspace."
Gavin Makea2e3302023-03-11 06:46:20 +00004415 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004416 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004417 "Either delete the .repo folder in this workspace, or "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004418 "initialize in another location."
Gavin Makea2e3302023-03-11 06:46:20 +00004419 )
4420 return False
4421
4422 if mirror:
4423 if is_new:
4424 self.config.SetBoolean("repo.mirror", mirror)
4425 else:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004426 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004427 "fatal: --mirror is only supported when initializing a new "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004428 "workspace."
Gavin Makea2e3302023-03-11 06:46:20 +00004429 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004430 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004431 "Either delete the .repo folder in this workspace, or "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004432 "initialize in another location."
Gavin Makea2e3302023-03-11 06:46:20 +00004433 )
4434 return False
4435
4436 if partial_clone is not None:
4437 if mirror:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004438 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004439 "fatal: --mirror and --partial-clone are mutually "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004440 "exclusive"
Gavin Makea2e3302023-03-11 06:46:20 +00004441 )
4442 return False
4443 self.config.SetBoolean("repo.partialclone", partial_clone)
4444 if clone_filter:
4445 self.config.SetString("repo.clonefilter", clone_filter)
4446 elif self.partial_clone:
4447 clone_filter = self.clone_filter
4448 else:
4449 clone_filter = None
4450
4451 if partial_clone_exclude is not None:
4452 self.config.SetString(
4453 "repo.partialcloneexclude", partial_clone_exclude
4454 )
4455
4456 if clone_bundle is None:
4457 clone_bundle = False if partial_clone else True
4458 else:
4459 self.config.SetBoolean("repo.clonebundle", clone_bundle)
4460
4461 if submodules:
4462 self.config.SetBoolean("repo.submodules", submodules)
4463
4464 if git_lfs is not None:
4465 if git_lfs:
4466 git_require((2, 17, 0), fail=True, msg="Git LFS support")
4467
4468 self.config.SetBoolean("repo.git-lfs", git_lfs)
4469 if not is_new:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004470 logger.warning(
Gavin Makea2e3302023-03-11 06:46:20 +00004471 "warning: Changing --git-lfs settings will only affect new "
4472 "project checkouts.\n"
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004473 " Existing projects will require manual updates.\n"
Gavin Makea2e3302023-03-11 06:46:20 +00004474 )
4475
Jason Chang17833322023-05-23 13:06:55 -07004476 if clone_filter_for_depth is not None:
4477 self.ConfigureCloneFilterForDepth(clone_filter_for_depth)
4478
Gavin Makea2e3302023-03-11 06:46:20 +00004479 if use_superproject is not None:
4480 self.config.SetBoolean("repo.superproject", use_superproject)
4481
4482 if not standalone_manifest:
4483 success = self.Sync_NetworkHalf(
4484 is_new=is_new,
4485 quiet=not verbose,
4486 verbose=verbose,
4487 clone_bundle=clone_bundle,
4488 current_branch_only=current_branch_only,
4489 tags=tags,
4490 submodules=submodules,
4491 clone_filter=clone_filter,
4492 partial_clone_exclude=self.manifest.PartialCloneExclude,
Jason Chang17833322023-05-23 13:06:55 -07004493 clone_filter_for_depth=self.manifest.CloneFilterForDepth,
Gavin Makea2e3302023-03-11 06:46:20 +00004494 ).success
4495 if not success:
4496 r = self.GetRemote()
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004497 logger.error("fatal: cannot obtain manifest %s", r.url)
Gavin Makea2e3302023-03-11 06:46:20 +00004498
4499 # Better delete the manifest git dir if we created it; otherwise
4500 # next time (when user fixes problems) we won't go through the
4501 # "is_new" logic.
4502 if is_new:
4503 platform_utils.rmtree(self.gitdir)
4504 return False
4505
4506 if manifest_branch:
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08004507 self.MetaBranchSwitch(submodules=submodules, verbose=verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00004508
4509 syncbuf = SyncBuffer(self.config)
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08004510 self.Sync_LocalHalf(syncbuf, submodules=submodules, verbose=verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00004511 syncbuf.Finish()
4512
4513 if is_new or self.CurrentBranch is None:
Jason Chang1a3612f2023-08-08 14:12:53 -07004514 try:
4515 self.StartBranch("default")
4516 except GitError as e:
4517 msg = str(e)
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004518 logger.error(
4519 "fatal: cannot create default in manifest %s", msg
Gavin Makea2e3302023-03-11 06:46:20 +00004520 )
4521 return False
4522
4523 if not manifest_name:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004524 logger.error("fatal: manifest name (-m) is required.")
Gavin Makea2e3302023-03-11 06:46:20 +00004525 return False
4526
4527 elif is_new:
4528 # This is a new standalone manifest.
4529 manifest_name = "default.xml"
4530 manifest_data = fetch.fetch_file(manifest_url, verbose=verbose)
4531 dest = os.path.join(self.worktree, manifest_name)
4532 os.makedirs(os.path.dirname(dest), exist_ok=True)
4533 with open(dest, "wb") as f:
4534 f.write(manifest_data)
4535
4536 try:
4537 self.manifest.Link(manifest_name)
4538 except ManifestParseError as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004539 logger.error("fatal: manifest '%s' not available", manifest_name)
4540 logger.error("fatal: %s", e)
Gavin Makea2e3302023-03-11 06:46:20 +00004541 return False
4542
4543 if not this_manifest_only:
4544 for submanifest in self.manifest.submanifests.values():
4545 spec = submanifest.ToSubmanifestSpec()
4546 submanifest.repo_client.manifestProject.Sync(
4547 manifest_url=spec.manifestUrl,
4548 manifest_branch=spec.revision,
4549 standalone_manifest=standalone_manifest,
4550 groups=self.manifest_groups,
4551 platform=platform,
4552 mirror=mirror,
4553 dissociate=dissociate,
4554 reference=reference,
4555 worktree=worktree,
4556 submodules=submodules,
4557 archive=archive,
4558 partial_clone=partial_clone,
4559 clone_filter=clone_filter,
4560 partial_clone_exclude=partial_clone_exclude,
4561 clone_bundle=clone_bundle,
4562 git_lfs=git_lfs,
4563 use_superproject=use_superproject,
4564 verbose=verbose,
4565 current_branch_only=current_branch_only,
4566 tags=tags,
4567 depth=depth,
4568 git_event_log=git_event_log,
4569 manifest_name=spec.manifestName,
4570 this_manifest_only=False,
4571 outer_manifest=False,
4572 )
4573
4574 # Lastly, if the manifest has a <superproject> then have the
4575 # superproject sync it (if it will be used).
4576 if git_superproject.UseSuperproject(use_superproject, self.manifest):
4577 sync_result = self.manifest.superproject.Sync(git_event_log)
4578 if not sync_result.success:
4579 submanifest = ""
4580 if self.manifest.path_prefix:
4581 submanifest = f"for {self.manifest.path_prefix} "
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004582 logger.warning(
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004583 "warning: git update of superproject %s failed, "
Gavin Makea2e3302023-03-11 06:46:20 +00004584 "repo sync will not use superproject to fetch source; "
4585 "while this error is not fatal, and you can continue to "
4586 "run repo sync, please run repo init with the "
4587 "--no-use-superproject option to stop seeing this warning",
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004588 submanifest,
Gavin Makea2e3302023-03-11 06:46:20 +00004589 )
4590 if sync_result.fatal and use_superproject is not None:
4591 return False
4592
4593 return True
4594
Jason Chang17833322023-05-23 13:06:55 -07004595 def ConfigureCloneFilterForDepth(self, clone_filter_for_depth):
4596 """Configure clone filter to replace shallow clones.
4597
4598 Args:
4599 clone_filter_for_depth: a string or None, e.g. 'blob:none' will
4600 disable shallow clones and replace with partial clone. None will
4601 enable shallow clones.
4602 """
4603 self.config.SetString(
4604 "repo.clonefilterfordepth", clone_filter_for_depth
4605 )
4606
Gavin Makea2e3302023-03-11 06:46:20 +00004607 def _ConfigureDepth(self, depth):
4608 """Configure the depth we'll sync down.
4609
4610 Args:
4611 depth: an int, how deep of a partial clone to create.
4612 """
4613 # Opt.depth will be non-None if user actually passed --depth to repo
4614 # init.
4615 if depth is not None:
4616 if depth > 0:
4617 # Positive values will set the depth.
4618 depth = str(depth)
4619 else:
4620 # Negative numbers will clear the depth; passing None to
4621 # SetString will do that.
4622 depth = None
4623
4624 # We store the depth in the main manifest project.
4625 self.config.SetString("repo.depth", depth)