blob: 2ba2b766288bee097979510037bcb8d4c2554863 [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,
Josip Sokcevicedadb252024-02-29 09:48:37 -08001518 force_checkout=False,
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001519 submodules=False,
1520 errors=None,
1521 verbose=False,
Jason Chang32b59562023-07-14 16:45:35 -07001522 ):
Gavin Makea2e3302023-03-11 06:46:20 +00001523 """Perform only the local IO portion of the sync process.
1524
1525 Network access is not required.
1526 """
Jason Chang32b59562023-07-14 16:45:35 -07001527 if errors is None:
1528 errors = []
1529
1530 def fail(error: Exception):
1531 errors.append(error)
1532 syncbuf.fail(self, error)
1533
Gavin Makea2e3302023-03-11 06:46:20 +00001534 if not os.path.exists(self.gitdir):
Jason Chang32b59562023-07-14 16:45:35 -07001535 fail(
1536 LocalSyncFail(
1537 "Cannot checkout %s due to missing network sync; Run "
1538 "`repo sync -n %s` first." % (self.name, self.name),
1539 project=self.name,
1540 )
Gavin Makea2e3302023-03-11 06:46:20 +00001541 )
1542 return
1543
1544 self._InitWorkTree(force_sync=force_sync, submodules=submodules)
1545 all_refs = self.bare_ref.all
1546 self.CleanPublishedCache(all_refs)
1547 revid = self.GetRevisionId(all_refs)
1548
1549 # Special case the root of the repo client checkout. Make sure it
1550 # doesn't contain files being checked out to dirs we don't allow.
1551 if self.relpath == ".":
1552 PROTECTED_PATHS = {".repo"}
1553 paths = set(
1554 self.work_git.ls_tree("-z", "--name-only", "--", revid).split(
1555 "\0"
1556 )
1557 )
1558 bad_paths = paths & PROTECTED_PATHS
1559 if bad_paths:
Jason Chang32b59562023-07-14 16:45:35 -07001560 fail(
1561 LocalSyncFail(
1562 "Refusing to checkout project that writes to protected "
1563 "paths: %s" % (", ".join(bad_paths),),
1564 project=self.name,
1565 )
Gavin Makea2e3302023-03-11 06:46:20 +00001566 )
1567 return
1568
1569 def _doff():
1570 self._FastForward(revid)
1571 self._CopyAndLinkFiles()
1572
1573 def _dosubmodules():
1574 self._SyncSubmodules(quiet=True)
1575
1576 head = self.work_git.GetHead()
1577 if head.startswith(R_HEADS):
1578 branch = head[len(R_HEADS) :]
1579 try:
1580 head = all_refs[head]
1581 except KeyError:
1582 head = None
1583 else:
1584 branch = None
1585
1586 if branch is None or syncbuf.detach_head:
1587 # Currently on a detached HEAD. The user is assumed to
1588 # not have any local modifications worth worrying about.
1589 if self.IsRebaseInProgress():
Jason Chang32b59562023-07-14 16:45:35 -07001590 fail(_PriorSyncFailedError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001591 return
1592
1593 if head == revid:
1594 # No changes; don't do anything further.
1595 # Except if the head needs to be detached.
1596 if not syncbuf.detach_head:
1597 # The copy/linkfile config may have changed.
1598 self._CopyAndLinkFiles()
1599 return
1600 else:
1601 lost = self._revlist(not_rev(revid), HEAD)
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001602 if lost and verbose:
Gavin Makea2e3302023-03-11 06:46:20 +00001603 syncbuf.info(self, "discarding %d commits", len(lost))
1604
1605 try:
Josip Sokcevicedadb252024-02-29 09:48:37 -08001606 self._Checkout(revid, force_checkout=force_checkout, quiet=True)
Gavin Makea2e3302023-03-11 06:46:20 +00001607 if submodules:
1608 self._SyncSubmodules(quiet=True)
1609 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001610 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001611 return
1612 self._CopyAndLinkFiles()
1613 return
1614
1615 if head == revid:
1616 # No changes; don't do anything further.
1617 #
1618 # The copy/linkfile config may have changed.
1619 self._CopyAndLinkFiles()
1620 return
1621
1622 branch = self.GetBranch(branch)
1623
1624 if not branch.LocalMerge:
1625 # The current branch has no tracking configuration.
1626 # Jump off it to a detached HEAD.
1627 syncbuf.info(
1628 self, "leaving %s; does not track upstream", branch.name
1629 )
1630 try:
1631 self._Checkout(revid, quiet=True)
1632 if submodules:
1633 self._SyncSubmodules(quiet=True)
1634 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001635 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001636 return
1637 self._CopyAndLinkFiles()
1638 return
1639
1640 upstream_gain = self._revlist(not_rev(HEAD), revid)
1641
1642 # See if we can perform a fast forward merge. This can happen if our
1643 # branch isn't in the exact same state as we last published.
1644 try:
Jason Chang87058c62023-09-27 11:34:43 -07001645 self.work_git.merge_base(
1646 "--is-ancestor", HEAD, revid, log_as_error=False
1647 )
Gavin Makea2e3302023-03-11 06:46:20 +00001648 # Skip the published logic.
1649 pub = False
1650 except GitError:
1651 pub = self.WasPublished(branch.name, all_refs)
1652
1653 if pub:
1654 not_merged = self._revlist(not_rev(revid), pub)
1655 if not_merged:
1656 if upstream_gain:
1657 # The user has published this branch and some of those
1658 # commits are not yet merged upstream. We do not want
1659 # to rewrite the published commits so we punt.
Jason Chang32b59562023-07-14 16:45:35 -07001660 fail(
1661 LocalSyncFail(
1662 "branch %s is published (but not merged) and is "
1663 "now %d commits behind"
1664 % (branch.name, len(upstream_gain)),
1665 project=self.name,
1666 )
Gavin Makea2e3302023-03-11 06:46:20 +00001667 )
1668 return
1669 elif pub == head:
1670 # All published commits are merged, and thus we are a
1671 # strict subset. We can fast-forward safely.
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001672 syncbuf.later1(self, _doff, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001673 if submodules:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001674 syncbuf.later1(self, _dosubmodules, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001675 return
1676
1677 # Examine the local commits not in the remote. Find the
1678 # last one attributed to this user, if any.
1679 local_changes = self._revlist(not_rev(revid), HEAD, format="%H %ce")
1680 last_mine = None
1681 cnt_mine = 0
1682 for commit in local_changes:
1683 commit_id, committer_email = commit.split(" ", 1)
1684 if committer_email == self.UserEmail:
1685 last_mine = commit_id
1686 cnt_mine += 1
1687
1688 if not upstream_gain and cnt_mine == len(local_changes):
1689 # The copy/linkfile config may have changed.
1690 self._CopyAndLinkFiles()
1691 return
1692
1693 if self.IsDirty(consider_untracked=False):
Jason Chang32b59562023-07-14 16:45:35 -07001694 fail(_DirtyError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001695 return
1696
1697 # If the upstream switched on us, warn the user.
1698 if branch.merge != self.revisionExpr:
1699 if branch.merge and self.revisionExpr:
1700 syncbuf.info(
1701 self,
1702 "manifest switched %s...%s",
1703 branch.merge,
1704 self.revisionExpr,
1705 )
1706 elif branch.merge:
1707 syncbuf.info(self, "manifest no longer tracks %s", branch.merge)
1708
1709 if cnt_mine < len(local_changes):
1710 # Upstream rebased. Not everything in HEAD was created by this user.
1711 syncbuf.info(
1712 self,
1713 "discarding %d commits removed from upstream",
1714 len(local_changes) - cnt_mine,
1715 )
1716
1717 branch.remote = self.GetRemote()
Sylvain56a5a012023-09-11 13:38:00 +02001718 if not IsId(self.revisionExpr):
Gavin Makea2e3302023-03-11 06:46:20 +00001719 # In case of manifest sync the revisionExpr might be a SHA1.
1720 branch.merge = self.revisionExpr
1721 if not branch.merge.startswith("refs/"):
1722 branch.merge = R_HEADS + branch.merge
1723 branch.Save()
1724
1725 if cnt_mine > 0 and self.rebase:
1726
1727 def _docopyandlink():
1728 self._CopyAndLinkFiles()
1729
1730 def _dorebase():
1731 self._Rebase(upstream="%s^1" % last_mine, onto=revid)
1732
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001733 syncbuf.later2(self, _dorebase, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001734 if submodules:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001735 syncbuf.later2(self, _dosubmodules, not verbose)
1736 syncbuf.later2(self, _docopyandlink, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001737 elif local_changes:
1738 try:
1739 self._ResetHard(revid)
1740 if submodules:
1741 self._SyncSubmodules(quiet=True)
1742 self._CopyAndLinkFiles()
1743 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001744 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001745 return
1746 else:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001747 syncbuf.later1(self, _doff, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001748 if submodules:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001749 syncbuf.later1(self, _dosubmodules, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001750
1751 def AddCopyFile(self, src, dest, topdir):
1752 """Mark |src| for copying to |dest| (relative to |topdir|).
1753
1754 No filesystem changes occur here. Actual copying happens later on.
1755
1756 Paths should have basic validation run on them before being queued.
1757 Further checking will be handled when the actual copy happens.
1758 """
1759 self.copyfiles.append(_CopyFile(self.worktree, src, topdir, dest))
1760
1761 def AddLinkFile(self, src, dest, topdir):
1762 """Mark |dest| to create a symlink (relative to |topdir|) pointing to
1763 |src|.
1764
1765 No filesystem changes occur here. Actual linking happens later on.
1766
1767 Paths should have basic validation run on them before being queued.
1768 Further checking will be handled when the actual link happens.
1769 """
1770 self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest))
1771
1772 def AddAnnotation(self, name, value, keep):
1773 self.annotations.append(Annotation(name, value, keep))
1774
1775 def DownloadPatchSet(self, change_id, patch_id):
1776 """Download a single patch set of a single change to FETCH_HEAD."""
1777 remote = self.GetRemote()
1778
1779 cmd = ["fetch", remote.name]
1780 cmd.append(
1781 "refs/changes/%2.2d/%d/%d" % (change_id % 100, change_id, patch_id)
1782 )
Jason Chang1a3612f2023-08-08 14:12:53 -07001783 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001784 return DownloadedChange(
1785 self,
1786 self.GetRevisionId(),
1787 change_id,
1788 patch_id,
1789 self.bare_git.rev_parse("FETCH_HEAD"),
1790 )
1791
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001792 def DeleteWorktree(self, verbose=False, force=False):
Gavin Makea2e3302023-03-11 06:46:20 +00001793 """Delete the source checkout and any other housekeeping tasks.
1794
1795 This currently leaves behind the internal .repo/ cache state. This
1796 helps when switching branches or manifest changes get reverted as we
1797 don't have to redownload all the git objects. But we should do some GC
1798 at some point.
1799
1800 Args:
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001801 verbose: Whether to show verbose messages.
Gavin Makea2e3302023-03-11 06:46:20 +00001802 force: Always delete tree even if dirty.
Doug Anderson3ba5f952011-04-07 12:51:04 -07001803
1804 Returns:
Gavin Makea2e3302023-03-11 06:46:20 +00001805 True if the worktree was completely cleaned out.
1806 """
1807 if self.IsDirty():
1808 if force:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00001809 logger.warning(
Gavin Makea2e3302023-03-11 06:46:20 +00001810 "warning: %s: Removing dirty project: uncommitted changes "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001811 "lost.",
1812 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001813 )
1814 else:
Jason Chang32b59562023-07-14 16:45:35 -07001815 msg = (
1816 "error: %s: Cannot remove project: uncommitted"
1817 "changes are present.\n" % self.RelPath(local=False)
Gavin Makea2e3302023-03-11 06:46:20 +00001818 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001819 logger.error(msg)
Jason Chang32b59562023-07-14 16:45:35 -07001820 raise DeleteDirtyWorktreeError(msg, project=self)
Wink Saville02d79452009-04-10 13:01:24 -07001821
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001822 if verbose:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001823 print(f"{self.RelPath(local=False)}: Deleting obsolete checkout.")
Wink Saville02d79452009-04-10 13:01:24 -07001824
Gavin Makea2e3302023-03-11 06:46:20 +00001825 # Unlock and delink from the main worktree. We don't use git's worktree
1826 # remove because it will recursively delete projects -- we handle that
1827 # ourselves below. https://crbug.com/git/48
1828 if self.use_git_worktrees:
1829 needle = platform_utils.realpath(self.gitdir)
1830 # Find the git worktree commondir under .repo/worktrees/.
1831 output = self.bare_git.worktree("list", "--porcelain").splitlines()[
1832 0
1833 ]
1834 assert output.startswith("worktree "), output
1835 commondir = output[9:]
1836 # Walk each of the git worktrees to see where they point.
1837 configs = os.path.join(commondir, "worktrees")
1838 for name in os.listdir(configs):
1839 gitdir = os.path.join(configs, name, "gitdir")
1840 with open(gitdir) as fp:
1841 relpath = fp.read().strip()
1842 # Resolve the checkout path and see if it matches this project.
1843 fullpath = platform_utils.realpath(
1844 os.path.join(configs, name, relpath)
1845 )
1846 if fullpath == needle:
1847 platform_utils.rmtree(os.path.join(configs, name))
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001848
Gavin Makea2e3302023-03-11 06:46:20 +00001849 # Delete the .git directory first, so we're less likely to have a
1850 # partially working git repository around. There shouldn't be any git
1851 # projects here, so rmtree works.
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001852
Gavin Makea2e3302023-03-11 06:46:20 +00001853 # Try to remove plain files first in case of git worktrees. If this
1854 # fails for any reason, we'll fall back to rmtree, and that'll display
1855 # errors if it can't remove things either.
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001856 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001857 platform_utils.remove(self.gitdir)
1858 except OSError:
1859 pass
1860 try:
1861 platform_utils.rmtree(self.gitdir)
1862 except OSError as e:
1863 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001864 logger.error("error: %s: %s", self.gitdir, e)
1865 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00001866 "error: %s: Failed to delete obsolete checkout; remove "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001867 "manually, then run `repo sync -l`.",
1868 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001869 )
Jason Chang32b59562023-07-14 16:45:35 -07001870 raise DeleteWorktreeError(aggregate_errors=[e])
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001871
Gavin Makea2e3302023-03-11 06:46:20 +00001872 # Delete everything under the worktree, except for directories that
1873 # contain another git project.
1874 dirs_to_remove = []
1875 failed = False
Jason Chang32b59562023-07-14 16:45:35 -07001876 errors = []
Gavin Makea2e3302023-03-11 06:46:20 +00001877 for root, dirs, files in platform_utils.walk(self.worktree):
1878 for f in files:
1879 path = os.path.join(root, f)
1880 try:
1881 platform_utils.remove(path)
1882 except OSError as e:
1883 if e.errno != errno.ENOENT:
Josip Sokcevic4217a822024-01-24 13:54:25 -08001884 logger.warning("%s: Failed to remove: %s", path, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001885 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001886 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001887 dirs[:] = [
1888 d
1889 for d in dirs
1890 if not os.path.lexists(os.path.join(root, d, ".git"))
1891 ]
1892 dirs_to_remove += [
1893 os.path.join(root, d)
1894 for d in dirs
1895 if os.path.join(root, d) not in dirs_to_remove
1896 ]
1897 for d in reversed(dirs_to_remove):
1898 if platform_utils.islink(d):
1899 try:
1900 platform_utils.remove(d)
1901 except OSError as e:
1902 if e.errno != errno.ENOENT:
Josip Sokcevic4217a822024-01-24 13:54:25 -08001903 logger.warning("%s: Failed to remove: %s", d, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001904 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001905 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001906 elif not platform_utils.listdir(d):
1907 try:
1908 platform_utils.rmdir(d)
1909 except OSError as e:
1910 if e.errno != errno.ENOENT:
Josip Sokcevic4217a822024-01-24 13:54:25 -08001911 logger.warning("%s: Failed to remove: %s", d, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001912 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001913 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001914 if failed:
Josip Sokcevic4217a822024-01-24 13:54:25 -08001915 rename_path = (
1916 f"{self.worktree}_repo_to_be_deleted_{int(time.time())}"
Gavin Makea2e3302023-03-11 06:46:20 +00001917 )
Josip Sokcevic4217a822024-01-24 13:54:25 -08001918 try:
1919 platform_utils.rename(self.worktree, rename_path)
1920 logger.warning(
1921 "warning: renamed %s to %s. You can delete it, but you "
1922 "might need elevated permissions (e.g. root)",
1923 self.worktree,
1924 rename_path,
1925 )
1926 # Rename successful! Clear the errors.
1927 errors = []
1928 except OSError:
1929 logger.error(
1930 "%s: Failed to delete obsolete checkout.\n",
1931 " Remove manually, then run `repo sync -l`.",
1932 self.RelPath(local=False),
1933 )
1934 raise DeleteWorktreeError(aggregate_errors=errors)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07001935
Gavin Makea2e3302023-03-11 06:46:20 +00001936 # Try deleting parent dirs if they are empty.
1937 path = self.worktree
1938 while path != self.manifest.topdir:
1939 try:
1940 platform_utils.rmdir(path)
1941 except OSError as e:
1942 if e.errno != errno.ENOENT:
1943 break
1944 path = os.path.dirname(path)
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001945
Gavin Makea2e3302023-03-11 06:46:20 +00001946 return True
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001947
Gavin Makea2e3302023-03-11 06:46:20 +00001948 def StartBranch(self, name, branch_merge="", revision=None):
1949 """Create a new branch off the manifest's revision."""
1950 if not branch_merge:
1951 branch_merge = self.revisionExpr
1952 head = self.work_git.GetHead()
1953 if head == (R_HEADS + name):
1954 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001955
David Pursehouse8a68ff92012-09-24 12:15:13 +09001956 all_refs = self.bare_ref.all
Gavin Makea2e3302023-03-11 06:46:20 +00001957 if R_HEADS + name in all_refs:
Jason Chang1a3612f2023-08-08 14:12:53 -07001958 GitCommand(
1959 self, ["checkout", "-q", name, "--"], verify_command=True
1960 ).Wait()
1961 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001962
Gavin Makea2e3302023-03-11 06:46:20 +00001963 branch = self.GetBranch(name)
1964 branch.remote = self.GetRemote()
1965 branch.merge = branch_merge
Sylvain56a5a012023-09-11 13:38:00 +02001966 if not branch.merge.startswith("refs/") and not IsId(branch_merge):
Gavin Makea2e3302023-03-11 06:46:20 +00001967 branch.merge = R_HEADS + branch_merge
Shawn O. Pearce88443382010-10-08 10:02:09 +02001968
Gavin Makea2e3302023-03-11 06:46:20 +00001969 if revision is None:
1970 revid = self.GetRevisionId(all_refs)
Shawn O. Pearce88443382010-10-08 10:02:09 +02001971 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001972 revid = self.work_git.rev_parse(revision)
Brian Harring14a66742012-09-28 20:21:57 -07001973
Gavin Makea2e3302023-03-11 06:46:20 +00001974 if head.startswith(R_HEADS):
Kevin Degiabaa7f32014-11-12 11:27:45 -07001975 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001976 head = all_refs[head]
1977 except KeyError:
1978 head = None
1979 if revid and head and revid == head:
1980 ref = R_HEADS + name
1981 self.work_git.update_ref(ref, revid)
1982 self.work_git.symbolic_ref(HEAD, ref)
1983 branch.Save()
1984 return True
Kevin Degib1a07b82015-07-27 13:33:43 -06001985
Jason Chang1a3612f2023-08-08 14:12:53 -07001986 GitCommand(
1987 self,
1988 ["checkout", "-q", "-b", branch.name, revid],
1989 verify_command=True,
1990 ).Wait()
1991 branch.Save()
1992 return True
Kevin Degi384b3c52014-10-16 16:02:58 -06001993
Gavin Makea2e3302023-03-11 06:46:20 +00001994 def CheckoutBranch(self, name):
1995 """Checkout a local topic branch.
Shawn O. Pearce2816d4f2009-03-03 17:53:18 -08001996
Gavin Makea2e3302023-03-11 06:46:20 +00001997 Args:
1998 name: The name of the branch to checkout.
Shawn O. Pearce88443382010-10-08 10:02:09 +02001999
Gavin Makea2e3302023-03-11 06:46:20 +00002000 Returns:
Jason Chang1a3612f2023-08-08 14:12:53 -07002001 True if the checkout succeeded; False if the
2002 branch doesn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00002003 """
2004 rev = R_HEADS + name
2005 head = self.work_git.GetHead()
2006 if head == rev:
2007 # Already on the branch.
2008 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02002009
Gavin Makea2e3302023-03-11 06:46:20 +00002010 all_refs = self.bare_ref.all
2011 try:
2012 revid = all_refs[rev]
2013 except KeyError:
2014 # Branch does not exist in this project.
Jason Chang1a3612f2023-08-08 14:12:53 -07002015 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002016
Gavin Makea2e3302023-03-11 06:46:20 +00002017 if head.startswith(R_HEADS):
2018 try:
2019 head = all_refs[head]
2020 except KeyError:
2021 head = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002022
Gavin Makea2e3302023-03-11 06:46:20 +00002023 if head == revid:
2024 # Same revision; just update HEAD to point to the new
2025 # target branch, but otherwise take no other action.
2026 _lwrite(
2027 self.work_git.GetDotgitPath(subpath=HEAD),
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002028 f"ref: {R_HEADS}{name}\n",
Gavin Makea2e3302023-03-11 06:46:20 +00002029 )
2030 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05002031
Jason Chang1a3612f2023-08-08 14:12:53 -07002032 GitCommand(
2033 self,
2034 ["checkout", name, "--"],
2035 capture_stdout=True,
2036 capture_stderr=True,
2037 verify_command=True,
2038 ).Wait()
2039 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05002040
Gavin Makea2e3302023-03-11 06:46:20 +00002041 def AbandonBranch(self, name):
2042 """Destroy a local topic branch.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002043
Gavin Makea2e3302023-03-11 06:46:20 +00002044 Args:
2045 name: The name of the branch to abandon.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002046
Gavin Makea2e3302023-03-11 06:46:20 +00002047 Returns:
Jason Changf9aacd42023-08-03 14:38:00 -07002048 True if the abandon succeeded; Raises GitCommandError if it didn't;
2049 None if the branch didn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00002050 """
2051 rev = R_HEADS + name
2052 all_refs = self.bare_ref.all
2053 if rev not in all_refs:
2054 # Doesn't exist
2055 return None
2056
2057 head = self.work_git.GetHead()
2058 if head == rev:
2059 # We can't destroy the branch while we are sitting
2060 # on it. Switch to a detached HEAD.
2061 head = all_refs[head]
2062
2063 revid = self.GetRevisionId(all_refs)
2064 if head == revid:
2065 _lwrite(
2066 self.work_git.GetDotgitPath(subpath=HEAD), "%s\n" % revid
2067 )
2068 else:
2069 self._Checkout(revid, quiet=True)
Jason Changf9aacd42023-08-03 14:38:00 -07002070 GitCommand(
2071 self,
2072 ["branch", "-D", name],
2073 capture_stdout=True,
2074 capture_stderr=True,
2075 verify_command=True,
2076 ).Wait()
2077 return True
Gavin Makea2e3302023-03-11 06:46:20 +00002078
2079 def PruneHeads(self):
2080 """Prune any topic branches already merged into upstream."""
2081 cb = self.CurrentBranch
2082 kill = []
2083 left = self._allrefs
2084 for name in left.keys():
2085 if name.startswith(R_HEADS):
2086 name = name[len(R_HEADS) :]
2087 if cb is None or name != cb:
2088 kill.append(name)
2089
2090 # Minor optimization: If there's nothing to prune, then don't try to
2091 # read any project state.
2092 if not kill and not cb:
2093 return []
2094
2095 rev = self.GetRevisionId(left)
2096 if (
2097 cb is not None
2098 and not self._revlist(HEAD + "..." + rev)
2099 and not self.IsDirty(consider_untracked=False)
2100 ):
2101 self.work_git.DetachHead(HEAD)
2102 kill.append(cb)
2103
2104 if kill:
2105 old = self.bare_git.GetHead()
2106
2107 try:
2108 self.bare_git.DetachHead(rev)
2109
2110 b = ["branch", "-d"]
2111 b.extend(kill)
2112 b = GitCommand(
2113 self, b, bare=True, capture_stdout=True, capture_stderr=True
2114 )
2115 b.Wait()
2116 finally:
Sylvain56a5a012023-09-11 13:38:00 +02002117 if IsId(old):
Gavin Makea2e3302023-03-11 06:46:20 +00002118 self.bare_git.DetachHead(old)
2119 else:
2120 self.bare_git.SetHead(old)
2121 left = self._allrefs
2122
2123 for branch in kill:
2124 if (R_HEADS + branch) not in left:
2125 self.CleanPublishedCache()
2126 break
2127
2128 if cb and cb not in kill:
2129 kill.append(cb)
2130 kill.sort()
2131
2132 kept = []
2133 for branch in kill:
2134 if R_HEADS + branch in left:
2135 branch = self.GetBranch(branch)
2136 base = branch.LocalMerge
2137 if not base:
2138 base = rev
2139 kept.append(ReviewableBranch(self, branch, base))
2140 return kept
2141
2142 def GetRegisteredSubprojects(self):
2143 result = []
2144
2145 def rec(subprojects):
2146 if not subprojects:
2147 return
2148 result.extend(subprojects)
2149 for p in subprojects:
2150 rec(p.subprojects)
2151
2152 rec(self.subprojects)
2153 return result
2154
2155 def _GetSubmodules(self):
2156 # Unfortunately we cannot call `git submodule status --recursive` here
2157 # because the working tree might not exist yet, and it cannot be used
2158 # without a working tree in its current implementation.
2159
2160 def get_submodules(gitdir, rev):
2161 # Parse .gitmodules for submodule sub_paths and sub_urls.
2162 sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
2163 if not sub_paths:
2164 return []
2165 # Run `git ls-tree` to read SHAs of submodule object, which happen
2166 # to be revision of submodule repository.
2167 sub_revs = git_ls_tree(gitdir, rev, sub_paths)
2168 submodules = []
2169 for sub_path, sub_url in zip(sub_paths, sub_urls):
2170 try:
2171 sub_rev = sub_revs[sub_path]
2172 except KeyError:
2173 # Ignore non-exist submodules.
2174 continue
2175 submodules.append((sub_rev, sub_path, sub_url))
2176 return submodules
2177
2178 re_path = re.compile(r"^submodule\.(.+)\.path=(.*)$")
2179 re_url = re.compile(r"^submodule\.(.+)\.url=(.*)$")
2180
2181 def parse_gitmodules(gitdir, rev):
2182 cmd = ["cat-file", "blob", "%s:.gitmodules" % rev]
2183 try:
2184 p = GitCommand(
2185 None,
2186 cmd,
2187 capture_stdout=True,
2188 capture_stderr=True,
2189 bare=True,
2190 gitdir=gitdir,
2191 )
2192 except GitError:
2193 return [], []
2194 if p.Wait() != 0:
2195 return [], []
2196
2197 gitmodules_lines = []
2198 fd, temp_gitmodules_path = tempfile.mkstemp()
2199 try:
2200 os.write(fd, p.stdout.encode("utf-8"))
2201 os.close(fd)
2202 cmd = ["config", "--file", temp_gitmodules_path, "--list"]
2203 p = GitCommand(
2204 None,
2205 cmd,
2206 capture_stdout=True,
2207 capture_stderr=True,
2208 bare=True,
2209 gitdir=gitdir,
2210 )
2211 if p.Wait() != 0:
2212 return [], []
2213 gitmodules_lines = p.stdout.split("\n")
2214 except GitError:
2215 return [], []
2216 finally:
2217 platform_utils.remove(temp_gitmodules_path)
2218
2219 names = set()
2220 paths = {}
2221 urls = {}
2222 for line in gitmodules_lines:
2223 if not line:
2224 continue
2225 m = re_path.match(line)
2226 if m:
2227 names.add(m.group(1))
2228 paths[m.group(1)] = m.group(2)
2229 continue
2230 m = re_url.match(line)
2231 if m:
2232 names.add(m.group(1))
2233 urls[m.group(1)] = m.group(2)
2234 continue
2235 names = sorted(names)
2236 return (
2237 [paths.get(name, "") for name in names],
2238 [urls.get(name, "") for name in names],
2239 )
2240
2241 def git_ls_tree(gitdir, rev, paths):
2242 cmd = ["ls-tree", rev, "--"]
2243 cmd.extend(paths)
2244 try:
2245 p = GitCommand(
2246 None,
2247 cmd,
2248 capture_stdout=True,
2249 capture_stderr=True,
2250 bare=True,
2251 gitdir=gitdir,
2252 )
2253 except GitError:
2254 return []
2255 if p.Wait() != 0:
2256 return []
2257 objects = {}
2258 for line in p.stdout.split("\n"):
2259 if not line.strip():
2260 continue
2261 object_rev, object_path = line.split()[2:4]
2262 objects[object_path] = object_rev
2263 return objects
2264
2265 try:
2266 rev = self.GetRevisionId()
2267 except GitError:
2268 return []
2269 return get_submodules(self.gitdir, rev)
2270
2271 def GetDerivedSubprojects(self):
2272 result = []
2273 if not self.Exists:
2274 # If git repo does not exist yet, querying its submodules will
2275 # mess up its states; so return here.
2276 return result
2277 for rev, path, url in self._GetSubmodules():
2278 name = self.manifest.GetSubprojectName(self, path)
2279 (
2280 relpath,
2281 worktree,
2282 gitdir,
2283 objdir,
2284 ) = self.manifest.GetSubprojectPaths(self, name, path)
2285 project = self.manifest.paths.get(relpath)
2286 if project:
2287 result.extend(project.GetDerivedSubprojects())
2288 continue
2289
2290 if url.startswith(".."):
2291 url = urllib.parse.urljoin("%s/" % self.remote.url, url)
2292 remote = RemoteSpec(
2293 self.remote.name,
2294 url=url,
2295 pushUrl=self.remote.pushUrl,
2296 review=self.remote.review,
2297 revision=self.remote.revision,
2298 )
2299 subproject = Project(
2300 manifest=self.manifest,
2301 name=name,
2302 remote=remote,
2303 gitdir=gitdir,
2304 objdir=objdir,
2305 worktree=worktree,
2306 relpath=relpath,
2307 revisionExpr=rev,
2308 revisionId=rev,
2309 rebase=self.rebase,
2310 groups=self.groups,
2311 sync_c=self.sync_c,
2312 sync_s=self.sync_s,
2313 sync_tags=self.sync_tags,
2314 parent=self,
2315 is_derived=True,
2316 )
2317 result.append(subproject)
2318 result.extend(subproject.GetDerivedSubprojects())
2319 return result
2320
2321 def EnableRepositoryExtension(self, key, value="true", version=1):
2322 """Enable git repository extension |key| with |value|.
2323
2324 Args:
2325 key: The extension to enabled. Omit the "extensions." prefix.
2326 value: The value to use for the extension.
2327 version: The minimum git repository version needed.
2328 """
2329 # Make sure the git repo version is new enough already.
2330 found_version = self.config.GetInt("core.repositoryFormatVersion")
2331 if found_version is None:
2332 found_version = 0
2333 if found_version < version:
2334 self.config.SetString("core.repositoryFormatVersion", str(version))
2335
2336 # Enable the extension!
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002337 self.config.SetString(f"extensions.{key}", value)
Gavin Makea2e3302023-03-11 06:46:20 +00002338
2339 def ResolveRemoteHead(self, name=None):
2340 """Find out what the default branch (HEAD) points to.
2341
2342 Normally this points to refs/heads/master, but projects are moving to
2343 main. Support whatever the server uses rather than hardcoding "master"
2344 ourselves.
2345 """
2346 if name is None:
2347 name = self.remote.name
2348
2349 # The output will look like (NB: tabs are separators):
2350 # ref: refs/heads/master HEAD
2351 # 5f6803b100bb3cd0f534e96e88c91373e8ed1c44 HEAD
2352 output = self.bare_git.ls_remote(
2353 "-q", "--symref", "--exit-code", name, "HEAD"
2354 )
2355
2356 for line in output.splitlines():
2357 lhs, rhs = line.split("\t", 1)
2358 if rhs == "HEAD" and lhs.startswith("ref:"):
2359 return lhs[4:].strip()
2360
2361 return None
2362
2363 def _CheckForImmutableRevision(self):
2364 try:
2365 # if revision (sha or tag) is not present then following function
2366 # throws an error.
2367 self.bare_git.rev_list(
Jason Chang87058c62023-09-27 11:34:43 -07002368 "-1",
2369 "--missing=allow-any",
2370 "%s^0" % self.revisionExpr,
2371 "--",
2372 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002373 )
2374 if self.upstream:
2375 rev = self.GetRemote().ToLocal(self.upstream)
2376 self.bare_git.rev_list(
Jason Chang87058c62023-09-27 11:34:43 -07002377 "-1",
2378 "--missing=allow-any",
2379 "%s^0" % rev,
2380 "--",
2381 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002382 )
2383 self.bare_git.merge_base(
Jason Chang87058c62023-09-27 11:34:43 -07002384 "--is-ancestor",
2385 self.revisionExpr,
2386 rev,
2387 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002388 )
2389 return True
2390 except GitError:
2391 # There is no such persistent revision. We have to fetch it.
2392 return False
2393
2394 def _FetchArchive(self, tarpath, cwd=None):
2395 cmd = ["archive", "-v", "-o", tarpath]
2396 cmd.append("--remote=%s" % self.remote.url)
2397 cmd.append("--prefix=%s/" % self.RelPath(local=False))
2398 cmd.append(self.revisionExpr)
2399
2400 command = GitCommand(
Jason Chang32b59562023-07-14 16:45:35 -07002401 self,
2402 cmd,
2403 cwd=cwd,
2404 capture_stdout=True,
2405 capture_stderr=True,
2406 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00002407 )
Jason Chang32b59562023-07-14 16:45:35 -07002408 command.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00002409
2410 def _RemoteFetch(
2411 self,
2412 name=None,
2413 current_branch_only=False,
2414 initial=False,
2415 quiet=False,
2416 verbose=False,
2417 output_redir=None,
2418 alt_dir=None,
2419 tags=True,
2420 prune=False,
2421 depth=None,
2422 submodules=False,
2423 ssh_proxy=None,
2424 force_sync=False,
2425 clone_filter=None,
2426 retry_fetches=2,
2427 retry_sleep_initial_sec=4.0,
2428 retry_exp_factor=2.0,
Jason Chang32b59562023-07-14 16:45:35 -07002429 ) -> bool:
Gavin Makea2e3302023-03-11 06:46:20 +00002430 tag_name = None
2431 # The depth should not be used when fetching to a mirror because
2432 # it will result in a shallow repository that cannot be cloned or
2433 # fetched from.
2434 # The repo project should also never be synced with partial depth.
2435 if self.manifest.IsMirror or self.relpath == ".repo/repo":
2436 depth = None
2437
2438 if depth:
2439 current_branch_only = True
2440
Sylvain56a5a012023-09-11 13:38:00 +02002441 is_sha1 = bool(IsId(self.revisionExpr))
Gavin Makea2e3302023-03-11 06:46:20 +00002442
2443 if current_branch_only:
2444 if self.revisionExpr.startswith(R_TAGS):
2445 # This is a tag and its commit id should never change.
2446 tag_name = self.revisionExpr[len(R_TAGS) :]
2447 elif self.upstream and self.upstream.startswith(R_TAGS):
2448 # This is a tag and its commit id should never change.
2449 tag_name = self.upstream[len(R_TAGS) :]
2450
2451 if is_sha1 or tag_name is not None:
2452 if self._CheckForImmutableRevision():
2453 if verbose:
2454 print(
2455 "Skipped fetching project %s (already have "
2456 "persistent ref)" % self.name
2457 )
2458 return True
2459 if is_sha1 and not depth:
2460 # When syncing a specific commit and --depth is not set:
2461 # * if upstream is explicitly specified and is not a sha1, fetch
2462 # only upstream as users expect only upstream to be fetch.
2463 # Note: The commit might not be in upstream in which case the
2464 # sync will fail.
2465 # * otherwise, fetch all branches to make sure we end up with
2466 # the specific commit.
2467 if self.upstream:
Sylvain56a5a012023-09-11 13:38:00 +02002468 current_branch_only = not IsId(self.upstream)
Gavin Makea2e3302023-03-11 06:46:20 +00002469 else:
2470 current_branch_only = False
2471
2472 if not name:
2473 name = self.remote.name
2474
2475 remote = self.GetRemote(name)
2476 if not remote.PreConnectFetch(ssh_proxy):
2477 ssh_proxy = None
2478
2479 if initial:
2480 if alt_dir and "objects" == os.path.basename(alt_dir):
2481 ref_dir = os.path.dirname(alt_dir)
2482 packed_refs = os.path.join(self.gitdir, "packed-refs")
2483
2484 all_refs = self.bare_ref.all
2485 ids = set(all_refs.values())
2486 tmp = set()
2487
2488 for r, ref_id in GitRefs(ref_dir).all.items():
2489 if r not in all_refs:
2490 if r.startswith(R_TAGS) or remote.WritesTo(r):
2491 all_refs[r] = ref_id
2492 ids.add(ref_id)
2493 continue
2494
2495 if ref_id in ids:
2496 continue
2497
2498 r = "refs/_alt/%s" % ref_id
2499 all_refs[r] = ref_id
2500 ids.add(ref_id)
2501 tmp.add(r)
2502
2503 tmp_packed_lines = []
2504 old_packed_lines = []
2505
2506 for r in sorted(all_refs):
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002507 line = f"{all_refs[r]} {r}\n"
Gavin Makea2e3302023-03-11 06:46:20 +00002508 tmp_packed_lines.append(line)
2509 if r not in tmp:
2510 old_packed_lines.append(line)
2511
2512 tmp_packed = "".join(tmp_packed_lines)
2513 old_packed = "".join(old_packed_lines)
2514 _lwrite(packed_refs, tmp_packed)
2515 else:
2516 alt_dir = None
2517
2518 cmd = ["fetch"]
2519
2520 if clone_filter:
2521 git_require((2, 19, 0), fail=True, msg="partial clones")
2522 cmd.append("--filter=%s" % clone_filter)
2523 self.EnableRepositoryExtension("partialclone", self.remote.name)
2524
2525 if depth:
2526 cmd.append("--depth=%s" % depth)
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002527 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002528 # If this repo has shallow objects, then we don't know which refs
2529 # have shallow objects or not. Tell git to unshallow all fetched
2530 # refs. Don't do this with projects that don't have shallow
2531 # objects, since it is less efficient.
2532 if os.path.exists(os.path.join(self.gitdir, "shallow")):
2533 cmd.append("--depth=2147483647")
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002534
Gavin Makea2e3302023-03-11 06:46:20 +00002535 if not verbose:
2536 cmd.append("--quiet")
2537 if not quiet and sys.stdout.isatty():
2538 cmd.append("--progress")
2539 if not self.worktree:
2540 cmd.append("--update-head-ok")
2541 cmd.append(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002542
Gavin Makea2e3302023-03-11 06:46:20 +00002543 if force_sync:
2544 cmd.append("--force")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002545
Gavin Makea2e3302023-03-11 06:46:20 +00002546 if prune:
2547 cmd.append("--prune")
Mike Frysinger29626b42021-05-01 09:37:13 -04002548
Gavin Makea2e3302023-03-11 06:46:20 +00002549 # Always pass something for --recurse-submodules, git with GIT_DIR
2550 # behaves incorrectly when not given `--recurse-submodules=no`.
2551 # (b/218891912)
2552 cmd.append(
2553 f'--recurse-submodules={"on-demand" if submodules else "no"}'
2554 )
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002555
Gavin Makea2e3302023-03-11 06:46:20 +00002556 spec = []
2557 if not current_branch_only:
2558 # Fetch whole repo.
2559 spec.append(
2560 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2561 )
2562 elif tag_name is not None:
2563 spec.append("tag")
2564 spec.append(tag_name)
Remy Böhmer1469c282020-12-15 18:49:02 +01002565
Gavin Makea2e3302023-03-11 06:46:20 +00002566 if self.manifest.IsMirror and not current_branch_only:
2567 branch = None
Remy Böhmer1469c282020-12-15 18:49:02 +01002568 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002569 branch = self.revisionExpr
2570 if (
2571 not self.manifest.IsMirror
2572 and is_sha1
2573 and depth
2574 and git_require((1, 8, 3))
2575 ):
2576 # Shallow checkout of a specific commit, fetch from that commit and
2577 # not the heads only as the commit might be deeper in the history.
2578 spec.append(branch)
2579 if self.upstream:
2580 spec.append(self.upstream)
David James8d201162013-10-11 17:03:19 -07002581 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002582 if is_sha1:
2583 branch = self.upstream
2584 if branch is not None and branch.strip():
2585 if not branch.startswith("refs/"):
2586 branch = R_HEADS + branch
2587 spec.append(str(("+%s:" % branch) + remote.ToLocal(branch)))
David James8d201162013-10-11 17:03:19 -07002588
Gavin Makea2e3302023-03-11 06:46:20 +00002589 # If mirroring repo and we cannot deduce the tag or branch to fetch,
2590 # fetch whole repo.
2591 if self.manifest.IsMirror and not spec:
2592 spec.append(
2593 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2594 )
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002595
Gavin Makea2e3302023-03-11 06:46:20 +00002596 # If using depth then we should not get all the tags since they may
2597 # be outside of the depth.
2598 if not tags or depth:
2599 cmd.append("--no-tags")
2600 else:
2601 cmd.append("--tags")
2602 spec.append(str(("+refs/tags/*:") + remote.ToLocal("refs/tags/*")))
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002603
Gavin Makea2e3302023-03-11 06:46:20 +00002604 cmd.extend(spec)
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002605
Gavin Makea2e3302023-03-11 06:46:20 +00002606 # At least one retry minimum due to git remote prune.
2607 retry_fetches = max(retry_fetches, 2)
2608 retry_cur_sleep = retry_sleep_initial_sec
2609 ok = prune_tried = False
2610 for try_n in range(retry_fetches):
Jason Chang32b59562023-07-14 16:45:35 -07002611 verify_command = try_n == retry_fetches - 1
Gavin Makea2e3302023-03-11 06:46:20 +00002612 gitcmd = GitCommand(
2613 self,
2614 cmd,
2615 bare=True,
2616 objdir=os.path.join(self.objdir, "objects"),
2617 ssh_proxy=ssh_proxy,
2618 merge_output=True,
2619 capture_stdout=quiet or bool(output_redir),
Jason Chang32b59562023-07-14 16:45:35 -07002620 verify_command=verify_command,
Gavin Makea2e3302023-03-11 06:46:20 +00002621 )
2622 if gitcmd.stdout and not quiet and output_redir:
2623 output_redir.write(gitcmd.stdout)
2624 ret = gitcmd.Wait()
2625 if ret == 0:
2626 ok = True
2627 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002628
Gavin Makea2e3302023-03-11 06:46:20 +00002629 # Retry later due to HTTP 429 Too Many Requests.
2630 elif (
2631 gitcmd.stdout
2632 and "error:" in gitcmd.stdout
2633 and "HTTP 429" in gitcmd.stdout
2634 ):
2635 # Fallthru to sleep+retry logic at the bottom.
2636 pass
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002637
Gavin Makea2e3302023-03-11 06:46:20 +00002638 # Try to prune remote branches once in case there are conflicts.
2639 # For example, if the remote had refs/heads/upstream, but deleted
2640 # that and now has refs/heads/upstream/foo.
2641 elif (
2642 gitcmd.stdout
2643 and "error:" in gitcmd.stdout
2644 and "git remote prune" in gitcmd.stdout
2645 and not prune_tried
2646 ):
2647 prune_tried = True
2648 prunecmd = GitCommand(
2649 self,
2650 ["remote", "prune", name],
2651 bare=True,
2652 ssh_proxy=ssh_proxy,
2653 )
2654 ret = prunecmd.Wait()
2655 if ret:
2656 break
2657 print(
2658 "retrying fetch after pruning remote branches",
2659 file=output_redir,
2660 )
2661 # Continue right away so we don't sleep as we shouldn't need to.
2662 continue
2663 elif current_branch_only and is_sha1 and ret == 128:
2664 # Exit code 128 means "couldn't find the ref you asked for"; if
2665 # we're in sha1 mode, we just tried sync'ing from the upstream
2666 # field; it doesn't exist, thus abort the optimization attempt
2667 # and do a full sync.
2668 break
2669 elif ret < 0:
2670 # Git died with a signal, exit immediately.
2671 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002672
Gavin Makea2e3302023-03-11 06:46:20 +00002673 # Figure out how long to sleep before the next attempt, if there is
2674 # one.
2675 if not verbose and gitcmd.stdout:
2676 print(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002677 f"\n{self.name}:\n{gitcmd.stdout}",
Gavin Makea2e3302023-03-11 06:46:20 +00002678 end="",
2679 file=output_redir,
2680 )
2681 if try_n < retry_fetches - 1:
2682 print(
2683 "%s: sleeping %s seconds before retrying"
2684 % (self.name, retry_cur_sleep),
2685 file=output_redir,
2686 )
2687 time.sleep(retry_cur_sleep)
2688 retry_cur_sleep = min(
2689 retry_exp_factor * retry_cur_sleep, MAXIMUM_RETRY_SLEEP_SEC
2690 )
2691 retry_cur_sleep *= 1 - random.uniform(
2692 -RETRY_JITTER_PERCENT, RETRY_JITTER_PERCENT
2693 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002694
Gavin Makea2e3302023-03-11 06:46:20 +00002695 if initial:
2696 if alt_dir:
2697 if old_packed != "":
2698 _lwrite(packed_refs, old_packed)
2699 else:
2700 platform_utils.remove(packed_refs)
2701 self.bare_git.pack_refs("--all", "--prune")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002702
Gavin Makea2e3302023-03-11 06:46:20 +00002703 if is_sha1 and current_branch_only:
2704 # We just synced the upstream given branch; verify we
2705 # got what we wanted, else trigger a second run of all
2706 # refs.
2707 if not self._CheckForImmutableRevision():
2708 # Sync the current branch only with depth set to None.
2709 # We always pass depth=None down to avoid infinite recursion.
2710 return self._RemoteFetch(
2711 name=name,
2712 quiet=quiet,
2713 verbose=verbose,
2714 output_redir=output_redir,
2715 current_branch_only=current_branch_only and depth,
2716 initial=False,
2717 alt_dir=alt_dir,
Nasser Grainawiaafed292023-05-24 12:51:03 -06002718 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00002719 depth=None,
2720 ssh_proxy=ssh_proxy,
2721 clone_filter=clone_filter,
2722 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002723
Gavin Makea2e3302023-03-11 06:46:20 +00002724 return ok
Mike Frysingerf4545122019-11-11 04:34:16 -05002725
Gavin Makea2e3302023-03-11 06:46:20 +00002726 def _ApplyCloneBundle(self, initial=False, quiet=False, verbose=False):
2727 if initial and (
2728 self.manifest.manifestProject.depth or self.clone_depth
2729 ):
2730 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002731
Gavin Makea2e3302023-03-11 06:46:20 +00002732 remote = self.GetRemote()
2733 bundle_url = remote.url + "/clone.bundle"
2734 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
2735 if GetSchemeFromUrl(bundle_url) not in (
2736 "http",
2737 "https",
2738 "persistent-http",
2739 "persistent-https",
2740 ):
2741 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06002742
Gavin Makea2e3302023-03-11 06:46:20 +00002743 bundle_dst = os.path.join(self.gitdir, "clone.bundle")
2744 bundle_tmp = os.path.join(self.gitdir, "clone.bundle.tmp")
2745
2746 exist_dst = os.path.exists(bundle_dst)
2747 exist_tmp = os.path.exists(bundle_tmp)
2748
2749 if not initial and not exist_dst and not exist_tmp:
2750 return False
2751
2752 if not exist_dst:
2753 exist_dst = self._FetchBundle(
2754 bundle_url, bundle_tmp, bundle_dst, quiet, verbose
2755 )
2756 if not exist_dst:
2757 return False
2758
2759 cmd = ["fetch"]
2760 if not verbose:
2761 cmd.append("--quiet")
2762 if not quiet and sys.stdout.isatty():
2763 cmd.append("--progress")
2764 if not self.worktree:
2765 cmd.append("--update-head-ok")
2766 cmd.append(bundle_dst)
2767 for f in remote.fetch:
2768 cmd.append(str(f))
2769 cmd.append("+refs/tags/*:refs/tags/*")
2770
2771 ok = (
2772 GitCommand(
2773 self,
2774 cmd,
2775 bare=True,
2776 objdir=os.path.join(self.objdir, "objects"),
2777 ).Wait()
2778 == 0
2779 )
2780 platform_utils.remove(bundle_dst, missing_ok=True)
2781 platform_utils.remove(bundle_tmp, missing_ok=True)
2782 return ok
2783
2784 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
2785 platform_utils.remove(dstPath, missing_ok=True)
2786
2787 cmd = ["curl", "--fail", "--output", tmpPath, "--netrc", "--location"]
2788 if quiet:
2789 cmd += ["--silent", "--show-error"]
2790 if os.path.exists(tmpPath):
2791 size = os.stat(tmpPath).st_size
2792 if size >= 1024:
2793 cmd += ["--continue-at", "%d" % (size,)]
2794 else:
2795 platform_utils.remove(tmpPath)
2796 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
2797 if cookiefile:
2798 cmd += ["--cookie", cookiefile]
2799 if proxy:
2800 cmd += ["--proxy", proxy]
2801 elif "http_proxy" in os.environ and "darwin" == sys.platform:
2802 cmd += ["--proxy", os.environ["http_proxy"]]
2803 if srcUrl.startswith("persistent-https"):
2804 srcUrl = "http" + srcUrl[len("persistent-https") :]
2805 elif srcUrl.startswith("persistent-http"):
2806 srcUrl = "http" + srcUrl[len("persistent-http") :]
2807 cmd += [srcUrl]
2808
2809 proc = None
2810 with Trace("Fetching bundle: %s", " ".join(cmd)):
2811 if verbose:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002812 print(f"{self.name}: Downloading bundle: {srcUrl}")
Gavin Makea2e3302023-03-11 06:46:20 +00002813 stdout = None if verbose else subprocess.PIPE
2814 stderr = None if verbose else subprocess.STDOUT
2815 try:
2816 proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
2817 except OSError:
2818 return False
2819
2820 (output, _) = proc.communicate()
2821 curlret = proc.returncode
2822
2823 if curlret == 22:
2824 # From curl man page:
2825 # 22: HTTP page not retrieved. The requested url was not found
2826 # or returned another error with the HTTP error code being 400
2827 # or above. This return code only appears if -f, --fail is used.
2828 if verbose:
2829 print(
2830 "%s: Unable to retrieve clone.bundle; ignoring."
2831 % self.name
2832 )
2833 if output:
2834 print("Curl output:\n%s" % output)
2835 return False
2836 elif curlret and not verbose and output:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002837 logger.error("%s", output)
Gavin Makea2e3302023-03-11 06:46:20 +00002838
2839 if os.path.exists(tmpPath):
2840 if curlret == 0 and self._IsValidBundle(tmpPath, quiet):
2841 platform_utils.rename(tmpPath, dstPath)
2842 return True
2843 else:
2844 platform_utils.remove(tmpPath)
2845 return False
2846 else:
2847 return False
2848
2849 def _IsValidBundle(self, path, quiet):
2850 try:
2851 with open(path, "rb") as f:
2852 if f.read(16) == b"# v2 git bundle\n":
2853 return True
2854 else:
2855 if not quiet:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002856 logger.error("Invalid clone.bundle file; ignoring.")
Gavin Makea2e3302023-03-11 06:46:20 +00002857 return False
2858 except OSError:
2859 return False
2860
Josip Sokcevicedadb252024-02-29 09:48:37 -08002861 def _Checkout(self, rev, force_checkout=False, quiet=False):
Gavin Makea2e3302023-03-11 06:46:20 +00002862 cmd = ["checkout"]
2863 if quiet:
2864 cmd.append("-q")
Josip Sokcevicedadb252024-02-29 09:48:37 -08002865 if force_checkout:
2866 cmd.append("-f")
Gavin Makea2e3302023-03-11 06:46:20 +00002867 cmd.append(rev)
2868 cmd.append("--")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002869 if GitCommand(self, cmd).Wait() != 0:
Gavin Makea2e3302023-03-11 06:46:20 +00002870 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002871 raise GitError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002872 f"{self.name} checkout {rev} ", project=self.name
Jason Chang32b59562023-07-14 16:45:35 -07002873 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002874
Gavin Makea2e3302023-03-11 06:46:20 +00002875 def _CherryPick(self, rev, ffonly=False, record_origin=False):
2876 cmd = ["cherry-pick"]
2877 if ffonly:
2878 cmd.append("--ff")
2879 if record_origin:
2880 cmd.append("-x")
2881 cmd.append(rev)
2882 cmd.append("--")
2883 if GitCommand(self, cmd).Wait() != 0:
2884 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002885 raise GitError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002886 f"{self.name} cherry-pick {rev} ", project=self.name
Jason Chang32b59562023-07-14 16:45:35 -07002887 )
Victor Boivie0960b5b2010-11-26 13:42:13 +01002888
Gavin Makea2e3302023-03-11 06:46:20 +00002889 def _LsRemote(self, refs):
2890 cmd = ["ls-remote", self.remote.name, refs]
2891 p = GitCommand(self, cmd, capture_stdout=True)
2892 if p.Wait() == 0:
2893 return p.stdout
2894 return None
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002895
Gavin Makea2e3302023-03-11 06:46:20 +00002896 def _Revert(self, rev):
2897 cmd = ["revert"]
2898 cmd.append("--no-edit")
2899 cmd.append(rev)
2900 cmd.append("--")
2901 if GitCommand(self, cmd).Wait() != 0:
2902 if self._allrefs:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002903 raise GitError(f"{self.name} revert {rev} ", project=self.name)
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002904
Gavin Makea2e3302023-03-11 06:46:20 +00002905 def _ResetHard(self, rev, quiet=True):
2906 cmd = ["reset", "--hard"]
2907 if quiet:
2908 cmd.append("-q")
2909 cmd.append(rev)
2910 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002911 raise GitError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002912 f"{self.name} reset --hard {rev} ", project=self.name
Jason Chang32b59562023-07-14 16:45:35 -07002913 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002914
Gavin Makea2e3302023-03-11 06:46:20 +00002915 def _SyncSubmodules(self, quiet=True):
2916 cmd = ["submodule", "update", "--init", "--recursive"]
2917 if quiet:
2918 cmd.append("-q")
2919 if GitCommand(self, cmd).Wait() != 0:
2920 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07002921 "%s submodule update --init --recursive " % self.name,
2922 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00002923 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002924
Gavin Makea2e3302023-03-11 06:46:20 +00002925 def _Rebase(self, upstream, onto=None):
2926 cmd = ["rebase"]
2927 if onto is not None:
2928 cmd.extend(["--onto", onto])
2929 cmd.append(upstream)
2930 if GitCommand(self, cmd).Wait() != 0:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002931 raise GitError(f"{self.name} rebase {upstream} ", project=self.name)
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002932
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08002933 def _FastForward(self, head, ffonly=False, quiet=True):
Gavin Makea2e3302023-03-11 06:46:20 +00002934 cmd = ["merge", "--no-stat", head]
2935 if ffonly:
2936 cmd.append("--ff-only")
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08002937 if quiet:
2938 cmd.append("-q")
Gavin Makea2e3302023-03-11 06:46:20 +00002939 if GitCommand(self, cmd).Wait() != 0:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002940 raise GitError(f"{self.name} merge {head} ", project=self.name)
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002941
Gavin Makea2e3302023-03-11 06:46:20 +00002942 def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False):
2943 init_git_dir = not os.path.exists(self.gitdir)
2944 init_obj_dir = not os.path.exists(self.objdir)
2945 try:
2946 # Initialize the bare repository, which contains all of the objects.
2947 if init_obj_dir:
2948 os.makedirs(self.objdir)
2949 self.bare_objdir.init()
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002950
Gavin Makea2e3302023-03-11 06:46:20 +00002951 self._UpdateHooks(quiet=quiet)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002952
Gavin Makea2e3302023-03-11 06:46:20 +00002953 if self.use_git_worktrees:
2954 # Enable per-worktree config file support if possible. This
2955 # is more a nice-to-have feature for users rather than a
2956 # hard requirement.
2957 if git_require((2, 20, 0)):
2958 self.EnableRepositoryExtension("worktreeConfig")
Renaud Paquay788e9622017-01-27 11:41:12 -08002959
Gavin Makea2e3302023-03-11 06:46:20 +00002960 # If we have a separate directory to hold refs, initialize it as
2961 # well.
2962 if self.objdir != self.gitdir:
2963 if init_git_dir:
2964 os.makedirs(self.gitdir)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002965
Gavin Makea2e3302023-03-11 06:46:20 +00002966 if init_obj_dir or init_git_dir:
2967 self._ReferenceGitDir(
2968 self.objdir, self.gitdir, copy_all=True
2969 )
2970 try:
2971 self._CheckDirReference(self.objdir, self.gitdir)
2972 except GitError as e:
2973 if force_sync:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002974 logger.error(
2975 "Retrying clone after deleting %s", self.gitdir
Gavin Makea2e3302023-03-11 06:46:20 +00002976 )
2977 try:
2978 platform_utils.rmtree(
2979 platform_utils.realpath(self.gitdir)
2980 )
2981 if self.worktree and os.path.exists(
2982 platform_utils.realpath(self.worktree)
2983 ):
2984 platform_utils.rmtree(
2985 platform_utils.realpath(self.worktree)
2986 )
2987 return self._InitGitDir(
2988 mirror_git=mirror_git,
2989 force_sync=False,
2990 quiet=quiet,
2991 )
2992 except Exception:
2993 raise e
2994 raise e
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002995
Gavin Makea2e3302023-03-11 06:46:20 +00002996 if init_git_dir:
2997 mp = self.manifest.manifestProject
2998 ref_dir = mp.reference or ""
Julien Camperguedd654222014-01-09 16:21:37 +01002999
Gavin Makea2e3302023-03-11 06:46:20 +00003000 def _expanded_ref_dirs():
3001 """Iterate through possible git reference dir paths."""
3002 name = self.name + ".git"
3003 yield mirror_git or os.path.join(ref_dir, name)
3004 for prefix in "", self.remote.name:
3005 yield os.path.join(
3006 ref_dir, ".repo", "project-objects", prefix, name
3007 )
3008 yield os.path.join(
3009 ref_dir, ".repo", "worktrees", prefix, name
3010 )
3011
3012 if ref_dir or mirror_git:
3013 found_ref_dir = None
3014 for path in _expanded_ref_dirs():
3015 if os.path.exists(path):
3016 found_ref_dir = path
3017 break
3018 ref_dir = found_ref_dir
3019
3020 if ref_dir:
3021 if not os.path.isabs(ref_dir):
3022 # The alternate directory is relative to the object
3023 # database.
3024 ref_dir = os.path.relpath(
3025 ref_dir, os.path.join(self.objdir, "objects")
3026 )
3027 _lwrite(
3028 os.path.join(
3029 self.objdir, "objects/info/alternates"
3030 ),
3031 os.path.join(ref_dir, "objects") + "\n",
3032 )
3033
3034 m = self.manifest.manifestProject.config
3035 for key in ["user.name", "user.email"]:
3036 if m.Has(key, include_defaults=False):
3037 self.config.SetString(key, m.GetString(key))
3038 if not self.manifest.EnableGitLfs:
3039 self.config.SetString(
3040 "filter.lfs.smudge", "git-lfs smudge --skip -- %f"
3041 )
3042 self.config.SetString(
3043 "filter.lfs.process", "git-lfs filter-process --skip"
3044 )
3045 self.config.SetBoolean(
3046 "core.bare", True if self.manifest.IsMirror else None
3047 )
Josip Sokcevic9267d582023-10-19 14:46:11 -07003048
3049 if not init_obj_dir:
3050 # The project might be shared (obj_dir already initialized), but
3051 # such information is not available here. Instead of passing it,
3052 # set it as shared, and rely to be unset down the execution
3053 # path.
3054 if git_require((2, 7, 0)):
3055 self.EnableRepositoryExtension("preciousObjects")
3056 else:
3057 self.config.SetString("gc.pruneExpire", "never")
3058
Gavin Makea2e3302023-03-11 06:46:20 +00003059 except Exception:
3060 if init_obj_dir and os.path.exists(self.objdir):
3061 platform_utils.rmtree(self.objdir)
3062 if init_git_dir and os.path.exists(self.gitdir):
3063 platform_utils.rmtree(self.gitdir)
3064 raise
3065
3066 def _UpdateHooks(self, quiet=False):
3067 if os.path.exists(self.objdir):
3068 self._InitHooks(quiet=quiet)
3069
3070 def _InitHooks(self, quiet=False):
3071 hooks = platform_utils.realpath(os.path.join(self.objdir, "hooks"))
3072 if not os.path.exists(hooks):
3073 os.makedirs(hooks)
3074
3075 # Delete sample hooks. They're noise.
3076 for hook in glob.glob(os.path.join(hooks, "*.sample")):
3077 try:
3078 platform_utils.remove(hook, missing_ok=True)
3079 except PermissionError:
3080 pass
3081
3082 for stock_hook in _ProjectHooks():
3083 name = os.path.basename(stock_hook)
3084
3085 if (
3086 name in ("commit-msg",)
3087 and not self.remote.review
3088 and self is not self.manifest.manifestProject
3089 ):
3090 # Don't install a Gerrit Code Review hook if this
3091 # project does not appear to use it for reviews.
3092 #
3093 # Since the manifest project is one of those, but also
3094 # managed through gerrit, it's excluded.
3095 continue
3096
3097 dst = os.path.join(hooks, name)
3098 if platform_utils.islink(dst):
3099 continue
3100 if os.path.exists(dst):
3101 # If the files are the same, we'll leave it alone. We create
3102 # symlinks below by default but fallback to hardlinks if the OS
3103 # blocks them. So if we're here, it's probably because we made a
3104 # hardlink below.
3105 if not filecmp.cmp(stock_hook, dst, shallow=False):
3106 if not quiet:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00003107 logger.warning(
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00003108 "warn: %s: Not replacing locally modified %s hook",
Gavin Makea2e3302023-03-11 06:46:20 +00003109 self.RelPath(local=False),
3110 name,
3111 )
3112 continue
3113 try:
3114 platform_utils.symlink(
3115 os.path.relpath(stock_hook, os.path.dirname(dst)), dst
3116 )
3117 except OSError as e:
3118 if e.errno == errno.EPERM:
3119 try:
3120 os.link(stock_hook, dst)
3121 except OSError:
Jason Chang32b59562023-07-14 16:45:35 -07003122 raise GitError(
3123 self._get_symlink_error_message(), project=self.name
3124 )
Gavin Makea2e3302023-03-11 06:46:20 +00003125 else:
3126 raise
3127
3128 def _InitRemote(self):
3129 if self.remote.url:
3130 remote = self.GetRemote()
3131 remote.url = self.remote.url
3132 remote.pushUrl = self.remote.pushUrl
3133 remote.review = self.remote.review
3134 remote.projectname = self.name
3135
3136 if self.worktree:
3137 remote.ResetFetch(mirror=False)
3138 else:
3139 remote.ResetFetch(mirror=True)
3140 remote.Save()
3141
3142 def _InitMRef(self):
3143 """Initialize the pseudo m/<manifest branch> ref."""
3144 if self.manifest.branch:
3145 if self.use_git_worktrees:
3146 # Set up the m/ space to point to the worktree-specific ref
3147 # space. We'll update the worktree-specific ref space on each
3148 # checkout.
3149 ref = R_M + self.manifest.branch
3150 if not self.bare_ref.symref(ref):
3151 self.bare_git.symbolic_ref(
3152 "-m",
3153 "redirecting to worktree scope",
3154 ref,
3155 R_WORKTREE_M + self.manifest.branch,
3156 )
3157
3158 # We can't update this ref with git worktrees until it exists.
3159 # We'll wait until the initial checkout to set it.
3160 if not os.path.exists(self.worktree):
3161 return
3162
3163 base = R_WORKTREE_M
3164 active_git = self.work_git
3165
3166 self._InitAnyMRef(HEAD, self.bare_git, detach=True)
3167 else:
3168 base = R_M
3169 active_git = self.bare_git
3170
3171 self._InitAnyMRef(base + self.manifest.branch, active_git)
3172
3173 def _InitMirrorHead(self):
3174 self._InitAnyMRef(HEAD, self.bare_git)
3175
3176 def _InitAnyMRef(self, ref, active_git, detach=False):
3177 """Initialize |ref| in |active_git| to the value in the manifest.
3178
3179 This points |ref| to the <project> setting in the manifest.
3180
3181 Args:
3182 ref: The branch to update.
3183 active_git: The git repository to make updates in.
3184 detach: Whether to update target of symbolic refs, or overwrite the
3185 ref directly (and thus make it non-symbolic).
3186 """
3187 cur = self.bare_ref.symref(ref)
3188
3189 if self.revisionId:
3190 if cur != "" or self.bare_ref.get(ref) != self.revisionId:
3191 msg = "manifest set to %s" % self.revisionId
3192 dst = self.revisionId + "^0"
3193 active_git.UpdateRef(ref, dst, message=msg, detach=True)
Julien Camperguedd654222014-01-09 16:21:37 +01003194 else:
Gavin Makea2e3302023-03-11 06:46:20 +00003195 remote = self.GetRemote()
3196 dst = remote.ToLocal(self.revisionExpr)
3197 if cur != dst:
3198 msg = "manifest set to %s" % self.revisionExpr
3199 if detach:
3200 active_git.UpdateRef(ref, dst, message=msg, detach=True)
3201 else:
3202 active_git.symbolic_ref("-m", msg, ref, dst)
Julien Camperguedd654222014-01-09 16:21:37 +01003203
Gavin Makea2e3302023-03-11 06:46:20 +00003204 def _CheckDirReference(self, srcdir, destdir):
3205 # Git worktrees don't use symlinks to share at all.
3206 if self.use_git_worktrees:
3207 return
Julien Camperguedd654222014-01-09 16:21:37 +01003208
Gavin Makea2e3302023-03-11 06:46:20 +00003209 for name in self.shareable_dirs:
3210 # Try to self-heal a bit in simple cases.
3211 dst_path = os.path.join(destdir, name)
3212 src_path = os.path.join(srcdir, name)
Julien Camperguedd654222014-01-09 16:21:37 +01003213
Gavin Makea2e3302023-03-11 06:46:20 +00003214 dst = platform_utils.realpath(dst_path)
3215 if os.path.lexists(dst):
3216 src = platform_utils.realpath(src_path)
3217 # Fail if the links are pointing to the wrong place.
3218 if src != dst:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00003219 logger.error(
3220 "error: %s is different in %s vs %s",
3221 name,
3222 destdir,
3223 srcdir,
3224 )
Gavin Makea2e3302023-03-11 06:46:20 +00003225 raise GitError(
3226 "--force-sync not enabled; cannot overwrite a local "
3227 "work tree. If you're comfortable with the "
3228 "possibility of losing the work tree's git metadata,"
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003229 " use "
3230 f"`repo sync --force-sync {self.RelPath(local=False)}` "
3231 "to proceed.",
Jason Chang32b59562023-07-14 16:45:35 -07003232 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003233 )
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003234
Gavin Makea2e3302023-03-11 06:46:20 +00003235 def _ReferenceGitDir(self, gitdir, dotgit, copy_all):
3236 """Update |dotgit| to reference |gitdir|, using symlinks where possible.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003237
Gavin Makea2e3302023-03-11 06:46:20 +00003238 Args:
3239 gitdir: The bare git repository. Must already be initialized.
3240 dotgit: The repository you would like to initialize.
3241 copy_all: If true, copy all remaining files from |gitdir| ->
3242 |dotgit|. This saves you the effort of initializing |dotgit|
3243 yourself.
3244 """
3245 symlink_dirs = self.shareable_dirs[:]
3246 to_symlink = symlink_dirs
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003247
Gavin Makea2e3302023-03-11 06:46:20 +00003248 to_copy = []
3249 if copy_all:
3250 to_copy = platform_utils.listdir(gitdir)
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003251
Gavin Makea2e3302023-03-11 06:46:20 +00003252 dotgit = platform_utils.realpath(dotgit)
3253 for name in set(to_copy).union(to_symlink):
3254 try:
3255 src = platform_utils.realpath(os.path.join(gitdir, name))
3256 dst = os.path.join(dotgit, name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003257
Gavin Makea2e3302023-03-11 06:46:20 +00003258 if os.path.lexists(dst):
3259 continue
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003260
Gavin Makea2e3302023-03-11 06:46:20 +00003261 # If the source dir doesn't exist, create an empty dir.
3262 if name in symlink_dirs and not os.path.lexists(src):
3263 os.makedirs(src)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003264
Gavin Makea2e3302023-03-11 06:46:20 +00003265 if name in to_symlink:
3266 platform_utils.symlink(
3267 os.path.relpath(src, os.path.dirname(dst)), dst
3268 )
3269 elif copy_all and not platform_utils.islink(dst):
3270 if platform_utils.isdir(src):
3271 shutil.copytree(src, dst)
3272 elif os.path.isfile(src):
3273 shutil.copy(src, dst)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003274
Gavin Makea2e3302023-03-11 06:46:20 +00003275 except OSError as e:
3276 if e.errno == errno.EPERM:
3277 raise DownloadError(self._get_symlink_error_message())
3278 else:
3279 raise
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003280
Gavin Makea2e3302023-03-11 06:46:20 +00003281 def _InitGitWorktree(self):
3282 """Init the project using git worktrees."""
3283 self.bare_git.worktree("prune")
3284 self.bare_git.worktree(
3285 "add",
3286 "-ff",
3287 "--checkout",
3288 "--detach",
3289 "--lock",
3290 self.worktree,
3291 self.GetRevisionId(),
3292 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003293
Gavin Makea2e3302023-03-11 06:46:20 +00003294 # Rewrite the internal state files to use relative paths between the
3295 # checkouts & worktrees.
3296 dotgit = os.path.join(self.worktree, ".git")
Jason R. Coombs034950b2023-10-20 23:32:02 +05453297 with open(dotgit) as fp:
Gavin Makea2e3302023-03-11 06:46:20 +00003298 # Figure out the checkout->worktree path.
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003299 setting = fp.read()
Gavin Makea2e3302023-03-11 06:46:20 +00003300 assert setting.startswith("gitdir:")
3301 git_worktree_path = setting.split(":", 1)[1].strip()
3302 # Some platforms (e.g. Windows) won't let us update dotgit in situ
3303 # because of file permissions. Delete it and recreate it from scratch
3304 # to avoid.
3305 platform_utils.remove(dotgit)
3306 # Use relative path from checkout->worktree & maintain Unix line endings
3307 # on all OS's to match git behavior.
3308 with open(dotgit, "w", newline="\n") as fp:
3309 print(
3310 "gitdir:",
3311 os.path.relpath(git_worktree_path, self.worktree),
3312 file=fp,
3313 )
3314 # Use relative path from worktree->checkout & maintain Unix line endings
3315 # on all OS's to match git behavior.
3316 with open(
3317 os.path.join(git_worktree_path, "gitdir"), "w", newline="\n"
3318 ) as fp:
3319 print(os.path.relpath(dotgit, git_worktree_path), file=fp)
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003320
Gavin Makea2e3302023-03-11 06:46:20 +00003321 self._InitMRef()
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003322
Gavin Makea2e3302023-03-11 06:46:20 +00003323 def _InitWorkTree(self, force_sync=False, submodules=False):
3324 """Setup the worktree .git path.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003325
Gavin Makea2e3302023-03-11 06:46:20 +00003326 This is the user-visible path like src/foo/.git/.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003327
Gavin Makea2e3302023-03-11 06:46:20 +00003328 With non-git-worktrees, this will be a symlink to the .repo/projects/
3329 path. With git-worktrees, this will be a .git file using "gitdir: ..."
3330 syntax.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003331
Gavin Makea2e3302023-03-11 06:46:20 +00003332 Older checkouts had .git/ directories. If we see that, migrate it.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003333
Gavin Makea2e3302023-03-11 06:46:20 +00003334 This also handles changes in the manifest. Maybe this project was
3335 backed by "foo/bar" on the server, but now it's "new/foo/bar". We have
3336 to update the path we point to under .repo/projects/ to match.
3337 """
3338 dotgit = os.path.join(self.worktree, ".git")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003339
Gavin Makea2e3302023-03-11 06:46:20 +00003340 # If using an old layout style (a directory), migrate it.
3341 if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
Jason Chang32b59562023-07-14 16:45:35 -07003342 self._MigrateOldWorkTreeGitDir(dotgit, project=self.name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003343
Gavin Makea2e3302023-03-11 06:46:20 +00003344 init_dotgit = not os.path.exists(dotgit)
3345 if self.use_git_worktrees:
3346 if init_dotgit:
3347 self._InitGitWorktree()
3348 self._CopyAndLinkFiles()
3349 else:
3350 if not init_dotgit:
3351 # See if the project has changed.
3352 if platform_utils.realpath(
3353 self.gitdir
3354 ) != platform_utils.realpath(dotgit):
3355 platform_utils.remove(dotgit)
Doug Anderson37282b42011-03-04 11:54:18 -08003356
Gavin Makea2e3302023-03-11 06:46:20 +00003357 if init_dotgit or not os.path.exists(dotgit):
3358 os.makedirs(self.worktree, exist_ok=True)
3359 platform_utils.symlink(
3360 os.path.relpath(self.gitdir, self.worktree), dotgit
3361 )
Doug Anderson37282b42011-03-04 11:54:18 -08003362
Gavin Makea2e3302023-03-11 06:46:20 +00003363 if init_dotgit:
3364 _lwrite(
3365 os.path.join(dotgit, HEAD), "%s\n" % self.GetRevisionId()
3366 )
Doug Anderson37282b42011-03-04 11:54:18 -08003367
Gavin Makea2e3302023-03-11 06:46:20 +00003368 # Finish checking out the worktree.
3369 cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
3370 if GitCommand(self, cmd).Wait() != 0:
3371 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07003372 "Cannot initialize work tree for " + self.name,
3373 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003374 )
Doug Anderson37282b42011-03-04 11:54:18 -08003375
Gavin Makea2e3302023-03-11 06:46:20 +00003376 if submodules:
3377 self._SyncSubmodules(quiet=True)
3378 self._CopyAndLinkFiles()
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003379
Gavin Makea2e3302023-03-11 06:46:20 +00003380 @classmethod
Jason Chang32b59562023-07-14 16:45:35 -07003381 def _MigrateOldWorkTreeGitDir(cls, dotgit, project=None):
Gavin Makea2e3302023-03-11 06:46:20 +00003382 """Migrate the old worktree .git/ dir style to a symlink.
3383
3384 This logic specifically only uses state from |dotgit| to figure out
3385 where to move content and not |self|. This way if the backing project
3386 also changed places, we only do the .git/ dir to .git symlink migration
3387 here. The path updates will happen independently.
3388 """
3389 # Figure out where in .repo/projects/ it's pointing to.
3390 if not os.path.islink(os.path.join(dotgit, "refs")):
Jason Chang32b59562023-07-14 16:45:35 -07003391 raise GitError(
3392 f"{dotgit}: unsupported checkout state", project=project
3393 )
Gavin Makea2e3302023-03-11 06:46:20 +00003394 gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, "refs")))
3395
3396 # Remove known symlink paths that exist in .repo/projects/.
3397 KNOWN_LINKS = {
3398 "config",
3399 "description",
3400 "hooks",
3401 "info",
3402 "logs",
3403 "objects",
3404 "packed-refs",
3405 "refs",
3406 "rr-cache",
3407 "shallow",
3408 "svn",
3409 }
3410 # Paths that we know will be in both, but are safe to clobber in
3411 # .repo/projects/.
3412 SAFE_TO_CLOBBER = {
3413 "COMMIT_EDITMSG",
3414 "FETCH_HEAD",
3415 "HEAD",
3416 "gc.log",
3417 "gitk.cache",
3418 "index",
3419 "ORIG_HEAD",
3420 }
3421
3422 # First see if we'd succeed before starting the migration.
3423 unknown_paths = []
3424 for name in platform_utils.listdir(dotgit):
3425 # Ignore all temporary/backup names. These are common with vim &
3426 # emacs.
3427 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3428 continue
3429
3430 dotgit_path = os.path.join(dotgit, name)
3431 if name in KNOWN_LINKS:
3432 if not platform_utils.islink(dotgit_path):
3433 unknown_paths.append(f"{dotgit_path}: should be a symlink")
3434 else:
3435 gitdir_path = os.path.join(gitdir, name)
3436 if name not in SAFE_TO_CLOBBER and os.path.exists(gitdir_path):
3437 unknown_paths.append(
3438 f"{dotgit_path}: unknown file; please file a bug"
3439 )
3440 if unknown_paths:
Jason Chang32b59562023-07-14 16:45:35 -07003441 raise GitError(
3442 "Aborting migration: " + "\n".join(unknown_paths),
3443 project=project,
3444 )
Gavin Makea2e3302023-03-11 06:46:20 +00003445
3446 # Now walk the paths and sync the .git/ to .repo/projects/.
3447 for name in platform_utils.listdir(dotgit):
3448 dotgit_path = os.path.join(dotgit, name)
3449
3450 # Ignore all temporary/backup names. These are common with vim &
3451 # emacs.
3452 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3453 platform_utils.remove(dotgit_path)
3454 elif name in KNOWN_LINKS:
3455 platform_utils.remove(dotgit_path)
3456 else:
3457 gitdir_path = os.path.join(gitdir, name)
3458 platform_utils.remove(gitdir_path, missing_ok=True)
3459 platform_utils.rename(dotgit_path, gitdir_path)
3460
3461 # Now that the dir should be empty, clear it out, and symlink it over.
3462 platform_utils.rmdir(dotgit)
3463 platform_utils.symlink(
Jason R. Coombs47944bb2023-09-29 12:42:22 -04003464 os.path.relpath(gitdir, os.path.dirname(os.path.realpath(dotgit))),
3465 dotgit,
Gavin Makea2e3302023-03-11 06:46:20 +00003466 )
3467
3468 def _get_symlink_error_message(self):
3469 if platform_utils.isWindows():
3470 return (
3471 "Unable to create symbolic link. Please re-run the command as "
3472 "Administrator, or see "
3473 "https://github.com/git-for-windows/git/wiki/Symbolic-Links "
3474 "for other options."
3475 )
3476 return "filesystem must support symlinks"
3477
3478 def _revlist(self, *args, **kw):
3479 a = []
3480 a.extend(args)
3481 a.append("--")
3482 return self.work_git.rev_list(*a, **kw)
3483
3484 @property
3485 def _allrefs(self):
3486 return self.bare_ref.all
3487
3488 def _getLogs(
3489 self, rev1, rev2, oneline=False, color=True, pretty_format=None
3490 ):
3491 """Get logs between two revisions of this project."""
3492 comp = ".."
3493 if rev1:
3494 revs = [rev1]
3495 if rev2:
3496 revs.extend([comp, rev2])
3497 cmd = ["log", "".join(revs)]
3498 out = DiffColoring(self.config)
3499 if out.is_on and color:
3500 cmd.append("--color")
3501 if pretty_format is not None:
3502 cmd.append("--pretty=format:%s" % pretty_format)
3503 if oneline:
3504 cmd.append("--oneline")
3505
3506 try:
3507 log = GitCommand(
3508 self, cmd, capture_stdout=True, capture_stderr=True
3509 )
3510 if log.Wait() == 0:
3511 return log.stdout
3512 except GitError:
3513 # worktree may not exist if groups changed for example. In that
3514 # case, try in gitdir instead.
3515 if not os.path.exists(self.worktree):
3516 return self.bare_git.log(*cmd[1:])
3517 else:
3518 raise
3519 return None
3520
3521 def getAddedAndRemovedLogs(
3522 self, toProject, oneline=False, color=True, pretty_format=None
3523 ):
3524 """Get the list of logs from this revision to given revisionId"""
3525 logs = {}
3526 selfId = self.GetRevisionId(self._allrefs)
3527 toId = toProject.GetRevisionId(toProject._allrefs)
3528
3529 logs["added"] = self._getLogs(
3530 selfId,
3531 toId,
3532 oneline=oneline,
3533 color=color,
3534 pretty_format=pretty_format,
3535 )
3536 logs["removed"] = self._getLogs(
3537 toId,
3538 selfId,
3539 oneline=oneline,
3540 color=color,
3541 pretty_format=pretty_format,
3542 )
3543 return logs
3544
Mike Frysingerd4aee652023-10-19 05:13:32 -04003545 class _GitGetByExec:
Gavin Makea2e3302023-03-11 06:46:20 +00003546 def __init__(self, project, bare, gitdir):
3547 self._project = project
3548 self._bare = bare
3549 self._gitdir = gitdir
3550
3551 # __getstate__ and __setstate__ are required for pickling because
3552 # __getattr__ exists.
3553 def __getstate__(self):
3554 return (self._project, self._bare, self._gitdir)
3555
3556 def __setstate__(self, state):
3557 self._project, self._bare, self._gitdir = state
3558
3559 def LsOthers(self):
3560 p = GitCommand(
3561 self._project,
3562 ["ls-files", "-z", "--others", "--exclude-standard"],
3563 bare=False,
3564 gitdir=self._gitdir,
3565 capture_stdout=True,
3566 capture_stderr=True,
3567 )
3568 if p.Wait() == 0:
3569 out = p.stdout
3570 if out:
3571 # Backslash is not anomalous.
3572 return out[:-1].split("\0")
3573 return []
3574
3575 def DiffZ(self, name, *args):
3576 cmd = [name]
3577 cmd.append("-z")
3578 cmd.append("--ignore-submodules")
3579 cmd.extend(args)
3580 p = GitCommand(
3581 self._project,
3582 cmd,
3583 gitdir=self._gitdir,
3584 bare=False,
3585 capture_stdout=True,
3586 capture_stderr=True,
3587 )
3588 p.Wait()
3589 r = {}
3590 out = p.stdout
3591 if out:
3592 out = iter(out[:-1].split("\0"))
3593 while out:
3594 try:
3595 info = next(out)
3596 path = next(out)
3597 except StopIteration:
3598 break
3599
Mike Frysingerd4aee652023-10-19 05:13:32 -04003600 class _Info:
Gavin Makea2e3302023-03-11 06:46:20 +00003601 def __init__(self, path, omode, nmode, oid, nid, state):
3602 self.path = path
3603 self.src_path = None
3604 self.old_mode = omode
3605 self.new_mode = nmode
3606 self.old_id = oid
3607 self.new_id = nid
3608
3609 if len(state) == 1:
3610 self.status = state
3611 self.level = None
3612 else:
3613 self.status = state[:1]
3614 self.level = state[1:]
3615 while self.level.startswith("0"):
3616 self.level = self.level[1:]
3617
3618 info = info[1:].split(" ")
3619 info = _Info(path, *info)
3620 if info.status in ("R", "C"):
3621 info.src_path = info.path
3622 info.path = next(out)
3623 r[info.path] = info
3624 return r
3625
3626 def GetDotgitPath(self, subpath=None):
3627 """Return the full path to the .git dir.
3628
3629 As a convenience, append |subpath| if provided.
3630 """
3631 if self._bare:
3632 dotgit = self._gitdir
3633 else:
3634 dotgit = os.path.join(self._project.worktree, ".git")
3635 if os.path.isfile(dotgit):
3636 # Git worktrees use a "gitdir:" syntax to point to the
3637 # scratch space.
3638 with open(dotgit) as fp:
3639 setting = fp.read()
3640 assert setting.startswith("gitdir:")
3641 gitdir = setting.split(":", 1)[1].strip()
3642 dotgit = os.path.normpath(
3643 os.path.join(self._project.worktree, gitdir)
3644 )
3645
3646 return dotgit if subpath is None else os.path.join(dotgit, subpath)
3647
3648 def GetHead(self):
3649 """Return the ref that HEAD points to."""
3650 path = self.GetDotgitPath(subpath=HEAD)
3651 try:
3652 with open(path) as fd:
3653 line = fd.readline()
Jason R. Coombsae824fb2023-10-20 23:32:40 +05453654 except OSError as e:
Gavin Makea2e3302023-03-11 06:46:20 +00003655 raise NoManifestException(path, str(e))
3656 try:
3657 line = line.decode()
3658 except AttributeError:
3659 pass
3660 if line.startswith("ref: "):
3661 return line[5:-1]
3662 return line[:-1]
3663
3664 def SetHead(self, ref, message=None):
3665 cmdv = []
3666 if message is not None:
3667 cmdv.extend(["-m", message])
3668 cmdv.append(HEAD)
3669 cmdv.append(ref)
3670 self.symbolic_ref(*cmdv)
3671
3672 def DetachHead(self, new, message=None):
3673 cmdv = ["--no-deref"]
3674 if message is not None:
3675 cmdv.extend(["-m", message])
3676 cmdv.append(HEAD)
3677 cmdv.append(new)
3678 self.update_ref(*cmdv)
3679
3680 def UpdateRef(self, name, new, old=None, message=None, detach=False):
3681 cmdv = []
3682 if message is not None:
3683 cmdv.extend(["-m", message])
3684 if detach:
3685 cmdv.append("--no-deref")
3686 cmdv.append(name)
3687 cmdv.append(new)
3688 if old is not None:
3689 cmdv.append(old)
3690 self.update_ref(*cmdv)
3691
3692 def DeleteRef(self, name, old=None):
3693 if not old:
3694 old = self.rev_parse(name)
3695 self.update_ref("-d", name, old)
3696 self._project.bare_ref.deleted(name)
3697
Jason Chang87058c62023-09-27 11:34:43 -07003698 def rev_list(self, *args, log_as_error=True, **kw):
Gavin Makea2e3302023-03-11 06:46:20 +00003699 if "format" in kw:
3700 cmdv = ["log", "--pretty=format:%s" % kw["format"]]
3701 else:
3702 cmdv = ["rev-list"]
3703 cmdv.extend(args)
3704 p = GitCommand(
3705 self._project,
3706 cmdv,
3707 bare=self._bare,
3708 gitdir=self._gitdir,
3709 capture_stdout=True,
3710 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003711 verify_command=True,
Jason Chang87058c62023-09-27 11:34:43 -07003712 log_as_error=log_as_error,
Gavin Makea2e3302023-03-11 06:46:20 +00003713 )
Jason Chang32b59562023-07-14 16:45:35 -07003714 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003715 return p.stdout.splitlines()
3716
3717 def __getattr__(self, name):
3718 """Allow arbitrary git commands using pythonic syntax.
3719
3720 This allows you to do things like:
3721 git_obj.rev_parse('HEAD')
3722
3723 Since we don't have a 'rev_parse' method defined, the __getattr__
3724 will run. We'll replace the '_' with a '-' and try to run a git
3725 command. Any other positional arguments will be passed to the git
3726 command, and the following keyword arguments are supported:
3727 config: An optional dict of git config options to be passed with
3728 '-c'.
3729
3730 Args:
3731 name: The name of the git command to call. Any '_' characters
3732 will be replaced with '-'.
3733
3734 Returns:
3735 A callable object that will try to call git with the named
3736 command.
3737 """
3738 name = name.replace("_", "-")
3739
Jason Chang87058c62023-09-27 11:34:43 -07003740 def runner(*args, log_as_error=True, **kwargs):
Gavin Makea2e3302023-03-11 06:46:20 +00003741 cmdv = []
3742 config = kwargs.pop("config", None)
3743 for k in kwargs:
3744 raise TypeError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003745 f"{name}() got an unexpected keyword argument {k!r}"
Gavin Makea2e3302023-03-11 06:46:20 +00003746 )
3747 if config is not None:
3748 for k, v in config.items():
3749 cmdv.append("-c")
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003750 cmdv.append(f"{k}={v}")
Gavin Makea2e3302023-03-11 06:46:20 +00003751 cmdv.append(name)
3752 cmdv.extend(args)
3753 p = GitCommand(
3754 self._project,
3755 cmdv,
3756 bare=self._bare,
3757 gitdir=self._gitdir,
3758 capture_stdout=True,
3759 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003760 verify_command=True,
Jason Chang87058c62023-09-27 11:34:43 -07003761 log_as_error=log_as_error,
Gavin Makea2e3302023-03-11 06:46:20 +00003762 )
Jason Chang32b59562023-07-14 16:45:35 -07003763 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003764 r = p.stdout
3765 if r.endswith("\n") and r.index("\n") == len(r) - 1:
3766 return r[:-1]
3767 return r
3768
3769 return runner
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003770
3771
Jason Chang32b59562023-07-14 16:45:35 -07003772class LocalSyncFail(RepoError):
3773 """Default error when there is an Sync_LocalHalf error."""
3774
3775
3776class _PriorSyncFailedError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003777 def __str__(self):
3778 return "prior sync failed; rebase still in progress"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003779
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003780
Jason Chang32b59562023-07-14 16:45:35 -07003781class _DirtyError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003782 def __str__(self):
3783 return "contains uncommitted changes"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003784
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003785
Mike Frysingerd4aee652023-10-19 05:13:32 -04003786class _InfoMessage:
Gavin Makea2e3302023-03-11 06:46:20 +00003787 def __init__(self, project, text):
3788 self.project = project
3789 self.text = text
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003790
Gavin Makea2e3302023-03-11 06:46:20 +00003791 def Print(self, syncbuf):
3792 syncbuf.out.info(
3793 "%s/: %s", self.project.RelPath(local=False), self.text
3794 )
3795 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003796
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003797
Mike Frysingerd4aee652023-10-19 05:13:32 -04003798class _Failure:
Gavin Makea2e3302023-03-11 06:46:20 +00003799 def __init__(self, project, why):
3800 self.project = project
3801 self.why = why
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003802
Gavin Makea2e3302023-03-11 06:46:20 +00003803 def Print(self, syncbuf):
3804 syncbuf.out.fail(
3805 "error: %s/: %s", self.project.RelPath(local=False), str(self.why)
3806 )
3807 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003808
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003809
Mike Frysingerd4aee652023-10-19 05:13:32 -04003810class _Later:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003811 def __init__(self, project, action, quiet):
Gavin Makea2e3302023-03-11 06:46:20 +00003812 self.project = project
3813 self.action = action
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003814 self.quiet = quiet
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003815
Gavin Makea2e3302023-03-11 06:46:20 +00003816 def Run(self, syncbuf):
3817 out = syncbuf.out
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003818 if not self.quiet:
3819 out.project("project %s/", self.project.RelPath(local=False))
3820 out.nl()
Gavin Makea2e3302023-03-11 06:46:20 +00003821 try:
3822 self.action()
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003823 if not self.quiet:
3824 out.nl()
Gavin Makea2e3302023-03-11 06:46:20 +00003825 return True
3826 except GitError:
3827 out.nl()
3828 return False
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003829
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003830
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003831class _SyncColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +00003832 def __init__(self, config):
3833 super().__init__(config, "reposync")
3834 self.project = self.printer("header", attr="bold")
3835 self.info = self.printer("info")
3836 self.fail = self.printer("fail", fg="red")
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003837
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003838
Mike Frysingerd4aee652023-10-19 05:13:32 -04003839class SyncBuffer:
Gavin Makea2e3302023-03-11 06:46:20 +00003840 def __init__(self, config, detach_head=False):
3841 self._messages = []
3842 self._failures = []
3843 self._later_queue1 = []
3844 self._later_queue2 = []
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003845
Gavin Makea2e3302023-03-11 06:46:20 +00003846 self.out = _SyncColoring(config)
3847 self.out.redirect(sys.stderr)
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003848
Gavin Makea2e3302023-03-11 06:46:20 +00003849 self.detach_head = detach_head
3850 self.clean = True
3851 self.recent_clean = True
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003852
Gavin Makea2e3302023-03-11 06:46:20 +00003853 def info(self, project, fmt, *args):
3854 self._messages.append(_InfoMessage(project, fmt % args))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003855
Gavin Makea2e3302023-03-11 06:46:20 +00003856 def fail(self, project, err=None):
3857 self._failures.append(_Failure(project, err))
David Rileye0684ad2017-04-05 00:02:59 -07003858 self._MarkUnclean()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003859
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003860 def later1(self, project, what, quiet):
3861 self._later_queue1.append(_Later(project, what, quiet))
Mike Frysinger70d861f2019-08-26 15:22:36 -04003862
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003863 def later2(self, project, what, quiet):
3864 self._later_queue2.append(_Later(project, what, quiet))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003865
Gavin Makea2e3302023-03-11 06:46:20 +00003866 def Finish(self):
3867 self._PrintMessages()
3868 self._RunLater()
3869 self._PrintMessages()
3870 return self.clean
3871
3872 def Recently(self):
3873 recent_clean = self.recent_clean
3874 self.recent_clean = True
3875 return recent_clean
3876
3877 def _MarkUnclean(self):
3878 self.clean = False
3879 self.recent_clean = False
3880
3881 def _RunLater(self):
3882 for q in ["_later_queue1", "_later_queue2"]:
3883 if not self._RunQueue(q):
3884 return
3885
3886 def _RunQueue(self, queue):
3887 for m in getattr(self, queue):
3888 if not m.Run(self):
3889 self._MarkUnclean()
3890 return False
3891 setattr(self, queue, [])
3892 return True
3893
3894 def _PrintMessages(self):
3895 if self._messages or self._failures:
3896 if os.isatty(2):
3897 self.out.write(progress.CSI_ERASE_LINE)
3898 self.out.write("\r")
3899
3900 for m in self._messages:
3901 m.Print(self)
3902 for m in self._failures:
3903 m.Print(self)
3904
3905 self._messages = []
3906 self._failures = []
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003907
3908
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003909class MetaProject(Project):
Gavin Makea2e3302023-03-11 06:46:20 +00003910 """A special project housed under .repo."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003911
Gavin Makea2e3302023-03-11 06:46:20 +00003912 def __init__(self, manifest, name, gitdir, worktree):
3913 Project.__init__(
3914 self,
3915 manifest=manifest,
3916 name=name,
3917 gitdir=gitdir,
3918 objdir=gitdir,
3919 worktree=worktree,
3920 remote=RemoteSpec("origin"),
3921 relpath=".repo/%s" % name,
3922 revisionExpr="refs/heads/master",
3923 revisionId=None,
3924 groups=None,
3925 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003926
Gavin Makea2e3302023-03-11 06:46:20 +00003927 def PreSync(self):
3928 if self.Exists:
3929 cb = self.CurrentBranch
3930 if cb:
3931 base = self.GetBranch(cb).merge
3932 if base:
3933 self.revisionExpr = base
3934 self.revisionId = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003935
Gavin Makea2e3302023-03-11 06:46:20 +00003936 @property
3937 def HasChanges(self):
3938 """Has the remote received new commits not yet checked out?"""
3939 if not self.remote or not self.revisionExpr:
3940 return False
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003941
Gavin Makea2e3302023-03-11 06:46:20 +00003942 all_refs = self.bare_ref.all
3943 revid = self.GetRevisionId(all_refs)
3944 head = self.work_git.GetHead()
3945 if head.startswith(R_HEADS):
3946 try:
3947 head = all_refs[head]
3948 except KeyError:
3949 head = None
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003950
Gavin Makea2e3302023-03-11 06:46:20 +00003951 if revid == head:
3952 return False
3953 elif self._revlist(not_rev(HEAD), revid):
3954 return True
3955 return False
LaMont Jones9b72cf22022-03-29 21:54:22 +00003956
3957
3958class RepoProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003959 """The MetaProject for repo itself."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003960
Gavin Makea2e3302023-03-11 06:46:20 +00003961 @property
3962 def LastFetch(self):
3963 try:
3964 fh = os.path.join(self.gitdir, "FETCH_HEAD")
3965 return os.path.getmtime(fh)
3966 except OSError:
3967 return 0
LaMont Jones9b72cf22022-03-29 21:54:22 +00003968
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +01003969
LaMont Jones9b72cf22022-03-29 21:54:22 +00003970class ManifestProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003971 """The MetaProject for manifests."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003972
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08003973 def MetaBranchSwitch(self, submodules=False, verbose=False):
Gavin Makea2e3302023-03-11 06:46:20 +00003974 """Prepare for manifest branch switch."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003975
Gavin Makea2e3302023-03-11 06:46:20 +00003976 # detach and delete manifest branch, allowing a new
3977 # branch to take over
3978 syncbuf = SyncBuffer(self.config, detach_head=True)
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08003979 self.Sync_LocalHalf(syncbuf, submodules=submodules, verbose=verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00003980 syncbuf.Finish()
LaMont Jones9b72cf22022-03-29 21:54:22 +00003981
Gavin Makea2e3302023-03-11 06:46:20 +00003982 return (
3983 GitCommand(
3984 self,
3985 ["update-ref", "-d", "refs/heads/default"],
3986 capture_stdout=True,
3987 capture_stderr=True,
3988 ).Wait()
3989 == 0
3990 )
LaMont Jones9b03f152022-03-29 23:01:18 +00003991
Gavin Makea2e3302023-03-11 06:46:20 +00003992 @property
3993 def standalone_manifest_url(self):
3994 """The URL of the standalone manifest, or None."""
3995 return self.config.GetString("manifest.standalone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003996
Gavin Makea2e3302023-03-11 06:46:20 +00003997 @property
3998 def manifest_groups(self):
3999 """The manifest groups string."""
4000 return self.config.GetString("manifest.groups")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004001
Gavin Makea2e3302023-03-11 06:46:20 +00004002 @property
4003 def reference(self):
4004 """The --reference for this manifest."""
4005 return self.config.GetString("repo.reference")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004006
Gavin Makea2e3302023-03-11 06:46:20 +00004007 @property
4008 def dissociate(self):
4009 """Whether to dissociate."""
4010 return self.config.GetBoolean("repo.dissociate")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004011
Gavin Makea2e3302023-03-11 06:46:20 +00004012 @property
4013 def archive(self):
4014 """Whether we use archive."""
4015 return self.config.GetBoolean("repo.archive")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004016
Gavin Makea2e3302023-03-11 06:46:20 +00004017 @property
4018 def mirror(self):
4019 """Whether we use mirror."""
4020 return self.config.GetBoolean("repo.mirror")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004021
Gavin Makea2e3302023-03-11 06:46:20 +00004022 @property
4023 def use_worktree(self):
4024 """Whether we use worktree."""
4025 return self.config.GetBoolean("repo.worktree")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004026
Gavin Makea2e3302023-03-11 06:46:20 +00004027 @property
4028 def clone_bundle(self):
4029 """Whether we use clone_bundle."""
4030 return self.config.GetBoolean("repo.clonebundle")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004031
Gavin Makea2e3302023-03-11 06:46:20 +00004032 @property
4033 def submodules(self):
4034 """Whether we use submodules."""
4035 return self.config.GetBoolean("repo.submodules")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004036
Gavin Makea2e3302023-03-11 06:46:20 +00004037 @property
4038 def git_lfs(self):
4039 """Whether we use git_lfs."""
4040 return self.config.GetBoolean("repo.git-lfs")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004041
Gavin Makea2e3302023-03-11 06:46:20 +00004042 @property
4043 def use_superproject(self):
4044 """Whether we use superproject."""
4045 return self.config.GetBoolean("repo.superproject")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004046
Gavin Makea2e3302023-03-11 06:46:20 +00004047 @property
4048 def partial_clone(self):
4049 """Whether this is a partial clone."""
4050 return self.config.GetBoolean("repo.partialclone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004051
Gavin Makea2e3302023-03-11 06:46:20 +00004052 @property
4053 def depth(self):
4054 """Partial clone depth."""
Roberto Vladimir Prado Carranza3d58d212023-09-13 10:27:26 +02004055 return self.config.GetInt("repo.depth")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004056
Gavin Makea2e3302023-03-11 06:46:20 +00004057 @property
4058 def clone_filter(self):
4059 """The clone filter."""
4060 return self.config.GetString("repo.clonefilter")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004061
Gavin Makea2e3302023-03-11 06:46:20 +00004062 @property
4063 def partial_clone_exclude(self):
4064 """Partial clone exclude string"""
4065 return self.config.GetString("repo.partialcloneexclude")
LaMont Jones4ada0432022-04-14 15:10:43 +00004066
Gavin Makea2e3302023-03-11 06:46:20 +00004067 @property
Jason Chang17833322023-05-23 13:06:55 -07004068 def clone_filter_for_depth(self):
4069 """Replace shallow clone with partial clone."""
4070 return self.config.GetString("repo.clonefilterfordepth")
4071
4072 @property
Gavin Makea2e3302023-03-11 06:46:20 +00004073 def manifest_platform(self):
4074 """The --platform argument from `repo init`."""
4075 return self.config.GetString("manifest.platform")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004076
Gavin Makea2e3302023-03-11 06:46:20 +00004077 @property
4078 def _platform_name(self):
4079 """Return the name of the platform."""
4080 return platform.system().lower()
LaMont Jones9b03f152022-03-29 23:01:18 +00004081
Gavin Makea2e3302023-03-11 06:46:20 +00004082 def SyncWithPossibleInit(
4083 self,
4084 submanifest,
4085 verbose=False,
4086 current_branch_only=False,
4087 tags="",
4088 git_event_log=None,
4089 ):
4090 """Sync a manifestProject, possibly for the first time.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004091
Gavin Makea2e3302023-03-11 06:46:20 +00004092 Call Sync() with arguments from the most recent `repo init`. If this is
4093 a new sub manifest, then inherit options from the parent's
4094 manifestProject.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004095
Gavin Makea2e3302023-03-11 06:46:20 +00004096 This is used by subcmds.Sync() to do an initial download of new sub
4097 manifests.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004098
Gavin Makea2e3302023-03-11 06:46:20 +00004099 Args:
4100 submanifest: an XmlSubmanifest, the submanifest to re-sync.
4101 verbose: a boolean, whether to show all output, rather than only
4102 errors.
4103 current_branch_only: a boolean, whether to only fetch the current
4104 manifest branch from the server.
4105 tags: a boolean, whether to fetch tags.
4106 git_event_log: an EventLog, for git tracing.
4107 """
4108 # TODO(lamontjones): when refactoring sync (and init?) consider how to
4109 # better get the init options that we should use for new submanifests
4110 # that are added when syncing an existing workspace.
4111 git_event_log = git_event_log or EventLog()
LaMont Jonesb90a4222022-04-14 15:00:09 +00004112 spec = submanifest.ToSubmanifestSpec()
Gavin Makea2e3302023-03-11 06:46:20 +00004113 # Use the init options from the existing manifestProject, or the parent
4114 # if it doesn't exist.
4115 #
4116 # Today, we only support changing manifest_groups on the sub-manifest,
4117 # with no supported-for-the-user way to change the other arguments from
4118 # those specified by the outermost manifest.
4119 #
4120 # TODO(lamontjones): determine which of these should come from the
4121 # outermost manifest and which should come from the parent manifest.
4122 mp = self if self.Exists else submanifest.parent.manifestProject
4123 return self.Sync(
LaMont Jones55ee3042022-04-06 17:10:21 +00004124 manifest_url=spec.manifestUrl,
4125 manifest_branch=spec.revision,
Gavin Makea2e3302023-03-11 06:46:20 +00004126 standalone_manifest=mp.standalone_manifest_url,
4127 groups=mp.manifest_groups,
4128 platform=mp.manifest_platform,
4129 mirror=mp.mirror,
4130 dissociate=mp.dissociate,
4131 reference=mp.reference,
4132 worktree=mp.use_worktree,
4133 submodules=mp.submodules,
4134 archive=mp.archive,
4135 partial_clone=mp.partial_clone,
4136 clone_filter=mp.clone_filter,
4137 partial_clone_exclude=mp.partial_clone_exclude,
4138 clone_bundle=mp.clone_bundle,
4139 git_lfs=mp.git_lfs,
4140 use_superproject=mp.use_superproject,
LaMont Jones55ee3042022-04-06 17:10:21 +00004141 verbose=verbose,
4142 current_branch_only=current_branch_only,
4143 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00004144 depth=mp.depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004145 git_event_log=git_event_log,
4146 manifest_name=spec.manifestName,
Gavin Makea2e3302023-03-11 06:46:20 +00004147 this_manifest_only=True,
LaMont Jones55ee3042022-04-06 17:10:21 +00004148 outer_manifest=False,
Jason Chang17833322023-05-23 13:06:55 -07004149 clone_filter_for_depth=mp.clone_filter_for_depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004150 )
LaMont Jones409407a2022-04-05 21:21:56 +00004151
Gavin Makea2e3302023-03-11 06:46:20 +00004152 def Sync(
4153 self,
4154 _kwargs_only=(),
4155 manifest_url="",
4156 manifest_branch=None,
4157 standalone_manifest=False,
4158 groups="",
4159 mirror=False,
4160 reference="",
4161 dissociate=False,
4162 worktree=False,
4163 submodules=False,
4164 archive=False,
4165 partial_clone=None,
4166 depth=None,
4167 clone_filter="blob:none",
4168 partial_clone_exclude=None,
4169 clone_bundle=None,
4170 git_lfs=None,
4171 use_superproject=None,
4172 verbose=False,
4173 current_branch_only=False,
4174 git_event_log=None,
4175 platform="",
4176 manifest_name="default.xml",
4177 tags="",
4178 this_manifest_only=False,
4179 outer_manifest=True,
Jason Chang17833322023-05-23 13:06:55 -07004180 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00004181 ):
4182 """Sync the manifest and all submanifests.
LaMont Jones409407a2022-04-05 21:21:56 +00004183
Gavin Makea2e3302023-03-11 06:46:20 +00004184 Args:
4185 manifest_url: a string, the URL of the manifest project.
4186 manifest_branch: a string, the manifest branch to use.
4187 standalone_manifest: a boolean, whether to store the manifest as a
4188 static file.
4189 groups: a string, restricts the checkout to projects with the
4190 specified groups.
4191 mirror: a boolean, whether to create a mirror of the remote
4192 repository.
4193 reference: a string, location of a repo instance to use as a
4194 reference.
4195 dissociate: a boolean, whether to dissociate from reference mirrors
4196 after clone.
4197 worktree: a boolean, whether to use git-worktree to manage projects.
4198 submodules: a boolean, whether sync submodules associated with the
4199 manifest project.
4200 archive: a boolean, whether to checkout each project as an archive.
4201 See git-archive.
4202 partial_clone: a boolean, whether to perform a partial clone.
4203 depth: an int, how deep of a shallow clone to create.
4204 clone_filter: a string, filter to use with partial_clone.
4205 partial_clone_exclude : a string, comma-delimeted list of project
4206 names to exclude from partial clone.
4207 clone_bundle: a boolean, whether to enable /clone.bundle on
4208 HTTP/HTTPS.
4209 git_lfs: a boolean, whether to enable git LFS support.
4210 use_superproject: a boolean, whether to use the manifest
4211 superproject to sync projects.
4212 verbose: a boolean, whether to show all output, rather than only
4213 errors.
4214 current_branch_only: a boolean, whether to only fetch the current
4215 manifest branch from the server.
4216 platform: a string, restrict the checkout to projects with the
4217 specified platform group.
4218 git_event_log: an EventLog, for git tracing.
4219 tags: a boolean, whether to fetch tags.
4220 manifest_name: a string, the name of the manifest file to use.
4221 this_manifest_only: a boolean, whether to only operate on the
4222 current sub manifest.
4223 outer_manifest: a boolean, whether to start at the outermost
4224 manifest.
Jason Chang17833322023-05-23 13:06:55 -07004225 clone_filter_for_depth: a string, when specified replaces shallow
4226 clones with partial.
LaMont Jones9b03f152022-03-29 23:01:18 +00004227
Gavin Makea2e3302023-03-11 06:46:20 +00004228 Returns:
4229 a boolean, whether the sync was successful.
4230 """
4231 assert _kwargs_only == (), "Sync only accepts keyword arguments."
LaMont Jones9b03f152022-03-29 23:01:18 +00004232
Gavin Makea2e3302023-03-11 06:46:20 +00004233 groups = groups or self.manifest.GetDefaultGroupsStr(
4234 with_platform=False
4235 )
4236 platform = platform or "auto"
4237 git_event_log = git_event_log or EventLog()
4238 if outer_manifest and self.manifest.is_submanifest:
4239 # In a multi-manifest checkout, use the outer manifest unless we are
4240 # told not to.
4241 return self.client.outer_manifest.manifestProject.Sync(
4242 manifest_url=manifest_url,
4243 manifest_branch=manifest_branch,
4244 standalone_manifest=standalone_manifest,
4245 groups=groups,
4246 platform=platform,
4247 mirror=mirror,
4248 dissociate=dissociate,
4249 reference=reference,
4250 worktree=worktree,
4251 submodules=submodules,
4252 archive=archive,
4253 partial_clone=partial_clone,
4254 clone_filter=clone_filter,
4255 partial_clone_exclude=partial_clone_exclude,
4256 clone_bundle=clone_bundle,
4257 git_lfs=git_lfs,
4258 use_superproject=use_superproject,
4259 verbose=verbose,
4260 current_branch_only=current_branch_only,
4261 tags=tags,
4262 depth=depth,
4263 git_event_log=git_event_log,
4264 manifest_name=manifest_name,
4265 this_manifest_only=this_manifest_only,
4266 outer_manifest=False,
4267 )
LaMont Jones9b03f152022-03-29 23:01:18 +00004268
Gavin Makea2e3302023-03-11 06:46:20 +00004269 # If repo has already been initialized, we take -u with the absence of
4270 # --standalone-manifest to mean "transition to a standard repo set up",
4271 # which necessitates starting fresh.
4272 # If --standalone-manifest is set, we always tear everything down and
4273 # start anew.
4274 if self.Exists:
4275 was_standalone_manifest = self.config.GetString(
4276 "manifest.standalone"
4277 )
4278 if was_standalone_manifest and not manifest_url:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004279 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004280 "fatal: repo was initialized with a standlone manifest, "
4281 "cannot be re-initialized without --manifest-url/-u"
4282 )
4283 return False
4284
4285 if standalone_manifest or (
4286 was_standalone_manifest and manifest_url
4287 ):
4288 self.config.ClearCache()
4289 if self.gitdir and os.path.exists(self.gitdir):
4290 platform_utils.rmtree(self.gitdir)
4291 if self.worktree and os.path.exists(self.worktree):
4292 platform_utils.rmtree(self.worktree)
4293
4294 is_new = not self.Exists
4295 if is_new:
4296 if not manifest_url:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004297 logger.error("fatal: manifest url is required.")
Gavin Makea2e3302023-03-11 06:46:20 +00004298 return False
4299
4300 if verbose:
4301 print(
4302 "Downloading manifest from %s"
4303 % (GitConfig.ForUser().UrlInsteadOf(manifest_url),),
4304 file=sys.stderr,
4305 )
4306
4307 # The manifest project object doesn't keep track of the path on the
4308 # server where this git is located, so let's save that here.
4309 mirrored_manifest_git = None
4310 if reference:
4311 manifest_git_path = urllib.parse.urlparse(manifest_url).path[1:]
4312 mirrored_manifest_git = os.path.join(
4313 reference, manifest_git_path
4314 )
4315 if not mirrored_manifest_git.endswith(".git"):
4316 mirrored_manifest_git += ".git"
4317 if not os.path.exists(mirrored_manifest_git):
4318 mirrored_manifest_git = os.path.join(
4319 reference, ".repo/manifests.git"
4320 )
4321
4322 self._InitGitDir(mirror_git=mirrored_manifest_git)
4323
4324 # If standalone_manifest is set, mark the project as "standalone" --
4325 # we'll still do much of the manifests.git set up, but will avoid actual
4326 # syncs to a remote.
4327 if standalone_manifest:
4328 self.config.SetString("manifest.standalone", manifest_url)
4329 elif not manifest_url and not manifest_branch:
4330 # If -u is set and --standalone-manifest is not, then we're not in
4331 # standalone mode. Otherwise, use config to infer what we were in
4332 # the last init.
4333 standalone_manifest = bool(
4334 self.config.GetString("manifest.standalone")
4335 )
4336 if not standalone_manifest:
4337 self.config.SetString("manifest.standalone", None)
4338
4339 self._ConfigureDepth(depth)
4340
4341 # Set the remote URL before the remote branch as we might need it below.
4342 if manifest_url:
4343 r = self.GetRemote()
4344 r.url = manifest_url
4345 r.ResetFetch()
4346 r.Save()
4347
4348 if not standalone_manifest:
4349 if manifest_branch:
4350 if manifest_branch == "HEAD":
4351 manifest_branch = self.ResolveRemoteHead()
4352 if manifest_branch is None:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004353 logger.error("fatal: unable to resolve HEAD")
Gavin Makea2e3302023-03-11 06:46:20 +00004354 return False
4355 self.revisionExpr = manifest_branch
4356 else:
4357 if is_new:
4358 default_branch = self.ResolveRemoteHead()
4359 if default_branch is None:
4360 # If the remote doesn't have HEAD configured, default to
4361 # master.
4362 default_branch = "refs/heads/master"
4363 self.revisionExpr = default_branch
4364 else:
4365 self.PreSync()
4366
4367 groups = re.split(r"[,\s]+", groups or "")
4368 all_platforms = ["linux", "darwin", "windows"]
4369 platformize = lambda x: "platform-" + x
4370 if platform == "auto":
4371 if not mirror and not self.mirror:
4372 groups.append(platformize(self._platform_name))
4373 elif platform == "all":
4374 groups.extend(map(platformize, all_platforms))
4375 elif platform in all_platforms:
4376 groups.append(platformize(platform))
4377 elif platform != "none":
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004378 logger.error("fatal: invalid platform flag", file=sys.stderr)
Gavin Makea2e3302023-03-11 06:46:20 +00004379 return False
4380 self.config.SetString("manifest.platform", platform)
4381
4382 groups = [x for x in groups if x]
4383 groupstr = ",".join(groups)
4384 if (
4385 platform == "auto"
4386 and groupstr == self.manifest.GetDefaultGroupsStr()
4387 ):
4388 groupstr = None
4389 self.config.SetString("manifest.groups", groupstr)
4390
4391 if reference:
4392 self.config.SetString("repo.reference", reference)
4393
4394 if dissociate:
4395 self.config.SetBoolean("repo.dissociate", dissociate)
4396
4397 if worktree:
4398 if mirror:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004399 logger.error("fatal: --mirror and --worktree are incompatible")
Gavin Makea2e3302023-03-11 06:46:20 +00004400 return False
4401 if submodules:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004402 logger.error(
4403 "fatal: --submodules and --worktree are incompatible"
Gavin Makea2e3302023-03-11 06:46:20 +00004404 )
4405 return False
4406 self.config.SetBoolean("repo.worktree", worktree)
4407 if is_new:
4408 self.use_git_worktrees = True
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004409 logger.warning("warning: --worktree is experimental!")
Gavin Makea2e3302023-03-11 06:46:20 +00004410
4411 if archive:
4412 if is_new:
4413 self.config.SetBoolean("repo.archive", archive)
4414 else:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004415 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004416 "fatal: --archive is only supported when initializing a "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004417 "new workspace."
Gavin Makea2e3302023-03-11 06:46:20 +00004418 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004419 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004420 "Either delete the .repo folder in this workspace, or "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004421 "initialize in another location."
Gavin Makea2e3302023-03-11 06:46:20 +00004422 )
4423 return False
4424
4425 if mirror:
4426 if is_new:
4427 self.config.SetBoolean("repo.mirror", mirror)
4428 else:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004429 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004430 "fatal: --mirror is only supported when initializing a new "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004431 "workspace."
Gavin Makea2e3302023-03-11 06:46:20 +00004432 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004433 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004434 "Either delete the .repo folder in this workspace, or "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004435 "initialize in another location."
Gavin Makea2e3302023-03-11 06:46:20 +00004436 )
4437 return False
4438
4439 if partial_clone is not None:
4440 if mirror:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004441 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004442 "fatal: --mirror and --partial-clone are mutually "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004443 "exclusive"
Gavin Makea2e3302023-03-11 06:46:20 +00004444 )
4445 return False
4446 self.config.SetBoolean("repo.partialclone", partial_clone)
4447 if clone_filter:
4448 self.config.SetString("repo.clonefilter", clone_filter)
4449 elif self.partial_clone:
4450 clone_filter = self.clone_filter
4451 else:
4452 clone_filter = None
4453
4454 if partial_clone_exclude is not None:
4455 self.config.SetString(
4456 "repo.partialcloneexclude", partial_clone_exclude
4457 )
4458
4459 if clone_bundle is None:
4460 clone_bundle = False if partial_clone else True
4461 else:
4462 self.config.SetBoolean("repo.clonebundle", clone_bundle)
4463
4464 if submodules:
4465 self.config.SetBoolean("repo.submodules", submodules)
4466
4467 if git_lfs is not None:
4468 if git_lfs:
4469 git_require((2, 17, 0), fail=True, msg="Git LFS support")
4470
4471 self.config.SetBoolean("repo.git-lfs", git_lfs)
4472 if not is_new:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004473 logger.warning(
Gavin Makea2e3302023-03-11 06:46:20 +00004474 "warning: Changing --git-lfs settings will only affect new "
4475 "project checkouts.\n"
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004476 " Existing projects will require manual updates.\n"
Gavin Makea2e3302023-03-11 06:46:20 +00004477 )
4478
Jason Chang17833322023-05-23 13:06:55 -07004479 if clone_filter_for_depth is not None:
4480 self.ConfigureCloneFilterForDepth(clone_filter_for_depth)
4481
Gavin Makea2e3302023-03-11 06:46:20 +00004482 if use_superproject is not None:
4483 self.config.SetBoolean("repo.superproject", use_superproject)
4484
4485 if not standalone_manifest:
4486 success = self.Sync_NetworkHalf(
4487 is_new=is_new,
4488 quiet=not verbose,
4489 verbose=verbose,
4490 clone_bundle=clone_bundle,
4491 current_branch_only=current_branch_only,
4492 tags=tags,
4493 submodules=submodules,
4494 clone_filter=clone_filter,
4495 partial_clone_exclude=self.manifest.PartialCloneExclude,
Jason Chang17833322023-05-23 13:06:55 -07004496 clone_filter_for_depth=self.manifest.CloneFilterForDepth,
Gavin Makea2e3302023-03-11 06:46:20 +00004497 ).success
4498 if not success:
4499 r = self.GetRemote()
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004500 logger.error("fatal: cannot obtain manifest %s", r.url)
Gavin Makea2e3302023-03-11 06:46:20 +00004501
4502 # Better delete the manifest git dir if we created it; otherwise
4503 # next time (when user fixes problems) we won't go through the
4504 # "is_new" logic.
4505 if is_new:
4506 platform_utils.rmtree(self.gitdir)
4507 return False
4508
4509 if manifest_branch:
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08004510 self.MetaBranchSwitch(submodules=submodules, verbose=verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00004511
4512 syncbuf = SyncBuffer(self.config)
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08004513 self.Sync_LocalHalf(syncbuf, submodules=submodules, verbose=verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00004514 syncbuf.Finish()
4515
4516 if is_new or self.CurrentBranch is None:
Jason Chang1a3612f2023-08-08 14:12:53 -07004517 try:
4518 self.StartBranch("default")
4519 except GitError as e:
4520 msg = str(e)
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004521 logger.error(
4522 "fatal: cannot create default in manifest %s", msg
Gavin Makea2e3302023-03-11 06:46:20 +00004523 )
4524 return False
4525
4526 if not manifest_name:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004527 logger.error("fatal: manifest name (-m) is required.")
Gavin Makea2e3302023-03-11 06:46:20 +00004528 return False
4529
4530 elif is_new:
4531 # This is a new standalone manifest.
4532 manifest_name = "default.xml"
4533 manifest_data = fetch.fetch_file(manifest_url, verbose=verbose)
4534 dest = os.path.join(self.worktree, manifest_name)
4535 os.makedirs(os.path.dirname(dest), exist_ok=True)
4536 with open(dest, "wb") as f:
4537 f.write(manifest_data)
4538
4539 try:
4540 self.manifest.Link(manifest_name)
4541 except ManifestParseError as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004542 logger.error("fatal: manifest '%s' not available", manifest_name)
4543 logger.error("fatal: %s", e)
Gavin Makea2e3302023-03-11 06:46:20 +00004544 return False
4545
4546 if not this_manifest_only:
4547 for submanifest in self.manifest.submanifests.values():
4548 spec = submanifest.ToSubmanifestSpec()
4549 submanifest.repo_client.manifestProject.Sync(
4550 manifest_url=spec.manifestUrl,
4551 manifest_branch=spec.revision,
4552 standalone_manifest=standalone_manifest,
4553 groups=self.manifest_groups,
4554 platform=platform,
4555 mirror=mirror,
4556 dissociate=dissociate,
4557 reference=reference,
4558 worktree=worktree,
4559 submodules=submodules,
4560 archive=archive,
4561 partial_clone=partial_clone,
4562 clone_filter=clone_filter,
4563 partial_clone_exclude=partial_clone_exclude,
4564 clone_bundle=clone_bundle,
4565 git_lfs=git_lfs,
4566 use_superproject=use_superproject,
4567 verbose=verbose,
4568 current_branch_only=current_branch_only,
4569 tags=tags,
4570 depth=depth,
4571 git_event_log=git_event_log,
4572 manifest_name=spec.manifestName,
4573 this_manifest_only=False,
4574 outer_manifest=False,
4575 )
4576
4577 # Lastly, if the manifest has a <superproject> then have the
4578 # superproject sync it (if it will be used).
4579 if git_superproject.UseSuperproject(use_superproject, self.manifest):
4580 sync_result = self.manifest.superproject.Sync(git_event_log)
4581 if not sync_result.success:
4582 submanifest = ""
4583 if self.manifest.path_prefix:
4584 submanifest = f"for {self.manifest.path_prefix} "
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004585 logger.warning(
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004586 "warning: git update of superproject %s failed, "
Gavin Makea2e3302023-03-11 06:46:20 +00004587 "repo sync will not use superproject to fetch source; "
4588 "while this error is not fatal, and you can continue to "
4589 "run repo sync, please run repo init with the "
4590 "--no-use-superproject option to stop seeing this warning",
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004591 submanifest,
Gavin Makea2e3302023-03-11 06:46:20 +00004592 )
4593 if sync_result.fatal and use_superproject is not None:
4594 return False
4595
4596 return True
4597
Jason Chang17833322023-05-23 13:06:55 -07004598 def ConfigureCloneFilterForDepth(self, clone_filter_for_depth):
4599 """Configure clone filter to replace shallow clones.
4600
4601 Args:
4602 clone_filter_for_depth: a string or None, e.g. 'blob:none' will
4603 disable shallow clones and replace with partial clone. None will
4604 enable shallow clones.
4605 """
4606 self.config.SetString(
4607 "repo.clonefilterfordepth", clone_filter_for_depth
4608 )
4609
Gavin Makea2e3302023-03-11 06:46:20 +00004610 def _ConfigureDepth(self, depth):
4611 """Configure the depth we'll sync down.
4612
4613 Args:
4614 depth: an int, how deep of a partial clone to create.
4615 """
4616 # Opt.depth will be non-None if user actually passed --depth to repo
4617 # init.
4618 if depth is not None:
4619 if depth > 0:
4620 # Positive values will set the depth.
4621 depth = str(depth)
4622 else:
4623 # Negative numbers will clear the depth; passing None to
4624 # SetString will do that.
4625 depth = None
4626
4627 # We store the depth in the main manifest project.
4628 self.config.SetString("repo.depth", depth)