blob: 1821c354c238f405145caa314d5bf366eef2dd4b [file] [log] [blame]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001# Copyright (C) 2008 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
Shawn O. Pearce438ee1c2008-11-03 09:59:36 -080015import errno
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070016import filecmp
Wink Saville4c426ef2015-06-03 08:05:17 -070017import glob
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070018import os
LaMont Jonesd82be3e2022-04-05 19:30:46 +000019import platform
Shawn O. Pearcec325dc32011-10-03 08:30:24 -070020import random
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070021import re
22import shutil
23import stat
Shawn O. Pearce5e7127d2012-08-02 14:57:37 -070024import subprocess
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070025import sys
Julien Campergue335f5ef2013-10-16 11:02:35 +020026import tarfile
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +080027import tempfile
Shawn O. Pearcec325dc32011-10-03 08:30:24 -070028import time
Mike Frysinger64477332023-08-21 21:20:32 -040029from typing import List, NamedTuple
Mike Frysingeracf63b22019-06-13 02:24:21 -040030import urllib.parse
Shawn O. Pearcedf5ee522011-10-11 14:05:21 -070031
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070032from color import Coloring
Mike Frysinger64477332023-08-21 21:20:32 -040033from error import DownloadError
34from error import GitError
35from error import ManifestInvalidPathError
36from error import ManifestInvalidRevisionError
37from error import ManifestParseError
38from error import NoManifestException
39from error import RepoError
40from error import UploadError
LaMont Jones0de4fc32022-04-21 17:18:35 +000041import fetch
Mike Frysinger64477332023-08-21 21:20:32 -040042from git_command import git_require
43from git_command import GitCommand
44from git_config import GetSchemeFromUrl
45from git_config import GetUrlCookieFile
46from git_config import GitConfig
Mike Frysinger64477332023-08-21 21:20:32 -040047from git_config import IsId
48from git_refs import GitRefs
49from git_refs import HEAD
50from git_refs import R_HEADS
51from git_refs import R_M
52from git_refs import R_PUB
53from git_refs import R_TAGS
54from git_refs import R_WORKTREE_M
LaMont Jonesff6b1da2022-06-01 21:03:34 +000055import git_superproject
LaMont Jones55ee3042022-04-06 17:10:21 +000056from git_trace2_event_log import EventLog
Renaud Paquayd5cec5e2016-11-01 11:24:03 -070057import platform_utils
Mike Frysinger70d861f2019-08-26 15:22:36 -040058import progress
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +000059from repo_logging import RepoLogger
Joanna Wanga6c52f52022-11-03 16:51:19 -040060from repo_trace import Trace
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070061
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -070062
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +000063logger = RepoLogger(__file__)
64
65
LaMont Jones1eddca82022-09-01 15:15:04 +000066class SyncNetworkHalfResult(NamedTuple):
Gavin Makea2e3302023-03-11 06:46:20 +000067 """Sync_NetworkHalf return value."""
68
Gavin Makea2e3302023-03-11 06:46:20 +000069 # Did we query the remote? False when optimized_fetch is True and we have
70 # the commit already present.
71 remote_fetched: bool
Jason Chang32b59562023-07-14 16:45:35 -070072 # Error from SyncNetworkHalf
73 error: Exception = None
74
75 @property
76 def success(self) -> bool:
77 return not self.error
78
79
80class SyncNetworkHalfError(RepoError):
81 """Failure trying to sync."""
82
83
84class DeleteWorktreeError(RepoError):
85 """Failure to delete worktree."""
86
87 def __init__(
88 self, *args, aggregate_errors: List[Exception] = None, **kwargs
89 ) -> None:
90 super().__init__(*args, **kwargs)
91 self.aggregate_errors = aggregate_errors or []
92
93
94class DeleteDirtyWorktreeError(DeleteWorktreeError):
95 """Failure to delete worktree due to uncommitted changes."""
LaMont Jones1eddca82022-09-01 15:15:04 +000096
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +010097
George Engelbrecht9bc283e2020-04-02 12:36:09 -060098# Maximum sleep time allowed during retries.
99MAXIMUM_RETRY_SLEEP_SEC = 3600.0
100# +-10% random jitter is added to each Fetches retry sleep duration.
101RETRY_JITTER_PERCENT = 0.1
102
LaMont Jonesfa8d9392022-11-02 22:01:29 +0000103# Whether to use alternates. Switching back and forth is *NOT* supported.
Mike Frysinger1d00a7e2021-12-21 00:40:31 -0500104# TODO(vapier): Remove knob once behavior is verified.
Gavin Makea2e3302023-03-11 06:46:20 +0000105_ALTERNATES = os.environ.get("REPO_USE_ALTERNATES") == "1"
George Engelbrecht9bc283e2020-04-02 12:36:09 -0600106
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +0100107
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700108def _lwrite(path, content):
Gavin Makea2e3302023-03-11 06:46:20 +0000109 lock = "%s.lock" % path
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700110
Gavin Makea2e3302023-03-11 06:46:20 +0000111 # Maintain Unix line endings on all OS's to match git behavior.
112 with open(lock, "w", newline="\n") as fd:
113 fd.write(content)
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700114
Gavin Makea2e3302023-03-11 06:46:20 +0000115 try:
116 platform_utils.rename(lock, path)
117 except OSError:
118 platform_utils.remove(lock)
119 raise
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700120
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700121
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700122def not_rev(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000123 return "^" + r
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700124
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700125
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800126def sq(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000127 return "'" + r.replace("'", "'''") + "'"
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -0800128
David Pursehouse819827a2020-02-12 15:20:19 +0900129
Jonathan Nieder93719792015-03-17 11:29:58 -0700130_project_hook_list = None
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700131
132
Jonathan Nieder93719792015-03-17 11:29:58 -0700133def _ProjectHooks():
Gavin Makea2e3302023-03-11 06:46:20 +0000134 """List the hooks present in the 'hooks' directory.
Jonathan Nieder93719792015-03-17 11:29:58 -0700135
Gavin Makea2e3302023-03-11 06:46:20 +0000136 These hooks are project hooks and are copied to the '.git/hooks' directory
137 of all subprojects.
Jonathan Nieder93719792015-03-17 11:29:58 -0700138
Gavin Makea2e3302023-03-11 06:46:20 +0000139 This function caches the list of hooks (based on the contents of the
140 'repo/hooks' directory) on the first call.
Jonathan Nieder93719792015-03-17 11:29:58 -0700141
Gavin Makea2e3302023-03-11 06:46:20 +0000142 Returns:
143 A list of absolute paths to all of the files in the hooks directory.
144 """
145 global _project_hook_list
146 if _project_hook_list is None:
147 d = platform_utils.realpath(os.path.abspath(os.path.dirname(__file__)))
148 d = os.path.join(d, "hooks")
149 _project_hook_list = [
150 os.path.join(d, x) for x in platform_utils.listdir(d)
151 ]
152 return _project_hook_list
Jonathan Nieder93719792015-03-17 11:29:58 -0700153
154
Mike Frysingerd4aee652023-10-19 05:13:32 -0400155class DownloadedChange:
Gavin Makea2e3302023-03-11 06:46:20 +0000156 _commit_cache = None
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700157
Gavin Makea2e3302023-03-11 06:46:20 +0000158 def __init__(self, project, base, change_id, ps_id, commit):
159 self.project = project
160 self.base = base
161 self.change_id = change_id
162 self.ps_id = ps_id
163 self.commit = commit
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700164
Gavin Makea2e3302023-03-11 06:46:20 +0000165 @property
166 def commits(self):
167 if self._commit_cache is None:
168 self._commit_cache = self.project.bare_git.rev_list(
169 "--abbrev=8",
170 "--abbrev-commit",
171 "--pretty=oneline",
172 "--reverse",
173 "--date-order",
174 not_rev(self.base),
175 self.commit,
176 "--",
177 )
178 return self._commit_cache
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700179
180
Mike Frysingerd4aee652023-10-19 05:13:32 -0400181class ReviewableBranch:
Gavin Makea2e3302023-03-11 06:46:20 +0000182 _commit_cache = None
183 _base_exists = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700184
Gavin Makea2e3302023-03-11 06:46:20 +0000185 def __init__(self, project, branch, base):
186 self.project = project
187 self.branch = branch
188 self.base = base
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700189
Gavin Makea2e3302023-03-11 06:46:20 +0000190 @property
191 def name(self):
192 return self.branch.name
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700193
Gavin Makea2e3302023-03-11 06:46:20 +0000194 @property
195 def commits(self):
196 if self._commit_cache is None:
197 args = (
198 "--abbrev=8",
199 "--abbrev-commit",
200 "--pretty=oneline",
201 "--reverse",
202 "--date-order",
203 not_rev(self.base),
204 R_HEADS + self.name,
205 "--",
206 )
207 try:
Jason Chang87058c62023-09-27 11:34:43 -0700208 self._commit_cache = self.project.bare_git.rev_list(
209 *args, log_as_error=self.base_exists
210 )
Gavin Makea2e3302023-03-11 06:46:20 +0000211 except GitError:
212 # We weren't able to probe the commits for this branch. Was it
213 # tracking a branch that no longer exists? If so, return no
214 # commits. Otherwise, rethrow the error as we don't know what's
215 # going on.
216 if self.base_exists:
217 raise
Mike Frysinger6da17752019-09-11 18:43:17 -0400218
Gavin Makea2e3302023-03-11 06:46:20 +0000219 self._commit_cache = []
Mike Frysinger6da17752019-09-11 18:43:17 -0400220
Gavin Makea2e3302023-03-11 06:46:20 +0000221 return self._commit_cache
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700222
Gavin Makea2e3302023-03-11 06:46:20 +0000223 @property
224 def unabbrev_commits(self):
225 r = dict()
226 for commit in self.project.bare_git.rev_list(
227 not_rev(self.base), R_HEADS + self.name, "--"
228 ):
229 r[commit[0:8]] = commit
230 return r
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800231
Gavin Makea2e3302023-03-11 06:46:20 +0000232 @property
233 def date(self):
234 return self.project.bare_git.log(
235 "--pretty=format:%cd", "-n", "1", R_HEADS + self.name, "--"
236 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700237
Gavin Makea2e3302023-03-11 06:46:20 +0000238 @property
239 def base_exists(self):
240 """Whether the branch we're tracking exists.
Mike Frysinger6da17752019-09-11 18:43:17 -0400241
Gavin Makea2e3302023-03-11 06:46:20 +0000242 Normally it should, but sometimes branches we track can get deleted.
243 """
244 if self._base_exists is None:
245 try:
246 self.project.bare_git.rev_parse("--verify", not_rev(self.base))
247 # If we're still here, the base branch exists.
248 self._base_exists = True
249 except GitError:
250 # If we failed to verify, the base branch doesn't exist.
251 self._base_exists = False
Mike Frysinger6da17752019-09-11 18:43:17 -0400252
Gavin Makea2e3302023-03-11 06:46:20 +0000253 return self._base_exists
Mike Frysinger6da17752019-09-11 18:43:17 -0400254
Gavin Makea2e3302023-03-11 06:46:20 +0000255 def UploadForReview(
256 self,
257 people,
258 dryrun=False,
259 auto_topic=False,
260 hashtags=(),
261 labels=(),
262 private=False,
263 notify=None,
264 wip=False,
265 ready=False,
266 dest_branch=None,
267 validate_certs=True,
268 push_options=None,
269 ):
270 self.project.UploadForReview(
271 branch=self.name,
272 people=people,
273 dryrun=dryrun,
274 auto_topic=auto_topic,
275 hashtags=hashtags,
276 labels=labels,
277 private=private,
278 notify=notify,
279 wip=wip,
280 ready=ready,
281 dest_branch=dest_branch,
282 validate_certs=validate_certs,
283 push_options=push_options,
284 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700285
Gavin Makea2e3302023-03-11 06:46:20 +0000286 def GetPublishedRefs(self):
287 refs = {}
288 output = self.project.bare_git.ls_remote(
289 self.branch.remote.SshReviewUrl(self.project.UserEmail),
290 "refs/changes/*",
291 )
292 for line in output.split("\n"):
293 try:
294 (sha, ref) = line.split()
295 refs[sha] = ref
296 except ValueError:
297 pass
Ficus Kirkpatrickbc7ef672009-05-04 12:45:11 -0700298
Gavin Makea2e3302023-03-11 06:46:20 +0000299 return refs
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700300
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700301
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700302class StatusColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000303 def __init__(self, config):
304 super().__init__(config, "status")
305 self.project = self.printer("header", attr="bold")
306 self.branch = self.printer("header", attr="bold")
307 self.nobranch = self.printer("nobranch", fg="red")
308 self.important = self.printer("important", fg="red")
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700309
Gavin Makea2e3302023-03-11 06:46:20 +0000310 self.added = self.printer("added", fg="green")
311 self.changed = self.printer("changed", fg="red")
312 self.untracked = self.printer("untracked", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700313
314
315class DiffColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000316 def __init__(self, config):
317 super().__init__(config, "diff")
318 self.project = self.printer("header", attr="bold")
319 self.fail = self.printer("fail", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700320
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700321
Mike Frysingerd4aee652023-10-19 05:13:32 -0400322class Annotation:
Gavin Makea2e3302023-03-11 06:46:20 +0000323 def __init__(self, name, value, keep):
324 self.name = name
325 self.value = value
326 self.keep = keep
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700327
Gavin Makea2e3302023-03-11 06:46:20 +0000328 def __eq__(self, other):
329 if not isinstance(other, Annotation):
330 return False
331 return self.__dict__ == other.__dict__
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700332
Gavin Makea2e3302023-03-11 06:46:20 +0000333 def __lt__(self, other):
334 # This exists just so that lists of Annotation objects can be sorted,
335 # for use in comparisons.
336 if not isinstance(other, Annotation):
337 raise ValueError("comparison is not between two Annotation objects")
338 if self.name == other.name:
339 if self.value == other.value:
340 return self.keep < other.keep
341 return self.value < other.value
342 return self.name < other.name
Jack Neus6ea0cae2021-07-20 20:52:33 +0000343
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700344
Mike Frysingere6a202f2019-08-02 15:57:57 -0400345def _SafeExpandPath(base, subpath, skipfinal=False):
Gavin Makea2e3302023-03-11 06:46:20 +0000346 """Make sure |subpath| is completely safe under |base|.
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700347
Gavin Makea2e3302023-03-11 06:46:20 +0000348 We make sure no intermediate symlinks are traversed, and that the final path
349 is not a special file (e.g. not a socket or fifo).
Mike Frysingere6a202f2019-08-02 15:57:57 -0400350
Gavin Makea2e3302023-03-11 06:46:20 +0000351 NB: We rely on a number of paths already being filtered out while parsing
352 the manifest. See the validation logic in manifest_xml.py for more details.
353 """
354 # Split up the path by its components. We can't use os.path.sep exclusively
355 # as some platforms (like Windows) will convert / to \ and that bypasses all
356 # our constructed logic here. Especially since manifest authors only use
357 # / in their paths.
358 resep = re.compile(r"[/%s]" % re.escape(os.path.sep))
359 components = resep.split(subpath)
360 if skipfinal:
361 # Whether the caller handles the final component itself.
362 finalpart = components.pop()
Mike Frysingere6a202f2019-08-02 15:57:57 -0400363
Gavin Makea2e3302023-03-11 06:46:20 +0000364 path = base
365 for part in components:
366 if part in {".", ".."}:
367 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400368 f'{subpath}: "{part}" not allowed in paths'
Gavin Makea2e3302023-03-11 06:46:20 +0000369 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400370
Gavin Makea2e3302023-03-11 06:46:20 +0000371 path = os.path.join(path, part)
372 if platform_utils.islink(path):
373 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400374 f"{path}: traversing symlinks not allow"
Gavin Makea2e3302023-03-11 06:46:20 +0000375 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400376
Gavin Makea2e3302023-03-11 06:46:20 +0000377 if os.path.exists(path):
378 if not os.path.isfile(path) and not platform_utils.isdir(path):
379 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400380 f"{path}: only regular files & directories allowed"
Gavin Makea2e3302023-03-11 06:46:20 +0000381 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400382
Gavin Makea2e3302023-03-11 06:46:20 +0000383 if skipfinal:
384 path = os.path.join(path, finalpart)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400385
Gavin Makea2e3302023-03-11 06:46:20 +0000386 return path
Mike Frysingere6a202f2019-08-02 15:57:57 -0400387
388
Mike Frysingerd4aee652023-10-19 05:13:32 -0400389class _CopyFile:
Gavin Makea2e3302023-03-11 06:46:20 +0000390 """Container for <copyfile> manifest element."""
Mike Frysingere6a202f2019-08-02 15:57:57 -0400391
Gavin Makea2e3302023-03-11 06:46:20 +0000392 def __init__(self, git_worktree, src, topdir, dest):
393 """Register a <copyfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400394
Gavin Makea2e3302023-03-11 06:46:20 +0000395 Args:
396 git_worktree: Absolute path to the git project checkout.
397 src: Relative path under |git_worktree| of file to read.
398 topdir: Absolute path to the top of the repo client checkout.
399 dest: Relative path under |topdir| of file to write.
400 """
401 self.git_worktree = git_worktree
402 self.topdir = topdir
403 self.src = src
404 self.dest = dest
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700405
Gavin Makea2e3302023-03-11 06:46:20 +0000406 def _Copy(self):
407 src = _SafeExpandPath(self.git_worktree, self.src)
408 dest = _SafeExpandPath(self.topdir, self.dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400409
Gavin Makea2e3302023-03-11 06:46:20 +0000410 if platform_utils.isdir(src):
411 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400412 f"{self.src}: copying from directory not supported"
Gavin Makea2e3302023-03-11 06:46:20 +0000413 )
414 if platform_utils.isdir(dest):
415 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400416 f"{self.dest}: copying to directory not allowed"
Gavin Makea2e3302023-03-11 06:46:20 +0000417 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400418
Gavin Makea2e3302023-03-11 06:46:20 +0000419 # Copy file if it does not exist or is out of date.
420 if not os.path.exists(dest) or not filecmp.cmp(src, dest):
421 try:
422 # Remove existing file first, since it might be read-only.
423 if os.path.exists(dest):
424 platform_utils.remove(dest)
425 else:
426 dest_dir = os.path.dirname(dest)
427 if not platform_utils.isdir(dest_dir):
428 os.makedirs(dest_dir)
429 shutil.copy(src, dest)
430 # Make the file read-only.
431 mode = os.stat(dest)[stat.ST_MODE]
432 mode = mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
433 os.chmod(dest, mode)
Jason R. Coombsae824fb2023-10-20 23:32:40 +0545434 except OSError:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +0000435 logger.error("error: Cannot copy file %s to %s", src, dest)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700436
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700437
Mike Frysingerd4aee652023-10-19 05:13:32 -0400438class _LinkFile:
Gavin Makea2e3302023-03-11 06:46:20 +0000439 """Container for <linkfile> manifest element."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700440
Gavin Makea2e3302023-03-11 06:46:20 +0000441 def __init__(self, git_worktree, src, topdir, dest):
442 """Register a <linkfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400443
Gavin Makea2e3302023-03-11 06:46:20 +0000444 Args:
445 git_worktree: Absolute path to the git project checkout.
446 src: Target of symlink relative to path under |git_worktree|.
447 topdir: Absolute path to the top of the repo client checkout.
448 dest: Relative path under |topdir| of symlink to create.
449 """
450 self.git_worktree = git_worktree
451 self.topdir = topdir
452 self.src = src
453 self.dest = dest
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500454
Gavin Makea2e3302023-03-11 06:46:20 +0000455 def __linkIt(self, relSrc, absDest):
456 # Link file if it does not exist or is out of date.
457 if not platform_utils.islink(absDest) or (
458 platform_utils.readlink(absDest) != relSrc
459 ):
460 try:
461 # Remove existing file first, since it might be read-only.
462 if os.path.lexists(absDest):
463 platform_utils.remove(absDest)
464 else:
465 dest_dir = os.path.dirname(absDest)
466 if not platform_utils.isdir(dest_dir):
467 os.makedirs(dest_dir)
468 platform_utils.symlink(relSrc, absDest)
Jason R. Coombsae824fb2023-10-20 23:32:40 +0545469 except OSError:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +0000470 logger.error(
471 "error: Cannot link file %s to %s", relSrc, absDest
472 )
Gavin Makea2e3302023-03-11 06:46:20 +0000473
474 def _Link(self):
475 """Link the self.src & self.dest paths.
476
477 Handles wild cards on the src linking all of the files in the source in
478 to the destination directory.
479 """
480 # Some people use src="." to create stable links to projects. Let's
481 # allow that but reject all other uses of "." to keep things simple.
482 if self.src == ".":
483 src = self.git_worktree
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500484 else:
Gavin Makea2e3302023-03-11 06:46:20 +0000485 src = _SafeExpandPath(self.git_worktree, self.src)
Wink Saville4c426ef2015-06-03 08:05:17 -0700486
Gavin Makea2e3302023-03-11 06:46:20 +0000487 if not glob.has_magic(src):
488 # Entity does not contain a wild card so just a simple one to one
489 # link operation.
490 dest = _SafeExpandPath(self.topdir, self.dest, skipfinal=True)
491 # dest & src are absolute paths at this point. Make sure the target
492 # of the symlink is relative in the context of the repo client
493 # checkout.
494 relpath = os.path.relpath(src, os.path.dirname(dest))
495 self.__linkIt(relpath, dest)
496 else:
497 dest = _SafeExpandPath(self.topdir, self.dest)
498 # Entity contains a wild card.
499 if os.path.exists(dest) and not platform_utils.isdir(dest):
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +0000500 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +0000501 "Link error: src with wildcard, %s must be a directory",
502 dest,
503 )
504 else:
505 for absSrcFile in glob.glob(src):
506 # Create a releative path from source dir to destination
507 # dir.
508 absSrcDir = os.path.dirname(absSrcFile)
509 relSrcDir = os.path.relpath(absSrcDir, dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400510
Gavin Makea2e3302023-03-11 06:46:20 +0000511 # Get the source file name.
512 srcFile = os.path.basename(absSrcFile)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400513
Gavin Makea2e3302023-03-11 06:46:20 +0000514 # Now form the final full paths to srcFile. They will be
515 # absolute for the desintaiton and relative for the source.
516 absDest = os.path.join(dest, srcFile)
517 relSrc = os.path.join(relSrcDir, srcFile)
518 self.__linkIt(relSrc, absDest)
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500519
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700520
Mike Frysingerd4aee652023-10-19 05:13:32 -0400521class RemoteSpec:
Gavin Makea2e3302023-03-11 06:46:20 +0000522 def __init__(
523 self,
524 name,
525 url=None,
526 pushUrl=None,
527 review=None,
528 revision=None,
529 orig_name=None,
530 fetchUrl=None,
531 ):
532 self.name = name
533 self.url = url
534 self.pushUrl = pushUrl
535 self.review = review
536 self.revision = revision
537 self.orig_name = orig_name
538 self.fetchUrl = fetchUrl
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700539
Ian Kasprzak0286e312021-02-05 10:06:18 -0800540
Mike Frysingerd4aee652023-10-19 05:13:32 -0400541class Project:
Gavin Makea2e3302023-03-11 06:46:20 +0000542 # These objects can be shared between several working trees.
543 @property
544 def shareable_dirs(self):
545 """Return the shareable directories"""
546 if self.UseAlternates:
547 return ["hooks", "rr-cache"]
548 else:
549 return ["hooks", "objects", "rr-cache"]
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700550
Gavin Makea2e3302023-03-11 06:46:20 +0000551 def __init__(
552 self,
553 manifest,
554 name,
555 remote,
556 gitdir,
557 objdir,
558 worktree,
559 relpath,
560 revisionExpr,
561 revisionId,
562 rebase=True,
563 groups=None,
564 sync_c=False,
565 sync_s=False,
566 sync_tags=True,
567 clone_depth=None,
568 upstream=None,
569 parent=None,
570 use_git_worktrees=False,
571 is_derived=False,
572 dest_branch=None,
573 optimized_fetch=False,
574 retry_fetches=0,
575 old_revision=None,
576 ):
577 """Init a Project object.
Doug Anderson3ba5f952011-04-07 12:51:04 -0700578
579 Args:
Gavin Makea2e3302023-03-11 06:46:20 +0000580 manifest: The XmlManifest object.
581 name: The `name` attribute of manifest.xml's project element.
582 remote: RemoteSpec object specifying its remote's properties.
583 gitdir: Absolute path of git directory.
584 objdir: Absolute path of directory to store git objects.
585 worktree: Absolute path of git working tree.
586 relpath: Relative path of git working tree to repo's top directory.
587 revisionExpr: The `revision` attribute of manifest.xml's project
588 element.
589 revisionId: git commit id for checking out.
590 rebase: The `rebase` attribute of manifest.xml's project element.
591 groups: The `groups` attribute of manifest.xml's project element.
592 sync_c: The `sync-c` attribute of manifest.xml's project element.
593 sync_s: The `sync-s` attribute of manifest.xml's project element.
594 sync_tags: The `sync-tags` attribute of manifest.xml's project
595 element.
596 upstream: The `upstream` attribute of manifest.xml's project
597 element.
598 parent: The parent Project object.
599 use_git_worktrees: Whether to use `git worktree` for this project.
600 is_derived: False if the project was explicitly defined in the
601 manifest; True if the project is a discovered submodule.
602 dest_branch: The branch to which to push changes for review by
603 default.
604 optimized_fetch: If True, when a project is set to a sha1 revision,
605 only fetch from the remote if the sha1 is not present locally.
606 retry_fetches: Retry remote fetches n times upon receiving transient
607 error with exponential backoff and jitter.
608 old_revision: saved git commit id for open GITC projects.
609 """
610 self.client = self.manifest = manifest
611 self.name = name
612 self.remote = remote
613 self.UpdatePaths(relpath, worktree, gitdir, objdir)
614 self.SetRevision(revisionExpr, revisionId=revisionId)
615
616 self.rebase = rebase
617 self.groups = groups
618 self.sync_c = sync_c
619 self.sync_s = sync_s
620 self.sync_tags = sync_tags
621 self.clone_depth = clone_depth
622 self.upstream = upstream
623 self.parent = parent
624 # NB: Do not use this setting in __init__ to change behavior so that the
625 # manifest.git checkout can inspect & change it after instantiating.
626 # See the XmlManifest init code for more info.
627 self.use_git_worktrees = use_git_worktrees
628 self.is_derived = is_derived
629 self.optimized_fetch = optimized_fetch
630 self.retry_fetches = max(0, retry_fetches)
631 self.subprojects = []
632
633 self.snapshots = {}
634 self.copyfiles = []
635 self.linkfiles = []
636 self.annotations = []
637 self.dest_branch = dest_branch
638 self.old_revision = old_revision
639
640 # This will be filled in if a project is later identified to be the
641 # project containing repo hooks.
642 self.enabled_repo_hooks = []
643
644 def RelPath(self, local=True):
645 """Return the path for the project relative to a manifest.
646
647 Args:
648 local: a boolean, if True, the path is relative to the local
649 (sub)manifest. If false, the path is relative to the outermost
650 manifest.
651 """
652 if local:
653 return self.relpath
654 return os.path.join(self.manifest.path_prefix, self.relpath)
655
656 def SetRevision(self, revisionExpr, revisionId=None):
657 """Set revisionId based on revision expression and id"""
658 self.revisionExpr = revisionExpr
659 if revisionId is None and revisionExpr and IsId(revisionExpr):
660 self.revisionId = self.revisionExpr
661 else:
662 self.revisionId = revisionId
663
664 def UpdatePaths(self, relpath, worktree, gitdir, objdir):
665 """Update paths used by this project"""
666 self.gitdir = gitdir.replace("\\", "/")
667 self.objdir = objdir.replace("\\", "/")
668 if worktree:
669 self.worktree = os.path.normpath(worktree).replace("\\", "/")
670 else:
671 self.worktree = None
672 self.relpath = relpath
673
674 self.config = GitConfig.ForRepository(
675 gitdir=self.gitdir, defaults=self.manifest.globalConfig
676 )
677
678 if self.worktree:
679 self.work_git = self._GitGetByExec(
680 self, bare=False, gitdir=self.gitdir
681 )
682 else:
683 self.work_git = None
684 self.bare_git = self._GitGetByExec(self, bare=True, gitdir=self.gitdir)
685 self.bare_ref = GitRefs(self.gitdir)
686 self.bare_objdir = self._GitGetByExec(
687 self, bare=True, gitdir=self.objdir
688 )
689
690 @property
691 def UseAlternates(self):
692 """Whether git alternates are in use.
693
694 This will be removed once migration to alternates is complete.
695 """
696 return _ALTERNATES or self.manifest.is_multimanifest
697
698 @property
699 def Derived(self):
700 return self.is_derived
701
702 @property
703 def Exists(self):
704 return platform_utils.isdir(self.gitdir) and platform_utils.isdir(
705 self.objdir
706 )
707
708 @property
709 def CurrentBranch(self):
710 """Obtain the name of the currently checked out branch.
711
712 The branch name omits the 'refs/heads/' prefix.
713 None is returned if the project is on a detached HEAD, or if the
714 work_git is otheriwse inaccessible (e.g. an incomplete sync).
715 """
716 try:
717 b = self.work_git.GetHead()
718 except NoManifestException:
719 # If the local checkout is in a bad state, don't barf. Let the
720 # callers process this like the head is unreadable.
721 return None
722 if b.startswith(R_HEADS):
723 return b[len(R_HEADS) :]
724 return None
725
726 def IsRebaseInProgress(self):
727 return (
728 os.path.exists(self.work_git.GetDotgitPath("rebase-apply"))
729 or os.path.exists(self.work_git.GetDotgitPath("rebase-merge"))
730 or os.path.exists(os.path.join(self.worktree, ".dotest"))
731 )
732
733 def IsDirty(self, consider_untracked=True):
734 """Is the working directory modified in some way?"""
735 self.work_git.update_index(
736 "-q", "--unmerged", "--ignore-missing", "--refresh"
737 )
738 if self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD):
739 return True
740 if self.work_git.DiffZ("diff-files"):
741 return True
742 if consider_untracked and self.UntrackedFiles():
743 return True
744 return False
745
746 _userident_name = None
747 _userident_email = None
748
749 @property
750 def UserName(self):
751 """Obtain the user's personal name."""
752 if self._userident_name is None:
753 self._LoadUserIdentity()
754 return self._userident_name
755
756 @property
757 def UserEmail(self):
758 """Obtain the user's email address. This is very likely
759 to be their Gerrit login.
760 """
761 if self._userident_email is None:
762 self._LoadUserIdentity()
763 return self._userident_email
764
765 def _LoadUserIdentity(self):
766 u = self.bare_git.var("GIT_COMMITTER_IDENT")
767 m = re.compile("^(.*) <([^>]*)> ").match(u)
768 if m:
769 self._userident_name = m.group(1)
770 self._userident_email = m.group(2)
771 else:
772 self._userident_name = ""
773 self._userident_email = ""
774
775 def GetRemote(self, name=None):
776 """Get the configuration for a single remote.
777
778 Defaults to the current project's remote.
779 """
780 if name is None:
781 name = self.remote.name
782 return self.config.GetRemote(name)
783
784 def GetBranch(self, name):
785 """Get the configuration for a single branch."""
786 return self.config.GetBranch(name)
787
788 def GetBranches(self):
789 """Get all existing local branches."""
790 current = self.CurrentBranch
791 all_refs = self._allrefs
792 heads = {}
793
794 for name, ref_id in all_refs.items():
795 if name.startswith(R_HEADS):
796 name = name[len(R_HEADS) :]
797 b = self.GetBranch(name)
798 b.current = name == current
799 b.published = None
800 b.revision = ref_id
801 heads[name] = b
802
803 for name, ref_id in all_refs.items():
804 if name.startswith(R_PUB):
805 name = name[len(R_PUB) :]
806 b = heads.get(name)
807 if b:
808 b.published = ref_id
809
810 return heads
811
812 def MatchesGroups(self, manifest_groups):
813 """Returns true if the manifest groups specified at init should cause
814 this project to be synced.
815 Prefixing a manifest group with "-" inverts the meaning of a group.
816 All projects are implicitly labelled with "all".
817
818 labels are resolved in order. In the example case of
819 project_groups: "all,group1,group2"
820 manifest_groups: "-group1,group2"
821 the project will be matched.
822
823 The special manifest group "default" will match any project that
824 does not have the special project group "notdefault"
825 """
826 default_groups = self.manifest.default_groups or ["default"]
827 expanded_manifest_groups = manifest_groups or default_groups
828 expanded_project_groups = ["all"] + (self.groups or [])
829 if "notdefault" not in expanded_project_groups:
830 expanded_project_groups += ["default"]
831
832 matched = False
833 for group in expanded_manifest_groups:
834 if group.startswith("-") and group[1:] in expanded_project_groups:
835 matched = False
836 elif group in expanded_project_groups:
837 matched = True
838
839 return matched
840
841 def UncommitedFiles(self, get_all=True):
842 """Returns a list of strings, uncommitted files in the git tree.
843
844 Args:
845 get_all: a boolean, if True - get information about all different
846 uncommitted files. If False - return as soon as any kind of
847 uncommitted files is detected.
848 """
849 details = []
850 self.work_git.update_index(
851 "-q", "--unmerged", "--ignore-missing", "--refresh"
852 )
853 if self.IsRebaseInProgress():
854 details.append("rebase in progress")
855 if not get_all:
856 return details
857
858 changes = self.work_git.DiffZ("diff-index", "--cached", HEAD).keys()
859 if changes:
860 details.extend(changes)
861 if not get_all:
862 return details
863
864 changes = self.work_git.DiffZ("diff-files").keys()
865 if changes:
866 details.extend(changes)
867 if not get_all:
868 return details
869
870 changes = self.UntrackedFiles()
871 if changes:
872 details.extend(changes)
873
874 return details
875
876 def UntrackedFiles(self):
877 """Returns a list of strings, untracked files in the git tree."""
878 return self.work_git.LsOthers()
879
880 def HasChanges(self):
881 """Returns true if there are uncommitted changes."""
882 return bool(self.UncommitedFiles(get_all=False))
883
884 def PrintWorkTreeStatus(self, output_redir=None, quiet=False, local=False):
885 """Prints the status of the repository to stdout.
886
887 Args:
888 output_redir: If specified, redirect the output to this object.
889 quiet: If True then only print the project name. Do not print
890 the modified files, branch name, etc.
891 local: a boolean, if True, the path is relative to the local
892 (sub)manifest. If false, the path is relative to the outermost
893 manifest.
894 """
895 if not platform_utils.isdir(self.worktree):
896 if output_redir is None:
897 output_redir = sys.stdout
898 print(file=output_redir)
899 print("project %s/" % self.RelPath(local), file=output_redir)
900 print(' missing (run "repo sync")', file=output_redir)
901 return
902
903 self.work_git.update_index(
904 "-q", "--unmerged", "--ignore-missing", "--refresh"
905 )
906 rb = self.IsRebaseInProgress()
907 di = self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD)
908 df = self.work_git.DiffZ("diff-files")
909 do = self.work_git.LsOthers()
910 if not rb and not di and not df and not do and not self.CurrentBranch:
911 return "CLEAN"
912
913 out = StatusColoring(self.config)
914 if output_redir is not None:
915 out.redirect(output_redir)
916 out.project("project %-40s", self.RelPath(local) + "/ ")
917
918 if quiet:
919 out.nl()
920 return "DIRTY"
921
922 branch = self.CurrentBranch
923 if branch is None:
924 out.nobranch("(*** NO BRANCH ***)")
925 else:
926 out.branch("branch %s", branch)
927 out.nl()
928
929 if rb:
930 out.important("prior sync failed; rebase still in progress")
931 out.nl()
932
933 paths = list()
934 paths.extend(di.keys())
935 paths.extend(df.keys())
936 paths.extend(do)
937
938 for p in sorted(set(paths)):
939 try:
940 i = di[p]
941 except KeyError:
942 i = None
943
944 try:
945 f = df[p]
946 except KeyError:
947 f = None
948
949 if i:
950 i_status = i.status.upper()
951 else:
952 i_status = "-"
953
954 if f:
955 f_status = f.status.lower()
956 else:
957 f_status = "-"
958
959 if i and i.src_path:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400960 line = (
961 f" {i_status}{f_status}\t{i.src_path} => {p} ({i.level}%)"
Gavin Makea2e3302023-03-11 06:46:20 +0000962 )
963 else:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400964 line = f" {i_status}{f_status}\t{p}"
Gavin Makea2e3302023-03-11 06:46:20 +0000965
966 if i and not f:
967 out.added("%s", line)
968 elif (i and f) or (not i and f):
969 out.changed("%s", line)
970 elif not i and not f:
971 out.untracked("%s", line)
972 else:
973 out.write("%s", line)
974 out.nl()
975
976 return "DIRTY"
977
978 def PrintWorkTreeDiff(
979 self, absolute_paths=False, output_redir=None, local=False
980 ):
981 """Prints the status of the repository to stdout."""
982 out = DiffColoring(self.config)
983 if output_redir:
984 out.redirect(output_redir)
985 cmd = ["diff"]
986 if out.is_on:
987 cmd.append("--color")
988 cmd.append(HEAD)
989 if absolute_paths:
990 cmd.append("--src-prefix=a/%s/" % self.RelPath(local))
991 cmd.append("--dst-prefix=b/%s/" % self.RelPath(local))
992 cmd.append("--")
993 try:
994 p = GitCommand(self, cmd, capture_stdout=True, capture_stderr=True)
995 p.Wait()
996 except GitError as e:
997 out.nl()
998 out.project("project %s/" % self.RelPath(local))
999 out.nl()
1000 out.fail("%s", str(e))
1001 out.nl()
1002 return False
1003 if p.stdout:
1004 out.nl()
1005 out.project("project %s/" % self.RelPath(local))
1006 out.nl()
1007 out.write("%s", p.stdout)
1008 return p.Wait() == 0
1009
1010 def WasPublished(self, branch, all_refs=None):
1011 """Was the branch published (uploaded) for code review?
1012 If so, returns the SHA-1 hash of the last published
1013 state for the branch.
1014 """
1015 key = R_PUB + branch
1016 if all_refs is None:
1017 try:
1018 return self.bare_git.rev_parse(key)
1019 except GitError:
1020 return None
1021 else:
1022 try:
1023 return all_refs[key]
1024 except KeyError:
1025 return None
1026
1027 def CleanPublishedCache(self, all_refs=None):
1028 """Prunes any stale published refs."""
1029 if all_refs is None:
1030 all_refs = self._allrefs
1031 heads = set()
1032 canrm = {}
1033 for name, ref_id in all_refs.items():
1034 if name.startswith(R_HEADS):
1035 heads.add(name)
1036 elif name.startswith(R_PUB):
1037 canrm[name] = ref_id
1038
1039 for name, ref_id in canrm.items():
1040 n = name[len(R_PUB) :]
1041 if R_HEADS + n not in heads:
1042 self.bare_git.DeleteRef(name, ref_id)
1043
1044 def GetUploadableBranches(self, selected_branch=None):
1045 """List any branches which can be uploaded for review."""
1046 heads = {}
1047 pubed = {}
1048
1049 for name, ref_id in self._allrefs.items():
1050 if name.startswith(R_HEADS):
1051 heads[name[len(R_HEADS) :]] = ref_id
1052 elif name.startswith(R_PUB):
1053 pubed[name[len(R_PUB) :]] = ref_id
1054
1055 ready = []
1056 for branch, ref_id in heads.items():
1057 if branch in pubed and pubed[branch] == ref_id:
1058 continue
1059 if selected_branch and branch != selected_branch:
1060 continue
1061
1062 rb = self.GetUploadableBranch(branch)
1063 if rb:
1064 ready.append(rb)
1065 return ready
1066
1067 def GetUploadableBranch(self, branch_name):
1068 """Get a single uploadable branch, or None."""
1069 branch = self.GetBranch(branch_name)
1070 base = branch.LocalMerge
1071 if branch.LocalMerge:
1072 rb = ReviewableBranch(self, branch, base)
1073 if rb.commits:
1074 return rb
1075 return None
1076
1077 def UploadForReview(
1078 self,
1079 branch=None,
1080 people=([], []),
1081 dryrun=False,
1082 auto_topic=False,
1083 hashtags=(),
1084 labels=(),
1085 private=False,
1086 notify=None,
1087 wip=False,
1088 ready=False,
1089 dest_branch=None,
1090 validate_certs=True,
1091 push_options=None,
1092 ):
1093 """Uploads the named branch for code review."""
1094 if branch is None:
1095 branch = self.CurrentBranch
1096 if branch is None:
Jason Chang32b59562023-07-14 16:45:35 -07001097 raise GitError("not currently on a branch", project=self.name)
Gavin Makea2e3302023-03-11 06:46:20 +00001098
1099 branch = self.GetBranch(branch)
1100 if not branch.LocalMerge:
Jason Chang32b59562023-07-14 16:45:35 -07001101 raise GitError(
1102 "branch %s does not track a remote" % branch.name,
1103 project=self.name,
1104 )
Gavin Makea2e3302023-03-11 06:46:20 +00001105 if not branch.remote.review:
Jason Chang32b59562023-07-14 16:45:35 -07001106 raise GitError(
1107 "remote %s has no review url" % branch.remote.name,
1108 project=self.name,
1109 )
Gavin Makea2e3302023-03-11 06:46:20 +00001110
1111 # Basic validity check on label syntax.
1112 for label in labels:
1113 if not re.match(r"^.+[+-][0-9]+$", label):
1114 raise UploadError(
1115 f'invalid label syntax "{label}": labels use forms like '
Jason Chang5a3a5f72023-08-17 11:36:41 -07001116 "CodeReview+1 or Verified-1",
1117 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00001118 )
1119
1120 if dest_branch is None:
1121 dest_branch = self.dest_branch
1122 if dest_branch is None:
1123 dest_branch = branch.merge
1124 if not dest_branch.startswith(R_HEADS):
1125 dest_branch = R_HEADS + dest_branch
1126
1127 if not branch.remote.projectname:
1128 branch.remote.projectname = self.name
1129 branch.remote.Save()
1130
1131 url = branch.remote.ReviewUrl(self.UserEmail, validate_certs)
1132 if url is None:
Jason Chang5a3a5f72023-08-17 11:36:41 -07001133 raise UploadError("review not configured", project=self.name)
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +00001134 cmd = ["push", "--progress"]
Gavin Makea2e3302023-03-11 06:46:20 +00001135 if dryrun:
1136 cmd.append("-n")
1137
1138 if url.startswith("ssh://"):
1139 cmd.append("--receive-pack=gerrit receive-pack")
1140
Aravind Vasudevan99ebf622023-04-04 23:44:37 +00001141 # This stops git from pushing all reachable annotated tags when
1142 # push.followTags is configured. Gerrit does not accept any tags
1143 # pushed to a CL.
1144 if git_require((1, 8, 3)):
1145 cmd.append("--no-follow-tags")
1146
Gavin Makea2e3302023-03-11 06:46:20 +00001147 for push_option in push_options or []:
1148 cmd.append("-o")
1149 cmd.append(push_option)
1150
1151 cmd.append(url)
1152
1153 if dest_branch.startswith(R_HEADS):
1154 dest_branch = dest_branch[len(R_HEADS) :]
1155
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001156 ref_spec = f"{R_HEADS + branch.name}:refs/for/{dest_branch}"
Gavin Makea2e3302023-03-11 06:46:20 +00001157 opts = []
1158 if auto_topic:
1159 opts += ["topic=" + branch.name]
1160 opts += ["t=%s" % p for p in hashtags]
1161 # NB: No need to encode labels as they've been validated above.
1162 opts += ["l=%s" % p for p in labels]
1163
1164 opts += ["r=%s" % p for p in people[0]]
1165 opts += ["cc=%s" % p for p in people[1]]
1166 if notify:
1167 opts += ["notify=" + notify]
1168 if private:
1169 opts += ["private"]
1170 if wip:
1171 opts += ["wip"]
1172 if ready:
1173 opts += ["ready"]
1174 if opts:
1175 ref_spec = ref_spec + "%" + ",".join(opts)
1176 cmd.append(ref_spec)
1177
Jason Chang1e9f7b92023-08-25 10:31:04 -07001178 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001179
1180 if not dryrun:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001181 msg = f"posted to {branch.remote.review} for {dest_branch}"
Gavin Makea2e3302023-03-11 06:46:20 +00001182 self.bare_git.UpdateRef(
1183 R_PUB + branch.name, R_HEADS + branch.name, message=msg
1184 )
1185
1186 def _ExtractArchive(self, tarpath, path=None):
1187 """Extract the given tar on its current location
1188
1189 Args:
1190 tarpath: The path to the actual tar file
1191
1192 """
1193 try:
1194 with tarfile.open(tarpath, "r") as tar:
1195 tar.extractall(path=path)
1196 return True
Jason R. Coombsae824fb2023-10-20 23:32:40 +05451197 except (OSError, tarfile.TarError) as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001198 logger.error("error: Cannot extract archive %s: %s", tarpath, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001199 return False
1200
1201 def Sync_NetworkHalf(
1202 self,
1203 quiet=False,
1204 verbose=False,
1205 output_redir=None,
1206 is_new=None,
1207 current_branch_only=None,
1208 force_sync=False,
1209 clone_bundle=True,
1210 tags=None,
1211 archive=False,
1212 optimized_fetch=False,
1213 retry_fetches=0,
1214 prune=False,
1215 submodules=False,
1216 ssh_proxy=None,
1217 clone_filter=None,
1218 partial_clone_exclude=set(),
Jason Chang17833322023-05-23 13:06:55 -07001219 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001220 ):
1221 """Perform only the network IO portion of the sync process.
1222 Local working directory/branch state is not affected.
1223 """
1224 if archive and not isinstance(self, MetaProject):
1225 if self.remote.url.startswith(("http://", "https://")):
Jason Chang32b59562023-07-14 16:45:35 -07001226 msg_template = (
1227 "%s: Cannot fetch archives from http/https remotes."
Gavin Makea2e3302023-03-11 06:46:20 +00001228 )
Jason Chang32b59562023-07-14 16:45:35 -07001229 msg_args = self.name
1230 msg = msg_template % msg_args
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001231 logger.error(msg_template, msg_args)
Jason Chang32b59562023-07-14 16:45:35 -07001232 return SyncNetworkHalfResult(
1233 False, SyncNetworkHalfError(msg, project=self.name)
1234 )
Gavin Makea2e3302023-03-11 06:46:20 +00001235
1236 name = self.relpath.replace("\\", "/")
1237 name = name.replace("/", "_")
1238 tarpath = "%s.tar" % name
1239 topdir = self.manifest.topdir
1240
1241 try:
1242 self._FetchArchive(tarpath, cwd=topdir)
1243 except GitError as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001244 logger.error("error: %s", e)
Jason Chang32b59562023-07-14 16:45:35 -07001245 return SyncNetworkHalfResult(False, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001246
1247 # From now on, we only need absolute tarpath.
1248 tarpath = os.path.join(topdir, tarpath)
1249
1250 if not self._ExtractArchive(tarpath, path=topdir):
Jason Chang32b59562023-07-14 16:45:35 -07001251 return SyncNetworkHalfResult(
1252 True,
1253 SyncNetworkHalfError(
1254 f"Unable to Extract Archive {tarpath}",
1255 project=self.name,
1256 ),
1257 )
Gavin Makea2e3302023-03-11 06:46:20 +00001258 try:
1259 platform_utils.remove(tarpath)
1260 except OSError as e:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00001261 logger.warning("warn: Cannot remove archive %s: %s", tarpath, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001262 self._CopyAndLinkFiles()
Jason Chang32b59562023-07-14 16:45:35 -07001263 return SyncNetworkHalfResult(True)
Gavin Makea2e3302023-03-11 06:46:20 +00001264
1265 # If the shared object dir already exists, don't try to rebootstrap with
1266 # a clone bundle download. We should have the majority of objects
1267 # already.
1268 if clone_bundle and os.path.exists(self.objdir):
1269 clone_bundle = False
1270
1271 if self.name in partial_clone_exclude:
1272 clone_bundle = True
1273 clone_filter = None
1274
1275 if is_new is None:
1276 is_new = not self.Exists
1277 if is_new:
1278 self._InitGitDir(force_sync=force_sync, quiet=quiet)
1279 else:
Josip Sokcevic9b57aa02023-12-01 23:01:52 +00001280 try:
1281 # At this point, it's possible that gitdir points to an old
1282 # objdir (e.g. name changed, but objdir exists). Check
1283 # references to ensure that's not the case. See
1284 # https://issues.gerritcodereview.com/40013418 for more
1285 # details.
1286 self._CheckDirReference(self.objdir, self.gitdir)
1287
1288 self._UpdateHooks(quiet=quiet)
1289 except GitError as e:
1290 if not force_sync:
1291 raise e
1292 # Let _InitGitDir fix the issue, force_sync is always True here.
1293 self._InitGitDir(force_sync=True, quiet=quiet)
Gavin Makea2e3302023-03-11 06:46:20 +00001294 self._InitRemote()
1295
1296 if self.UseAlternates:
1297 # If gitdir/objects is a symlink, migrate it from the old layout.
1298 gitdir_objects = os.path.join(self.gitdir, "objects")
1299 if platform_utils.islink(gitdir_objects):
1300 platform_utils.remove(gitdir_objects, missing_ok=True)
1301 gitdir_alt = os.path.join(self.gitdir, "objects/info/alternates")
1302 if not os.path.exists(gitdir_alt):
1303 os.makedirs(os.path.dirname(gitdir_alt), exist_ok=True)
1304 _lwrite(
1305 gitdir_alt,
1306 os.path.join(
1307 os.path.relpath(self.objdir, gitdir_objects), "objects"
1308 )
1309 + "\n",
1310 )
1311
1312 if is_new:
1313 alt = os.path.join(self.objdir, "objects/info/alternates")
1314 try:
1315 with open(alt) as fd:
1316 # This works for both absolute and relative alternate
1317 # directories.
1318 alt_dir = os.path.join(
1319 self.objdir, "objects", fd.readline().rstrip()
1320 )
Jason R. Coombsae824fb2023-10-20 23:32:40 +05451321 except OSError:
Gavin Makea2e3302023-03-11 06:46:20 +00001322 alt_dir = None
1323 else:
1324 alt_dir = None
1325
1326 if (
1327 clone_bundle
1328 and alt_dir is None
1329 and self._ApplyCloneBundle(
1330 initial=is_new, quiet=quiet, verbose=verbose
1331 )
1332 ):
1333 is_new = False
1334
1335 if current_branch_only is None:
1336 if self.sync_c:
1337 current_branch_only = True
1338 elif not self.manifest._loaded:
1339 # Manifest cannot check defaults until it syncs.
1340 current_branch_only = False
1341 elif self.manifest.default.sync_c:
1342 current_branch_only = True
1343
1344 if tags is None:
1345 tags = self.sync_tags
1346
1347 if self.clone_depth:
1348 depth = self.clone_depth
1349 else:
1350 depth = self.manifest.manifestProject.depth
1351
Jason Chang17833322023-05-23 13:06:55 -07001352 if depth and clone_filter_for_depth:
1353 depth = None
1354 clone_filter = clone_filter_for_depth
1355
Gavin Makea2e3302023-03-11 06:46:20 +00001356 # See if we can skip the network fetch entirely.
1357 remote_fetched = False
1358 if not (
1359 optimized_fetch
Sylvain56a5a012023-09-11 13:38:00 +02001360 and IsId(self.revisionExpr)
1361 and self._CheckForImmutableRevision()
Gavin Makea2e3302023-03-11 06:46:20 +00001362 ):
1363 remote_fetched = True
Jason Chang32b59562023-07-14 16:45:35 -07001364 try:
1365 if not self._RemoteFetch(
1366 initial=is_new,
1367 quiet=quiet,
1368 verbose=verbose,
1369 output_redir=output_redir,
1370 alt_dir=alt_dir,
1371 current_branch_only=current_branch_only,
1372 tags=tags,
1373 prune=prune,
1374 depth=depth,
1375 submodules=submodules,
1376 force_sync=force_sync,
1377 ssh_proxy=ssh_proxy,
1378 clone_filter=clone_filter,
1379 retry_fetches=retry_fetches,
1380 ):
1381 return SyncNetworkHalfResult(
1382 remote_fetched,
1383 SyncNetworkHalfError(
1384 f"Unable to remote fetch project {self.name}",
1385 project=self.name,
1386 ),
1387 )
1388 except RepoError as e:
1389 return SyncNetworkHalfResult(
1390 remote_fetched,
1391 e,
1392 )
Gavin Makea2e3302023-03-11 06:46:20 +00001393
1394 mp = self.manifest.manifestProject
1395 dissociate = mp.dissociate
1396 if dissociate:
1397 alternates_file = os.path.join(
1398 self.objdir, "objects/info/alternates"
1399 )
1400 if os.path.exists(alternates_file):
1401 cmd = ["repack", "-a", "-d"]
1402 p = GitCommand(
1403 self,
1404 cmd,
1405 bare=True,
1406 capture_stdout=bool(output_redir),
1407 merge_output=bool(output_redir),
1408 )
1409 if p.stdout and output_redir:
1410 output_redir.write(p.stdout)
1411 if p.Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07001412 return SyncNetworkHalfResult(
1413 remote_fetched,
1414 GitError(
1415 "Unable to repack alternates", project=self.name
1416 ),
1417 )
Gavin Makea2e3302023-03-11 06:46:20 +00001418 platform_utils.remove(alternates_file)
1419
1420 if self.worktree:
1421 self._InitMRef()
1422 else:
1423 self._InitMirrorHead()
1424 platform_utils.remove(
1425 os.path.join(self.gitdir, "FETCH_HEAD"), missing_ok=True
1426 )
Jason Chang32b59562023-07-14 16:45:35 -07001427 return SyncNetworkHalfResult(remote_fetched)
Gavin Makea2e3302023-03-11 06:46:20 +00001428
1429 def PostRepoUpgrade(self):
1430 self._InitHooks()
1431
1432 def _CopyAndLinkFiles(self):
1433 if self.client.isGitcClient:
1434 return
1435 for copyfile in self.copyfiles:
1436 copyfile._Copy()
1437 for linkfile in self.linkfiles:
1438 linkfile._Link()
1439
1440 def GetCommitRevisionId(self):
1441 """Get revisionId of a commit.
1442
1443 Use this method instead of GetRevisionId to get the id of the commit
1444 rather than the id of the current git object (for example, a tag)
1445
1446 """
Sylvaine9cb3912023-09-10 23:35:01 +02001447 if self.revisionId:
1448 return self.revisionId
Gavin Makea2e3302023-03-11 06:46:20 +00001449 if not self.revisionExpr.startswith(R_TAGS):
1450 return self.GetRevisionId(self._allrefs)
1451
1452 try:
1453 return self.bare_git.rev_list(self.revisionExpr, "-1")[0]
1454 except GitError:
1455 raise ManifestInvalidRevisionError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001456 f"revision {self.revisionExpr} in {self.name} not found"
Gavin Makea2e3302023-03-11 06:46:20 +00001457 )
1458
1459 def GetRevisionId(self, all_refs=None):
1460 if self.revisionId:
1461 return self.revisionId
1462
1463 rem = self.GetRemote()
1464 rev = rem.ToLocal(self.revisionExpr)
1465
1466 if all_refs is not None and rev in all_refs:
1467 return all_refs[rev]
1468
1469 try:
1470 return self.bare_git.rev_parse("--verify", "%s^0" % rev)
1471 except GitError:
1472 raise ManifestInvalidRevisionError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001473 f"revision {self.revisionExpr} in {self.name} not found"
Gavin Makea2e3302023-03-11 06:46:20 +00001474 )
1475
1476 def SetRevisionId(self, revisionId):
1477 if self.revisionExpr:
1478 self.upstream = self.revisionExpr
1479
1480 self.revisionId = revisionId
1481
Jason Chang32b59562023-07-14 16:45:35 -07001482 def Sync_LocalHalf(
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001483 self,
1484 syncbuf,
1485 force_sync=False,
1486 submodules=False,
1487 errors=None,
1488 verbose=False,
Jason Chang32b59562023-07-14 16:45:35 -07001489 ):
Gavin Makea2e3302023-03-11 06:46:20 +00001490 """Perform only the local IO portion of the sync process.
1491
1492 Network access is not required.
1493 """
Jason Chang32b59562023-07-14 16:45:35 -07001494 if errors is None:
1495 errors = []
1496
1497 def fail(error: Exception):
1498 errors.append(error)
1499 syncbuf.fail(self, error)
1500
Gavin Makea2e3302023-03-11 06:46:20 +00001501 if not os.path.exists(self.gitdir):
Jason Chang32b59562023-07-14 16:45:35 -07001502 fail(
1503 LocalSyncFail(
1504 "Cannot checkout %s due to missing network sync; Run "
1505 "`repo sync -n %s` first." % (self.name, self.name),
1506 project=self.name,
1507 )
Gavin Makea2e3302023-03-11 06:46:20 +00001508 )
1509 return
1510
1511 self._InitWorkTree(force_sync=force_sync, submodules=submodules)
1512 all_refs = self.bare_ref.all
1513 self.CleanPublishedCache(all_refs)
1514 revid = self.GetRevisionId(all_refs)
1515
1516 # Special case the root of the repo client checkout. Make sure it
1517 # doesn't contain files being checked out to dirs we don't allow.
1518 if self.relpath == ".":
1519 PROTECTED_PATHS = {".repo"}
1520 paths = set(
1521 self.work_git.ls_tree("-z", "--name-only", "--", revid).split(
1522 "\0"
1523 )
1524 )
1525 bad_paths = paths & PROTECTED_PATHS
1526 if bad_paths:
Jason Chang32b59562023-07-14 16:45:35 -07001527 fail(
1528 LocalSyncFail(
1529 "Refusing to checkout project that writes to protected "
1530 "paths: %s" % (", ".join(bad_paths),),
1531 project=self.name,
1532 )
Gavin Makea2e3302023-03-11 06:46:20 +00001533 )
1534 return
1535
1536 def _doff():
1537 self._FastForward(revid)
1538 self._CopyAndLinkFiles()
1539
1540 def _dosubmodules():
1541 self._SyncSubmodules(quiet=True)
1542
1543 head = self.work_git.GetHead()
1544 if head.startswith(R_HEADS):
1545 branch = head[len(R_HEADS) :]
1546 try:
1547 head = all_refs[head]
1548 except KeyError:
1549 head = None
1550 else:
1551 branch = None
1552
1553 if branch is None or syncbuf.detach_head:
1554 # Currently on a detached HEAD. The user is assumed to
1555 # not have any local modifications worth worrying about.
1556 if self.IsRebaseInProgress():
Jason Chang32b59562023-07-14 16:45:35 -07001557 fail(_PriorSyncFailedError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001558 return
1559
1560 if head == revid:
1561 # No changes; don't do anything further.
1562 # Except if the head needs to be detached.
1563 if not syncbuf.detach_head:
1564 # The copy/linkfile config may have changed.
1565 self._CopyAndLinkFiles()
1566 return
1567 else:
1568 lost = self._revlist(not_rev(revid), HEAD)
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001569 if lost and verbose:
Gavin Makea2e3302023-03-11 06:46:20 +00001570 syncbuf.info(self, "discarding %d commits", len(lost))
1571
1572 try:
1573 self._Checkout(revid, quiet=True)
1574 if submodules:
1575 self._SyncSubmodules(quiet=True)
1576 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001577 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001578 return
1579 self._CopyAndLinkFiles()
1580 return
1581
1582 if head == revid:
1583 # No changes; don't do anything further.
1584 #
1585 # The copy/linkfile config may have changed.
1586 self._CopyAndLinkFiles()
1587 return
1588
1589 branch = self.GetBranch(branch)
1590
1591 if not branch.LocalMerge:
1592 # The current branch has no tracking configuration.
1593 # Jump off it to a detached HEAD.
1594 syncbuf.info(
1595 self, "leaving %s; does not track upstream", branch.name
1596 )
1597 try:
1598 self._Checkout(revid, quiet=True)
1599 if submodules:
1600 self._SyncSubmodules(quiet=True)
1601 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001602 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001603 return
1604 self._CopyAndLinkFiles()
1605 return
1606
1607 upstream_gain = self._revlist(not_rev(HEAD), revid)
1608
1609 # See if we can perform a fast forward merge. This can happen if our
1610 # branch isn't in the exact same state as we last published.
1611 try:
Jason Chang87058c62023-09-27 11:34:43 -07001612 self.work_git.merge_base(
1613 "--is-ancestor", HEAD, revid, log_as_error=False
1614 )
Gavin Makea2e3302023-03-11 06:46:20 +00001615 # Skip the published logic.
1616 pub = False
1617 except GitError:
1618 pub = self.WasPublished(branch.name, all_refs)
1619
1620 if pub:
1621 not_merged = self._revlist(not_rev(revid), pub)
1622 if not_merged:
1623 if upstream_gain:
1624 # The user has published this branch and some of those
1625 # commits are not yet merged upstream. We do not want
1626 # to rewrite the published commits so we punt.
Jason Chang32b59562023-07-14 16:45:35 -07001627 fail(
1628 LocalSyncFail(
1629 "branch %s is published (but not merged) and is "
1630 "now %d commits behind"
1631 % (branch.name, len(upstream_gain)),
1632 project=self.name,
1633 )
Gavin Makea2e3302023-03-11 06:46:20 +00001634 )
1635 return
1636 elif pub == head:
1637 # All published commits are merged, and thus we are a
1638 # strict subset. We can fast-forward safely.
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001639 syncbuf.later1(self, _doff, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001640 if submodules:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001641 syncbuf.later1(self, _dosubmodules, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001642 return
1643
1644 # Examine the local commits not in the remote. Find the
1645 # last one attributed to this user, if any.
1646 local_changes = self._revlist(not_rev(revid), HEAD, format="%H %ce")
1647 last_mine = None
1648 cnt_mine = 0
1649 for commit in local_changes:
1650 commit_id, committer_email = commit.split(" ", 1)
1651 if committer_email == self.UserEmail:
1652 last_mine = commit_id
1653 cnt_mine += 1
1654
1655 if not upstream_gain and cnt_mine == len(local_changes):
1656 # The copy/linkfile config may have changed.
1657 self._CopyAndLinkFiles()
1658 return
1659
1660 if self.IsDirty(consider_untracked=False):
Jason Chang32b59562023-07-14 16:45:35 -07001661 fail(_DirtyError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001662 return
1663
1664 # If the upstream switched on us, warn the user.
1665 if branch.merge != self.revisionExpr:
1666 if branch.merge and self.revisionExpr:
1667 syncbuf.info(
1668 self,
1669 "manifest switched %s...%s",
1670 branch.merge,
1671 self.revisionExpr,
1672 )
1673 elif branch.merge:
1674 syncbuf.info(self, "manifest no longer tracks %s", branch.merge)
1675
1676 if cnt_mine < len(local_changes):
1677 # Upstream rebased. Not everything in HEAD was created by this user.
1678 syncbuf.info(
1679 self,
1680 "discarding %d commits removed from upstream",
1681 len(local_changes) - cnt_mine,
1682 )
1683
1684 branch.remote = self.GetRemote()
Sylvain56a5a012023-09-11 13:38:00 +02001685 if not IsId(self.revisionExpr):
Gavin Makea2e3302023-03-11 06:46:20 +00001686 # In case of manifest sync the revisionExpr might be a SHA1.
1687 branch.merge = self.revisionExpr
1688 if not branch.merge.startswith("refs/"):
1689 branch.merge = R_HEADS + branch.merge
1690 branch.Save()
1691
1692 if cnt_mine > 0 and self.rebase:
1693
1694 def _docopyandlink():
1695 self._CopyAndLinkFiles()
1696
1697 def _dorebase():
1698 self._Rebase(upstream="%s^1" % last_mine, onto=revid)
1699
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001700 syncbuf.later2(self, _dorebase, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001701 if submodules:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001702 syncbuf.later2(self, _dosubmodules, not verbose)
1703 syncbuf.later2(self, _docopyandlink, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001704 elif local_changes:
1705 try:
1706 self._ResetHard(revid)
1707 if submodules:
1708 self._SyncSubmodules(quiet=True)
1709 self._CopyAndLinkFiles()
1710 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001711 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001712 return
1713 else:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001714 syncbuf.later1(self, _doff, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001715 if submodules:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001716 syncbuf.later1(self, _dosubmodules, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001717
1718 def AddCopyFile(self, src, dest, topdir):
1719 """Mark |src| for copying to |dest| (relative to |topdir|).
1720
1721 No filesystem changes occur here. Actual copying happens later on.
1722
1723 Paths should have basic validation run on them before being queued.
1724 Further checking will be handled when the actual copy happens.
1725 """
1726 self.copyfiles.append(_CopyFile(self.worktree, src, topdir, dest))
1727
1728 def AddLinkFile(self, src, dest, topdir):
1729 """Mark |dest| to create a symlink (relative to |topdir|) pointing to
1730 |src|.
1731
1732 No filesystem changes occur here. Actual linking happens later on.
1733
1734 Paths should have basic validation run on them before being queued.
1735 Further checking will be handled when the actual link happens.
1736 """
1737 self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest))
1738
1739 def AddAnnotation(self, name, value, keep):
1740 self.annotations.append(Annotation(name, value, keep))
1741
1742 def DownloadPatchSet(self, change_id, patch_id):
1743 """Download a single patch set of a single change to FETCH_HEAD."""
1744 remote = self.GetRemote()
1745
1746 cmd = ["fetch", remote.name]
1747 cmd.append(
1748 "refs/changes/%2.2d/%d/%d" % (change_id % 100, change_id, patch_id)
1749 )
Jason Chang1a3612f2023-08-08 14:12:53 -07001750 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001751 return DownloadedChange(
1752 self,
1753 self.GetRevisionId(),
1754 change_id,
1755 patch_id,
1756 self.bare_git.rev_parse("FETCH_HEAD"),
1757 )
1758
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001759 def DeleteWorktree(self, verbose=False, force=False):
Gavin Makea2e3302023-03-11 06:46:20 +00001760 """Delete the source checkout and any other housekeeping tasks.
1761
1762 This currently leaves behind the internal .repo/ cache state. This
1763 helps when switching branches or manifest changes get reverted as we
1764 don't have to redownload all the git objects. But we should do some GC
1765 at some point.
1766
1767 Args:
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001768 verbose: Whether to show verbose messages.
Gavin Makea2e3302023-03-11 06:46:20 +00001769 force: Always delete tree even if dirty.
Doug Anderson3ba5f952011-04-07 12:51:04 -07001770
1771 Returns:
Gavin Makea2e3302023-03-11 06:46:20 +00001772 True if the worktree was completely cleaned out.
1773 """
1774 if self.IsDirty():
1775 if force:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00001776 logger.warning(
Gavin Makea2e3302023-03-11 06:46:20 +00001777 "warning: %s: Removing dirty project: uncommitted changes "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001778 "lost.",
1779 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001780 )
1781 else:
Jason Chang32b59562023-07-14 16:45:35 -07001782 msg = (
1783 "error: %s: Cannot remove project: uncommitted"
1784 "changes are present.\n" % self.RelPath(local=False)
Gavin Makea2e3302023-03-11 06:46:20 +00001785 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001786 logger.error(msg)
Jason Chang32b59562023-07-14 16:45:35 -07001787 raise DeleteDirtyWorktreeError(msg, project=self)
Wink Saville02d79452009-04-10 13:01:24 -07001788
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001789 if verbose:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001790 print(f"{self.RelPath(local=False)}: Deleting obsolete checkout.")
Wink Saville02d79452009-04-10 13:01:24 -07001791
Gavin Makea2e3302023-03-11 06:46:20 +00001792 # Unlock and delink from the main worktree. We don't use git's worktree
1793 # remove because it will recursively delete projects -- we handle that
1794 # ourselves below. https://crbug.com/git/48
1795 if self.use_git_worktrees:
1796 needle = platform_utils.realpath(self.gitdir)
1797 # Find the git worktree commondir under .repo/worktrees/.
1798 output = self.bare_git.worktree("list", "--porcelain").splitlines()[
1799 0
1800 ]
1801 assert output.startswith("worktree "), output
1802 commondir = output[9:]
1803 # Walk each of the git worktrees to see where they point.
1804 configs = os.path.join(commondir, "worktrees")
1805 for name in os.listdir(configs):
1806 gitdir = os.path.join(configs, name, "gitdir")
1807 with open(gitdir) as fp:
1808 relpath = fp.read().strip()
1809 # Resolve the checkout path and see if it matches this project.
1810 fullpath = platform_utils.realpath(
1811 os.path.join(configs, name, relpath)
1812 )
1813 if fullpath == needle:
1814 platform_utils.rmtree(os.path.join(configs, name))
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001815
Gavin Makea2e3302023-03-11 06:46:20 +00001816 # Delete the .git directory first, so we're less likely to have a
1817 # partially working git repository around. There shouldn't be any git
1818 # projects here, so rmtree works.
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001819
Gavin Makea2e3302023-03-11 06:46:20 +00001820 # Try to remove plain files first in case of git worktrees. If this
1821 # fails for any reason, we'll fall back to rmtree, and that'll display
1822 # errors if it can't remove things either.
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001823 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001824 platform_utils.remove(self.gitdir)
1825 except OSError:
1826 pass
1827 try:
1828 platform_utils.rmtree(self.gitdir)
1829 except OSError as e:
1830 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001831 logger.error("error: %s: %s", self.gitdir, e)
1832 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00001833 "error: %s: Failed to delete obsolete checkout; remove "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001834 "manually, then run `repo sync -l`.",
1835 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001836 )
Jason Chang32b59562023-07-14 16:45:35 -07001837 raise DeleteWorktreeError(aggregate_errors=[e])
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001838
Gavin Makea2e3302023-03-11 06:46:20 +00001839 # Delete everything under the worktree, except for directories that
1840 # contain another git project.
1841 dirs_to_remove = []
1842 failed = False
Jason Chang32b59562023-07-14 16:45:35 -07001843 errors = []
Gavin Makea2e3302023-03-11 06:46:20 +00001844 for root, dirs, files in platform_utils.walk(self.worktree):
1845 for f in files:
1846 path = os.path.join(root, f)
1847 try:
1848 platform_utils.remove(path)
1849 except OSError as e:
1850 if e.errno != errno.ENOENT:
Josip Sokcevic4217a822024-01-24 13:54:25 -08001851 logger.warning("%s: Failed to remove: %s", path, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001852 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001853 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001854 dirs[:] = [
1855 d
1856 for d in dirs
1857 if not os.path.lexists(os.path.join(root, d, ".git"))
1858 ]
1859 dirs_to_remove += [
1860 os.path.join(root, d)
1861 for d in dirs
1862 if os.path.join(root, d) not in dirs_to_remove
1863 ]
1864 for d in reversed(dirs_to_remove):
1865 if platform_utils.islink(d):
1866 try:
1867 platform_utils.remove(d)
1868 except OSError as e:
1869 if e.errno != errno.ENOENT:
Josip Sokcevic4217a822024-01-24 13:54:25 -08001870 logger.warning("%s: Failed to remove: %s", d, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001871 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001872 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001873 elif not platform_utils.listdir(d):
1874 try:
1875 platform_utils.rmdir(d)
1876 except OSError as e:
1877 if e.errno != errno.ENOENT:
Josip Sokcevic4217a822024-01-24 13:54:25 -08001878 logger.warning("%s: Failed to remove: %s", d, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001879 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001880 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001881 if failed:
Josip Sokcevic4217a822024-01-24 13:54:25 -08001882 rename_path = (
1883 f"{self.worktree}_repo_to_be_deleted_{int(time.time())}"
Gavin Makea2e3302023-03-11 06:46:20 +00001884 )
Josip Sokcevic4217a822024-01-24 13:54:25 -08001885 try:
1886 platform_utils.rename(self.worktree, rename_path)
1887 logger.warning(
1888 "warning: renamed %s to %s. You can delete it, but you "
1889 "might need elevated permissions (e.g. root)",
1890 self.worktree,
1891 rename_path,
1892 )
1893 # Rename successful! Clear the errors.
1894 errors = []
1895 except OSError:
1896 logger.error(
1897 "%s: Failed to delete obsolete checkout.\n",
1898 " Remove manually, then run `repo sync -l`.",
1899 self.RelPath(local=False),
1900 )
1901 raise DeleteWorktreeError(aggregate_errors=errors)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07001902
Gavin Makea2e3302023-03-11 06:46:20 +00001903 # Try deleting parent dirs if they are empty.
1904 path = self.worktree
1905 while path != self.manifest.topdir:
1906 try:
1907 platform_utils.rmdir(path)
1908 except OSError as e:
1909 if e.errno != errno.ENOENT:
1910 break
1911 path = os.path.dirname(path)
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001912
Gavin Makea2e3302023-03-11 06:46:20 +00001913 return True
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001914
Gavin Makea2e3302023-03-11 06:46:20 +00001915 def StartBranch(self, name, branch_merge="", revision=None):
1916 """Create a new branch off the manifest's revision."""
1917 if not branch_merge:
1918 branch_merge = self.revisionExpr
1919 head = self.work_git.GetHead()
1920 if head == (R_HEADS + name):
1921 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001922
David Pursehouse8a68ff92012-09-24 12:15:13 +09001923 all_refs = self.bare_ref.all
Gavin Makea2e3302023-03-11 06:46:20 +00001924 if R_HEADS + name in all_refs:
Jason Chang1a3612f2023-08-08 14:12:53 -07001925 GitCommand(
1926 self, ["checkout", "-q", name, "--"], verify_command=True
1927 ).Wait()
1928 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001929
Gavin Makea2e3302023-03-11 06:46:20 +00001930 branch = self.GetBranch(name)
1931 branch.remote = self.GetRemote()
1932 branch.merge = branch_merge
Sylvain56a5a012023-09-11 13:38:00 +02001933 if not branch.merge.startswith("refs/") and not IsId(branch_merge):
Gavin Makea2e3302023-03-11 06:46:20 +00001934 branch.merge = R_HEADS + branch_merge
Shawn O. Pearce88443382010-10-08 10:02:09 +02001935
Gavin Makea2e3302023-03-11 06:46:20 +00001936 if revision is None:
1937 revid = self.GetRevisionId(all_refs)
Shawn O. Pearce88443382010-10-08 10:02:09 +02001938 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001939 revid = self.work_git.rev_parse(revision)
Brian Harring14a66742012-09-28 20:21:57 -07001940
Gavin Makea2e3302023-03-11 06:46:20 +00001941 if head.startswith(R_HEADS):
Kevin Degiabaa7f32014-11-12 11:27:45 -07001942 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001943 head = all_refs[head]
1944 except KeyError:
1945 head = None
1946 if revid and head and revid == head:
1947 ref = R_HEADS + name
1948 self.work_git.update_ref(ref, revid)
1949 self.work_git.symbolic_ref(HEAD, ref)
1950 branch.Save()
1951 return True
Kevin Degib1a07b82015-07-27 13:33:43 -06001952
Jason Chang1a3612f2023-08-08 14:12:53 -07001953 GitCommand(
1954 self,
1955 ["checkout", "-q", "-b", branch.name, revid],
1956 verify_command=True,
1957 ).Wait()
1958 branch.Save()
1959 return True
Kevin Degi384b3c52014-10-16 16:02:58 -06001960
Gavin Makea2e3302023-03-11 06:46:20 +00001961 def CheckoutBranch(self, name):
1962 """Checkout a local topic branch.
Shawn O. Pearce2816d4f2009-03-03 17:53:18 -08001963
Gavin Makea2e3302023-03-11 06:46:20 +00001964 Args:
1965 name: The name of the branch to checkout.
Shawn O. Pearce88443382010-10-08 10:02:09 +02001966
Gavin Makea2e3302023-03-11 06:46:20 +00001967 Returns:
Jason Chang1a3612f2023-08-08 14:12:53 -07001968 True if the checkout succeeded; False if the
1969 branch doesn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00001970 """
1971 rev = R_HEADS + name
1972 head = self.work_git.GetHead()
1973 if head == rev:
1974 # Already on the branch.
1975 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001976
Gavin Makea2e3302023-03-11 06:46:20 +00001977 all_refs = self.bare_ref.all
1978 try:
1979 revid = all_refs[rev]
1980 except KeyError:
1981 # Branch does not exist in this project.
Jason Chang1a3612f2023-08-08 14:12:53 -07001982 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001983
Gavin Makea2e3302023-03-11 06:46:20 +00001984 if head.startswith(R_HEADS):
1985 try:
1986 head = all_refs[head]
1987 except KeyError:
1988 head = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001989
Gavin Makea2e3302023-03-11 06:46:20 +00001990 if head == revid:
1991 # Same revision; just update HEAD to point to the new
1992 # target branch, but otherwise take no other action.
1993 _lwrite(
1994 self.work_git.GetDotgitPath(subpath=HEAD),
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001995 f"ref: {R_HEADS}{name}\n",
Gavin Makea2e3302023-03-11 06:46:20 +00001996 )
1997 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05001998
Jason Chang1a3612f2023-08-08 14:12:53 -07001999 GitCommand(
2000 self,
2001 ["checkout", name, "--"],
2002 capture_stdout=True,
2003 capture_stderr=True,
2004 verify_command=True,
2005 ).Wait()
2006 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05002007
Gavin Makea2e3302023-03-11 06:46:20 +00002008 def AbandonBranch(self, name):
2009 """Destroy a local topic branch.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002010
Gavin Makea2e3302023-03-11 06:46:20 +00002011 Args:
2012 name: The name of the branch to abandon.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002013
Gavin Makea2e3302023-03-11 06:46:20 +00002014 Returns:
Jason Changf9aacd42023-08-03 14:38:00 -07002015 True if the abandon succeeded; Raises GitCommandError if it didn't;
2016 None if the branch didn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00002017 """
2018 rev = R_HEADS + name
2019 all_refs = self.bare_ref.all
2020 if rev not in all_refs:
2021 # Doesn't exist
2022 return None
2023
2024 head = self.work_git.GetHead()
2025 if head == rev:
2026 # We can't destroy the branch while we are sitting
2027 # on it. Switch to a detached HEAD.
2028 head = all_refs[head]
2029
2030 revid = self.GetRevisionId(all_refs)
2031 if head == revid:
2032 _lwrite(
2033 self.work_git.GetDotgitPath(subpath=HEAD), "%s\n" % revid
2034 )
2035 else:
2036 self._Checkout(revid, quiet=True)
Jason Changf9aacd42023-08-03 14:38:00 -07002037 GitCommand(
2038 self,
2039 ["branch", "-D", name],
2040 capture_stdout=True,
2041 capture_stderr=True,
2042 verify_command=True,
2043 ).Wait()
2044 return True
Gavin Makea2e3302023-03-11 06:46:20 +00002045
2046 def PruneHeads(self):
2047 """Prune any topic branches already merged into upstream."""
2048 cb = self.CurrentBranch
2049 kill = []
2050 left = self._allrefs
2051 for name in left.keys():
2052 if name.startswith(R_HEADS):
2053 name = name[len(R_HEADS) :]
2054 if cb is None or name != cb:
2055 kill.append(name)
2056
2057 # Minor optimization: If there's nothing to prune, then don't try to
2058 # read any project state.
2059 if not kill and not cb:
2060 return []
2061
2062 rev = self.GetRevisionId(left)
2063 if (
2064 cb is not None
2065 and not self._revlist(HEAD + "..." + rev)
2066 and not self.IsDirty(consider_untracked=False)
2067 ):
2068 self.work_git.DetachHead(HEAD)
2069 kill.append(cb)
2070
2071 if kill:
2072 old = self.bare_git.GetHead()
2073
2074 try:
2075 self.bare_git.DetachHead(rev)
2076
2077 b = ["branch", "-d"]
2078 b.extend(kill)
2079 b = GitCommand(
2080 self, b, bare=True, capture_stdout=True, capture_stderr=True
2081 )
2082 b.Wait()
2083 finally:
Sylvain56a5a012023-09-11 13:38:00 +02002084 if IsId(old):
Gavin Makea2e3302023-03-11 06:46:20 +00002085 self.bare_git.DetachHead(old)
2086 else:
2087 self.bare_git.SetHead(old)
2088 left = self._allrefs
2089
2090 for branch in kill:
2091 if (R_HEADS + branch) not in left:
2092 self.CleanPublishedCache()
2093 break
2094
2095 if cb and cb not in kill:
2096 kill.append(cb)
2097 kill.sort()
2098
2099 kept = []
2100 for branch in kill:
2101 if R_HEADS + branch in left:
2102 branch = self.GetBranch(branch)
2103 base = branch.LocalMerge
2104 if not base:
2105 base = rev
2106 kept.append(ReviewableBranch(self, branch, base))
2107 return kept
2108
2109 def GetRegisteredSubprojects(self):
2110 result = []
2111
2112 def rec(subprojects):
2113 if not subprojects:
2114 return
2115 result.extend(subprojects)
2116 for p in subprojects:
2117 rec(p.subprojects)
2118
2119 rec(self.subprojects)
2120 return result
2121
2122 def _GetSubmodules(self):
2123 # Unfortunately we cannot call `git submodule status --recursive` here
2124 # because the working tree might not exist yet, and it cannot be used
2125 # without a working tree in its current implementation.
2126
2127 def get_submodules(gitdir, rev):
2128 # Parse .gitmodules for submodule sub_paths and sub_urls.
2129 sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
2130 if not sub_paths:
2131 return []
2132 # Run `git ls-tree` to read SHAs of submodule object, which happen
2133 # to be revision of submodule repository.
2134 sub_revs = git_ls_tree(gitdir, rev, sub_paths)
2135 submodules = []
2136 for sub_path, sub_url in zip(sub_paths, sub_urls):
2137 try:
2138 sub_rev = sub_revs[sub_path]
2139 except KeyError:
2140 # Ignore non-exist submodules.
2141 continue
2142 submodules.append((sub_rev, sub_path, sub_url))
2143 return submodules
2144
2145 re_path = re.compile(r"^submodule\.(.+)\.path=(.*)$")
2146 re_url = re.compile(r"^submodule\.(.+)\.url=(.*)$")
2147
2148 def parse_gitmodules(gitdir, rev):
2149 cmd = ["cat-file", "blob", "%s:.gitmodules" % rev]
2150 try:
2151 p = GitCommand(
2152 None,
2153 cmd,
2154 capture_stdout=True,
2155 capture_stderr=True,
2156 bare=True,
2157 gitdir=gitdir,
2158 )
2159 except GitError:
2160 return [], []
2161 if p.Wait() != 0:
2162 return [], []
2163
2164 gitmodules_lines = []
2165 fd, temp_gitmodules_path = tempfile.mkstemp()
2166 try:
2167 os.write(fd, p.stdout.encode("utf-8"))
2168 os.close(fd)
2169 cmd = ["config", "--file", temp_gitmodules_path, "--list"]
2170 p = GitCommand(
2171 None,
2172 cmd,
2173 capture_stdout=True,
2174 capture_stderr=True,
2175 bare=True,
2176 gitdir=gitdir,
2177 )
2178 if p.Wait() != 0:
2179 return [], []
2180 gitmodules_lines = p.stdout.split("\n")
2181 except GitError:
2182 return [], []
2183 finally:
2184 platform_utils.remove(temp_gitmodules_path)
2185
2186 names = set()
2187 paths = {}
2188 urls = {}
2189 for line in gitmodules_lines:
2190 if not line:
2191 continue
2192 m = re_path.match(line)
2193 if m:
2194 names.add(m.group(1))
2195 paths[m.group(1)] = m.group(2)
2196 continue
2197 m = re_url.match(line)
2198 if m:
2199 names.add(m.group(1))
2200 urls[m.group(1)] = m.group(2)
2201 continue
2202 names = sorted(names)
2203 return (
2204 [paths.get(name, "") for name in names],
2205 [urls.get(name, "") for name in names],
2206 )
2207
2208 def git_ls_tree(gitdir, rev, paths):
2209 cmd = ["ls-tree", rev, "--"]
2210 cmd.extend(paths)
2211 try:
2212 p = GitCommand(
2213 None,
2214 cmd,
2215 capture_stdout=True,
2216 capture_stderr=True,
2217 bare=True,
2218 gitdir=gitdir,
2219 )
2220 except GitError:
2221 return []
2222 if p.Wait() != 0:
2223 return []
2224 objects = {}
2225 for line in p.stdout.split("\n"):
2226 if not line.strip():
2227 continue
2228 object_rev, object_path = line.split()[2:4]
2229 objects[object_path] = object_rev
2230 return objects
2231
2232 try:
2233 rev = self.GetRevisionId()
2234 except GitError:
2235 return []
2236 return get_submodules(self.gitdir, rev)
2237
2238 def GetDerivedSubprojects(self):
2239 result = []
2240 if not self.Exists:
2241 # If git repo does not exist yet, querying its submodules will
2242 # mess up its states; so return here.
2243 return result
2244 for rev, path, url in self._GetSubmodules():
2245 name = self.manifest.GetSubprojectName(self, path)
2246 (
2247 relpath,
2248 worktree,
2249 gitdir,
2250 objdir,
2251 ) = self.manifest.GetSubprojectPaths(self, name, path)
2252 project = self.manifest.paths.get(relpath)
2253 if project:
2254 result.extend(project.GetDerivedSubprojects())
2255 continue
2256
2257 if url.startswith(".."):
2258 url = urllib.parse.urljoin("%s/" % self.remote.url, url)
2259 remote = RemoteSpec(
2260 self.remote.name,
2261 url=url,
2262 pushUrl=self.remote.pushUrl,
2263 review=self.remote.review,
2264 revision=self.remote.revision,
2265 )
2266 subproject = Project(
2267 manifest=self.manifest,
2268 name=name,
2269 remote=remote,
2270 gitdir=gitdir,
2271 objdir=objdir,
2272 worktree=worktree,
2273 relpath=relpath,
2274 revisionExpr=rev,
2275 revisionId=rev,
2276 rebase=self.rebase,
2277 groups=self.groups,
2278 sync_c=self.sync_c,
2279 sync_s=self.sync_s,
2280 sync_tags=self.sync_tags,
2281 parent=self,
2282 is_derived=True,
2283 )
2284 result.append(subproject)
2285 result.extend(subproject.GetDerivedSubprojects())
2286 return result
2287
2288 def EnableRepositoryExtension(self, key, value="true", version=1):
2289 """Enable git repository extension |key| with |value|.
2290
2291 Args:
2292 key: The extension to enabled. Omit the "extensions." prefix.
2293 value: The value to use for the extension.
2294 version: The minimum git repository version needed.
2295 """
2296 # Make sure the git repo version is new enough already.
2297 found_version = self.config.GetInt("core.repositoryFormatVersion")
2298 if found_version is None:
2299 found_version = 0
2300 if found_version < version:
2301 self.config.SetString("core.repositoryFormatVersion", str(version))
2302
2303 # Enable the extension!
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002304 self.config.SetString(f"extensions.{key}", value)
Gavin Makea2e3302023-03-11 06:46:20 +00002305
2306 def ResolveRemoteHead(self, name=None):
2307 """Find out what the default branch (HEAD) points to.
2308
2309 Normally this points to refs/heads/master, but projects are moving to
2310 main. Support whatever the server uses rather than hardcoding "master"
2311 ourselves.
2312 """
2313 if name is None:
2314 name = self.remote.name
2315
2316 # The output will look like (NB: tabs are separators):
2317 # ref: refs/heads/master HEAD
2318 # 5f6803b100bb3cd0f534e96e88c91373e8ed1c44 HEAD
2319 output = self.bare_git.ls_remote(
2320 "-q", "--symref", "--exit-code", name, "HEAD"
2321 )
2322
2323 for line in output.splitlines():
2324 lhs, rhs = line.split("\t", 1)
2325 if rhs == "HEAD" and lhs.startswith("ref:"):
2326 return lhs[4:].strip()
2327
2328 return None
2329
2330 def _CheckForImmutableRevision(self):
2331 try:
2332 # if revision (sha or tag) is not present then following function
2333 # throws an error.
2334 self.bare_git.rev_list(
Jason Chang87058c62023-09-27 11:34:43 -07002335 "-1",
2336 "--missing=allow-any",
2337 "%s^0" % self.revisionExpr,
2338 "--",
2339 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002340 )
2341 if self.upstream:
2342 rev = self.GetRemote().ToLocal(self.upstream)
2343 self.bare_git.rev_list(
Jason Chang87058c62023-09-27 11:34:43 -07002344 "-1",
2345 "--missing=allow-any",
2346 "%s^0" % rev,
2347 "--",
2348 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002349 )
2350 self.bare_git.merge_base(
Jason Chang87058c62023-09-27 11:34:43 -07002351 "--is-ancestor",
2352 self.revisionExpr,
2353 rev,
2354 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002355 )
2356 return True
2357 except GitError:
2358 # There is no such persistent revision. We have to fetch it.
2359 return False
2360
2361 def _FetchArchive(self, tarpath, cwd=None):
2362 cmd = ["archive", "-v", "-o", tarpath]
2363 cmd.append("--remote=%s" % self.remote.url)
2364 cmd.append("--prefix=%s/" % self.RelPath(local=False))
2365 cmd.append(self.revisionExpr)
2366
2367 command = GitCommand(
Jason Chang32b59562023-07-14 16:45:35 -07002368 self,
2369 cmd,
2370 cwd=cwd,
2371 capture_stdout=True,
2372 capture_stderr=True,
2373 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00002374 )
Jason Chang32b59562023-07-14 16:45:35 -07002375 command.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00002376
2377 def _RemoteFetch(
2378 self,
2379 name=None,
2380 current_branch_only=False,
2381 initial=False,
2382 quiet=False,
2383 verbose=False,
2384 output_redir=None,
2385 alt_dir=None,
2386 tags=True,
2387 prune=False,
2388 depth=None,
2389 submodules=False,
2390 ssh_proxy=None,
2391 force_sync=False,
2392 clone_filter=None,
2393 retry_fetches=2,
2394 retry_sleep_initial_sec=4.0,
2395 retry_exp_factor=2.0,
Jason Chang32b59562023-07-14 16:45:35 -07002396 ) -> bool:
Gavin Makea2e3302023-03-11 06:46:20 +00002397 tag_name = None
2398 # The depth should not be used when fetching to a mirror because
2399 # it will result in a shallow repository that cannot be cloned or
2400 # fetched from.
2401 # The repo project should also never be synced with partial depth.
2402 if self.manifest.IsMirror or self.relpath == ".repo/repo":
2403 depth = None
2404
2405 if depth:
2406 current_branch_only = True
2407
Sylvain56a5a012023-09-11 13:38:00 +02002408 is_sha1 = bool(IsId(self.revisionExpr))
Gavin Makea2e3302023-03-11 06:46:20 +00002409
2410 if current_branch_only:
2411 if self.revisionExpr.startswith(R_TAGS):
2412 # This is a tag and its commit id should never change.
2413 tag_name = self.revisionExpr[len(R_TAGS) :]
2414 elif self.upstream and self.upstream.startswith(R_TAGS):
2415 # This is a tag and its commit id should never change.
2416 tag_name = self.upstream[len(R_TAGS) :]
2417
2418 if is_sha1 or tag_name is not None:
2419 if self._CheckForImmutableRevision():
2420 if verbose:
2421 print(
2422 "Skipped fetching project %s (already have "
2423 "persistent ref)" % self.name
2424 )
2425 return True
2426 if is_sha1 and not depth:
2427 # When syncing a specific commit and --depth is not set:
2428 # * if upstream is explicitly specified and is not a sha1, fetch
2429 # only upstream as users expect only upstream to be fetch.
2430 # Note: The commit might not be in upstream in which case the
2431 # sync will fail.
2432 # * otherwise, fetch all branches to make sure we end up with
2433 # the specific commit.
2434 if self.upstream:
Sylvain56a5a012023-09-11 13:38:00 +02002435 current_branch_only = not IsId(self.upstream)
Gavin Makea2e3302023-03-11 06:46:20 +00002436 else:
2437 current_branch_only = False
2438
2439 if not name:
2440 name = self.remote.name
2441
2442 remote = self.GetRemote(name)
2443 if not remote.PreConnectFetch(ssh_proxy):
2444 ssh_proxy = None
2445
2446 if initial:
2447 if alt_dir and "objects" == os.path.basename(alt_dir):
2448 ref_dir = os.path.dirname(alt_dir)
2449 packed_refs = os.path.join(self.gitdir, "packed-refs")
2450
2451 all_refs = self.bare_ref.all
2452 ids = set(all_refs.values())
2453 tmp = set()
2454
2455 for r, ref_id in GitRefs(ref_dir).all.items():
2456 if r not in all_refs:
2457 if r.startswith(R_TAGS) or remote.WritesTo(r):
2458 all_refs[r] = ref_id
2459 ids.add(ref_id)
2460 continue
2461
2462 if ref_id in ids:
2463 continue
2464
2465 r = "refs/_alt/%s" % ref_id
2466 all_refs[r] = ref_id
2467 ids.add(ref_id)
2468 tmp.add(r)
2469
2470 tmp_packed_lines = []
2471 old_packed_lines = []
2472
2473 for r in sorted(all_refs):
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002474 line = f"{all_refs[r]} {r}\n"
Gavin Makea2e3302023-03-11 06:46:20 +00002475 tmp_packed_lines.append(line)
2476 if r not in tmp:
2477 old_packed_lines.append(line)
2478
2479 tmp_packed = "".join(tmp_packed_lines)
2480 old_packed = "".join(old_packed_lines)
2481 _lwrite(packed_refs, tmp_packed)
2482 else:
2483 alt_dir = None
2484
2485 cmd = ["fetch"]
2486
2487 if clone_filter:
2488 git_require((2, 19, 0), fail=True, msg="partial clones")
2489 cmd.append("--filter=%s" % clone_filter)
2490 self.EnableRepositoryExtension("partialclone", self.remote.name)
2491
2492 if depth:
2493 cmd.append("--depth=%s" % depth)
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002494 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002495 # If this repo has shallow objects, then we don't know which refs
2496 # have shallow objects or not. Tell git to unshallow all fetched
2497 # refs. Don't do this with projects that don't have shallow
2498 # objects, since it is less efficient.
2499 if os.path.exists(os.path.join(self.gitdir, "shallow")):
2500 cmd.append("--depth=2147483647")
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002501
Gavin Makea2e3302023-03-11 06:46:20 +00002502 if not verbose:
2503 cmd.append("--quiet")
2504 if not quiet and sys.stdout.isatty():
2505 cmd.append("--progress")
2506 if not self.worktree:
2507 cmd.append("--update-head-ok")
2508 cmd.append(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002509
Gavin Makea2e3302023-03-11 06:46:20 +00002510 if force_sync:
2511 cmd.append("--force")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002512
Gavin Makea2e3302023-03-11 06:46:20 +00002513 if prune:
2514 cmd.append("--prune")
Mike Frysinger29626b42021-05-01 09:37:13 -04002515
Gavin Makea2e3302023-03-11 06:46:20 +00002516 # Always pass something for --recurse-submodules, git with GIT_DIR
2517 # behaves incorrectly when not given `--recurse-submodules=no`.
2518 # (b/218891912)
2519 cmd.append(
2520 f'--recurse-submodules={"on-demand" if submodules else "no"}'
2521 )
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002522
Gavin Makea2e3302023-03-11 06:46:20 +00002523 spec = []
2524 if not current_branch_only:
2525 # Fetch whole repo.
2526 spec.append(
2527 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2528 )
2529 elif tag_name is not None:
2530 spec.append("tag")
2531 spec.append(tag_name)
Remy Böhmer1469c282020-12-15 18:49:02 +01002532
Gavin Makea2e3302023-03-11 06:46:20 +00002533 if self.manifest.IsMirror and not current_branch_only:
2534 branch = None
Remy Böhmer1469c282020-12-15 18:49:02 +01002535 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002536 branch = self.revisionExpr
2537 if (
2538 not self.manifest.IsMirror
2539 and is_sha1
2540 and depth
2541 and git_require((1, 8, 3))
2542 ):
2543 # Shallow checkout of a specific commit, fetch from that commit and
2544 # not the heads only as the commit might be deeper in the history.
2545 spec.append(branch)
2546 if self.upstream:
2547 spec.append(self.upstream)
David James8d201162013-10-11 17:03:19 -07002548 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002549 if is_sha1:
2550 branch = self.upstream
2551 if branch is not None and branch.strip():
2552 if not branch.startswith("refs/"):
2553 branch = R_HEADS + branch
2554 spec.append(str(("+%s:" % branch) + remote.ToLocal(branch)))
David James8d201162013-10-11 17:03:19 -07002555
Gavin Makea2e3302023-03-11 06:46:20 +00002556 # If mirroring repo and we cannot deduce the tag or branch to fetch,
2557 # fetch whole repo.
2558 if self.manifest.IsMirror and not spec:
2559 spec.append(
2560 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2561 )
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002562
Gavin Makea2e3302023-03-11 06:46:20 +00002563 # If using depth then we should not get all the tags since they may
2564 # be outside of the depth.
2565 if not tags or depth:
2566 cmd.append("--no-tags")
2567 else:
2568 cmd.append("--tags")
2569 spec.append(str(("+refs/tags/*:") + remote.ToLocal("refs/tags/*")))
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002570
Gavin Makea2e3302023-03-11 06:46:20 +00002571 cmd.extend(spec)
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002572
Gavin Makea2e3302023-03-11 06:46:20 +00002573 # At least one retry minimum due to git remote prune.
2574 retry_fetches = max(retry_fetches, 2)
2575 retry_cur_sleep = retry_sleep_initial_sec
2576 ok = prune_tried = False
2577 for try_n in range(retry_fetches):
Jason Chang32b59562023-07-14 16:45:35 -07002578 verify_command = try_n == retry_fetches - 1
Gavin Makea2e3302023-03-11 06:46:20 +00002579 gitcmd = GitCommand(
2580 self,
2581 cmd,
2582 bare=True,
2583 objdir=os.path.join(self.objdir, "objects"),
2584 ssh_proxy=ssh_proxy,
2585 merge_output=True,
2586 capture_stdout=quiet or bool(output_redir),
Jason Chang32b59562023-07-14 16:45:35 -07002587 verify_command=verify_command,
Gavin Makea2e3302023-03-11 06:46:20 +00002588 )
2589 if gitcmd.stdout and not quiet and output_redir:
2590 output_redir.write(gitcmd.stdout)
2591 ret = gitcmd.Wait()
2592 if ret == 0:
2593 ok = True
2594 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002595
Gavin Makea2e3302023-03-11 06:46:20 +00002596 # Retry later due to HTTP 429 Too Many Requests.
2597 elif (
2598 gitcmd.stdout
2599 and "error:" in gitcmd.stdout
2600 and "HTTP 429" in gitcmd.stdout
2601 ):
2602 # Fallthru to sleep+retry logic at the bottom.
2603 pass
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002604
Gavin Makea2e3302023-03-11 06:46:20 +00002605 # Try to prune remote branches once in case there are conflicts.
2606 # For example, if the remote had refs/heads/upstream, but deleted
2607 # that and now has refs/heads/upstream/foo.
2608 elif (
2609 gitcmd.stdout
2610 and "error:" in gitcmd.stdout
2611 and "git remote prune" in gitcmd.stdout
2612 and not prune_tried
2613 ):
2614 prune_tried = True
2615 prunecmd = GitCommand(
2616 self,
2617 ["remote", "prune", name],
2618 bare=True,
2619 ssh_proxy=ssh_proxy,
2620 )
2621 ret = prunecmd.Wait()
2622 if ret:
2623 break
2624 print(
2625 "retrying fetch after pruning remote branches",
2626 file=output_redir,
2627 )
2628 # Continue right away so we don't sleep as we shouldn't need to.
2629 continue
2630 elif current_branch_only and is_sha1 and ret == 128:
2631 # Exit code 128 means "couldn't find the ref you asked for"; if
2632 # we're in sha1 mode, we just tried sync'ing from the upstream
2633 # field; it doesn't exist, thus abort the optimization attempt
2634 # and do a full sync.
2635 break
2636 elif ret < 0:
2637 # Git died with a signal, exit immediately.
2638 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002639
Gavin Makea2e3302023-03-11 06:46:20 +00002640 # Figure out how long to sleep before the next attempt, if there is
2641 # one.
2642 if not verbose and gitcmd.stdout:
2643 print(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002644 f"\n{self.name}:\n{gitcmd.stdout}",
Gavin Makea2e3302023-03-11 06:46:20 +00002645 end="",
2646 file=output_redir,
2647 )
2648 if try_n < retry_fetches - 1:
2649 print(
2650 "%s: sleeping %s seconds before retrying"
2651 % (self.name, retry_cur_sleep),
2652 file=output_redir,
2653 )
2654 time.sleep(retry_cur_sleep)
2655 retry_cur_sleep = min(
2656 retry_exp_factor * retry_cur_sleep, MAXIMUM_RETRY_SLEEP_SEC
2657 )
2658 retry_cur_sleep *= 1 - random.uniform(
2659 -RETRY_JITTER_PERCENT, RETRY_JITTER_PERCENT
2660 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002661
Gavin Makea2e3302023-03-11 06:46:20 +00002662 if initial:
2663 if alt_dir:
2664 if old_packed != "":
2665 _lwrite(packed_refs, old_packed)
2666 else:
2667 platform_utils.remove(packed_refs)
2668 self.bare_git.pack_refs("--all", "--prune")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002669
Gavin Makea2e3302023-03-11 06:46:20 +00002670 if is_sha1 and current_branch_only:
2671 # We just synced the upstream given branch; verify we
2672 # got what we wanted, else trigger a second run of all
2673 # refs.
2674 if not self._CheckForImmutableRevision():
2675 # Sync the current branch only with depth set to None.
2676 # We always pass depth=None down to avoid infinite recursion.
2677 return self._RemoteFetch(
2678 name=name,
2679 quiet=quiet,
2680 verbose=verbose,
2681 output_redir=output_redir,
2682 current_branch_only=current_branch_only and depth,
2683 initial=False,
2684 alt_dir=alt_dir,
Nasser Grainawiaafed292023-05-24 12:51:03 -06002685 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00002686 depth=None,
2687 ssh_proxy=ssh_proxy,
2688 clone_filter=clone_filter,
2689 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002690
Gavin Makea2e3302023-03-11 06:46:20 +00002691 return ok
Mike Frysingerf4545122019-11-11 04:34:16 -05002692
Gavin Makea2e3302023-03-11 06:46:20 +00002693 def _ApplyCloneBundle(self, initial=False, quiet=False, verbose=False):
2694 if initial and (
2695 self.manifest.manifestProject.depth or self.clone_depth
2696 ):
2697 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002698
Gavin Makea2e3302023-03-11 06:46:20 +00002699 remote = self.GetRemote()
2700 bundle_url = remote.url + "/clone.bundle"
2701 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
2702 if GetSchemeFromUrl(bundle_url) not in (
2703 "http",
2704 "https",
2705 "persistent-http",
2706 "persistent-https",
2707 ):
2708 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06002709
Gavin Makea2e3302023-03-11 06:46:20 +00002710 bundle_dst = os.path.join(self.gitdir, "clone.bundle")
2711 bundle_tmp = os.path.join(self.gitdir, "clone.bundle.tmp")
2712
2713 exist_dst = os.path.exists(bundle_dst)
2714 exist_tmp = os.path.exists(bundle_tmp)
2715
2716 if not initial and not exist_dst and not exist_tmp:
2717 return False
2718
2719 if not exist_dst:
2720 exist_dst = self._FetchBundle(
2721 bundle_url, bundle_tmp, bundle_dst, quiet, verbose
2722 )
2723 if not exist_dst:
2724 return False
2725
2726 cmd = ["fetch"]
2727 if not verbose:
2728 cmd.append("--quiet")
2729 if not quiet and sys.stdout.isatty():
2730 cmd.append("--progress")
2731 if not self.worktree:
2732 cmd.append("--update-head-ok")
2733 cmd.append(bundle_dst)
2734 for f in remote.fetch:
2735 cmd.append(str(f))
2736 cmd.append("+refs/tags/*:refs/tags/*")
2737
2738 ok = (
2739 GitCommand(
2740 self,
2741 cmd,
2742 bare=True,
2743 objdir=os.path.join(self.objdir, "objects"),
2744 ).Wait()
2745 == 0
2746 )
2747 platform_utils.remove(bundle_dst, missing_ok=True)
2748 platform_utils.remove(bundle_tmp, missing_ok=True)
2749 return ok
2750
2751 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
2752 platform_utils.remove(dstPath, missing_ok=True)
2753
2754 cmd = ["curl", "--fail", "--output", tmpPath, "--netrc", "--location"]
2755 if quiet:
2756 cmd += ["--silent", "--show-error"]
2757 if os.path.exists(tmpPath):
2758 size = os.stat(tmpPath).st_size
2759 if size >= 1024:
2760 cmd += ["--continue-at", "%d" % (size,)]
2761 else:
2762 platform_utils.remove(tmpPath)
2763 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
2764 if cookiefile:
2765 cmd += ["--cookie", cookiefile]
2766 if proxy:
2767 cmd += ["--proxy", proxy]
2768 elif "http_proxy" in os.environ and "darwin" == sys.platform:
2769 cmd += ["--proxy", os.environ["http_proxy"]]
2770 if srcUrl.startswith("persistent-https"):
2771 srcUrl = "http" + srcUrl[len("persistent-https") :]
2772 elif srcUrl.startswith("persistent-http"):
2773 srcUrl = "http" + srcUrl[len("persistent-http") :]
2774 cmd += [srcUrl]
2775
2776 proc = None
2777 with Trace("Fetching bundle: %s", " ".join(cmd)):
2778 if verbose:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002779 print(f"{self.name}: Downloading bundle: {srcUrl}")
Gavin Makea2e3302023-03-11 06:46:20 +00002780 stdout = None if verbose else subprocess.PIPE
2781 stderr = None if verbose else subprocess.STDOUT
2782 try:
2783 proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
2784 except OSError:
2785 return False
2786
2787 (output, _) = proc.communicate()
2788 curlret = proc.returncode
2789
2790 if curlret == 22:
2791 # From curl man page:
2792 # 22: HTTP page not retrieved. The requested url was not found
2793 # or returned another error with the HTTP error code being 400
2794 # or above. This return code only appears if -f, --fail is used.
2795 if verbose:
2796 print(
2797 "%s: Unable to retrieve clone.bundle; ignoring."
2798 % self.name
2799 )
2800 if output:
2801 print("Curl output:\n%s" % output)
2802 return False
2803 elif curlret and not verbose and output:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002804 logger.error("%s", output)
Gavin Makea2e3302023-03-11 06:46:20 +00002805
2806 if os.path.exists(tmpPath):
2807 if curlret == 0 and self._IsValidBundle(tmpPath, quiet):
2808 platform_utils.rename(tmpPath, dstPath)
2809 return True
2810 else:
2811 platform_utils.remove(tmpPath)
2812 return False
2813 else:
2814 return False
2815
2816 def _IsValidBundle(self, path, quiet):
2817 try:
2818 with open(path, "rb") as f:
2819 if f.read(16) == b"# v2 git bundle\n":
2820 return True
2821 else:
2822 if not quiet:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002823 logger.error("Invalid clone.bundle file; ignoring.")
Gavin Makea2e3302023-03-11 06:46:20 +00002824 return False
2825 except OSError:
2826 return False
2827
2828 def _Checkout(self, rev, quiet=False):
2829 cmd = ["checkout"]
2830 if quiet:
2831 cmd.append("-q")
2832 cmd.append(rev)
2833 cmd.append("--")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002834 if GitCommand(self, cmd).Wait() != 0:
Gavin Makea2e3302023-03-11 06:46:20 +00002835 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002836 raise GitError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002837 f"{self.name} checkout {rev} ", project=self.name
Jason Chang32b59562023-07-14 16:45:35 -07002838 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002839
Gavin Makea2e3302023-03-11 06:46:20 +00002840 def _CherryPick(self, rev, ffonly=False, record_origin=False):
2841 cmd = ["cherry-pick"]
2842 if ffonly:
2843 cmd.append("--ff")
2844 if record_origin:
2845 cmd.append("-x")
2846 cmd.append(rev)
2847 cmd.append("--")
2848 if GitCommand(self, cmd).Wait() != 0:
2849 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002850 raise GitError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002851 f"{self.name} cherry-pick {rev} ", project=self.name
Jason Chang32b59562023-07-14 16:45:35 -07002852 )
Victor Boivie0960b5b2010-11-26 13:42:13 +01002853
Gavin Makea2e3302023-03-11 06:46:20 +00002854 def _LsRemote(self, refs):
2855 cmd = ["ls-remote", self.remote.name, refs]
2856 p = GitCommand(self, cmd, capture_stdout=True)
2857 if p.Wait() == 0:
2858 return p.stdout
2859 return None
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002860
Gavin Makea2e3302023-03-11 06:46:20 +00002861 def _Revert(self, rev):
2862 cmd = ["revert"]
2863 cmd.append("--no-edit")
2864 cmd.append(rev)
2865 cmd.append("--")
2866 if GitCommand(self, cmd).Wait() != 0:
2867 if self._allrefs:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002868 raise GitError(f"{self.name} revert {rev} ", project=self.name)
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002869
Gavin Makea2e3302023-03-11 06:46:20 +00002870 def _ResetHard(self, rev, quiet=True):
2871 cmd = ["reset", "--hard"]
2872 if quiet:
2873 cmd.append("-q")
2874 cmd.append(rev)
2875 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002876 raise GitError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002877 f"{self.name} reset --hard {rev} ", project=self.name
Jason Chang32b59562023-07-14 16:45:35 -07002878 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002879
Gavin Makea2e3302023-03-11 06:46:20 +00002880 def _SyncSubmodules(self, quiet=True):
2881 cmd = ["submodule", "update", "--init", "--recursive"]
2882 if quiet:
2883 cmd.append("-q")
2884 if GitCommand(self, cmd).Wait() != 0:
2885 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07002886 "%s submodule update --init --recursive " % self.name,
2887 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00002888 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002889
Gavin Makea2e3302023-03-11 06:46:20 +00002890 def _Rebase(self, upstream, onto=None):
2891 cmd = ["rebase"]
2892 if onto is not None:
2893 cmd.extend(["--onto", onto])
2894 cmd.append(upstream)
2895 if GitCommand(self, cmd).Wait() != 0:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002896 raise GitError(f"{self.name} rebase {upstream} ", project=self.name)
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002897
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08002898 def _FastForward(self, head, ffonly=False, quiet=True):
Gavin Makea2e3302023-03-11 06:46:20 +00002899 cmd = ["merge", "--no-stat", head]
2900 if ffonly:
2901 cmd.append("--ff-only")
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08002902 if quiet:
2903 cmd.append("-q")
Gavin Makea2e3302023-03-11 06:46:20 +00002904 if GitCommand(self, cmd).Wait() != 0:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002905 raise GitError(f"{self.name} merge {head} ", project=self.name)
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002906
Gavin Makea2e3302023-03-11 06:46:20 +00002907 def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False):
2908 init_git_dir = not os.path.exists(self.gitdir)
2909 init_obj_dir = not os.path.exists(self.objdir)
2910 try:
2911 # Initialize the bare repository, which contains all of the objects.
2912 if init_obj_dir:
2913 os.makedirs(self.objdir)
2914 self.bare_objdir.init()
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002915
Gavin Makea2e3302023-03-11 06:46:20 +00002916 self._UpdateHooks(quiet=quiet)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002917
Gavin Makea2e3302023-03-11 06:46:20 +00002918 if self.use_git_worktrees:
2919 # Enable per-worktree config file support if possible. This
2920 # is more a nice-to-have feature for users rather than a
2921 # hard requirement.
2922 if git_require((2, 20, 0)):
2923 self.EnableRepositoryExtension("worktreeConfig")
Renaud Paquay788e9622017-01-27 11:41:12 -08002924
Gavin Makea2e3302023-03-11 06:46:20 +00002925 # If we have a separate directory to hold refs, initialize it as
2926 # well.
2927 if self.objdir != self.gitdir:
2928 if init_git_dir:
2929 os.makedirs(self.gitdir)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002930
Gavin Makea2e3302023-03-11 06:46:20 +00002931 if init_obj_dir or init_git_dir:
2932 self._ReferenceGitDir(
2933 self.objdir, self.gitdir, copy_all=True
2934 )
2935 try:
2936 self._CheckDirReference(self.objdir, self.gitdir)
2937 except GitError as e:
2938 if force_sync:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002939 logger.error(
2940 "Retrying clone after deleting %s", self.gitdir
Gavin Makea2e3302023-03-11 06:46:20 +00002941 )
2942 try:
2943 platform_utils.rmtree(
2944 platform_utils.realpath(self.gitdir)
2945 )
2946 if self.worktree and os.path.exists(
2947 platform_utils.realpath(self.worktree)
2948 ):
2949 platform_utils.rmtree(
2950 platform_utils.realpath(self.worktree)
2951 )
2952 return self._InitGitDir(
2953 mirror_git=mirror_git,
2954 force_sync=False,
2955 quiet=quiet,
2956 )
2957 except Exception:
2958 raise e
2959 raise e
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002960
Gavin Makea2e3302023-03-11 06:46:20 +00002961 if init_git_dir:
2962 mp = self.manifest.manifestProject
2963 ref_dir = mp.reference or ""
Julien Camperguedd654222014-01-09 16:21:37 +01002964
Gavin Makea2e3302023-03-11 06:46:20 +00002965 def _expanded_ref_dirs():
2966 """Iterate through possible git reference dir paths."""
2967 name = self.name + ".git"
2968 yield mirror_git or os.path.join(ref_dir, name)
2969 for prefix in "", self.remote.name:
2970 yield os.path.join(
2971 ref_dir, ".repo", "project-objects", prefix, name
2972 )
2973 yield os.path.join(
2974 ref_dir, ".repo", "worktrees", prefix, name
2975 )
2976
2977 if ref_dir or mirror_git:
2978 found_ref_dir = None
2979 for path in _expanded_ref_dirs():
2980 if os.path.exists(path):
2981 found_ref_dir = path
2982 break
2983 ref_dir = found_ref_dir
2984
2985 if ref_dir:
2986 if not os.path.isabs(ref_dir):
2987 # The alternate directory is relative to the object
2988 # database.
2989 ref_dir = os.path.relpath(
2990 ref_dir, os.path.join(self.objdir, "objects")
2991 )
2992 _lwrite(
2993 os.path.join(
2994 self.objdir, "objects/info/alternates"
2995 ),
2996 os.path.join(ref_dir, "objects") + "\n",
2997 )
2998
2999 m = self.manifest.manifestProject.config
3000 for key in ["user.name", "user.email"]:
3001 if m.Has(key, include_defaults=False):
3002 self.config.SetString(key, m.GetString(key))
3003 if not self.manifest.EnableGitLfs:
3004 self.config.SetString(
3005 "filter.lfs.smudge", "git-lfs smudge --skip -- %f"
3006 )
3007 self.config.SetString(
3008 "filter.lfs.process", "git-lfs filter-process --skip"
3009 )
3010 self.config.SetBoolean(
3011 "core.bare", True if self.manifest.IsMirror else None
3012 )
Josip Sokcevic9267d582023-10-19 14:46:11 -07003013
3014 if not init_obj_dir:
3015 # The project might be shared (obj_dir already initialized), but
3016 # such information is not available here. Instead of passing it,
3017 # set it as shared, and rely to be unset down the execution
3018 # path.
3019 if git_require((2, 7, 0)):
3020 self.EnableRepositoryExtension("preciousObjects")
3021 else:
3022 self.config.SetString("gc.pruneExpire", "never")
3023
Gavin Makea2e3302023-03-11 06:46:20 +00003024 except Exception:
3025 if init_obj_dir and os.path.exists(self.objdir):
3026 platform_utils.rmtree(self.objdir)
3027 if init_git_dir and os.path.exists(self.gitdir):
3028 platform_utils.rmtree(self.gitdir)
3029 raise
3030
3031 def _UpdateHooks(self, quiet=False):
3032 if os.path.exists(self.objdir):
3033 self._InitHooks(quiet=quiet)
3034
3035 def _InitHooks(self, quiet=False):
3036 hooks = platform_utils.realpath(os.path.join(self.objdir, "hooks"))
3037 if not os.path.exists(hooks):
3038 os.makedirs(hooks)
3039
3040 # Delete sample hooks. They're noise.
3041 for hook in glob.glob(os.path.join(hooks, "*.sample")):
3042 try:
3043 platform_utils.remove(hook, missing_ok=True)
3044 except PermissionError:
3045 pass
3046
3047 for stock_hook in _ProjectHooks():
3048 name = os.path.basename(stock_hook)
3049
3050 if (
3051 name in ("commit-msg",)
3052 and not self.remote.review
3053 and self is not self.manifest.manifestProject
3054 ):
3055 # Don't install a Gerrit Code Review hook if this
3056 # project does not appear to use it for reviews.
3057 #
3058 # Since the manifest project is one of those, but also
3059 # managed through gerrit, it's excluded.
3060 continue
3061
3062 dst = os.path.join(hooks, name)
3063 if platform_utils.islink(dst):
3064 continue
3065 if os.path.exists(dst):
3066 # If the files are the same, we'll leave it alone. We create
3067 # symlinks below by default but fallback to hardlinks if the OS
3068 # blocks them. So if we're here, it's probably because we made a
3069 # hardlink below.
3070 if not filecmp.cmp(stock_hook, dst, shallow=False):
3071 if not quiet:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00003072 logger.warning(
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00003073 "warn: %s: Not replacing locally modified %s hook",
Gavin Makea2e3302023-03-11 06:46:20 +00003074 self.RelPath(local=False),
3075 name,
3076 )
3077 continue
3078 try:
3079 platform_utils.symlink(
3080 os.path.relpath(stock_hook, os.path.dirname(dst)), dst
3081 )
3082 except OSError as e:
3083 if e.errno == errno.EPERM:
3084 try:
3085 os.link(stock_hook, dst)
3086 except OSError:
Jason Chang32b59562023-07-14 16:45:35 -07003087 raise GitError(
3088 self._get_symlink_error_message(), project=self.name
3089 )
Gavin Makea2e3302023-03-11 06:46:20 +00003090 else:
3091 raise
3092
3093 def _InitRemote(self):
3094 if self.remote.url:
3095 remote = self.GetRemote()
3096 remote.url = self.remote.url
3097 remote.pushUrl = self.remote.pushUrl
3098 remote.review = self.remote.review
3099 remote.projectname = self.name
3100
3101 if self.worktree:
3102 remote.ResetFetch(mirror=False)
3103 else:
3104 remote.ResetFetch(mirror=True)
3105 remote.Save()
3106
3107 def _InitMRef(self):
3108 """Initialize the pseudo m/<manifest branch> ref."""
3109 if self.manifest.branch:
3110 if self.use_git_worktrees:
3111 # Set up the m/ space to point to the worktree-specific ref
3112 # space. We'll update the worktree-specific ref space on each
3113 # checkout.
3114 ref = R_M + self.manifest.branch
3115 if not self.bare_ref.symref(ref):
3116 self.bare_git.symbolic_ref(
3117 "-m",
3118 "redirecting to worktree scope",
3119 ref,
3120 R_WORKTREE_M + self.manifest.branch,
3121 )
3122
3123 # We can't update this ref with git worktrees until it exists.
3124 # We'll wait until the initial checkout to set it.
3125 if not os.path.exists(self.worktree):
3126 return
3127
3128 base = R_WORKTREE_M
3129 active_git = self.work_git
3130
3131 self._InitAnyMRef(HEAD, self.bare_git, detach=True)
3132 else:
3133 base = R_M
3134 active_git = self.bare_git
3135
3136 self._InitAnyMRef(base + self.manifest.branch, active_git)
3137
3138 def _InitMirrorHead(self):
3139 self._InitAnyMRef(HEAD, self.bare_git)
3140
3141 def _InitAnyMRef(self, ref, active_git, detach=False):
3142 """Initialize |ref| in |active_git| to the value in the manifest.
3143
3144 This points |ref| to the <project> setting in the manifest.
3145
3146 Args:
3147 ref: The branch to update.
3148 active_git: The git repository to make updates in.
3149 detach: Whether to update target of symbolic refs, or overwrite the
3150 ref directly (and thus make it non-symbolic).
3151 """
3152 cur = self.bare_ref.symref(ref)
3153
3154 if self.revisionId:
3155 if cur != "" or self.bare_ref.get(ref) != self.revisionId:
3156 msg = "manifest set to %s" % self.revisionId
3157 dst = self.revisionId + "^0"
3158 active_git.UpdateRef(ref, dst, message=msg, detach=True)
Julien Camperguedd654222014-01-09 16:21:37 +01003159 else:
Gavin Makea2e3302023-03-11 06:46:20 +00003160 remote = self.GetRemote()
3161 dst = remote.ToLocal(self.revisionExpr)
3162 if cur != dst:
3163 msg = "manifest set to %s" % self.revisionExpr
3164 if detach:
3165 active_git.UpdateRef(ref, dst, message=msg, detach=True)
3166 else:
3167 active_git.symbolic_ref("-m", msg, ref, dst)
Julien Camperguedd654222014-01-09 16:21:37 +01003168
Gavin Makea2e3302023-03-11 06:46:20 +00003169 def _CheckDirReference(self, srcdir, destdir):
3170 # Git worktrees don't use symlinks to share at all.
3171 if self.use_git_worktrees:
3172 return
Julien Camperguedd654222014-01-09 16:21:37 +01003173
Gavin Makea2e3302023-03-11 06:46:20 +00003174 for name in self.shareable_dirs:
3175 # Try to self-heal a bit in simple cases.
3176 dst_path = os.path.join(destdir, name)
3177 src_path = os.path.join(srcdir, name)
Julien Camperguedd654222014-01-09 16:21:37 +01003178
Gavin Makea2e3302023-03-11 06:46:20 +00003179 dst = platform_utils.realpath(dst_path)
3180 if os.path.lexists(dst):
3181 src = platform_utils.realpath(src_path)
3182 # Fail if the links are pointing to the wrong place.
3183 if src != dst:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00003184 logger.error(
3185 "error: %s is different in %s vs %s",
3186 name,
3187 destdir,
3188 srcdir,
3189 )
Gavin Makea2e3302023-03-11 06:46:20 +00003190 raise GitError(
3191 "--force-sync not enabled; cannot overwrite a local "
3192 "work tree. If you're comfortable with the "
3193 "possibility of losing the work tree's git metadata,"
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003194 " use "
3195 f"`repo sync --force-sync {self.RelPath(local=False)}` "
3196 "to proceed.",
Jason Chang32b59562023-07-14 16:45:35 -07003197 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003198 )
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003199
Gavin Makea2e3302023-03-11 06:46:20 +00003200 def _ReferenceGitDir(self, gitdir, dotgit, copy_all):
3201 """Update |dotgit| to reference |gitdir|, using symlinks where possible.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003202
Gavin Makea2e3302023-03-11 06:46:20 +00003203 Args:
3204 gitdir: The bare git repository. Must already be initialized.
3205 dotgit: The repository you would like to initialize.
3206 copy_all: If true, copy all remaining files from |gitdir| ->
3207 |dotgit|. This saves you the effort of initializing |dotgit|
3208 yourself.
3209 """
3210 symlink_dirs = self.shareable_dirs[:]
3211 to_symlink = symlink_dirs
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003212
Gavin Makea2e3302023-03-11 06:46:20 +00003213 to_copy = []
3214 if copy_all:
3215 to_copy = platform_utils.listdir(gitdir)
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003216
Gavin Makea2e3302023-03-11 06:46:20 +00003217 dotgit = platform_utils.realpath(dotgit)
3218 for name in set(to_copy).union(to_symlink):
3219 try:
3220 src = platform_utils.realpath(os.path.join(gitdir, name))
3221 dst = os.path.join(dotgit, name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003222
Gavin Makea2e3302023-03-11 06:46:20 +00003223 if os.path.lexists(dst):
3224 continue
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003225
Gavin Makea2e3302023-03-11 06:46:20 +00003226 # If the source dir doesn't exist, create an empty dir.
3227 if name in symlink_dirs and not os.path.lexists(src):
3228 os.makedirs(src)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003229
Gavin Makea2e3302023-03-11 06:46:20 +00003230 if name in to_symlink:
3231 platform_utils.symlink(
3232 os.path.relpath(src, os.path.dirname(dst)), dst
3233 )
3234 elif copy_all and not platform_utils.islink(dst):
3235 if platform_utils.isdir(src):
3236 shutil.copytree(src, dst)
3237 elif os.path.isfile(src):
3238 shutil.copy(src, dst)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003239
Gavin Makea2e3302023-03-11 06:46:20 +00003240 except OSError as e:
3241 if e.errno == errno.EPERM:
3242 raise DownloadError(self._get_symlink_error_message())
3243 else:
3244 raise
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003245
Gavin Makea2e3302023-03-11 06:46:20 +00003246 def _InitGitWorktree(self):
3247 """Init the project using git worktrees."""
3248 self.bare_git.worktree("prune")
3249 self.bare_git.worktree(
3250 "add",
3251 "-ff",
3252 "--checkout",
3253 "--detach",
3254 "--lock",
3255 self.worktree,
3256 self.GetRevisionId(),
3257 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003258
Gavin Makea2e3302023-03-11 06:46:20 +00003259 # Rewrite the internal state files to use relative paths between the
3260 # checkouts & worktrees.
3261 dotgit = os.path.join(self.worktree, ".git")
Jason R. Coombs034950b2023-10-20 23:32:02 +05453262 with open(dotgit) as fp:
Gavin Makea2e3302023-03-11 06:46:20 +00003263 # Figure out the checkout->worktree path.
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003264 setting = fp.read()
Gavin Makea2e3302023-03-11 06:46:20 +00003265 assert setting.startswith("gitdir:")
3266 git_worktree_path = setting.split(":", 1)[1].strip()
3267 # Some platforms (e.g. Windows) won't let us update dotgit in situ
3268 # because of file permissions. Delete it and recreate it from scratch
3269 # to avoid.
3270 platform_utils.remove(dotgit)
3271 # Use relative path from checkout->worktree & maintain Unix line endings
3272 # on all OS's to match git behavior.
3273 with open(dotgit, "w", newline="\n") as fp:
3274 print(
3275 "gitdir:",
3276 os.path.relpath(git_worktree_path, self.worktree),
3277 file=fp,
3278 )
3279 # Use relative path from worktree->checkout & maintain Unix line endings
3280 # on all OS's to match git behavior.
3281 with open(
3282 os.path.join(git_worktree_path, "gitdir"), "w", newline="\n"
3283 ) as fp:
3284 print(os.path.relpath(dotgit, git_worktree_path), file=fp)
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003285
Gavin Makea2e3302023-03-11 06:46:20 +00003286 self._InitMRef()
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003287
Gavin Makea2e3302023-03-11 06:46:20 +00003288 def _InitWorkTree(self, force_sync=False, submodules=False):
3289 """Setup the worktree .git path.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003290
Gavin Makea2e3302023-03-11 06:46:20 +00003291 This is the user-visible path like src/foo/.git/.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003292
Gavin Makea2e3302023-03-11 06:46:20 +00003293 With non-git-worktrees, this will be a symlink to the .repo/projects/
3294 path. With git-worktrees, this will be a .git file using "gitdir: ..."
3295 syntax.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003296
Gavin Makea2e3302023-03-11 06:46:20 +00003297 Older checkouts had .git/ directories. If we see that, migrate it.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003298
Gavin Makea2e3302023-03-11 06:46:20 +00003299 This also handles changes in the manifest. Maybe this project was
3300 backed by "foo/bar" on the server, but now it's "new/foo/bar". We have
3301 to update the path we point to under .repo/projects/ to match.
3302 """
3303 dotgit = os.path.join(self.worktree, ".git")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003304
Gavin Makea2e3302023-03-11 06:46:20 +00003305 # If using an old layout style (a directory), migrate it.
3306 if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
Jason Chang32b59562023-07-14 16:45:35 -07003307 self._MigrateOldWorkTreeGitDir(dotgit, project=self.name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003308
Gavin Makea2e3302023-03-11 06:46:20 +00003309 init_dotgit = not os.path.exists(dotgit)
3310 if self.use_git_worktrees:
3311 if init_dotgit:
3312 self._InitGitWorktree()
3313 self._CopyAndLinkFiles()
3314 else:
3315 if not init_dotgit:
3316 # See if the project has changed.
3317 if platform_utils.realpath(
3318 self.gitdir
3319 ) != platform_utils.realpath(dotgit):
3320 platform_utils.remove(dotgit)
Doug Anderson37282b42011-03-04 11:54:18 -08003321
Gavin Makea2e3302023-03-11 06:46:20 +00003322 if init_dotgit or not os.path.exists(dotgit):
3323 os.makedirs(self.worktree, exist_ok=True)
3324 platform_utils.symlink(
3325 os.path.relpath(self.gitdir, self.worktree), dotgit
3326 )
Doug Anderson37282b42011-03-04 11:54:18 -08003327
Gavin Makea2e3302023-03-11 06:46:20 +00003328 if init_dotgit:
3329 _lwrite(
3330 os.path.join(dotgit, HEAD), "%s\n" % self.GetRevisionId()
3331 )
Doug Anderson37282b42011-03-04 11:54:18 -08003332
Gavin Makea2e3302023-03-11 06:46:20 +00003333 # Finish checking out the worktree.
3334 cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
3335 if GitCommand(self, cmd).Wait() != 0:
3336 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07003337 "Cannot initialize work tree for " + self.name,
3338 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003339 )
Doug Anderson37282b42011-03-04 11:54:18 -08003340
Gavin Makea2e3302023-03-11 06:46:20 +00003341 if submodules:
3342 self._SyncSubmodules(quiet=True)
3343 self._CopyAndLinkFiles()
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003344
Gavin Makea2e3302023-03-11 06:46:20 +00003345 @classmethod
Jason Chang32b59562023-07-14 16:45:35 -07003346 def _MigrateOldWorkTreeGitDir(cls, dotgit, project=None):
Gavin Makea2e3302023-03-11 06:46:20 +00003347 """Migrate the old worktree .git/ dir style to a symlink.
3348
3349 This logic specifically only uses state from |dotgit| to figure out
3350 where to move content and not |self|. This way if the backing project
3351 also changed places, we only do the .git/ dir to .git symlink migration
3352 here. The path updates will happen independently.
3353 """
3354 # Figure out where in .repo/projects/ it's pointing to.
3355 if not os.path.islink(os.path.join(dotgit, "refs")):
Jason Chang32b59562023-07-14 16:45:35 -07003356 raise GitError(
3357 f"{dotgit}: unsupported checkout state", project=project
3358 )
Gavin Makea2e3302023-03-11 06:46:20 +00003359 gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, "refs")))
3360
3361 # Remove known symlink paths that exist in .repo/projects/.
3362 KNOWN_LINKS = {
3363 "config",
3364 "description",
3365 "hooks",
3366 "info",
3367 "logs",
3368 "objects",
3369 "packed-refs",
3370 "refs",
3371 "rr-cache",
3372 "shallow",
3373 "svn",
3374 }
3375 # Paths that we know will be in both, but are safe to clobber in
3376 # .repo/projects/.
3377 SAFE_TO_CLOBBER = {
3378 "COMMIT_EDITMSG",
3379 "FETCH_HEAD",
3380 "HEAD",
3381 "gc.log",
3382 "gitk.cache",
3383 "index",
3384 "ORIG_HEAD",
3385 }
3386
3387 # First see if we'd succeed before starting the migration.
3388 unknown_paths = []
3389 for name in platform_utils.listdir(dotgit):
3390 # Ignore all temporary/backup names. These are common with vim &
3391 # emacs.
3392 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3393 continue
3394
3395 dotgit_path = os.path.join(dotgit, name)
3396 if name in KNOWN_LINKS:
3397 if not platform_utils.islink(dotgit_path):
3398 unknown_paths.append(f"{dotgit_path}: should be a symlink")
3399 else:
3400 gitdir_path = os.path.join(gitdir, name)
3401 if name not in SAFE_TO_CLOBBER and os.path.exists(gitdir_path):
3402 unknown_paths.append(
3403 f"{dotgit_path}: unknown file; please file a bug"
3404 )
3405 if unknown_paths:
Jason Chang32b59562023-07-14 16:45:35 -07003406 raise GitError(
3407 "Aborting migration: " + "\n".join(unknown_paths),
3408 project=project,
3409 )
Gavin Makea2e3302023-03-11 06:46:20 +00003410
3411 # Now walk the paths and sync the .git/ to .repo/projects/.
3412 for name in platform_utils.listdir(dotgit):
3413 dotgit_path = os.path.join(dotgit, name)
3414
3415 # Ignore all temporary/backup names. These are common with vim &
3416 # emacs.
3417 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3418 platform_utils.remove(dotgit_path)
3419 elif name in KNOWN_LINKS:
3420 platform_utils.remove(dotgit_path)
3421 else:
3422 gitdir_path = os.path.join(gitdir, name)
3423 platform_utils.remove(gitdir_path, missing_ok=True)
3424 platform_utils.rename(dotgit_path, gitdir_path)
3425
3426 # Now that the dir should be empty, clear it out, and symlink it over.
3427 platform_utils.rmdir(dotgit)
3428 platform_utils.symlink(
Jason R. Coombs47944bb2023-09-29 12:42:22 -04003429 os.path.relpath(gitdir, os.path.dirname(os.path.realpath(dotgit))),
3430 dotgit,
Gavin Makea2e3302023-03-11 06:46:20 +00003431 )
3432
3433 def _get_symlink_error_message(self):
3434 if platform_utils.isWindows():
3435 return (
3436 "Unable to create symbolic link. Please re-run the command as "
3437 "Administrator, or see "
3438 "https://github.com/git-for-windows/git/wiki/Symbolic-Links "
3439 "for other options."
3440 )
3441 return "filesystem must support symlinks"
3442
3443 def _revlist(self, *args, **kw):
3444 a = []
3445 a.extend(args)
3446 a.append("--")
3447 return self.work_git.rev_list(*a, **kw)
3448
3449 @property
3450 def _allrefs(self):
3451 return self.bare_ref.all
3452
3453 def _getLogs(
3454 self, rev1, rev2, oneline=False, color=True, pretty_format=None
3455 ):
3456 """Get logs between two revisions of this project."""
3457 comp = ".."
3458 if rev1:
3459 revs = [rev1]
3460 if rev2:
3461 revs.extend([comp, rev2])
3462 cmd = ["log", "".join(revs)]
3463 out = DiffColoring(self.config)
3464 if out.is_on and color:
3465 cmd.append("--color")
3466 if pretty_format is not None:
3467 cmd.append("--pretty=format:%s" % pretty_format)
3468 if oneline:
3469 cmd.append("--oneline")
3470
3471 try:
3472 log = GitCommand(
3473 self, cmd, capture_stdout=True, capture_stderr=True
3474 )
3475 if log.Wait() == 0:
3476 return log.stdout
3477 except GitError:
3478 # worktree may not exist if groups changed for example. In that
3479 # case, try in gitdir instead.
3480 if not os.path.exists(self.worktree):
3481 return self.bare_git.log(*cmd[1:])
3482 else:
3483 raise
3484 return None
3485
3486 def getAddedAndRemovedLogs(
3487 self, toProject, oneline=False, color=True, pretty_format=None
3488 ):
3489 """Get the list of logs from this revision to given revisionId"""
3490 logs = {}
3491 selfId = self.GetRevisionId(self._allrefs)
3492 toId = toProject.GetRevisionId(toProject._allrefs)
3493
3494 logs["added"] = self._getLogs(
3495 selfId,
3496 toId,
3497 oneline=oneline,
3498 color=color,
3499 pretty_format=pretty_format,
3500 )
3501 logs["removed"] = self._getLogs(
3502 toId,
3503 selfId,
3504 oneline=oneline,
3505 color=color,
3506 pretty_format=pretty_format,
3507 )
3508 return logs
3509
Mike Frysingerd4aee652023-10-19 05:13:32 -04003510 class _GitGetByExec:
Gavin Makea2e3302023-03-11 06:46:20 +00003511 def __init__(self, project, bare, gitdir):
3512 self._project = project
3513 self._bare = bare
3514 self._gitdir = gitdir
3515
3516 # __getstate__ and __setstate__ are required for pickling because
3517 # __getattr__ exists.
3518 def __getstate__(self):
3519 return (self._project, self._bare, self._gitdir)
3520
3521 def __setstate__(self, state):
3522 self._project, self._bare, self._gitdir = state
3523
3524 def LsOthers(self):
3525 p = GitCommand(
3526 self._project,
3527 ["ls-files", "-z", "--others", "--exclude-standard"],
3528 bare=False,
3529 gitdir=self._gitdir,
3530 capture_stdout=True,
3531 capture_stderr=True,
3532 )
3533 if p.Wait() == 0:
3534 out = p.stdout
3535 if out:
3536 # Backslash is not anomalous.
3537 return out[:-1].split("\0")
3538 return []
3539
3540 def DiffZ(self, name, *args):
3541 cmd = [name]
3542 cmd.append("-z")
3543 cmd.append("--ignore-submodules")
3544 cmd.extend(args)
3545 p = GitCommand(
3546 self._project,
3547 cmd,
3548 gitdir=self._gitdir,
3549 bare=False,
3550 capture_stdout=True,
3551 capture_stderr=True,
3552 )
3553 p.Wait()
3554 r = {}
3555 out = p.stdout
3556 if out:
3557 out = iter(out[:-1].split("\0"))
3558 while out:
3559 try:
3560 info = next(out)
3561 path = next(out)
3562 except StopIteration:
3563 break
3564
Mike Frysingerd4aee652023-10-19 05:13:32 -04003565 class _Info:
Gavin Makea2e3302023-03-11 06:46:20 +00003566 def __init__(self, path, omode, nmode, oid, nid, state):
3567 self.path = path
3568 self.src_path = None
3569 self.old_mode = omode
3570 self.new_mode = nmode
3571 self.old_id = oid
3572 self.new_id = nid
3573
3574 if len(state) == 1:
3575 self.status = state
3576 self.level = None
3577 else:
3578 self.status = state[:1]
3579 self.level = state[1:]
3580 while self.level.startswith("0"):
3581 self.level = self.level[1:]
3582
3583 info = info[1:].split(" ")
3584 info = _Info(path, *info)
3585 if info.status in ("R", "C"):
3586 info.src_path = info.path
3587 info.path = next(out)
3588 r[info.path] = info
3589 return r
3590
3591 def GetDotgitPath(self, subpath=None):
3592 """Return the full path to the .git dir.
3593
3594 As a convenience, append |subpath| if provided.
3595 """
3596 if self._bare:
3597 dotgit = self._gitdir
3598 else:
3599 dotgit = os.path.join(self._project.worktree, ".git")
3600 if os.path.isfile(dotgit):
3601 # Git worktrees use a "gitdir:" syntax to point to the
3602 # scratch space.
3603 with open(dotgit) as fp:
3604 setting = fp.read()
3605 assert setting.startswith("gitdir:")
3606 gitdir = setting.split(":", 1)[1].strip()
3607 dotgit = os.path.normpath(
3608 os.path.join(self._project.worktree, gitdir)
3609 )
3610
3611 return dotgit if subpath is None else os.path.join(dotgit, subpath)
3612
3613 def GetHead(self):
3614 """Return the ref that HEAD points to."""
3615 path = self.GetDotgitPath(subpath=HEAD)
3616 try:
3617 with open(path) as fd:
3618 line = fd.readline()
Jason R. Coombsae824fb2023-10-20 23:32:40 +05453619 except OSError as e:
Gavin Makea2e3302023-03-11 06:46:20 +00003620 raise NoManifestException(path, str(e))
3621 try:
3622 line = line.decode()
3623 except AttributeError:
3624 pass
3625 if line.startswith("ref: "):
3626 return line[5:-1]
3627 return line[:-1]
3628
3629 def SetHead(self, ref, message=None):
3630 cmdv = []
3631 if message is not None:
3632 cmdv.extend(["-m", message])
3633 cmdv.append(HEAD)
3634 cmdv.append(ref)
3635 self.symbolic_ref(*cmdv)
3636
3637 def DetachHead(self, new, message=None):
3638 cmdv = ["--no-deref"]
3639 if message is not None:
3640 cmdv.extend(["-m", message])
3641 cmdv.append(HEAD)
3642 cmdv.append(new)
3643 self.update_ref(*cmdv)
3644
3645 def UpdateRef(self, name, new, old=None, message=None, detach=False):
3646 cmdv = []
3647 if message is not None:
3648 cmdv.extend(["-m", message])
3649 if detach:
3650 cmdv.append("--no-deref")
3651 cmdv.append(name)
3652 cmdv.append(new)
3653 if old is not None:
3654 cmdv.append(old)
3655 self.update_ref(*cmdv)
3656
3657 def DeleteRef(self, name, old=None):
3658 if not old:
3659 old = self.rev_parse(name)
3660 self.update_ref("-d", name, old)
3661 self._project.bare_ref.deleted(name)
3662
Jason Chang87058c62023-09-27 11:34:43 -07003663 def rev_list(self, *args, log_as_error=True, **kw):
Gavin Makea2e3302023-03-11 06:46:20 +00003664 if "format" in kw:
3665 cmdv = ["log", "--pretty=format:%s" % kw["format"]]
3666 else:
3667 cmdv = ["rev-list"]
3668 cmdv.extend(args)
3669 p = GitCommand(
3670 self._project,
3671 cmdv,
3672 bare=self._bare,
3673 gitdir=self._gitdir,
3674 capture_stdout=True,
3675 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003676 verify_command=True,
Jason Chang87058c62023-09-27 11:34:43 -07003677 log_as_error=log_as_error,
Gavin Makea2e3302023-03-11 06:46:20 +00003678 )
Jason Chang32b59562023-07-14 16:45:35 -07003679 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003680 return p.stdout.splitlines()
3681
3682 def __getattr__(self, name):
3683 """Allow arbitrary git commands using pythonic syntax.
3684
3685 This allows you to do things like:
3686 git_obj.rev_parse('HEAD')
3687
3688 Since we don't have a 'rev_parse' method defined, the __getattr__
3689 will run. We'll replace the '_' with a '-' and try to run a git
3690 command. Any other positional arguments will be passed to the git
3691 command, and the following keyword arguments are supported:
3692 config: An optional dict of git config options to be passed with
3693 '-c'.
3694
3695 Args:
3696 name: The name of the git command to call. Any '_' characters
3697 will be replaced with '-'.
3698
3699 Returns:
3700 A callable object that will try to call git with the named
3701 command.
3702 """
3703 name = name.replace("_", "-")
3704
Jason Chang87058c62023-09-27 11:34:43 -07003705 def runner(*args, log_as_error=True, **kwargs):
Gavin Makea2e3302023-03-11 06:46:20 +00003706 cmdv = []
3707 config = kwargs.pop("config", None)
3708 for k in kwargs:
3709 raise TypeError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003710 f"{name}() got an unexpected keyword argument {k!r}"
Gavin Makea2e3302023-03-11 06:46:20 +00003711 )
3712 if config is not None:
3713 for k, v in config.items():
3714 cmdv.append("-c")
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003715 cmdv.append(f"{k}={v}")
Gavin Makea2e3302023-03-11 06:46:20 +00003716 cmdv.append(name)
3717 cmdv.extend(args)
3718 p = GitCommand(
3719 self._project,
3720 cmdv,
3721 bare=self._bare,
3722 gitdir=self._gitdir,
3723 capture_stdout=True,
3724 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003725 verify_command=True,
Jason Chang87058c62023-09-27 11:34:43 -07003726 log_as_error=log_as_error,
Gavin Makea2e3302023-03-11 06:46:20 +00003727 )
Jason Chang32b59562023-07-14 16:45:35 -07003728 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003729 r = p.stdout
3730 if r.endswith("\n") and r.index("\n") == len(r) - 1:
3731 return r[:-1]
3732 return r
3733
3734 return runner
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003735
3736
Jason Chang32b59562023-07-14 16:45:35 -07003737class LocalSyncFail(RepoError):
3738 """Default error when there is an Sync_LocalHalf error."""
3739
3740
3741class _PriorSyncFailedError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003742 def __str__(self):
3743 return "prior sync failed; rebase still in progress"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003744
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003745
Jason Chang32b59562023-07-14 16:45:35 -07003746class _DirtyError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003747 def __str__(self):
3748 return "contains uncommitted changes"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003749
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003750
Mike Frysingerd4aee652023-10-19 05:13:32 -04003751class _InfoMessage:
Gavin Makea2e3302023-03-11 06:46:20 +00003752 def __init__(self, project, text):
3753 self.project = project
3754 self.text = text
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003755
Gavin Makea2e3302023-03-11 06:46:20 +00003756 def Print(self, syncbuf):
3757 syncbuf.out.info(
3758 "%s/: %s", self.project.RelPath(local=False), self.text
3759 )
3760 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003761
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003762
Mike Frysingerd4aee652023-10-19 05:13:32 -04003763class _Failure:
Gavin Makea2e3302023-03-11 06:46:20 +00003764 def __init__(self, project, why):
3765 self.project = project
3766 self.why = why
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003767
Gavin Makea2e3302023-03-11 06:46:20 +00003768 def Print(self, syncbuf):
3769 syncbuf.out.fail(
3770 "error: %s/: %s", self.project.RelPath(local=False), str(self.why)
3771 )
3772 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003773
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003774
Mike Frysingerd4aee652023-10-19 05:13:32 -04003775class _Later:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003776 def __init__(self, project, action, quiet):
Gavin Makea2e3302023-03-11 06:46:20 +00003777 self.project = project
3778 self.action = action
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003779 self.quiet = quiet
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003780
Gavin Makea2e3302023-03-11 06:46:20 +00003781 def Run(self, syncbuf):
3782 out = syncbuf.out
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003783 if not self.quiet:
3784 out.project("project %s/", self.project.RelPath(local=False))
3785 out.nl()
Gavin Makea2e3302023-03-11 06:46:20 +00003786 try:
3787 self.action()
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003788 if not self.quiet:
3789 out.nl()
Gavin Makea2e3302023-03-11 06:46:20 +00003790 return True
3791 except GitError:
3792 out.nl()
3793 return False
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003794
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003795
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003796class _SyncColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +00003797 def __init__(self, config):
3798 super().__init__(config, "reposync")
3799 self.project = self.printer("header", attr="bold")
3800 self.info = self.printer("info")
3801 self.fail = self.printer("fail", fg="red")
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003802
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003803
Mike Frysingerd4aee652023-10-19 05:13:32 -04003804class SyncBuffer:
Gavin Makea2e3302023-03-11 06:46:20 +00003805 def __init__(self, config, detach_head=False):
3806 self._messages = []
3807 self._failures = []
3808 self._later_queue1 = []
3809 self._later_queue2 = []
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003810
Gavin Makea2e3302023-03-11 06:46:20 +00003811 self.out = _SyncColoring(config)
3812 self.out.redirect(sys.stderr)
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003813
Gavin Makea2e3302023-03-11 06:46:20 +00003814 self.detach_head = detach_head
3815 self.clean = True
3816 self.recent_clean = True
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003817
Gavin Makea2e3302023-03-11 06:46:20 +00003818 def info(self, project, fmt, *args):
3819 self._messages.append(_InfoMessage(project, fmt % args))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003820
Gavin Makea2e3302023-03-11 06:46:20 +00003821 def fail(self, project, err=None):
3822 self._failures.append(_Failure(project, err))
David Rileye0684ad2017-04-05 00:02:59 -07003823 self._MarkUnclean()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003824
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003825 def later1(self, project, what, quiet):
3826 self._later_queue1.append(_Later(project, what, quiet))
Mike Frysinger70d861f2019-08-26 15:22:36 -04003827
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003828 def later2(self, project, what, quiet):
3829 self._later_queue2.append(_Later(project, what, quiet))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003830
Gavin Makea2e3302023-03-11 06:46:20 +00003831 def Finish(self):
3832 self._PrintMessages()
3833 self._RunLater()
3834 self._PrintMessages()
3835 return self.clean
3836
3837 def Recently(self):
3838 recent_clean = self.recent_clean
3839 self.recent_clean = True
3840 return recent_clean
3841
3842 def _MarkUnclean(self):
3843 self.clean = False
3844 self.recent_clean = False
3845
3846 def _RunLater(self):
3847 for q in ["_later_queue1", "_later_queue2"]:
3848 if not self._RunQueue(q):
3849 return
3850
3851 def _RunQueue(self, queue):
3852 for m in getattr(self, queue):
3853 if not m.Run(self):
3854 self._MarkUnclean()
3855 return False
3856 setattr(self, queue, [])
3857 return True
3858
3859 def _PrintMessages(self):
3860 if self._messages or self._failures:
3861 if os.isatty(2):
3862 self.out.write(progress.CSI_ERASE_LINE)
3863 self.out.write("\r")
3864
3865 for m in self._messages:
3866 m.Print(self)
3867 for m in self._failures:
3868 m.Print(self)
3869
3870 self._messages = []
3871 self._failures = []
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003872
3873
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003874class MetaProject(Project):
Gavin Makea2e3302023-03-11 06:46:20 +00003875 """A special project housed under .repo."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003876
Gavin Makea2e3302023-03-11 06:46:20 +00003877 def __init__(self, manifest, name, gitdir, worktree):
3878 Project.__init__(
3879 self,
3880 manifest=manifest,
3881 name=name,
3882 gitdir=gitdir,
3883 objdir=gitdir,
3884 worktree=worktree,
3885 remote=RemoteSpec("origin"),
3886 relpath=".repo/%s" % name,
3887 revisionExpr="refs/heads/master",
3888 revisionId=None,
3889 groups=None,
3890 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003891
Gavin Makea2e3302023-03-11 06:46:20 +00003892 def PreSync(self):
3893 if self.Exists:
3894 cb = self.CurrentBranch
3895 if cb:
3896 base = self.GetBranch(cb).merge
3897 if base:
3898 self.revisionExpr = base
3899 self.revisionId = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003900
Gavin Makea2e3302023-03-11 06:46:20 +00003901 @property
3902 def HasChanges(self):
3903 """Has the remote received new commits not yet checked out?"""
3904 if not self.remote or not self.revisionExpr:
3905 return False
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003906
Gavin Makea2e3302023-03-11 06:46:20 +00003907 all_refs = self.bare_ref.all
3908 revid = self.GetRevisionId(all_refs)
3909 head = self.work_git.GetHead()
3910 if head.startswith(R_HEADS):
3911 try:
3912 head = all_refs[head]
3913 except KeyError:
3914 head = None
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003915
Gavin Makea2e3302023-03-11 06:46:20 +00003916 if revid == head:
3917 return False
3918 elif self._revlist(not_rev(HEAD), revid):
3919 return True
3920 return False
LaMont Jones9b72cf22022-03-29 21:54:22 +00003921
3922
3923class RepoProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003924 """The MetaProject for repo itself."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003925
Gavin Makea2e3302023-03-11 06:46:20 +00003926 @property
3927 def LastFetch(self):
3928 try:
3929 fh = os.path.join(self.gitdir, "FETCH_HEAD")
3930 return os.path.getmtime(fh)
3931 except OSError:
3932 return 0
LaMont Jones9b72cf22022-03-29 21:54:22 +00003933
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +01003934
LaMont Jones9b72cf22022-03-29 21:54:22 +00003935class ManifestProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003936 """The MetaProject for manifests."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003937
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08003938 def MetaBranchSwitch(self, submodules=False, verbose=False):
Gavin Makea2e3302023-03-11 06:46:20 +00003939 """Prepare for manifest branch switch."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003940
Gavin Makea2e3302023-03-11 06:46:20 +00003941 # detach and delete manifest branch, allowing a new
3942 # branch to take over
3943 syncbuf = SyncBuffer(self.config, detach_head=True)
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08003944 self.Sync_LocalHalf(syncbuf, submodules=submodules, verbose=verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00003945 syncbuf.Finish()
LaMont Jones9b72cf22022-03-29 21:54:22 +00003946
Gavin Makea2e3302023-03-11 06:46:20 +00003947 return (
3948 GitCommand(
3949 self,
3950 ["update-ref", "-d", "refs/heads/default"],
3951 capture_stdout=True,
3952 capture_stderr=True,
3953 ).Wait()
3954 == 0
3955 )
LaMont Jones9b03f152022-03-29 23:01:18 +00003956
Gavin Makea2e3302023-03-11 06:46:20 +00003957 @property
3958 def standalone_manifest_url(self):
3959 """The URL of the standalone manifest, or None."""
3960 return self.config.GetString("manifest.standalone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003961
Gavin Makea2e3302023-03-11 06:46:20 +00003962 @property
3963 def manifest_groups(self):
3964 """The manifest groups string."""
3965 return self.config.GetString("manifest.groups")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003966
Gavin Makea2e3302023-03-11 06:46:20 +00003967 @property
3968 def reference(self):
3969 """The --reference for this manifest."""
3970 return self.config.GetString("repo.reference")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003971
Gavin Makea2e3302023-03-11 06:46:20 +00003972 @property
3973 def dissociate(self):
3974 """Whether to dissociate."""
3975 return self.config.GetBoolean("repo.dissociate")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003976
Gavin Makea2e3302023-03-11 06:46:20 +00003977 @property
3978 def archive(self):
3979 """Whether we use archive."""
3980 return self.config.GetBoolean("repo.archive")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003981
Gavin Makea2e3302023-03-11 06:46:20 +00003982 @property
3983 def mirror(self):
3984 """Whether we use mirror."""
3985 return self.config.GetBoolean("repo.mirror")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003986
Gavin Makea2e3302023-03-11 06:46:20 +00003987 @property
3988 def use_worktree(self):
3989 """Whether we use worktree."""
3990 return self.config.GetBoolean("repo.worktree")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003991
Gavin Makea2e3302023-03-11 06:46:20 +00003992 @property
3993 def clone_bundle(self):
3994 """Whether we use clone_bundle."""
3995 return self.config.GetBoolean("repo.clonebundle")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003996
Gavin Makea2e3302023-03-11 06:46:20 +00003997 @property
3998 def submodules(self):
3999 """Whether we use submodules."""
4000 return self.config.GetBoolean("repo.submodules")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004001
Gavin Makea2e3302023-03-11 06:46:20 +00004002 @property
4003 def git_lfs(self):
4004 """Whether we use git_lfs."""
4005 return self.config.GetBoolean("repo.git-lfs")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004006
Gavin Makea2e3302023-03-11 06:46:20 +00004007 @property
4008 def use_superproject(self):
4009 """Whether we use superproject."""
4010 return self.config.GetBoolean("repo.superproject")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004011
Gavin Makea2e3302023-03-11 06:46:20 +00004012 @property
4013 def partial_clone(self):
4014 """Whether this is a partial clone."""
4015 return self.config.GetBoolean("repo.partialclone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004016
Gavin Makea2e3302023-03-11 06:46:20 +00004017 @property
4018 def depth(self):
4019 """Partial clone depth."""
Roberto Vladimir Prado Carranza3d58d212023-09-13 10:27:26 +02004020 return self.config.GetInt("repo.depth")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004021
Gavin Makea2e3302023-03-11 06:46:20 +00004022 @property
4023 def clone_filter(self):
4024 """The clone filter."""
4025 return self.config.GetString("repo.clonefilter")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004026
Gavin Makea2e3302023-03-11 06:46:20 +00004027 @property
4028 def partial_clone_exclude(self):
4029 """Partial clone exclude string"""
4030 return self.config.GetString("repo.partialcloneexclude")
LaMont Jones4ada0432022-04-14 15:10:43 +00004031
Gavin Makea2e3302023-03-11 06:46:20 +00004032 @property
Jason Chang17833322023-05-23 13:06:55 -07004033 def clone_filter_for_depth(self):
4034 """Replace shallow clone with partial clone."""
4035 return self.config.GetString("repo.clonefilterfordepth")
4036
4037 @property
Gavin Makea2e3302023-03-11 06:46:20 +00004038 def manifest_platform(self):
4039 """The --platform argument from `repo init`."""
4040 return self.config.GetString("manifest.platform")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004041
Gavin Makea2e3302023-03-11 06:46:20 +00004042 @property
4043 def _platform_name(self):
4044 """Return the name of the platform."""
4045 return platform.system().lower()
LaMont Jones9b03f152022-03-29 23:01:18 +00004046
Gavin Makea2e3302023-03-11 06:46:20 +00004047 def SyncWithPossibleInit(
4048 self,
4049 submanifest,
4050 verbose=False,
4051 current_branch_only=False,
4052 tags="",
4053 git_event_log=None,
4054 ):
4055 """Sync a manifestProject, possibly for the first time.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004056
Gavin Makea2e3302023-03-11 06:46:20 +00004057 Call Sync() with arguments from the most recent `repo init`. If this is
4058 a new sub manifest, then inherit options from the parent's
4059 manifestProject.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004060
Gavin Makea2e3302023-03-11 06:46:20 +00004061 This is used by subcmds.Sync() to do an initial download of new sub
4062 manifests.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004063
Gavin Makea2e3302023-03-11 06:46:20 +00004064 Args:
4065 submanifest: an XmlSubmanifest, the submanifest to re-sync.
4066 verbose: a boolean, whether to show all output, rather than only
4067 errors.
4068 current_branch_only: a boolean, whether to only fetch the current
4069 manifest branch from the server.
4070 tags: a boolean, whether to fetch tags.
4071 git_event_log: an EventLog, for git tracing.
4072 """
4073 # TODO(lamontjones): when refactoring sync (and init?) consider how to
4074 # better get the init options that we should use for new submanifests
4075 # that are added when syncing an existing workspace.
4076 git_event_log = git_event_log or EventLog()
LaMont Jonesb90a4222022-04-14 15:00:09 +00004077 spec = submanifest.ToSubmanifestSpec()
Gavin Makea2e3302023-03-11 06:46:20 +00004078 # Use the init options from the existing manifestProject, or the parent
4079 # if it doesn't exist.
4080 #
4081 # Today, we only support changing manifest_groups on the sub-manifest,
4082 # with no supported-for-the-user way to change the other arguments from
4083 # those specified by the outermost manifest.
4084 #
4085 # TODO(lamontjones): determine which of these should come from the
4086 # outermost manifest and which should come from the parent manifest.
4087 mp = self if self.Exists else submanifest.parent.manifestProject
4088 return self.Sync(
LaMont Jones55ee3042022-04-06 17:10:21 +00004089 manifest_url=spec.manifestUrl,
4090 manifest_branch=spec.revision,
Gavin Makea2e3302023-03-11 06:46:20 +00004091 standalone_manifest=mp.standalone_manifest_url,
4092 groups=mp.manifest_groups,
4093 platform=mp.manifest_platform,
4094 mirror=mp.mirror,
4095 dissociate=mp.dissociate,
4096 reference=mp.reference,
4097 worktree=mp.use_worktree,
4098 submodules=mp.submodules,
4099 archive=mp.archive,
4100 partial_clone=mp.partial_clone,
4101 clone_filter=mp.clone_filter,
4102 partial_clone_exclude=mp.partial_clone_exclude,
4103 clone_bundle=mp.clone_bundle,
4104 git_lfs=mp.git_lfs,
4105 use_superproject=mp.use_superproject,
LaMont Jones55ee3042022-04-06 17:10:21 +00004106 verbose=verbose,
4107 current_branch_only=current_branch_only,
4108 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00004109 depth=mp.depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004110 git_event_log=git_event_log,
4111 manifest_name=spec.manifestName,
Gavin Makea2e3302023-03-11 06:46:20 +00004112 this_manifest_only=True,
LaMont Jones55ee3042022-04-06 17:10:21 +00004113 outer_manifest=False,
Jason Chang17833322023-05-23 13:06:55 -07004114 clone_filter_for_depth=mp.clone_filter_for_depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004115 )
LaMont Jones409407a2022-04-05 21:21:56 +00004116
Gavin Makea2e3302023-03-11 06:46:20 +00004117 def Sync(
4118 self,
4119 _kwargs_only=(),
4120 manifest_url="",
4121 manifest_branch=None,
4122 standalone_manifest=False,
4123 groups="",
4124 mirror=False,
4125 reference="",
4126 dissociate=False,
4127 worktree=False,
4128 submodules=False,
4129 archive=False,
4130 partial_clone=None,
4131 depth=None,
4132 clone_filter="blob:none",
4133 partial_clone_exclude=None,
4134 clone_bundle=None,
4135 git_lfs=None,
4136 use_superproject=None,
4137 verbose=False,
4138 current_branch_only=False,
4139 git_event_log=None,
4140 platform="",
4141 manifest_name="default.xml",
4142 tags="",
4143 this_manifest_only=False,
4144 outer_manifest=True,
Jason Chang17833322023-05-23 13:06:55 -07004145 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00004146 ):
4147 """Sync the manifest and all submanifests.
LaMont Jones409407a2022-04-05 21:21:56 +00004148
Gavin Makea2e3302023-03-11 06:46:20 +00004149 Args:
4150 manifest_url: a string, the URL of the manifest project.
4151 manifest_branch: a string, the manifest branch to use.
4152 standalone_manifest: a boolean, whether to store the manifest as a
4153 static file.
4154 groups: a string, restricts the checkout to projects with the
4155 specified groups.
4156 mirror: a boolean, whether to create a mirror of the remote
4157 repository.
4158 reference: a string, location of a repo instance to use as a
4159 reference.
4160 dissociate: a boolean, whether to dissociate from reference mirrors
4161 after clone.
4162 worktree: a boolean, whether to use git-worktree to manage projects.
4163 submodules: a boolean, whether sync submodules associated with the
4164 manifest project.
4165 archive: a boolean, whether to checkout each project as an archive.
4166 See git-archive.
4167 partial_clone: a boolean, whether to perform a partial clone.
4168 depth: an int, how deep of a shallow clone to create.
4169 clone_filter: a string, filter to use with partial_clone.
4170 partial_clone_exclude : a string, comma-delimeted list of project
4171 names to exclude from partial clone.
4172 clone_bundle: a boolean, whether to enable /clone.bundle on
4173 HTTP/HTTPS.
4174 git_lfs: a boolean, whether to enable git LFS support.
4175 use_superproject: a boolean, whether to use the manifest
4176 superproject to sync projects.
4177 verbose: a boolean, whether to show all output, rather than only
4178 errors.
4179 current_branch_only: a boolean, whether to only fetch the current
4180 manifest branch from the server.
4181 platform: a string, restrict the checkout to projects with the
4182 specified platform group.
4183 git_event_log: an EventLog, for git tracing.
4184 tags: a boolean, whether to fetch tags.
4185 manifest_name: a string, the name of the manifest file to use.
4186 this_manifest_only: a boolean, whether to only operate on the
4187 current sub manifest.
4188 outer_manifest: a boolean, whether to start at the outermost
4189 manifest.
Jason Chang17833322023-05-23 13:06:55 -07004190 clone_filter_for_depth: a string, when specified replaces shallow
4191 clones with partial.
LaMont Jones9b03f152022-03-29 23:01:18 +00004192
Gavin Makea2e3302023-03-11 06:46:20 +00004193 Returns:
4194 a boolean, whether the sync was successful.
4195 """
4196 assert _kwargs_only == (), "Sync only accepts keyword arguments."
LaMont Jones9b03f152022-03-29 23:01:18 +00004197
Gavin Makea2e3302023-03-11 06:46:20 +00004198 groups = groups or self.manifest.GetDefaultGroupsStr(
4199 with_platform=False
4200 )
4201 platform = platform or "auto"
4202 git_event_log = git_event_log or EventLog()
4203 if outer_manifest and self.manifest.is_submanifest:
4204 # In a multi-manifest checkout, use the outer manifest unless we are
4205 # told not to.
4206 return self.client.outer_manifest.manifestProject.Sync(
4207 manifest_url=manifest_url,
4208 manifest_branch=manifest_branch,
4209 standalone_manifest=standalone_manifest,
4210 groups=groups,
4211 platform=platform,
4212 mirror=mirror,
4213 dissociate=dissociate,
4214 reference=reference,
4215 worktree=worktree,
4216 submodules=submodules,
4217 archive=archive,
4218 partial_clone=partial_clone,
4219 clone_filter=clone_filter,
4220 partial_clone_exclude=partial_clone_exclude,
4221 clone_bundle=clone_bundle,
4222 git_lfs=git_lfs,
4223 use_superproject=use_superproject,
4224 verbose=verbose,
4225 current_branch_only=current_branch_only,
4226 tags=tags,
4227 depth=depth,
4228 git_event_log=git_event_log,
4229 manifest_name=manifest_name,
4230 this_manifest_only=this_manifest_only,
4231 outer_manifest=False,
4232 )
LaMont Jones9b03f152022-03-29 23:01:18 +00004233
Gavin Makea2e3302023-03-11 06:46:20 +00004234 # If repo has already been initialized, we take -u with the absence of
4235 # --standalone-manifest to mean "transition to a standard repo set up",
4236 # which necessitates starting fresh.
4237 # If --standalone-manifest is set, we always tear everything down and
4238 # start anew.
4239 if self.Exists:
4240 was_standalone_manifest = self.config.GetString(
4241 "manifest.standalone"
4242 )
4243 if was_standalone_manifest and not manifest_url:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004244 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004245 "fatal: repo was initialized with a standlone manifest, "
4246 "cannot be re-initialized without --manifest-url/-u"
4247 )
4248 return False
4249
4250 if standalone_manifest or (
4251 was_standalone_manifest and manifest_url
4252 ):
4253 self.config.ClearCache()
4254 if self.gitdir and os.path.exists(self.gitdir):
4255 platform_utils.rmtree(self.gitdir)
4256 if self.worktree and os.path.exists(self.worktree):
4257 platform_utils.rmtree(self.worktree)
4258
4259 is_new = not self.Exists
4260 if is_new:
4261 if not manifest_url:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004262 logger.error("fatal: manifest url is required.")
Gavin Makea2e3302023-03-11 06:46:20 +00004263 return False
4264
4265 if verbose:
4266 print(
4267 "Downloading manifest from %s"
4268 % (GitConfig.ForUser().UrlInsteadOf(manifest_url),),
4269 file=sys.stderr,
4270 )
4271
4272 # The manifest project object doesn't keep track of the path on the
4273 # server where this git is located, so let's save that here.
4274 mirrored_manifest_git = None
4275 if reference:
4276 manifest_git_path = urllib.parse.urlparse(manifest_url).path[1:]
4277 mirrored_manifest_git = os.path.join(
4278 reference, manifest_git_path
4279 )
4280 if not mirrored_manifest_git.endswith(".git"):
4281 mirrored_manifest_git += ".git"
4282 if not os.path.exists(mirrored_manifest_git):
4283 mirrored_manifest_git = os.path.join(
4284 reference, ".repo/manifests.git"
4285 )
4286
4287 self._InitGitDir(mirror_git=mirrored_manifest_git)
4288
4289 # If standalone_manifest is set, mark the project as "standalone" --
4290 # we'll still do much of the manifests.git set up, but will avoid actual
4291 # syncs to a remote.
4292 if standalone_manifest:
4293 self.config.SetString("manifest.standalone", manifest_url)
4294 elif not manifest_url and not manifest_branch:
4295 # If -u is set and --standalone-manifest is not, then we're not in
4296 # standalone mode. Otherwise, use config to infer what we were in
4297 # the last init.
4298 standalone_manifest = bool(
4299 self.config.GetString("manifest.standalone")
4300 )
4301 if not standalone_manifest:
4302 self.config.SetString("manifest.standalone", None)
4303
4304 self._ConfigureDepth(depth)
4305
4306 # Set the remote URL before the remote branch as we might need it below.
4307 if manifest_url:
4308 r = self.GetRemote()
4309 r.url = manifest_url
4310 r.ResetFetch()
4311 r.Save()
4312
4313 if not standalone_manifest:
4314 if manifest_branch:
4315 if manifest_branch == "HEAD":
4316 manifest_branch = self.ResolveRemoteHead()
4317 if manifest_branch is None:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004318 logger.error("fatal: unable to resolve HEAD")
Gavin Makea2e3302023-03-11 06:46:20 +00004319 return False
4320 self.revisionExpr = manifest_branch
4321 else:
4322 if is_new:
4323 default_branch = self.ResolveRemoteHead()
4324 if default_branch is None:
4325 # If the remote doesn't have HEAD configured, default to
4326 # master.
4327 default_branch = "refs/heads/master"
4328 self.revisionExpr = default_branch
4329 else:
4330 self.PreSync()
4331
4332 groups = re.split(r"[,\s]+", groups or "")
4333 all_platforms = ["linux", "darwin", "windows"]
4334 platformize = lambda x: "platform-" + x
4335 if platform == "auto":
4336 if not mirror and not self.mirror:
4337 groups.append(platformize(self._platform_name))
4338 elif platform == "all":
4339 groups.extend(map(platformize, all_platforms))
4340 elif platform in all_platforms:
4341 groups.append(platformize(platform))
4342 elif platform != "none":
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004343 logger.error("fatal: invalid platform flag", file=sys.stderr)
Gavin Makea2e3302023-03-11 06:46:20 +00004344 return False
4345 self.config.SetString("manifest.platform", platform)
4346
4347 groups = [x for x in groups if x]
4348 groupstr = ",".join(groups)
4349 if (
4350 platform == "auto"
4351 and groupstr == self.manifest.GetDefaultGroupsStr()
4352 ):
4353 groupstr = None
4354 self.config.SetString("manifest.groups", groupstr)
4355
4356 if reference:
4357 self.config.SetString("repo.reference", reference)
4358
4359 if dissociate:
4360 self.config.SetBoolean("repo.dissociate", dissociate)
4361
4362 if worktree:
4363 if mirror:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004364 logger.error("fatal: --mirror and --worktree are incompatible")
Gavin Makea2e3302023-03-11 06:46:20 +00004365 return False
4366 if submodules:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004367 logger.error(
4368 "fatal: --submodules and --worktree are incompatible"
Gavin Makea2e3302023-03-11 06:46:20 +00004369 )
4370 return False
4371 self.config.SetBoolean("repo.worktree", worktree)
4372 if is_new:
4373 self.use_git_worktrees = True
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004374 logger.warning("warning: --worktree is experimental!")
Gavin Makea2e3302023-03-11 06:46:20 +00004375
4376 if archive:
4377 if is_new:
4378 self.config.SetBoolean("repo.archive", archive)
4379 else:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004380 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004381 "fatal: --archive is only supported when initializing a "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004382 "new workspace."
Gavin Makea2e3302023-03-11 06:46:20 +00004383 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004384 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004385 "Either delete the .repo folder in this workspace, or "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004386 "initialize in another location."
Gavin Makea2e3302023-03-11 06:46:20 +00004387 )
4388 return False
4389
4390 if mirror:
4391 if is_new:
4392 self.config.SetBoolean("repo.mirror", mirror)
4393 else:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004394 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004395 "fatal: --mirror is only supported when initializing a new "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004396 "workspace."
Gavin Makea2e3302023-03-11 06:46:20 +00004397 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004398 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004399 "Either delete the .repo folder in this workspace, or "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004400 "initialize in another location."
Gavin Makea2e3302023-03-11 06:46:20 +00004401 )
4402 return False
4403
4404 if partial_clone is not None:
4405 if mirror:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004406 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004407 "fatal: --mirror and --partial-clone are mutually "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004408 "exclusive"
Gavin Makea2e3302023-03-11 06:46:20 +00004409 )
4410 return False
4411 self.config.SetBoolean("repo.partialclone", partial_clone)
4412 if clone_filter:
4413 self.config.SetString("repo.clonefilter", clone_filter)
4414 elif self.partial_clone:
4415 clone_filter = self.clone_filter
4416 else:
4417 clone_filter = None
4418
4419 if partial_clone_exclude is not None:
4420 self.config.SetString(
4421 "repo.partialcloneexclude", partial_clone_exclude
4422 )
4423
4424 if clone_bundle is None:
4425 clone_bundle = False if partial_clone else True
4426 else:
4427 self.config.SetBoolean("repo.clonebundle", clone_bundle)
4428
4429 if submodules:
4430 self.config.SetBoolean("repo.submodules", submodules)
4431
4432 if git_lfs is not None:
4433 if git_lfs:
4434 git_require((2, 17, 0), fail=True, msg="Git LFS support")
4435
4436 self.config.SetBoolean("repo.git-lfs", git_lfs)
4437 if not is_new:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004438 logger.warning(
Gavin Makea2e3302023-03-11 06:46:20 +00004439 "warning: Changing --git-lfs settings will only affect new "
4440 "project checkouts.\n"
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004441 " Existing projects will require manual updates.\n"
Gavin Makea2e3302023-03-11 06:46:20 +00004442 )
4443
Jason Chang17833322023-05-23 13:06:55 -07004444 if clone_filter_for_depth is not None:
4445 self.ConfigureCloneFilterForDepth(clone_filter_for_depth)
4446
Gavin Makea2e3302023-03-11 06:46:20 +00004447 if use_superproject is not None:
4448 self.config.SetBoolean("repo.superproject", use_superproject)
4449
4450 if not standalone_manifest:
4451 success = self.Sync_NetworkHalf(
4452 is_new=is_new,
4453 quiet=not verbose,
4454 verbose=verbose,
4455 clone_bundle=clone_bundle,
4456 current_branch_only=current_branch_only,
4457 tags=tags,
4458 submodules=submodules,
4459 clone_filter=clone_filter,
4460 partial_clone_exclude=self.manifest.PartialCloneExclude,
Jason Chang17833322023-05-23 13:06:55 -07004461 clone_filter_for_depth=self.manifest.CloneFilterForDepth,
Gavin Makea2e3302023-03-11 06:46:20 +00004462 ).success
4463 if not success:
4464 r = self.GetRemote()
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004465 logger.error("fatal: cannot obtain manifest %s", r.url)
Gavin Makea2e3302023-03-11 06:46:20 +00004466
4467 # Better delete the manifest git dir if we created it; otherwise
4468 # next time (when user fixes problems) we won't go through the
4469 # "is_new" logic.
4470 if is_new:
4471 platform_utils.rmtree(self.gitdir)
4472 return False
4473
4474 if manifest_branch:
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08004475 self.MetaBranchSwitch(submodules=submodules, verbose=verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00004476
4477 syncbuf = SyncBuffer(self.config)
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08004478 self.Sync_LocalHalf(syncbuf, submodules=submodules, verbose=verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00004479 syncbuf.Finish()
4480
4481 if is_new or self.CurrentBranch is None:
Jason Chang1a3612f2023-08-08 14:12:53 -07004482 try:
4483 self.StartBranch("default")
4484 except GitError as e:
4485 msg = str(e)
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004486 logger.error(
4487 "fatal: cannot create default in manifest %s", msg
Gavin Makea2e3302023-03-11 06:46:20 +00004488 )
4489 return False
4490
4491 if not manifest_name:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004492 logger.error("fatal: manifest name (-m) is required.")
Gavin Makea2e3302023-03-11 06:46:20 +00004493 return False
4494
4495 elif is_new:
4496 # This is a new standalone manifest.
4497 manifest_name = "default.xml"
4498 manifest_data = fetch.fetch_file(manifest_url, verbose=verbose)
4499 dest = os.path.join(self.worktree, manifest_name)
4500 os.makedirs(os.path.dirname(dest), exist_ok=True)
4501 with open(dest, "wb") as f:
4502 f.write(manifest_data)
4503
4504 try:
4505 self.manifest.Link(manifest_name)
4506 except ManifestParseError as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004507 logger.error("fatal: manifest '%s' not available", manifest_name)
4508 logger.error("fatal: %s", e)
Gavin Makea2e3302023-03-11 06:46:20 +00004509 return False
4510
4511 if not this_manifest_only:
4512 for submanifest in self.manifest.submanifests.values():
4513 spec = submanifest.ToSubmanifestSpec()
4514 submanifest.repo_client.manifestProject.Sync(
4515 manifest_url=spec.manifestUrl,
4516 manifest_branch=spec.revision,
4517 standalone_manifest=standalone_manifest,
4518 groups=self.manifest_groups,
4519 platform=platform,
4520 mirror=mirror,
4521 dissociate=dissociate,
4522 reference=reference,
4523 worktree=worktree,
4524 submodules=submodules,
4525 archive=archive,
4526 partial_clone=partial_clone,
4527 clone_filter=clone_filter,
4528 partial_clone_exclude=partial_clone_exclude,
4529 clone_bundle=clone_bundle,
4530 git_lfs=git_lfs,
4531 use_superproject=use_superproject,
4532 verbose=verbose,
4533 current_branch_only=current_branch_only,
4534 tags=tags,
4535 depth=depth,
4536 git_event_log=git_event_log,
4537 manifest_name=spec.manifestName,
4538 this_manifest_only=False,
4539 outer_manifest=False,
4540 )
4541
4542 # Lastly, if the manifest has a <superproject> then have the
4543 # superproject sync it (if it will be used).
4544 if git_superproject.UseSuperproject(use_superproject, self.manifest):
4545 sync_result = self.manifest.superproject.Sync(git_event_log)
4546 if not sync_result.success:
4547 submanifest = ""
4548 if self.manifest.path_prefix:
4549 submanifest = f"for {self.manifest.path_prefix} "
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004550 logger.warning(
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004551 "warning: git update of superproject %s failed, "
Gavin Makea2e3302023-03-11 06:46:20 +00004552 "repo sync will not use superproject to fetch source; "
4553 "while this error is not fatal, and you can continue to "
4554 "run repo sync, please run repo init with the "
4555 "--no-use-superproject option to stop seeing this warning",
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004556 submanifest,
Gavin Makea2e3302023-03-11 06:46:20 +00004557 )
4558 if sync_result.fatal and use_superproject is not None:
4559 return False
4560
4561 return True
4562
Jason Chang17833322023-05-23 13:06:55 -07004563 def ConfigureCloneFilterForDepth(self, clone_filter_for_depth):
4564 """Configure clone filter to replace shallow clones.
4565
4566 Args:
4567 clone_filter_for_depth: a string or None, e.g. 'blob:none' will
4568 disable shallow clones and replace with partial clone. None will
4569 enable shallow clones.
4570 """
4571 self.config.SetString(
4572 "repo.clonefilterfordepth", clone_filter_for_depth
4573 )
4574
Gavin Makea2e3302023-03-11 06:46:20 +00004575 def _ConfigureDepth(self, depth):
4576 """Configure the depth we'll sync down.
4577
4578 Args:
4579 depth: an int, how deep of a partial clone to create.
4580 """
4581 # Opt.depth will be non-None if user actually passed --depth to repo
4582 # init.
4583 if depth is not None:
4584 if depth > 0:
4585 # Positive values will set the depth.
4586 depth = str(depth)
4587 else:
4588 # Negative numbers will clear the depth; passing None to
4589 # SetString will do that.
4590 depth = None
4591
4592 # We store the depth in the main manifest project.
4593 self.config.SetString("repo.depth", depth)