blob: f4a856f5e20274bac0f796faeaeb9323e4cbd24b [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
Scott Leef1d916b2023-10-30 18:11:37 +000033from error import CacheApplyError
Mike Frysinger64477332023-08-21 21:20:32 -040034from error import DownloadError
35from error import GitError
36from error import ManifestInvalidPathError
37from error import ManifestInvalidRevisionError
38from error import ManifestParseError
39from error import NoManifestException
40from error import RepoError
41from error import UploadError
LaMont Jones0de4fc32022-04-21 17:18:35 +000042import fetch
Mike Frysinger64477332023-08-21 21:20:32 -040043from git_command import git_require
44from git_command import GitCommand
45from git_config import GetSchemeFromUrl
46from git_config import GetUrlCookieFile
47from git_config import GitConfig
Scott Leef1d916b2023-10-30 18:11:37 +000048from git_config import ID_RE
Mike Frysinger64477332023-08-21 21:20:32 -040049from git_config import IsId
Scott Leef1d916b2023-10-30 18:11:37 +000050from git_config import RefSpec
Mike Frysinger64477332023-08-21 21:20:32 -040051from git_refs import GitRefs
52from git_refs import HEAD
53from git_refs import R_HEADS
54from git_refs import R_M
55from git_refs import R_PUB
56from git_refs import R_TAGS
57from git_refs import R_WORKTREE_M
LaMont Jonesff6b1da2022-06-01 21:03:34 +000058import git_superproject
LaMont Jones55ee3042022-04-06 17:10:21 +000059from git_trace2_event_log import EventLog
Renaud Paquayd5cec5e2016-11-01 11:24:03 -070060import platform_utils
Mike Frysinger70d861f2019-08-26 15:22:36 -040061import progress
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +000062from repo_logging import RepoLogger
Joanna Wanga6c52f52022-11-03 16:51:19 -040063from repo_trace import Trace
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070064
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -070065
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +000066logger = RepoLogger(__file__)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070067
Chirayu Desai217ea7d2013-03-01 19:14:38 +053068
LaMont Jones1eddca82022-09-01 15:15:04 +000069class SyncNetworkHalfResult(NamedTuple):
Gavin Makea2e3302023-03-11 06:46:20 +000070 """Sync_NetworkHalf return value."""
71
Gavin Makea2e3302023-03-11 06:46:20 +000072 # Did we query the remote? False when optimized_fetch is True and we have
73 # the commit already present.
74 remote_fetched: bool
Jason Chang32b59562023-07-14 16:45:35 -070075 # Error from SyncNetworkHalf
76 error: Exception = None
77
78 @property
79 def success(self) -> bool:
80 return not self.error
81
82
83class SyncNetworkHalfError(RepoError):
84 """Failure trying to sync."""
85
86
87class DeleteWorktreeError(RepoError):
88 """Failure to delete worktree."""
89
90 def __init__(
91 self, *args, aggregate_errors: List[Exception] = None, **kwargs
92 ) -> None:
93 super().__init__(*args, **kwargs)
94 self.aggregate_errors = aggregate_errors or []
95
96
97class DeleteDirtyWorktreeError(DeleteWorktreeError):
98 """Failure to delete worktree due to uncommitted changes."""
LaMont Jones1eddca82022-09-01 15:15:04 +000099
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +0100100
George Engelbrecht9bc283e2020-04-02 12:36:09 -0600101# Maximum sleep time allowed during retries.
102MAXIMUM_RETRY_SLEEP_SEC = 3600.0
103# +-10% random jitter is added to each Fetches retry sleep duration.
104RETRY_JITTER_PERCENT = 0.1
105
LaMont Jonesfa8d9392022-11-02 22:01:29 +0000106# Whether to use alternates. Switching back and forth is *NOT* supported.
Mike Frysinger1d00a7e2021-12-21 00:40:31 -0500107# TODO(vapier): Remove knob once behavior is verified.
Gavin Makea2e3302023-03-11 06:46:20 +0000108_ALTERNATES = os.environ.get("REPO_USE_ALTERNATES") == "1"
George Engelbrecht9bc283e2020-04-02 12:36:09 -0600109
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +0100110
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700111def _lwrite(path, content):
Gavin Makea2e3302023-03-11 06:46:20 +0000112 lock = "%s.lock" % path
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700113
Gavin Makea2e3302023-03-11 06:46:20 +0000114 # Maintain Unix line endings on all OS's to match git behavior.
115 with open(lock, "w", newline="\n") as fd:
116 fd.write(content)
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700117
Gavin Makea2e3302023-03-11 06:46:20 +0000118 try:
119 platform_utils.rename(lock, path)
120 except OSError:
121 platform_utils.remove(lock)
122 raise
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700123
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700124
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700125def not_rev(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000126 return "^" + r
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700127
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700128
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800129def sq(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000130 return "'" + r.replace("'", "'''") + "'"
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -0800131
David Pursehouse819827a2020-02-12 15:20:19 +0900132
Jonathan Nieder93719792015-03-17 11:29:58 -0700133_project_hook_list = None
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700134
135
Jonathan Nieder93719792015-03-17 11:29:58 -0700136def _ProjectHooks():
Gavin Makea2e3302023-03-11 06:46:20 +0000137 """List the hooks present in the 'hooks' directory.
Jonathan Nieder93719792015-03-17 11:29:58 -0700138
Gavin Makea2e3302023-03-11 06:46:20 +0000139 These hooks are project hooks and are copied to the '.git/hooks' directory
140 of all subprojects.
Jonathan Nieder93719792015-03-17 11:29:58 -0700141
Gavin Makea2e3302023-03-11 06:46:20 +0000142 This function caches the list of hooks (based on the contents of the
143 'repo/hooks' directory) on the first call.
Jonathan Nieder93719792015-03-17 11:29:58 -0700144
Gavin Makea2e3302023-03-11 06:46:20 +0000145 Returns:
146 A list of absolute paths to all of the files in the hooks directory.
147 """
148 global _project_hook_list
149 if _project_hook_list is None:
150 d = platform_utils.realpath(os.path.abspath(os.path.dirname(__file__)))
151 d = os.path.join(d, "hooks")
152 _project_hook_list = [
153 os.path.join(d, x) for x in platform_utils.listdir(d)
154 ]
155 return _project_hook_list
Jonathan Nieder93719792015-03-17 11:29:58 -0700156
157
Mike Frysingerd4aee652023-10-19 05:13:32 -0400158class DownloadedChange:
Gavin Makea2e3302023-03-11 06:46:20 +0000159 _commit_cache = None
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700160
Gavin Makea2e3302023-03-11 06:46:20 +0000161 def __init__(self, project, base, change_id, ps_id, commit):
162 self.project = project
163 self.base = base
164 self.change_id = change_id
165 self.ps_id = ps_id
166 self.commit = commit
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700167
Gavin Makea2e3302023-03-11 06:46:20 +0000168 @property
169 def commits(self):
170 if self._commit_cache is None:
171 self._commit_cache = self.project.bare_git.rev_list(
172 "--abbrev=8",
173 "--abbrev-commit",
174 "--pretty=oneline",
175 "--reverse",
176 "--date-order",
177 not_rev(self.base),
178 self.commit,
179 "--",
180 )
181 return self._commit_cache
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700182
183
Mike Frysingerd4aee652023-10-19 05:13:32 -0400184class ReviewableBranch:
Gavin Makea2e3302023-03-11 06:46:20 +0000185 _commit_cache = None
186 _base_exists = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700187
Gavin Makea2e3302023-03-11 06:46:20 +0000188 def __init__(self, project, branch, base):
189 self.project = project
190 self.branch = branch
191 self.base = base
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700192
Gavin Makea2e3302023-03-11 06:46:20 +0000193 @property
194 def name(self):
195 return self.branch.name
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700196
Gavin Makea2e3302023-03-11 06:46:20 +0000197 @property
198 def commits(self):
199 if self._commit_cache is None:
200 args = (
201 "--abbrev=8",
202 "--abbrev-commit",
203 "--pretty=oneline",
204 "--reverse",
205 "--date-order",
206 not_rev(self.base),
207 R_HEADS + self.name,
208 "--",
209 )
210 try:
Jason Chang87058c62023-09-27 11:34:43 -0700211 self._commit_cache = self.project.bare_git.rev_list(
212 *args, log_as_error=self.base_exists
213 )
Gavin Makea2e3302023-03-11 06:46:20 +0000214 except GitError:
215 # We weren't able to probe the commits for this branch. Was it
216 # tracking a branch that no longer exists? If so, return no
217 # commits. Otherwise, rethrow the error as we don't know what's
218 # going on.
219 if self.base_exists:
220 raise
Mike Frysinger6da17752019-09-11 18:43:17 -0400221
Gavin Makea2e3302023-03-11 06:46:20 +0000222 self._commit_cache = []
Mike Frysinger6da17752019-09-11 18:43:17 -0400223
Gavin Makea2e3302023-03-11 06:46:20 +0000224 return self._commit_cache
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700225
Gavin Makea2e3302023-03-11 06:46:20 +0000226 @property
227 def unabbrev_commits(self):
228 r = dict()
229 for commit in self.project.bare_git.rev_list(
230 not_rev(self.base), R_HEADS + self.name, "--"
231 ):
232 r[commit[0:8]] = commit
233 return r
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800234
Gavin Makea2e3302023-03-11 06:46:20 +0000235 @property
236 def date(self):
237 return self.project.bare_git.log(
238 "--pretty=format:%cd", "-n", "1", R_HEADS + self.name, "--"
239 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700240
Gavin Makea2e3302023-03-11 06:46:20 +0000241 @property
242 def base_exists(self):
243 """Whether the branch we're tracking exists.
Mike Frysinger6da17752019-09-11 18:43:17 -0400244
Gavin Makea2e3302023-03-11 06:46:20 +0000245 Normally it should, but sometimes branches we track can get deleted.
246 """
247 if self._base_exists is None:
248 try:
249 self.project.bare_git.rev_parse("--verify", not_rev(self.base))
250 # If we're still here, the base branch exists.
251 self._base_exists = True
252 except GitError:
253 # If we failed to verify, the base branch doesn't exist.
254 self._base_exists = False
Mike Frysinger6da17752019-09-11 18:43:17 -0400255
Gavin Makea2e3302023-03-11 06:46:20 +0000256 return self._base_exists
Mike Frysinger6da17752019-09-11 18:43:17 -0400257
Gavin Makea2e3302023-03-11 06:46:20 +0000258 def UploadForReview(
259 self,
260 people,
261 dryrun=False,
262 auto_topic=False,
263 hashtags=(),
264 labels=(),
265 private=False,
266 notify=None,
267 wip=False,
268 ready=False,
269 dest_branch=None,
270 validate_certs=True,
271 push_options=None,
272 ):
273 self.project.UploadForReview(
274 branch=self.name,
275 people=people,
276 dryrun=dryrun,
277 auto_topic=auto_topic,
278 hashtags=hashtags,
279 labels=labels,
280 private=private,
281 notify=notify,
282 wip=wip,
283 ready=ready,
284 dest_branch=dest_branch,
285 validate_certs=validate_certs,
286 push_options=push_options,
287 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700288
Gavin Makea2e3302023-03-11 06:46:20 +0000289 def GetPublishedRefs(self):
290 refs = {}
291 output = self.project.bare_git.ls_remote(
292 self.branch.remote.SshReviewUrl(self.project.UserEmail),
293 "refs/changes/*",
294 )
295 for line in output.split("\n"):
296 try:
297 (sha, ref) = line.split()
298 refs[sha] = ref
299 except ValueError:
300 pass
Ficus Kirkpatrickbc7ef672009-05-04 12:45:11 -0700301
Gavin Makea2e3302023-03-11 06:46:20 +0000302 return refs
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700303
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700304
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700305class StatusColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000306 def __init__(self, config):
307 super().__init__(config, "status")
308 self.project = self.printer("header", attr="bold")
309 self.branch = self.printer("header", attr="bold")
310 self.nobranch = self.printer("nobranch", fg="red")
311 self.important = self.printer("important", fg="red")
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700312
Gavin Makea2e3302023-03-11 06:46:20 +0000313 self.added = self.printer("added", fg="green")
314 self.changed = self.printer("changed", fg="red")
315 self.untracked = self.printer("untracked", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700316
317
318class DiffColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000319 def __init__(self, config):
320 super().__init__(config, "diff")
321 self.project = self.printer("header", attr="bold")
322 self.fail = self.printer("fail", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700323
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700324
Mike Frysingerd4aee652023-10-19 05:13:32 -0400325class Annotation:
Gavin Makea2e3302023-03-11 06:46:20 +0000326 def __init__(self, name, value, keep):
327 self.name = name
328 self.value = value
329 self.keep = keep
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700330
Gavin Makea2e3302023-03-11 06:46:20 +0000331 def __eq__(self, other):
332 if not isinstance(other, Annotation):
333 return False
334 return self.__dict__ == other.__dict__
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700335
Gavin Makea2e3302023-03-11 06:46:20 +0000336 def __lt__(self, other):
337 # This exists just so that lists of Annotation objects can be sorted,
338 # for use in comparisons.
339 if not isinstance(other, Annotation):
340 raise ValueError("comparison is not between two Annotation objects")
341 if self.name == other.name:
342 if self.value == other.value:
343 return self.keep < other.keep
344 return self.value < other.value
345 return self.name < other.name
Jack Neus6ea0cae2021-07-20 20:52:33 +0000346
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700347
Mike Frysingere6a202f2019-08-02 15:57:57 -0400348def _SafeExpandPath(base, subpath, skipfinal=False):
Gavin Makea2e3302023-03-11 06:46:20 +0000349 """Make sure |subpath| is completely safe under |base|.
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700350
Gavin Makea2e3302023-03-11 06:46:20 +0000351 We make sure no intermediate symlinks are traversed, and that the final path
352 is not a special file (e.g. not a socket or fifo).
Mike Frysingere6a202f2019-08-02 15:57:57 -0400353
Gavin Makea2e3302023-03-11 06:46:20 +0000354 NB: We rely on a number of paths already being filtered out while parsing
355 the manifest. See the validation logic in manifest_xml.py for more details.
356 """
357 # Split up the path by its components. We can't use os.path.sep exclusively
358 # as some platforms (like Windows) will convert / to \ and that bypasses all
359 # our constructed logic here. Especially since manifest authors only use
360 # / in their paths.
361 resep = re.compile(r"[/%s]" % re.escape(os.path.sep))
362 components = resep.split(subpath)
363 if skipfinal:
364 # Whether the caller handles the final component itself.
365 finalpart = components.pop()
Mike Frysingere6a202f2019-08-02 15:57:57 -0400366
Gavin Makea2e3302023-03-11 06:46:20 +0000367 path = base
368 for part in components:
369 if part in {".", ".."}:
370 raise ManifestInvalidPathError(
371 '%s: "%s" not allowed in paths' % (subpath, part)
372 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400373
Gavin Makea2e3302023-03-11 06:46:20 +0000374 path = os.path.join(path, part)
375 if platform_utils.islink(path):
376 raise ManifestInvalidPathError(
377 "%s: traversing symlinks not allow" % (path,)
378 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400379
Gavin Makea2e3302023-03-11 06:46:20 +0000380 if os.path.exists(path):
381 if not os.path.isfile(path) and not platform_utils.isdir(path):
382 raise ManifestInvalidPathError(
383 "%s: only regular files & directories allowed" % (path,)
384 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400385
Gavin Makea2e3302023-03-11 06:46:20 +0000386 if skipfinal:
387 path = os.path.join(path, finalpart)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400388
Gavin Makea2e3302023-03-11 06:46:20 +0000389 return path
Mike Frysingere6a202f2019-08-02 15:57:57 -0400390
391
Mike Frysingerd4aee652023-10-19 05:13:32 -0400392class _CopyFile:
Gavin Makea2e3302023-03-11 06:46:20 +0000393 """Container for <copyfile> manifest element."""
Mike Frysingere6a202f2019-08-02 15:57:57 -0400394
Gavin Makea2e3302023-03-11 06:46:20 +0000395 def __init__(self, git_worktree, src, topdir, dest):
396 """Register a <copyfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400397
Gavin Makea2e3302023-03-11 06:46:20 +0000398 Args:
399 git_worktree: Absolute path to the git project checkout.
400 src: Relative path under |git_worktree| of file to read.
401 topdir: Absolute path to the top of the repo client checkout.
402 dest: Relative path under |topdir| of file to write.
403 """
404 self.git_worktree = git_worktree
405 self.topdir = topdir
406 self.src = src
407 self.dest = dest
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700408
Gavin Makea2e3302023-03-11 06:46:20 +0000409 def _Copy(self):
410 src = _SafeExpandPath(self.git_worktree, self.src)
411 dest = _SafeExpandPath(self.topdir, self.dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400412
Gavin Makea2e3302023-03-11 06:46:20 +0000413 if platform_utils.isdir(src):
414 raise ManifestInvalidPathError(
415 "%s: copying from directory not supported" % (self.src,)
416 )
417 if platform_utils.isdir(dest):
418 raise ManifestInvalidPathError(
419 "%s: copying to directory not allowed" % (self.dest,)
420 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400421
Gavin Makea2e3302023-03-11 06:46:20 +0000422 # Copy file if it does not exist or is out of date.
423 if not os.path.exists(dest) or not filecmp.cmp(src, dest):
424 try:
425 # Remove existing file first, since it might be read-only.
426 if os.path.exists(dest):
427 platform_utils.remove(dest)
428 else:
429 dest_dir = os.path.dirname(dest)
430 if not platform_utils.isdir(dest_dir):
431 os.makedirs(dest_dir)
432 shutil.copy(src, dest)
433 # Make the file read-only.
434 mode = os.stat(dest)[stat.ST_MODE]
435 mode = mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
436 os.chmod(dest, mode)
Jason R. Coombsae824fb2023-10-20 23:32:40 +0545437 except OSError:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +0000438 logger.error("error: Cannot copy file %s to %s", src, dest)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700439
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700440
Mike Frysingerd4aee652023-10-19 05:13:32 -0400441class _LinkFile:
Gavin Makea2e3302023-03-11 06:46:20 +0000442 """Container for <linkfile> manifest element."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700443
Gavin Makea2e3302023-03-11 06:46:20 +0000444 def __init__(self, git_worktree, src, topdir, dest):
445 """Register a <linkfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400446
Gavin Makea2e3302023-03-11 06:46:20 +0000447 Args:
448 git_worktree: Absolute path to the git project checkout.
449 src: Target of symlink relative to path under |git_worktree|.
450 topdir: Absolute path to the top of the repo client checkout.
451 dest: Relative path under |topdir| of symlink to create.
452 """
453 self.git_worktree = git_worktree
454 self.topdir = topdir
455 self.src = src
456 self.dest = dest
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500457
Gavin Makea2e3302023-03-11 06:46:20 +0000458 def __linkIt(self, relSrc, absDest):
459 # Link file if it does not exist or is out of date.
460 if not platform_utils.islink(absDest) or (
461 platform_utils.readlink(absDest) != relSrc
462 ):
463 try:
464 # Remove existing file first, since it might be read-only.
465 if os.path.lexists(absDest):
466 platform_utils.remove(absDest)
467 else:
468 dest_dir = os.path.dirname(absDest)
469 if not platform_utils.isdir(dest_dir):
470 os.makedirs(dest_dir)
471 platform_utils.symlink(relSrc, absDest)
Jason R. Coombsae824fb2023-10-20 23:32:40 +0545472 except OSError:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +0000473 logger.error(
474 "error: Cannot link file %s to %s", relSrc, absDest
475 )
Gavin Makea2e3302023-03-11 06:46:20 +0000476
477 def _Link(self):
478 """Link the self.src & self.dest paths.
479
480 Handles wild cards on the src linking all of the files in the source in
481 to the destination directory.
482 """
483 # Some people use src="." to create stable links to projects. Let's
484 # allow that but reject all other uses of "." to keep things simple.
485 if self.src == ".":
486 src = self.git_worktree
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500487 else:
Gavin Makea2e3302023-03-11 06:46:20 +0000488 src = _SafeExpandPath(self.git_worktree, self.src)
Wink Saville4c426ef2015-06-03 08:05:17 -0700489
Gavin Makea2e3302023-03-11 06:46:20 +0000490 if not glob.has_magic(src):
491 # Entity does not contain a wild card so just a simple one to one
492 # link operation.
493 dest = _SafeExpandPath(self.topdir, self.dest, skipfinal=True)
494 # dest & src are absolute paths at this point. Make sure the target
495 # of the symlink is relative in the context of the repo client
496 # checkout.
497 relpath = os.path.relpath(src, os.path.dirname(dest))
498 self.__linkIt(relpath, dest)
499 else:
500 dest = _SafeExpandPath(self.topdir, self.dest)
501 # Entity contains a wild card.
502 if os.path.exists(dest) and not platform_utils.isdir(dest):
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +0000503 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +0000504 "Link error: src with wildcard, %s must be a directory",
505 dest,
506 )
507 else:
508 for absSrcFile in glob.glob(src):
509 # Create a releative path from source dir to destination
510 # dir.
511 absSrcDir = os.path.dirname(absSrcFile)
512 relSrcDir = os.path.relpath(absSrcDir, dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400513
Gavin Makea2e3302023-03-11 06:46:20 +0000514 # Get the source file name.
515 srcFile = os.path.basename(absSrcFile)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400516
Gavin Makea2e3302023-03-11 06:46:20 +0000517 # Now form the final full paths to srcFile. They will be
518 # absolute for the desintaiton and relative for the source.
519 absDest = os.path.join(dest, srcFile)
520 relSrc = os.path.join(relSrcDir, srcFile)
521 self.__linkIt(relSrc, absDest)
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500522
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700523
Mike Frysingerd4aee652023-10-19 05:13:32 -0400524class RemoteSpec:
Gavin Makea2e3302023-03-11 06:46:20 +0000525 def __init__(
526 self,
527 name,
528 url=None,
529 pushUrl=None,
530 review=None,
531 revision=None,
532 orig_name=None,
533 fetchUrl=None,
534 ):
535 self.name = name
536 self.url = url
537 self.pushUrl = pushUrl
538 self.review = review
539 self.revision = revision
540 self.orig_name = orig_name
541 self.fetchUrl = fetchUrl
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700542
Ian Kasprzak0286e312021-02-05 10:06:18 -0800543
Mike Frysingerd4aee652023-10-19 05:13:32 -0400544class Project:
Gavin Makea2e3302023-03-11 06:46:20 +0000545 # These objects can be shared between several working trees.
546 @property
547 def shareable_dirs(self):
548 """Return the shareable directories"""
549 if self.UseAlternates:
550 return ["hooks", "rr-cache"]
551 else:
552 return ["hooks", "objects", "rr-cache"]
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700553
Gavin Makea2e3302023-03-11 06:46:20 +0000554 def __init__(
555 self,
556 manifest,
557 name,
558 remote,
559 gitdir,
560 objdir,
561 worktree,
562 relpath,
563 revisionExpr,
564 revisionId,
565 rebase=True,
566 groups=None,
567 sync_c=False,
568 sync_s=False,
569 sync_tags=True,
570 clone_depth=None,
571 upstream=None,
572 parent=None,
573 use_git_worktrees=False,
574 is_derived=False,
575 dest_branch=None,
576 optimized_fetch=False,
577 retry_fetches=0,
578 old_revision=None,
579 ):
580 """Init a Project object.
Doug Anderson3ba5f952011-04-07 12:51:04 -0700581
582 Args:
Gavin Makea2e3302023-03-11 06:46:20 +0000583 manifest: The XmlManifest object.
584 name: The `name` attribute of manifest.xml's project element.
585 remote: RemoteSpec object specifying its remote's properties.
586 gitdir: Absolute path of git directory.
587 objdir: Absolute path of directory to store git objects.
588 worktree: Absolute path of git working tree.
589 relpath: Relative path of git working tree to repo's top directory.
590 revisionExpr: The `revision` attribute of manifest.xml's project
591 element.
592 revisionId: git commit id for checking out.
593 rebase: The `rebase` attribute of manifest.xml's project element.
594 groups: The `groups` attribute of manifest.xml's project element.
595 sync_c: The `sync-c` attribute of manifest.xml's project element.
596 sync_s: The `sync-s` attribute of manifest.xml's project element.
597 sync_tags: The `sync-tags` attribute of manifest.xml's project
598 element.
599 upstream: The `upstream` attribute of manifest.xml's project
600 element.
601 parent: The parent Project object.
602 use_git_worktrees: Whether to use `git worktree` for this project.
603 is_derived: False if the project was explicitly defined in the
604 manifest; True if the project is a discovered submodule.
605 dest_branch: The branch to which to push changes for review by
606 default.
607 optimized_fetch: If True, when a project is set to a sha1 revision,
608 only fetch from the remote if the sha1 is not present locally.
609 retry_fetches: Retry remote fetches n times upon receiving transient
610 error with exponential backoff and jitter.
611 old_revision: saved git commit id for open GITC projects.
612 """
613 self.client = self.manifest = manifest
614 self.name = name
615 self.remote = remote
616 self.UpdatePaths(relpath, worktree, gitdir, objdir)
617 self.SetRevision(revisionExpr, revisionId=revisionId)
618
619 self.rebase = rebase
620 self.groups = groups
621 self.sync_c = sync_c
622 self.sync_s = sync_s
623 self.sync_tags = sync_tags
624 self.clone_depth = clone_depth
625 self.upstream = upstream
626 self.parent = parent
627 # NB: Do not use this setting in __init__ to change behavior so that the
628 # manifest.git checkout can inspect & change it after instantiating.
629 # See the XmlManifest init code for more info.
630 self.use_git_worktrees = use_git_worktrees
631 self.is_derived = is_derived
632 self.optimized_fetch = optimized_fetch
633 self.retry_fetches = max(0, retry_fetches)
634 self.subprojects = []
635
636 self.snapshots = {}
637 self.copyfiles = []
638 self.linkfiles = []
639 self.annotations = []
640 self.dest_branch = dest_branch
641 self.old_revision = old_revision
642
643 # This will be filled in if a project is later identified to be the
644 # project containing repo hooks.
645 self.enabled_repo_hooks = []
646
647 def RelPath(self, local=True):
648 """Return the path for the project relative to a manifest.
649
650 Args:
651 local: a boolean, if True, the path is relative to the local
652 (sub)manifest. If false, the path is relative to the outermost
653 manifest.
654 """
655 if local:
656 return self.relpath
657 return os.path.join(self.manifest.path_prefix, self.relpath)
658
659 def SetRevision(self, revisionExpr, revisionId=None):
660 """Set revisionId based on revision expression and id"""
661 self.revisionExpr = revisionExpr
662 if revisionId is None and revisionExpr and IsId(revisionExpr):
663 self.revisionId = self.revisionExpr
664 else:
665 self.revisionId = revisionId
666
667 def UpdatePaths(self, relpath, worktree, gitdir, objdir):
668 """Update paths used by this project"""
669 self.gitdir = gitdir.replace("\\", "/")
670 self.objdir = objdir.replace("\\", "/")
671 if worktree:
672 self.worktree = os.path.normpath(worktree).replace("\\", "/")
673 else:
674 self.worktree = None
675 self.relpath = relpath
676
677 self.config = GitConfig.ForRepository(
678 gitdir=self.gitdir, defaults=self.manifest.globalConfig
679 )
680
681 if self.worktree:
682 self.work_git = self._GitGetByExec(
683 self, bare=False, gitdir=self.gitdir
684 )
685 else:
686 self.work_git = None
687 self.bare_git = self._GitGetByExec(self, bare=True, gitdir=self.gitdir)
688 self.bare_ref = GitRefs(self.gitdir)
689 self.bare_objdir = self._GitGetByExec(
690 self, bare=True, gitdir=self.objdir
691 )
692
693 @property
694 def UseAlternates(self):
695 """Whether git alternates are in use.
696
697 This will be removed once migration to alternates is complete.
698 """
699 return _ALTERNATES or self.manifest.is_multimanifest
700
701 @property
702 def Derived(self):
703 return self.is_derived
704
705 @property
706 def Exists(self):
707 return platform_utils.isdir(self.gitdir) and platform_utils.isdir(
708 self.objdir
709 )
710
711 @property
712 def CurrentBranch(self):
713 """Obtain the name of the currently checked out branch.
714
715 The branch name omits the 'refs/heads/' prefix.
716 None is returned if the project is on a detached HEAD, or if the
717 work_git is otheriwse inaccessible (e.g. an incomplete sync).
718 """
719 try:
720 b = self.work_git.GetHead()
721 except NoManifestException:
722 # If the local checkout is in a bad state, don't barf. Let the
723 # callers process this like the head is unreadable.
724 return None
725 if b.startswith(R_HEADS):
726 return b[len(R_HEADS) :]
727 return None
728
729 def IsRebaseInProgress(self):
730 return (
731 os.path.exists(self.work_git.GetDotgitPath("rebase-apply"))
732 or os.path.exists(self.work_git.GetDotgitPath("rebase-merge"))
733 or os.path.exists(os.path.join(self.worktree, ".dotest"))
734 )
735
736 def IsDirty(self, consider_untracked=True):
737 """Is the working directory modified in some way?"""
738 self.work_git.update_index(
739 "-q", "--unmerged", "--ignore-missing", "--refresh"
740 )
741 if self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD):
742 return True
743 if self.work_git.DiffZ("diff-files"):
744 return True
745 if consider_untracked and self.UntrackedFiles():
746 return True
747 return False
748
749 _userident_name = None
750 _userident_email = None
751
752 @property
753 def UserName(self):
754 """Obtain the user's personal name."""
755 if self._userident_name is None:
756 self._LoadUserIdentity()
757 return self._userident_name
758
759 @property
760 def UserEmail(self):
761 """Obtain the user's email address. This is very likely
762 to be their Gerrit login.
763 """
764 if self._userident_email is None:
765 self._LoadUserIdentity()
766 return self._userident_email
767
768 def _LoadUserIdentity(self):
769 u = self.bare_git.var("GIT_COMMITTER_IDENT")
770 m = re.compile("^(.*) <([^>]*)> ").match(u)
771 if m:
772 self._userident_name = m.group(1)
773 self._userident_email = m.group(2)
774 else:
775 self._userident_name = ""
776 self._userident_email = ""
777
778 def GetRemote(self, name=None):
779 """Get the configuration for a single remote.
780
781 Defaults to the current project's remote.
782 """
783 if name is None:
784 name = self.remote.name
785 return self.config.GetRemote(name)
786
787 def GetBranch(self, name):
788 """Get the configuration for a single branch."""
789 return self.config.GetBranch(name)
790
791 def GetBranches(self):
792 """Get all existing local branches."""
793 current = self.CurrentBranch
794 all_refs = self._allrefs
795 heads = {}
796
797 for name, ref_id in all_refs.items():
798 if name.startswith(R_HEADS):
799 name = name[len(R_HEADS) :]
800 b = self.GetBranch(name)
801 b.current = name == current
802 b.published = None
803 b.revision = ref_id
804 heads[name] = b
805
806 for name, ref_id in all_refs.items():
807 if name.startswith(R_PUB):
808 name = name[len(R_PUB) :]
809 b = heads.get(name)
810 if b:
811 b.published = ref_id
812
813 return heads
814
815 def MatchesGroups(self, manifest_groups):
816 """Returns true if the manifest groups specified at init should cause
817 this project to be synced.
818 Prefixing a manifest group with "-" inverts the meaning of a group.
819 All projects are implicitly labelled with "all".
820
821 labels are resolved in order. In the example case of
822 project_groups: "all,group1,group2"
823 manifest_groups: "-group1,group2"
824 the project will be matched.
825
826 The special manifest group "default" will match any project that
827 does not have the special project group "notdefault"
828 """
829 default_groups = self.manifest.default_groups or ["default"]
830 expanded_manifest_groups = manifest_groups or default_groups
831 expanded_project_groups = ["all"] + (self.groups or [])
832 if "notdefault" not in expanded_project_groups:
833 expanded_project_groups += ["default"]
834
835 matched = False
836 for group in expanded_manifest_groups:
837 if group.startswith("-") and group[1:] in expanded_project_groups:
838 matched = False
839 elif group in expanded_project_groups:
840 matched = True
841
842 return matched
843
844 def UncommitedFiles(self, get_all=True):
845 """Returns a list of strings, uncommitted files in the git tree.
846
847 Args:
848 get_all: a boolean, if True - get information about all different
849 uncommitted files. If False - return as soon as any kind of
850 uncommitted files is detected.
851 """
852 details = []
853 self.work_git.update_index(
854 "-q", "--unmerged", "--ignore-missing", "--refresh"
855 )
856 if self.IsRebaseInProgress():
857 details.append("rebase in progress")
858 if not get_all:
859 return details
860
861 changes = self.work_git.DiffZ("diff-index", "--cached", HEAD).keys()
862 if changes:
863 details.extend(changes)
864 if not get_all:
865 return details
866
867 changes = self.work_git.DiffZ("diff-files").keys()
868 if changes:
869 details.extend(changes)
870 if not get_all:
871 return details
872
873 changes = self.UntrackedFiles()
874 if changes:
875 details.extend(changes)
876
877 return details
878
879 def UntrackedFiles(self):
880 """Returns a list of strings, untracked files in the git tree."""
881 return self.work_git.LsOthers()
882
883 def HasChanges(self):
884 """Returns true if there are uncommitted changes."""
885 return bool(self.UncommitedFiles(get_all=False))
886
887 def PrintWorkTreeStatus(self, output_redir=None, quiet=False, local=False):
888 """Prints the status of the repository to stdout.
889
890 Args:
891 output_redir: If specified, redirect the output to this object.
892 quiet: If True then only print the project name. Do not print
893 the modified files, branch name, etc.
894 local: a boolean, if True, the path is relative to the local
895 (sub)manifest. If false, the path is relative to the outermost
896 manifest.
897 """
898 if not platform_utils.isdir(self.worktree):
899 if output_redir is None:
900 output_redir = sys.stdout
901 print(file=output_redir)
902 print("project %s/" % self.RelPath(local), file=output_redir)
903 print(' missing (run "repo sync")', file=output_redir)
904 return
905
906 self.work_git.update_index(
907 "-q", "--unmerged", "--ignore-missing", "--refresh"
908 )
909 rb = self.IsRebaseInProgress()
910 di = self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD)
911 df = self.work_git.DiffZ("diff-files")
912 do = self.work_git.LsOthers()
913 if not rb and not di and not df and not do and not self.CurrentBranch:
914 return "CLEAN"
915
916 out = StatusColoring(self.config)
917 if output_redir is not None:
918 out.redirect(output_redir)
919 out.project("project %-40s", self.RelPath(local) + "/ ")
920
921 if quiet:
922 out.nl()
923 return "DIRTY"
924
925 branch = self.CurrentBranch
926 if branch is None:
927 out.nobranch("(*** NO BRANCH ***)")
928 else:
929 out.branch("branch %s", branch)
930 out.nl()
931
932 if rb:
933 out.important("prior sync failed; rebase still in progress")
934 out.nl()
935
936 paths = list()
937 paths.extend(di.keys())
938 paths.extend(df.keys())
939 paths.extend(do)
940
941 for p in sorted(set(paths)):
942 try:
943 i = di[p]
944 except KeyError:
945 i = None
946
947 try:
948 f = df[p]
949 except KeyError:
950 f = None
951
952 if i:
953 i_status = i.status.upper()
954 else:
955 i_status = "-"
956
957 if f:
958 f_status = f.status.lower()
959 else:
960 f_status = "-"
961
962 if i and i.src_path:
963 line = " %s%s\t%s => %s (%s%%)" % (
964 i_status,
965 f_status,
966 i.src_path,
967 p,
968 i.level,
969 )
970 else:
971 line = " %s%s\t%s" % (i_status, f_status, p)
972
973 if i and not f:
974 out.added("%s", line)
975 elif (i and f) or (not i and f):
976 out.changed("%s", line)
977 elif not i and not f:
978 out.untracked("%s", line)
979 else:
980 out.write("%s", line)
981 out.nl()
982
983 return "DIRTY"
984
985 def PrintWorkTreeDiff(
986 self, absolute_paths=False, output_redir=None, local=False
987 ):
988 """Prints the status of the repository to stdout."""
989 out = DiffColoring(self.config)
990 if output_redir:
991 out.redirect(output_redir)
992 cmd = ["diff"]
993 if out.is_on:
994 cmd.append("--color")
995 cmd.append(HEAD)
996 if absolute_paths:
997 cmd.append("--src-prefix=a/%s/" % self.RelPath(local))
998 cmd.append("--dst-prefix=b/%s/" % self.RelPath(local))
999 cmd.append("--")
1000 try:
1001 p = GitCommand(self, cmd, capture_stdout=True, capture_stderr=True)
1002 p.Wait()
1003 except GitError as e:
1004 out.nl()
1005 out.project("project %s/" % self.RelPath(local))
1006 out.nl()
1007 out.fail("%s", str(e))
1008 out.nl()
1009 return False
1010 if p.stdout:
1011 out.nl()
1012 out.project("project %s/" % self.RelPath(local))
1013 out.nl()
1014 out.write("%s", p.stdout)
1015 return p.Wait() == 0
1016
1017 def WasPublished(self, branch, all_refs=None):
1018 """Was the branch published (uploaded) for code review?
1019 If so, returns the SHA-1 hash of the last published
1020 state for the branch.
1021 """
1022 key = R_PUB + branch
1023 if all_refs is None:
1024 try:
1025 return self.bare_git.rev_parse(key)
1026 except GitError:
1027 return None
1028 else:
1029 try:
1030 return all_refs[key]
1031 except KeyError:
1032 return None
1033
1034 def CleanPublishedCache(self, all_refs=None):
1035 """Prunes any stale published refs."""
1036 if all_refs is None:
1037 all_refs = self._allrefs
1038 heads = set()
1039 canrm = {}
1040 for name, ref_id in all_refs.items():
1041 if name.startswith(R_HEADS):
1042 heads.add(name)
1043 elif name.startswith(R_PUB):
1044 canrm[name] = ref_id
1045
1046 for name, ref_id in canrm.items():
1047 n = name[len(R_PUB) :]
1048 if R_HEADS + n not in heads:
1049 self.bare_git.DeleteRef(name, ref_id)
1050
1051 def GetUploadableBranches(self, selected_branch=None):
1052 """List any branches which can be uploaded for review."""
1053 heads = {}
1054 pubed = {}
1055
1056 for name, ref_id in self._allrefs.items():
1057 if name.startswith(R_HEADS):
1058 heads[name[len(R_HEADS) :]] = ref_id
1059 elif name.startswith(R_PUB):
1060 pubed[name[len(R_PUB) :]] = ref_id
1061
1062 ready = []
1063 for branch, ref_id in heads.items():
1064 if branch in pubed and pubed[branch] == ref_id:
1065 continue
1066 if selected_branch and branch != selected_branch:
1067 continue
1068
1069 rb = self.GetUploadableBranch(branch)
1070 if rb:
1071 ready.append(rb)
1072 return ready
1073
1074 def GetUploadableBranch(self, branch_name):
1075 """Get a single uploadable branch, or None."""
1076 branch = self.GetBranch(branch_name)
1077 base = branch.LocalMerge
1078 if branch.LocalMerge:
1079 rb = ReviewableBranch(self, branch, base)
1080 if rb.commits:
1081 return rb
1082 return None
1083
1084 def UploadForReview(
1085 self,
1086 branch=None,
1087 people=([], []),
1088 dryrun=False,
1089 auto_topic=False,
1090 hashtags=(),
1091 labels=(),
1092 private=False,
1093 notify=None,
1094 wip=False,
1095 ready=False,
1096 dest_branch=None,
1097 validate_certs=True,
1098 push_options=None,
1099 ):
1100 """Uploads the named branch for code review."""
1101 if branch is None:
1102 branch = self.CurrentBranch
1103 if branch is None:
Jason Chang32b59562023-07-14 16:45:35 -07001104 raise GitError("not currently on a branch", project=self.name)
Gavin Makea2e3302023-03-11 06:46:20 +00001105
1106 branch = self.GetBranch(branch)
1107 if not branch.LocalMerge:
Jason Chang32b59562023-07-14 16:45:35 -07001108 raise GitError(
1109 "branch %s does not track a remote" % branch.name,
1110 project=self.name,
1111 )
Gavin Makea2e3302023-03-11 06:46:20 +00001112 if not branch.remote.review:
Jason Chang32b59562023-07-14 16:45:35 -07001113 raise GitError(
1114 "remote %s has no review url" % branch.remote.name,
1115 project=self.name,
1116 )
Gavin Makea2e3302023-03-11 06:46:20 +00001117
1118 # Basic validity check on label syntax.
1119 for label in labels:
1120 if not re.match(r"^.+[+-][0-9]+$", label):
1121 raise UploadError(
1122 f'invalid label syntax "{label}": labels use forms like '
Jason Chang5a3a5f72023-08-17 11:36:41 -07001123 "CodeReview+1 or Verified-1",
1124 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00001125 )
1126
1127 if dest_branch is None:
1128 dest_branch = self.dest_branch
1129 if dest_branch is None:
1130 dest_branch = branch.merge
1131 if not dest_branch.startswith(R_HEADS):
1132 dest_branch = R_HEADS + dest_branch
1133
1134 if not branch.remote.projectname:
1135 branch.remote.projectname = self.name
1136 branch.remote.Save()
1137
1138 url = branch.remote.ReviewUrl(self.UserEmail, validate_certs)
1139 if url is None:
Jason Chang5a3a5f72023-08-17 11:36:41 -07001140 raise UploadError("review not configured", project=self.name)
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +00001141 cmd = ["push", "--progress"]
Gavin Makea2e3302023-03-11 06:46:20 +00001142 if dryrun:
1143 cmd.append("-n")
1144
1145 if url.startswith("ssh://"):
1146 cmd.append("--receive-pack=gerrit receive-pack")
1147
Aravind Vasudevan99ebf622023-04-04 23:44:37 +00001148 # This stops git from pushing all reachable annotated tags when
1149 # push.followTags is configured. Gerrit does not accept any tags
1150 # pushed to a CL.
1151 if git_require((1, 8, 3)):
1152 cmd.append("--no-follow-tags")
1153
Gavin Makea2e3302023-03-11 06:46:20 +00001154 for push_option in push_options or []:
1155 cmd.append("-o")
1156 cmd.append(push_option)
1157
1158 cmd.append(url)
1159
1160 if dest_branch.startswith(R_HEADS):
1161 dest_branch = dest_branch[len(R_HEADS) :]
1162
1163 ref_spec = "%s:refs/for/%s" % (R_HEADS + branch.name, dest_branch)
1164 opts = []
1165 if auto_topic:
1166 opts += ["topic=" + branch.name]
1167 opts += ["t=%s" % p for p in hashtags]
1168 # NB: No need to encode labels as they've been validated above.
1169 opts += ["l=%s" % p for p in labels]
1170
1171 opts += ["r=%s" % p for p in people[0]]
1172 opts += ["cc=%s" % p for p in people[1]]
1173 if notify:
1174 opts += ["notify=" + notify]
1175 if private:
1176 opts += ["private"]
1177 if wip:
1178 opts += ["wip"]
1179 if ready:
1180 opts += ["ready"]
1181 if opts:
1182 ref_spec = ref_spec + "%" + ",".join(opts)
1183 cmd.append(ref_spec)
1184
Jason Chang1e9f7b92023-08-25 10:31:04 -07001185 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001186
1187 if not dryrun:
1188 msg = "posted to %s for %s" % (branch.remote.review, dest_branch)
1189 self.bare_git.UpdateRef(
1190 R_PUB + branch.name, R_HEADS + branch.name, message=msg
1191 )
1192
1193 def _ExtractArchive(self, tarpath, path=None):
1194 """Extract the given tar on its current location
1195
1196 Args:
1197 tarpath: The path to the actual tar file
1198
1199 """
1200 try:
1201 with tarfile.open(tarpath, "r") as tar:
1202 tar.extractall(path=path)
1203 return True
Jason R. Coombsae824fb2023-10-20 23:32:40 +05451204 except (OSError, tarfile.TarError) as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001205 logger.error("error: Cannot extract archive %s: %s", tarpath, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001206 return False
1207
Gavin Mak4feff3b2023-05-16 21:31:10 +00001208 def CachePopulate(self, cache_dir, url):
1209 """Populate cache in the cache_dir.
1210
1211 Args:
1212 cache_dir: Directory to cache git files from Google Storage.
1213 url: Git url of current repository.
1214
1215 Raises:
1216 CacheApplyError if it fails to populate the git cache.
1217 """
1218 cmd = [
1219 "cache",
1220 "populate",
1221 "--ignore_locks",
1222 "-v",
1223 "--cache-dir",
1224 cache_dir,
1225 url,
1226 ]
1227
1228 if GitCommand(self, cmd, cwd=cache_dir).Wait() != 0:
1229 raise CacheApplyError(
1230 "Failed to populate cache. cache_dir: %s "
1231 "url: %s" % (cache_dir, url)
1232 )
1233
1234 def CacheExists(self, cache_dir, url):
1235 """Check the existence of the cache files.
1236
1237 Args:
1238 cache_dir: Directory to cache git files.
1239 url: Git url of current repository.
1240
1241 Raises:
1242 CacheApplyError if the cache files do not exist.
1243 """
1244 cmd = ["cache", "exists", "--quiet", "--cache-dir", cache_dir, url]
1245
1246 exist = GitCommand(self, cmd, cwd=self.gitdir, capture_stdout=True)
1247 if exist.Wait() != 0:
1248 raise CacheApplyError(
1249 "Failed to execute git cache exists cmd. "
1250 "cache_dir: %s url: %s" % (cache_dir, url)
1251 )
1252
1253 if not exist.stdout or not exist.stdout.strip():
1254 raise CacheApplyError(
1255 "Failed to find cache. cache_dir: %s "
1256 "url: %s" % (cache_dir, url)
1257 )
1258 return exist.stdout.strip()
1259
1260 def CacheApply(self, cache_dir):
1261 """Apply git cache files populated from Google Storage buckets.
1262
1263 Args:
1264 cache_dir: Directory to cache git files.
1265
1266 Raises:
1267 CacheApplyError if it fails to apply git caches.
1268 """
1269 remote = self.GetRemote(self.remote.name)
1270
1271 self.CachePopulate(cache_dir, remote.url)
1272
1273 mirror_dir = self.CacheExists(cache_dir, remote.url)
1274
1275 refspec = RefSpec(
1276 True, "refs/heads/*", "refs/remotes/%s/*" % remote.name
1277 )
1278
1279 fetch_cache_cmd = ["fetch", mirror_dir, str(refspec)]
1280 if GitCommand(self, fetch_cache_cmd, self.gitdir).Wait() != 0:
1281 raise CacheApplyError(
1282 "Failed to fetch refs %s from %s" % (mirror_dir, str(refspec))
1283 )
1284
Gavin Makea2e3302023-03-11 06:46:20 +00001285 def Sync_NetworkHalf(
1286 self,
1287 quiet=False,
1288 verbose=False,
1289 output_redir=None,
1290 is_new=None,
1291 current_branch_only=None,
1292 force_sync=False,
1293 clone_bundle=True,
1294 tags=None,
1295 archive=False,
1296 optimized_fetch=False,
1297 retry_fetches=0,
1298 prune=False,
1299 submodules=False,
Gavin Mak4feff3b2023-05-16 21:31:10 +00001300 cache_dir=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001301 ssh_proxy=None,
1302 clone_filter=None,
1303 partial_clone_exclude=set(),
Jason Chang17833322023-05-23 13:06:55 -07001304 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001305 ):
1306 """Perform only the network IO portion of the sync process.
1307 Local working directory/branch state is not affected.
1308 """
1309 if archive and not isinstance(self, MetaProject):
1310 if self.remote.url.startswith(("http://", "https://")):
Jason Chang32b59562023-07-14 16:45:35 -07001311 msg_template = (
1312 "%s: Cannot fetch archives from http/https remotes."
Gavin Makea2e3302023-03-11 06:46:20 +00001313 )
Jason Chang32b59562023-07-14 16:45:35 -07001314 msg_args = self.name
1315 msg = msg_template % msg_args
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001316 logger.error(msg_template, msg_args)
Jason Chang32b59562023-07-14 16:45:35 -07001317 return SyncNetworkHalfResult(
1318 False, SyncNetworkHalfError(msg, project=self.name)
1319 )
Gavin Makea2e3302023-03-11 06:46:20 +00001320
1321 name = self.relpath.replace("\\", "/")
1322 name = name.replace("/", "_")
1323 tarpath = "%s.tar" % name
1324 topdir = self.manifest.topdir
1325
1326 try:
1327 self._FetchArchive(tarpath, cwd=topdir)
1328 except GitError as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001329 logger.error("error: %s", e)
Jason Chang32b59562023-07-14 16:45:35 -07001330 return SyncNetworkHalfResult(False, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001331
1332 # From now on, we only need absolute tarpath.
1333 tarpath = os.path.join(topdir, tarpath)
1334
1335 if not self._ExtractArchive(tarpath, path=topdir):
Jason Chang32b59562023-07-14 16:45:35 -07001336 return SyncNetworkHalfResult(
1337 True,
1338 SyncNetworkHalfError(
1339 f"Unable to Extract Archive {tarpath}",
1340 project=self.name,
1341 ),
1342 )
Gavin Makea2e3302023-03-11 06:46:20 +00001343 try:
1344 platform_utils.remove(tarpath)
1345 except OSError as e:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00001346 logger.warning("warn: Cannot remove archive %s: %s", tarpath, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001347 self._CopyAndLinkFiles()
Jason Chang32b59562023-07-14 16:45:35 -07001348 return SyncNetworkHalfResult(True)
Gavin Makea2e3302023-03-11 06:46:20 +00001349
1350 # If the shared object dir already exists, don't try to rebootstrap with
1351 # a clone bundle download. We should have the majority of objects
1352 # already.
1353 if clone_bundle and os.path.exists(self.objdir):
1354 clone_bundle = False
1355
1356 if self.name in partial_clone_exclude:
1357 clone_bundle = True
1358 clone_filter = None
1359
1360 if is_new is None:
1361 is_new = not self.Exists
1362 if is_new:
1363 self._InitGitDir(force_sync=force_sync, quiet=quiet)
1364 else:
1365 self._UpdateHooks(quiet=quiet)
1366 self._InitRemote()
1367
1368 if self.UseAlternates:
1369 # If gitdir/objects is a symlink, migrate it from the old layout.
1370 gitdir_objects = os.path.join(self.gitdir, "objects")
1371 if platform_utils.islink(gitdir_objects):
1372 platform_utils.remove(gitdir_objects, missing_ok=True)
1373 gitdir_alt = os.path.join(self.gitdir, "objects/info/alternates")
1374 if not os.path.exists(gitdir_alt):
1375 os.makedirs(os.path.dirname(gitdir_alt), exist_ok=True)
1376 _lwrite(
1377 gitdir_alt,
1378 os.path.join(
1379 os.path.relpath(self.objdir, gitdir_objects), "objects"
1380 )
1381 + "\n",
1382 )
1383
1384 if is_new:
1385 alt = os.path.join(self.objdir, "objects/info/alternates")
1386 try:
1387 with open(alt) as fd:
1388 # This works for both absolute and relative alternate
1389 # directories.
1390 alt_dir = os.path.join(
1391 self.objdir, "objects", fd.readline().rstrip()
1392 )
Jason R. Coombsae824fb2023-10-20 23:32:40 +05451393 except OSError:
Gavin Makea2e3302023-03-11 06:46:20 +00001394 alt_dir = None
1395 else:
1396 alt_dir = None
1397
Gavin Mak4feff3b2023-05-16 21:31:10 +00001398 applied_cache = False
1399 # If cache_dir is provided, and it's a new repository without
1400 # alternative_dir, bootstrap this project repo with the git
1401 # cache files.
1402 if cache_dir is not None and is_new and alt_dir is None:
1403 try:
1404 self.CacheApply(cache_dir)
1405 applied_cache = True
1406 is_new = False
1407 except CacheApplyError as e:
1408 _error("Could not apply git cache: %s", e)
1409 _error("Please check if you have the right GS credentials.")
1410 _error("Please check if the cache files exist in GS.")
1411
Gavin Makea2e3302023-03-11 06:46:20 +00001412 if (
1413 clone_bundle
Gavin Mak4feff3b2023-05-16 21:31:10 +00001414 and not applied_cache
Gavin Makea2e3302023-03-11 06:46:20 +00001415 and alt_dir is None
1416 and self._ApplyCloneBundle(
1417 initial=is_new, quiet=quiet, verbose=verbose
1418 )
1419 ):
1420 is_new = False
1421
1422 if current_branch_only is None:
1423 if self.sync_c:
1424 current_branch_only = True
1425 elif not self.manifest._loaded:
1426 # Manifest cannot check defaults until it syncs.
1427 current_branch_only = False
1428 elif self.manifest.default.sync_c:
1429 current_branch_only = True
1430
1431 if tags is None:
1432 tags = self.sync_tags
1433
1434 if self.clone_depth:
1435 depth = self.clone_depth
1436 else:
1437 depth = self.manifest.manifestProject.depth
1438
Jason Chang17833322023-05-23 13:06:55 -07001439 if depth and clone_filter_for_depth:
1440 depth = None
1441 clone_filter = clone_filter_for_depth
1442
Gavin Makea2e3302023-03-11 06:46:20 +00001443 # See if we can skip the network fetch entirely.
1444 remote_fetched = False
1445 if not (
1446 optimized_fetch
Sylvain56a5a012023-09-11 13:38:00 +02001447 and IsId(self.revisionExpr)
1448 and self._CheckForImmutableRevision()
Gavin Makea2e3302023-03-11 06:46:20 +00001449 ):
1450 remote_fetched = True
Jason Chang32b59562023-07-14 16:45:35 -07001451 try:
1452 if not self._RemoteFetch(
1453 initial=is_new,
1454 quiet=quiet,
1455 verbose=verbose,
1456 output_redir=output_redir,
1457 alt_dir=alt_dir,
1458 current_branch_only=current_branch_only,
1459 tags=tags,
1460 prune=prune,
1461 depth=depth,
1462 submodules=submodules,
1463 force_sync=force_sync,
1464 ssh_proxy=ssh_proxy,
1465 clone_filter=clone_filter,
1466 retry_fetches=retry_fetches,
1467 ):
1468 return SyncNetworkHalfResult(
1469 remote_fetched,
1470 SyncNetworkHalfError(
1471 f"Unable to remote fetch project {self.name}",
1472 project=self.name,
1473 ),
1474 )
1475 except RepoError as e:
1476 return SyncNetworkHalfResult(
1477 remote_fetched,
1478 e,
1479 )
Gavin Makea2e3302023-03-11 06:46:20 +00001480
1481 mp = self.manifest.manifestProject
1482 dissociate = mp.dissociate
1483 if dissociate:
1484 alternates_file = os.path.join(
1485 self.objdir, "objects/info/alternates"
1486 )
1487 if os.path.exists(alternates_file):
1488 cmd = ["repack", "-a", "-d"]
1489 p = GitCommand(
1490 self,
1491 cmd,
1492 bare=True,
1493 capture_stdout=bool(output_redir),
1494 merge_output=bool(output_redir),
1495 )
1496 if p.stdout and output_redir:
1497 output_redir.write(p.stdout)
1498 if p.Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07001499 return SyncNetworkHalfResult(
1500 remote_fetched,
1501 GitError(
1502 "Unable to repack alternates", project=self.name
1503 ),
1504 )
Gavin Makea2e3302023-03-11 06:46:20 +00001505 platform_utils.remove(alternates_file)
1506
1507 if self.worktree:
1508 self._InitMRef()
1509 else:
1510 self._InitMirrorHead()
1511 platform_utils.remove(
1512 os.path.join(self.gitdir, "FETCH_HEAD"), missing_ok=True
1513 )
Jason Chang32b59562023-07-14 16:45:35 -07001514 return SyncNetworkHalfResult(remote_fetched)
Gavin Makea2e3302023-03-11 06:46:20 +00001515
1516 def PostRepoUpgrade(self):
1517 self._InitHooks()
1518
1519 def _CopyAndLinkFiles(self):
1520 if self.client.isGitcClient:
1521 return
1522 for copyfile in self.copyfiles:
1523 copyfile._Copy()
1524 for linkfile in self.linkfiles:
1525 linkfile._Link()
1526
1527 def GetCommitRevisionId(self):
1528 """Get revisionId of a commit.
1529
1530 Use this method instead of GetRevisionId to get the id of the commit
1531 rather than the id of the current git object (for example, a tag)
1532
1533 """
Sylvaine9cb3912023-09-10 23:35:01 +02001534 if self.revisionId:
1535 return self.revisionId
Gavin Makea2e3302023-03-11 06:46:20 +00001536 if not self.revisionExpr.startswith(R_TAGS):
1537 return self.GetRevisionId(self._allrefs)
1538
1539 try:
1540 return self.bare_git.rev_list(self.revisionExpr, "-1")[0]
1541 except GitError:
1542 raise ManifestInvalidRevisionError(
1543 "revision %s in %s not found" % (self.revisionExpr, self.name)
1544 )
1545
1546 def GetRevisionId(self, all_refs=None):
1547 if self.revisionId:
1548 return self.revisionId
1549
1550 rem = self.GetRemote()
1551 rev = rem.ToLocal(self.revisionExpr)
1552
1553 if all_refs is not None and rev in all_refs:
1554 return all_refs[rev]
1555
1556 try:
1557 return self.bare_git.rev_parse("--verify", "%s^0" % rev)
1558 except GitError:
1559 raise ManifestInvalidRevisionError(
1560 "revision %s in %s not found" % (self.revisionExpr, self.name)
1561 )
1562
1563 def SetRevisionId(self, revisionId):
1564 if self.revisionExpr:
1565 self.upstream = self.revisionExpr
1566
1567 self.revisionId = revisionId
1568
Jason Chang32b59562023-07-14 16:45:35 -07001569 def Sync_LocalHalf(
1570 self, syncbuf, force_sync=False, submodules=False, errors=None
1571 ):
Gavin Makea2e3302023-03-11 06:46:20 +00001572 """Perform only the local IO portion of the sync process.
1573
1574 Network access is not required.
1575 """
Jason Chang32b59562023-07-14 16:45:35 -07001576 if errors is None:
1577 errors = []
1578
1579 def fail(error: Exception):
1580 errors.append(error)
1581 syncbuf.fail(self, error)
1582
Gavin Makea2e3302023-03-11 06:46:20 +00001583 if not os.path.exists(self.gitdir):
Jason Chang32b59562023-07-14 16:45:35 -07001584 fail(
1585 LocalSyncFail(
1586 "Cannot checkout %s due to missing network sync; Run "
1587 "`repo sync -n %s` first." % (self.name, self.name),
1588 project=self.name,
1589 )
Gavin Makea2e3302023-03-11 06:46:20 +00001590 )
1591 return
1592
1593 self._InitWorkTree(force_sync=force_sync, submodules=submodules)
1594 all_refs = self.bare_ref.all
1595 self.CleanPublishedCache(all_refs)
1596 revid = self.GetRevisionId(all_refs)
1597
1598 # Special case the root of the repo client checkout. Make sure it
1599 # doesn't contain files being checked out to dirs we don't allow.
1600 if self.relpath == ".":
1601 PROTECTED_PATHS = {".repo"}
1602 paths = set(
1603 self.work_git.ls_tree("-z", "--name-only", "--", revid).split(
1604 "\0"
1605 )
1606 )
1607 bad_paths = paths & PROTECTED_PATHS
1608 if bad_paths:
Jason Chang32b59562023-07-14 16:45:35 -07001609 fail(
1610 LocalSyncFail(
1611 "Refusing to checkout project that writes to protected "
1612 "paths: %s" % (", ".join(bad_paths),),
1613 project=self.name,
1614 )
Gavin Makea2e3302023-03-11 06:46:20 +00001615 )
1616 return
1617
1618 def _doff():
1619 self._FastForward(revid)
1620 self._CopyAndLinkFiles()
1621
1622 def _dosubmodules():
1623 self._SyncSubmodules(quiet=True)
1624
1625 head = self.work_git.GetHead()
1626 if head.startswith(R_HEADS):
1627 branch = head[len(R_HEADS) :]
1628 try:
1629 head = all_refs[head]
1630 except KeyError:
1631 head = None
1632 else:
1633 branch = None
1634
1635 if branch is None or syncbuf.detach_head:
1636 # Currently on a detached HEAD. The user is assumed to
1637 # not have any local modifications worth worrying about.
1638 if self.IsRebaseInProgress():
Jason Chang32b59562023-07-14 16:45:35 -07001639 fail(_PriorSyncFailedError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001640 return
1641
1642 if head == revid:
1643 # No changes; don't do anything further.
1644 # Except if the head needs to be detached.
1645 if not syncbuf.detach_head:
1646 # The copy/linkfile config may have changed.
1647 self._CopyAndLinkFiles()
1648 return
1649 else:
1650 lost = self._revlist(not_rev(revid), HEAD)
1651 if lost:
1652 syncbuf.info(self, "discarding %d commits", len(lost))
1653
1654 try:
1655 self._Checkout(revid, quiet=True)
1656 if submodules:
1657 self._SyncSubmodules(quiet=True)
1658 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001659 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001660 return
1661 self._CopyAndLinkFiles()
1662 return
1663
1664 if head == revid:
1665 # No changes; don't do anything further.
1666 #
1667 # The copy/linkfile config may have changed.
1668 self._CopyAndLinkFiles()
1669 return
1670
1671 branch = self.GetBranch(branch)
1672
1673 if not branch.LocalMerge:
1674 # The current branch has no tracking configuration.
1675 # Jump off it to a detached HEAD.
1676 syncbuf.info(
1677 self, "leaving %s; does not track upstream", branch.name
1678 )
1679 try:
1680 self._Checkout(revid, quiet=True)
1681 if submodules:
1682 self._SyncSubmodules(quiet=True)
1683 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001684 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001685 return
1686 self._CopyAndLinkFiles()
1687 return
1688
1689 upstream_gain = self._revlist(not_rev(HEAD), revid)
1690
1691 # See if we can perform a fast forward merge. This can happen if our
1692 # branch isn't in the exact same state as we last published.
1693 try:
Jason Chang87058c62023-09-27 11:34:43 -07001694 self.work_git.merge_base(
1695 "--is-ancestor", HEAD, revid, log_as_error=False
1696 )
Gavin Makea2e3302023-03-11 06:46:20 +00001697 # Skip the published logic.
1698 pub = False
1699 except GitError:
1700 pub = self.WasPublished(branch.name, all_refs)
1701
1702 if pub:
1703 not_merged = self._revlist(not_rev(revid), pub)
1704 if not_merged:
1705 if upstream_gain:
1706 # The user has published this branch and some of those
1707 # commits are not yet merged upstream. We do not want
1708 # to rewrite the published commits so we punt.
Jason Chang32b59562023-07-14 16:45:35 -07001709 fail(
1710 LocalSyncFail(
1711 "branch %s is published (but not merged) and is "
1712 "now %d commits behind"
1713 % (branch.name, len(upstream_gain)),
1714 project=self.name,
1715 )
Gavin Makea2e3302023-03-11 06:46:20 +00001716 )
1717 return
1718 elif pub == head:
1719 # All published commits are merged, and thus we are a
1720 # strict subset. We can fast-forward safely.
1721 syncbuf.later1(self, _doff)
1722 if submodules:
1723 syncbuf.later1(self, _dosubmodules)
1724 return
1725
1726 # Examine the local commits not in the remote. Find the
1727 # last one attributed to this user, if any.
1728 local_changes = self._revlist(not_rev(revid), HEAD, format="%H %ce")
1729 last_mine = None
1730 cnt_mine = 0
1731 for commit in local_changes:
1732 commit_id, committer_email = commit.split(" ", 1)
1733 if committer_email == self.UserEmail:
1734 last_mine = commit_id
1735 cnt_mine += 1
1736
1737 if not upstream_gain and cnt_mine == len(local_changes):
1738 # The copy/linkfile config may have changed.
1739 self._CopyAndLinkFiles()
1740 return
1741
1742 if self.IsDirty(consider_untracked=False):
Jason Chang32b59562023-07-14 16:45:35 -07001743 fail(_DirtyError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001744 return
1745
1746 # If the upstream switched on us, warn the user.
1747 if branch.merge != self.revisionExpr:
1748 if branch.merge and self.revisionExpr:
1749 syncbuf.info(
1750 self,
1751 "manifest switched %s...%s",
1752 branch.merge,
1753 self.revisionExpr,
1754 )
1755 elif branch.merge:
1756 syncbuf.info(self, "manifest no longer tracks %s", branch.merge)
1757
1758 if cnt_mine < len(local_changes):
1759 # Upstream rebased. Not everything in HEAD was created by this user.
1760 syncbuf.info(
1761 self,
1762 "discarding %d commits removed from upstream",
1763 len(local_changes) - cnt_mine,
1764 )
1765
1766 branch.remote = self.GetRemote()
Sylvain56a5a012023-09-11 13:38:00 +02001767 if not IsId(self.revisionExpr):
Gavin Makea2e3302023-03-11 06:46:20 +00001768 # In case of manifest sync the revisionExpr might be a SHA1.
1769 branch.merge = self.revisionExpr
1770 if not branch.merge.startswith("refs/"):
1771 branch.merge = R_HEADS + branch.merge
1772 branch.Save()
1773
1774 if cnt_mine > 0 and self.rebase:
1775
1776 def _docopyandlink():
1777 self._CopyAndLinkFiles()
1778
1779 def _dorebase():
1780 self._Rebase(upstream="%s^1" % last_mine, onto=revid)
1781
1782 syncbuf.later2(self, _dorebase)
1783 if submodules:
1784 syncbuf.later2(self, _dosubmodules)
1785 syncbuf.later2(self, _docopyandlink)
1786 elif local_changes:
1787 try:
1788 self._ResetHard(revid)
1789 if submodules:
1790 self._SyncSubmodules(quiet=True)
1791 self._CopyAndLinkFiles()
1792 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001793 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001794 return
1795 else:
1796 syncbuf.later1(self, _doff)
1797 if submodules:
1798 syncbuf.later1(self, _dosubmodules)
1799
1800 def AddCopyFile(self, src, dest, topdir):
1801 """Mark |src| for copying to |dest| (relative to |topdir|).
1802
1803 No filesystem changes occur here. Actual copying happens later on.
1804
1805 Paths should have basic validation run on them before being queued.
1806 Further checking will be handled when the actual copy happens.
1807 """
1808 self.copyfiles.append(_CopyFile(self.worktree, src, topdir, dest))
1809
1810 def AddLinkFile(self, src, dest, topdir):
1811 """Mark |dest| to create a symlink (relative to |topdir|) pointing to
1812 |src|.
1813
1814 No filesystem changes occur here. Actual linking happens later on.
1815
1816 Paths should have basic validation run on them before being queued.
1817 Further checking will be handled when the actual link happens.
1818 """
1819 self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest))
1820
1821 def AddAnnotation(self, name, value, keep):
1822 self.annotations.append(Annotation(name, value, keep))
1823
1824 def DownloadPatchSet(self, change_id, patch_id):
1825 """Download a single patch set of a single change to FETCH_HEAD."""
1826 remote = self.GetRemote()
1827
1828 cmd = ["fetch", remote.name]
1829 cmd.append(
1830 "refs/changes/%2.2d/%d/%d" % (change_id % 100, change_id, patch_id)
1831 )
Jason Chang1a3612f2023-08-08 14:12:53 -07001832 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001833 return DownloadedChange(
1834 self,
1835 self.GetRevisionId(),
1836 change_id,
1837 patch_id,
1838 self.bare_git.rev_parse("FETCH_HEAD"),
1839 )
1840
1841 def DeleteWorktree(self, quiet=False, force=False):
1842 """Delete the source checkout and any other housekeeping tasks.
1843
1844 This currently leaves behind the internal .repo/ cache state. This
1845 helps when switching branches or manifest changes get reverted as we
1846 don't have to redownload all the git objects. But we should do some GC
1847 at some point.
1848
1849 Args:
1850 quiet: Whether to hide normal messages.
1851 force: Always delete tree even if dirty.
Doug Anderson3ba5f952011-04-07 12:51:04 -07001852
1853 Returns:
Gavin Makea2e3302023-03-11 06:46:20 +00001854 True if the worktree was completely cleaned out.
1855 """
1856 if self.IsDirty():
1857 if force:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00001858 logger.warning(
Gavin Makea2e3302023-03-11 06:46:20 +00001859 "warning: %s: Removing dirty project: uncommitted changes "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001860 "lost.",
1861 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001862 )
1863 else:
Jason Chang32b59562023-07-14 16:45:35 -07001864 msg = (
1865 "error: %s: Cannot remove project: uncommitted"
1866 "changes are present.\n" % self.RelPath(local=False)
Gavin Makea2e3302023-03-11 06:46:20 +00001867 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001868 logger.error(msg)
Jason Chang32b59562023-07-14 16:45:35 -07001869 raise DeleteDirtyWorktreeError(msg, project=self)
Wink Saville02d79452009-04-10 13:01:24 -07001870
Gavin Makea2e3302023-03-11 06:46:20 +00001871 if not quiet:
1872 print(
1873 "%s: Deleting obsolete checkout." % (self.RelPath(local=False),)
1874 )
Wink Saville02d79452009-04-10 13:01:24 -07001875
Gavin Makea2e3302023-03-11 06:46:20 +00001876 # Unlock and delink from the main worktree. We don't use git's worktree
1877 # remove because it will recursively delete projects -- we handle that
1878 # ourselves below. https://crbug.com/git/48
1879 if self.use_git_worktrees:
1880 needle = platform_utils.realpath(self.gitdir)
1881 # Find the git worktree commondir under .repo/worktrees/.
1882 output = self.bare_git.worktree("list", "--porcelain").splitlines()[
1883 0
1884 ]
1885 assert output.startswith("worktree "), output
1886 commondir = output[9:]
1887 # Walk each of the git worktrees to see where they point.
1888 configs = os.path.join(commondir, "worktrees")
1889 for name in os.listdir(configs):
1890 gitdir = os.path.join(configs, name, "gitdir")
1891 with open(gitdir) as fp:
1892 relpath = fp.read().strip()
1893 # Resolve the checkout path and see if it matches this project.
1894 fullpath = platform_utils.realpath(
1895 os.path.join(configs, name, relpath)
1896 )
1897 if fullpath == needle:
1898 platform_utils.rmtree(os.path.join(configs, name))
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001899
Gavin Makea2e3302023-03-11 06:46:20 +00001900 # Delete the .git directory first, so we're less likely to have a
1901 # partially working git repository around. There shouldn't be any git
1902 # projects here, so rmtree works.
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001903
Gavin Makea2e3302023-03-11 06:46:20 +00001904 # Try to remove plain files first in case of git worktrees. If this
1905 # fails for any reason, we'll fall back to rmtree, and that'll display
1906 # errors if it can't remove things either.
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001907 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001908 platform_utils.remove(self.gitdir)
1909 except OSError:
1910 pass
1911 try:
1912 platform_utils.rmtree(self.gitdir)
1913 except OSError as e:
1914 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001915 logger.error("error: %s: %s", self.gitdir, e)
1916 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00001917 "error: %s: Failed to delete obsolete checkout; remove "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001918 "manually, then run `repo sync -l`.",
1919 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001920 )
Jason Chang32b59562023-07-14 16:45:35 -07001921 raise DeleteWorktreeError(aggregate_errors=[e])
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001922
Gavin Makea2e3302023-03-11 06:46:20 +00001923 # Delete everything under the worktree, except for directories that
1924 # contain another git project.
1925 dirs_to_remove = []
1926 failed = False
Jason Chang32b59562023-07-14 16:45:35 -07001927 errors = []
Gavin Makea2e3302023-03-11 06:46:20 +00001928 for root, dirs, files in platform_utils.walk(self.worktree):
1929 for f in files:
1930 path = os.path.join(root, f)
1931 try:
1932 platform_utils.remove(path)
1933 except OSError as e:
1934 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001935 logger.error("error: %s: Failed to remove: %s", path, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001936 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001937 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001938 dirs[:] = [
1939 d
1940 for d in dirs
1941 if not os.path.lexists(os.path.join(root, d, ".git"))
1942 ]
1943 dirs_to_remove += [
1944 os.path.join(root, d)
1945 for d in dirs
1946 if os.path.join(root, d) not in dirs_to_remove
1947 ]
1948 for d in reversed(dirs_to_remove):
1949 if platform_utils.islink(d):
1950 try:
1951 platform_utils.remove(d)
1952 except OSError as e:
1953 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001954 logger.error("error: %s: Failed to remove: %s", d, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001955 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001956 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001957 elif not platform_utils.listdir(d):
1958 try:
1959 platform_utils.rmdir(d)
1960 except OSError as e:
1961 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001962 logger.error("error: %s: Failed to remove: %s", d, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001963 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001964 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001965 if failed:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001966 logger.error(
1967 "error: %s: Failed to delete obsolete checkout.",
1968 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001969 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001970 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00001971 " Remove manually, then run `repo sync -l`.",
Gavin Makea2e3302023-03-11 06:46:20 +00001972 )
Jason Chang32b59562023-07-14 16:45:35 -07001973 raise DeleteWorktreeError(aggregate_errors=errors)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07001974
Gavin Makea2e3302023-03-11 06:46:20 +00001975 # Try deleting parent dirs if they are empty.
1976 path = self.worktree
1977 while path != self.manifest.topdir:
1978 try:
1979 platform_utils.rmdir(path)
1980 except OSError as e:
1981 if e.errno != errno.ENOENT:
1982 break
1983 path = os.path.dirname(path)
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001984
Gavin Makea2e3302023-03-11 06:46:20 +00001985 return True
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001986
Gavin Makea2e3302023-03-11 06:46:20 +00001987 def StartBranch(self, name, branch_merge="", revision=None):
1988 """Create a new branch off the manifest's revision."""
1989 if not branch_merge:
1990 branch_merge = self.revisionExpr
1991 head = self.work_git.GetHead()
1992 if head == (R_HEADS + name):
1993 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001994
David Pursehouse8a68ff92012-09-24 12:15:13 +09001995 all_refs = self.bare_ref.all
Gavin Makea2e3302023-03-11 06:46:20 +00001996 if R_HEADS + name in all_refs:
Jason Chang1a3612f2023-08-08 14:12:53 -07001997 GitCommand(
1998 self, ["checkout", "-q", name, "--"], verify_command=True
1999 ).Wait()
2000 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02002001
Gavin Makea2e3302023-03-11 06:46:20 +00002002 branch = self.GetBranch(name)
2003 branch.remote = self.GetRemote()
2004 branch.merge = branch_merge
Sylvain56a5a012023-09-11 13:38:00 +02002005 if not branch.merge.startswith("refs/") and not IsId(branch_merge):
Gavin Makea2e3302023-03-11 06:46:20 +00002006 branch.merge = R_HEADS + branch_merge
Shawn O. Pearce88443382010-10-08 10:02:09 +02002007
Gavin Makea2e3302023-03-11 06:46:20 +00002008 if revision is None:
2009 revid = self.GetRevisionId(all_refs)
Shawn O. Pearce88443382010-10-08 10:02:09 +02002010 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002011 revid = self.work_git.rev_parse(revision)
Brian Harring14a66742012-09-28 20:21:57 -07002012
Gavin Makea2e3302023-03-11 06:46:20 +00002013 if head.startswith(R_HEADS):
Kevin Degiabaa7f32014-11-12 11:27:45 -07002014 try:
Gavin Makea2e3302023-03-11 06:46:20 +00002015 head = all_refs[head]
2016 except KeyError:
2017 head = None
2018 if revid and head and revid == head:
2019 ref = R_HEADS + name
2020 self.work_git.update_ref(ref, revid)
2021 self.work_git.symbolic_ref(HEAD, ref)
2022 branch.Save()
2023 return True
Kevin Degib1a07b82015-07-27 13:33:43 -06002024
Jason Chang1a3612f2023-08-08 14:12:53 -07002025 GitCommand(
2026 self,
2027 ["checkout", "-q", "-b", branch.name, revid],
2028 verify_command=True,
2029 ).Wait()
2030 branch.Save()
2031 return True
Kevin Degi384b3c52014-10-16 16:02:58 -06002032
Gavin Makea2e3302023-03-11 06:46:20 +00002033 def CheckoutBranch(self, name):
2034 """Checkout a local topic branch.
Shawn O. Pearce2816d4f2009-03-03 17:53:18 -08002035
Gavin Makea2e3302023-03-11 06:46:20 +00002036 Args:
2037 name: The name of the branch to checkout.
Shawn O. Pearce88443382010-10-08 10:02:09 +02002038
Gavin Makea2e3302023-03-11 06:46:20 +00002039 Returns:
Jason Chang1a3612f2023-08-08 14:12:53 -07002040 True if the checkout succeeded; False if the
2041 branch doesn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00002042 """
2043 rev = R_HEADS + name
2044 head = self.work_git.GetHead()
2045 if head == rev:
2046 # Already on the branch.
2047 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02002048
Gavin Makea2e3302023-03-11 06:46:20 +00002049 all_refs = self.bare_ref.all
2050 try:
2051 revid = all_refs[rev]
2052 except KeyError:
2053 # Branch does not exist in this project.
Jason Chang1a3612f2023-08-08 14:12:53 -07002054 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002055
Gavin Makea2e3302023-03-11 06:46:20 +00002056 if head.startswith(R_HEADS):
2057 try:
2058 head = all_refs[head]
2059 except KeyError:
2060 head = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002061
Gavin Makea2e3302023-03-11 06:46:20 +00002062 if head == revid:
2063 # Same revision; just update HEAD to point to the new
2064 # target branch, but otherwise take no other action.
2065 _lwrite(
2066 self.work_git.GetDotgitPath(subpath=HEAD),
2067 "ref: %s%s\n" % (R_HEADS, name),
2068 )
2069 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05002070
Jason Chang1a3612f2023-08-08 14:12:53 -07002071 GitCommand(
2072 self,
2073 ["checkout", name, "--"],
2074 capture_stdout=True,
2075 capture_stderr=True,
2076 verify_command=True,
2077 ).Wait()
2078 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05002079
Gavin Makea2e3302023-03-11 06:46:20 +00002080 def AbandonBranch(self, name):
2081 """Destroy a local topic branch.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002082
Gavin Makea2e3302023-03-11 06:46:20 +00002083 Args:
2084 name: The name of the branch to abandon.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002085
Gavin Makea2e3302023-03-11 06:46:20 +00002086 Returns:
Jason Changf9aacd42023-08-03 14:38:00 -07002087 True if the abandon succeeded; Raises GitCommandError if it didn't;
2088 None if the branch didn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00002089 """
2090 rev = R_HEADS + name
2091 all_refs = self.bare_ref.all
2092 if rev not in all_refs:
2093 # Doesn't exist
2094 return None
2095
2096 head = self.work_git.GetHead()
2097 if head == rev:
2098 # We can't destroy the branch while we are sitting
2099 # on it. Switch to a detached HEAD.
2100 head = all_refs[head]
2101
2102 revid = self.GetRevisionId(all_refs)
2103 if head == revid:
2104 _lwrite(
2105 self.work_git.GetDotgitPath(subpath=HEAD), "%s\n" % revid
2106 )
2107 else:
2108 self._Checkout(revid, quiet=True)
Jason Changf9aacd42023-08-03 14:38:00 -07002109 GitCommand(
2110 self,
2111 ["branch", "-D", name],
2112 capture_stdout=True,
2113 capture_stderr=True,
2114 verify_command=True,
2115 ).Wait()
2116 return True
Gavin Makea2e3302023-03-11 06:46:20 +00002117
2118 def PruneHeads(self):
2119 """Prune any topic branches already merged into upstream."""
2120 cb = self.CurrentBranch
2121 kill = []
2122 left = self._allrefs
2123 for name in left.keys():
2124 if name.startswith(R_HEADS):
2125 name = name[len(R_HEADS) :]
2126 if cb is None or name != cb:
2127 kill.append(name)
2128
2129 # Minor optimization: If there's nothing to prune, then don't try to
2130 # read any project state.
2131 if not kill and not cb:
2132 return []
2133
2134 rev = self.GetRevisionId(left)
2135 if (
2136 cb is not None
2137 and not self._revlist(HEAD + "..." + rev)
2138 and not self.IsDirty(consider_untracked=False)
2139 ):
2140 self.work_git.DetachHead(HEAD)
2141 kill.append(cb)
2142
2143 if kill:
2144 old = self.bare_git.GetHead()
2145
2146 try:
2147 self.bare_git.DetachHead(rev)
2148
2149 b = ["branch", "-d"]
2150 b.extend(kill)
2151 b = GitCommand(
2152 self, b, bare=True, capture_stdout=True, capture_stderr=True
2153 )
2154 b.Wait()
2155 finally:
Sylvain56a5a012023-09-11 13:38:00 +02002156 if IsId(old):
Gavin Makea2e3302023-03-11 06:46:20 +00002157 self.bare_git.DetachHead(old)
2158 else:
2159 self.bare_git.SetHead(old)
2160 left = self._allrefs
2161
2162 for branch in kill:
2163 if (R_HEADS + branch) not in left:
2164 self.CleanPublishedCache()
2165 break
2166
2167 if cb and cb not in kill:
2168 kill.append(cb)
2169 kill.sort()
2170
2171 kept = []
2172 for branch in kill:
2173 if R_HEADS + branch in left:
2174 branch = self.GetBranch(branch)
2175 base = branch.LocalMerge
2176 if not base:
2177 base = rev
2178 kept.append(ReviewableBranch(self, branch, base))
2179 return kept
2180
2181 def GetRegisteredSubprojects(self):
2182 result = []
2183
2184 def rec(subprojects):
2185 if not subprojects:
2186 return
2187 result.extend(subprojects)
2188 for p in subprojects:
2189 rec(p.subprojects)
2190
2191 rec(self.subprojects)
2192 return result
2193
2194 def _GetSubmodules(self):
2195 # Unfortunately we cannot call `git submodule status --recursive` here
2196 # because the working tree might not exist yet, and it cannot be used
2197 # without a working tree in its current implementation.
2198
2199 def get_submodules(gitdir, rev):
2200 # Parse .gitmodules for submodule sub_paths and sub_urls.
2201 sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
2202 if not sub_paths:
2203 return []
2204 # Run `git ls-tree` to read SHAs of submodule object, which happen
2205 # to be revision of submodule repository.
2206 sub_revs = git_ls_tree(gitdir, rev, sub_paths)
2207 submodules = []
2208 for sub_path, sub_url in zip(sub_paths, sub_urls):
2209 try:
2210 sub_rev = sub_revs[sub_path]
2211 except KeyError:
2212 # Ignore non-exist submodules.
2213 continue
2214 submodules.append((sub_rev, sub_path, sub_url))
2215 return submodules
2216
2217 re_path = re.compile(r"^submodule\.(.+)\.path=(.*)$")
2218 re_url = re.compile(r"^submodule\.(.+)\.url=(.*)$")
2219
2220 def parse_gitmodules(gitdir, rev):
2221 cmd = ["cat-file", "blob", "%s:.gitmodules" % rev]
2222 try:
2223 p = GitCommand(
2224 None,
2225 cmd,
2226 capture_stdout=True,
2227 capture_stderr=True,
2228 bare=True,
2229 gitdir=gitdir,
2230 )
2231 except GitError:
2232 return [], []
2233 if p.Wait() != 0:
2234 return [], []
2235
2236 gitmodules_lines = []
2237 fd, temp_gitmodules_path = tempfile.mkstemp()
2238 try:
2239 os.write(fd, p.stdout.encode("utf-8"))
2240 os.close(fd)
2241 cmd = ["config", "--file", temp_gitmodules_path, "--list"]
2242 p = GitCommand(
2243 None,
2244 cmd,
2245 capture_stdout=True,
2246 capture_stderr=True,
2247 bare=True,
2248 gitdir=gitdir,
2249 )
2250 if p.Wait() != 0:
2251 return [], []
2252 gitmodules_lines = p.stdout.split("\n")
2253 except GitError:
2254 return [], []
2255 finally:
2256 platform_utils.remove(temp_gitmodules_path)
2257
2258 names = set()
2259 paths = {}
2260 urls = {}
2261 for line in gitmodules_lines:
2262 if not line:
2263 continue
2264 m = re_path.match(line)
2265 if m:
2266 names.add(m.group(1))
2267 paths[m.group(1)] = m.group(2)
2268 continue
2269 m = re_url.match(line)
2270 if m:
2271 names.add(m.group(1))
2272 urls[m.group(1)] = m.group(2)
2273 continue
2274 names = sorted(names)
2275 return (
2276 [paths.get(name, "") for name in names],
2277 [urls.get(name, "") for name in names],
2278 )
2279
2280 def git_ls_tree(gitdir, rev, paths):
2281 cmd = ["ls-tree", rev, "--"]
2282 cmd.extend(paths)
2283 try:
2284 p = GitCommand(
2285 None,
2286 cmd,
2287 capture_stdout=True,
2288 capture_stderr=True,
2289 bare=True,
2290 gitdir=gitdir,
2291 )
2292 except GitError:
2293 return []
2294 if p.Wait() != 0:
2295 return []
2296 objects = {}
2297 for line in p.stdout.split("\n"):
2298 if not line.strip():
2299 continue
2300 object_rev, object_path = line.split()[2:4]
2301 objects[object_path] = object_rev
2302 return objects
2303
2304 try:
2305 rev = self.GetRevisionId()
2306 except GitError:
2307 return []
2308 return get_submodules(self.gitdir, rev)
2309
2310 def GetDerivedSubprojects(self):
2311 result = []
2312 if not self.Exists:
2313 # If git repo does not exist yet, querying its submodules will
2314 # mess up its states; so return here.
2315 return result
2316 for rev, path, url in self._GetSubmodules():
2317 name = self.manifest.GetSubprojectName(self, path)
2318 (
2319 relpath,
2320 worktree,
2321 gitdir,
2322 objdir,
2323 ) = self.manifest.GetSubprojectPaths(self, name, path)
2324 project = self.manifest.paths.get(relpath)
2325 if project:
2326 result.extend(project.GetDerivedSubprojects())
2327 continue
2328
2329 if url.startswith(".."):
2330 url = urllib.parse.urljoin("%s/" % self.remote.url, url)
2331 remote = RemoteSpec(
2332 self.remote.name,
2333 url=url,
2334 pushUrl=self.remote.pushUrl,
2335 review=self.remote.review,
2336 revision=self.remote.revision,
2337 )
2338 subproject = Project(
2339 manifest=self.manifest,
2340 name=name,
2341 remote=remote,
2342 gitdir=gitdir,
2343 objdir=objdir,
2344 worktree=worktree,
2345 relpath=relpath,
2346 revisionExpr=rev,
2347 revisionId=rev,
2348 rebase=self.rebase,
2349 groups=self.groups,
2350 sync_c=self.sync_c,
2351 sync_s=self.sync_s,
2352 sync_tags=self.sync_tags,
2353 parent=self,
2354 is_derived=True,
2355 )
2356 result.append(subproject)
2357 result.extend(subproject.GetDerivedSubprojects())
2358 return result
2359
2360 def EnableRepositoryExtension(self, key, value="true", version=1):
2361 """Enable git repository extension |key| with |value|.
2362
2363 Args:
2364 key: The extension to enabled. Omit the "extensions." prefix.
2365 value: The value to use for the extension.
2366 version: The minimum git repository version needed.
2367 """
2368 # Make sure the git repo version is new enough already.
2369 found_version = self.config.GetInt("core.repositoryFormatVersion")
2370 if found_version is None:
2371 found_version = 0
2372 if found_version < version:
2373 self.config.SetString("core.repositoryFormatVersion", str(version))
2374
2375 # Enable the extension!
2376 self.config.SetString("extensions.%s" % (key,), value)
2377
2378 def ResolveRemoteHead(self, name=None):
2379 """Find out what the default branch (HEAD) points to.
2380
2381 Normally this points to refs/heads/master, but projects are moving to
2382 main. Support whatever the server uses rather than hardcoding "master"
2383 ourselves.
2384 """
2385 if name is None:
2386 name = self.remote.name
2387
2388 # The output will look like (NB: tabs are separators):
2389 # ref: refs/heads/master HEAD
2390 # 5f6803b100bb3cd0f534e96e88c91373e8ed1c44 HEAD
2391 output = self.bare_git.ls_remote(
2392 "-q", "--symref", "--exit-code", name, "HEAD"
2393 )
2394
2395 for line in output.splitlines():
2396 lhs, rhs = line.split("\t", 1)
2397 if rhs == "HEAD" and lhs.startswith("ref:"):
2398 return lhs[4:].strip()
2399
2400 return None
2401
2402 def _CheckForImmutableRevision(self):
2403 try:
2404 # if revision (sha or tag) is not present then following function
2405 # throws an error.
2406 self.bare_git.rev_list(
Jason Chang87058c62023-09-27 11:34:43 -07002407 "-1",
2408 "--missing=allow-any",
2409 "%s^0" % self.revisionExpr,
2410 "--",
2411 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002412 )
2413 if self.upstream:
2414 rev = self.GetRemote().ToLocal(self.upstream)
2415 self.bare_git.rev_list(
Jason Chang87058c62023-09-27 11:34:43 -07002416 "-1",
2417 "--missing=allow-any",
2418 "%s^0" % rev,
2419 "--",
2420 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002421 )
2422 self.bare_git.merge_base(
Jason Chang87058c62023-09-27 11:34:43 -07002423 "--is-ancestor",
2424 self.revisionExpr,
2425 rev,
2426 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002427 )
2428 return True
2429 except GitError:
2430 # There is no such persistent revision. We have to fetch it.
2431 return False
2432
2433 def _FetchArchive(self, tarpath, cwd=None):
2434 cmd = ["archive", "-v", "-o", tarpath]
2435 cmd.append("--remote=%s" % self.remote.url)
2436 cmd.append("--prefix=%s/" % self.RelPath(local=False))
2437 cmd.append(self.revisionExpr)
2438
2439 command = GitCommand(
Jason Chang32b59562023-07-14 16:45:35 -07002440 self,
2441 cmd,
2442 cwd=cwd,
2443 capture_stdout=True,
2444 capture_stderr=True,
2445 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00002446 )
Jason Chang32b59562023-07-14 16:45:35 -07002447 command.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00002448
2449 def _RemoteFetch(
2450 self,
2451 name=None,
2452 current_branch_only=False,
2453 initial=False,
2454 quiet=False,
2455 verbose=False,
2456 output_redir=None,
2457 alt_dir=None,
2458 tags=True,
2459 prune=False,
2460 depth=None,
2461 submodules=False,
2462 ssh_proxy=None,
2463 force_sync=False,
2464 clone_filter=None,
2465 retry_fetches=2,
2466 retry_sleep_initial_sec=4.0,
2467 retry_exp_factor=2.0,
Jason Chang32b59562023-07-14 16:45:35 -07002468 ) -> bool:
Gavin Makea2e3302023-03-11 06:46:20 +00002469 tag_name = None
2470 # The depth should not be used when fetching to a mirror because
2471 # it will result in a shallow repository that cannot be cloned or
2472 # fetched from.
2473 # The repo project should also never be synced with partial depth.
2474 if self.manifest.IsMirror or self.relpath == ".repo/repo":
2475 depth = None
2476
2477 if depth:
2478 current_branch_only = True
2479
Sylvain56a5a012023-09-11 13:38:00 +02002480 is_sha1 = bool(IsId(self.revisionExpr))
Gavin Makea2e3302023-03-11 06:46:20 +00002481
2482 if current_branch_only:
2483 if self.revisionExpr.startswith(R_TAGS):
2484 # This is a tag and its commit id should never change.
2485 tag_name = self.revisionExpr[len(R_TAGS) :]
2486 elif self.upstream and self.upstream.startswith(R_TAGS):
2487 # This is a tag and its commit id should never change.
2488 tag_name = self.upstream[len(R_TAGS) :]
2489
2490 if is_sha1 or tag_name is not None:
2491 if self._CheckForImmutableRevision():
2492 if verbose:
2493 print(
2494 "Skipped fetching project %s (already have "
2495 "persistent ref)" % self.name
2496 )
2497 return True
2498 if is_sha1 and not depth:
2499 # When syncing a specific commit and --depth is not set:
2500 # * if upstream is explicitly specified and is not a sha1, fetch
2501 # only upstream as users expect only upstream to be fetch.
2502 # Note: The commit might not be in upstream in which case the
2503 # sync will fail.
2504 # * otherwise, fetch all branches to make sure we end up with
2505 # the specific commit.
2506 if self.upstream:
Sylvain56a5a012023-09-11 13:38:00 +02002507 current_branch_only = not IsId(self.upstream)
Gavin Makea2e3302023-03-11 06:46:20 +00002508 else:
2509 current_branch_only = False
2510
2511 if not name:
2512 name = self.remote.name
2513
2514 remote = self.GetRemote(name)
2515 if not remote.PreConnectFetch(ssh_proxy):
2516 ssh_proxy = None
2517
2518 if initial:
2519 if alt_dir and "objects" == os.path.basename(alt_dir):
2520 ref_dir = os.path.dirname(alt_dir)
2521 packed_refs = os.path.join(self.gitdir, "packed-refs")
2522
2523 all_refs = self.bare_ref.all
2524 ids = set(all_refs.values())
2525 tmp = set()
2526
2527 for r, ref_id in GitRefs(ref_dir).all.items():
2528 if r not in all_refs:
2529 if r.startswith(R_TAGS) or remote.WritesTo(r):
2530 all_refs[r] = ref_id
2531 ids.add(ref_id)
2532 continue
2533
2534 if ref_id in ids:
2535 continue
2536
2537 r = "refs/_alt/%s" % ref_id
2538 all_refs[r] = ref_id
2539 ids.add(ref_id)
2540 tmp.add(r)
2541
2542 tmp_packed_lines = []
2543 old_packed_lines = []
2544
2545 for r in sorted(all_refs):
2546 line = "%s %s\n" % (all_refs[r], r)
2547 tmp_packed_lines.append(line)
2548 if r not in tmp:
2549 old_packed_lines.append(line)
2550
2551 tmp_packed = "".join(tmp_packed_lines)
2552 old_packed = "".join(old_packed_lines)
2553 _lwrite(packed_refs, tmp_packed)
2554 else:
2555 alt_dir = None
2556
2557 cmd = ["fetch"]
2558
2559 if clone_filter:
2560 git_require((2, 19, 0), fail=True, msg="partial clones")
2561 cmd.append("--filter=%s" % clone_filter)
2562 self.EnableRepositoryExtension("partialclone", self.remote.name)
2563
2564 if depth:
2565 cmd.append("--depth=%s" % depth)
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002566 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002567 # If this repo has shallow objects, then we don't know which refs
2568 # have shallow objects or not. Tell git to unshallow all fetched
2569 # refs. Don't do this with projects that don't have shallow
2570 # objects, since it is less efficient.
2571 if os.path.exists(os.path.join(self.gitdir, "shallow")):
2572 cmd.append("--depth=2147483647")
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002573
Gavin Makea2e3302023-03-11 06:46:20 +00002574 if not verbose:
2575 cmd.append("--quiet")
2576 if not quiet and sys.stdout.isatty():
2577 cmd.append("--progress")
2578 if not self.worktree:
2579 cmd.append("--update-head-ok")
2580 cmd.append(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002581
Gavin Makea2e3302023-03-11 06:46:20 +00002582 if force_sync:
2583 cmd.append("--force")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002584
Gavin Makea2e3302023-03-11 06:46:20 +00002585 if prune:
2586 cmd.append("--prune")
Mike Frysinger29626b42021-05-01 09:37:13 -04002587
Gavin Makea2e3302023-03-11 06:46:20 +00002588 # Always pass something for --recurse-submodules, git with GIT_DIR
2589 # behaves incorrectly when not given `--recurse-submodules=no`.
2590 # (b/218891912)
2591 cmd.append(
2592 f'--recurse-submodules={"on-demand" if submodules else "no"}'
2593 )
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002594
Gavin Makea2e3302023-03-11 06:46:20 +00002595 spec = []
2596 if not current_branch_only:
2597 # Fetch whole repo.
2598 spec.append(
2599 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2600 )
2601 elif tag_name is not None:
2602 spec.append("tag")
2603 spec.append(tag_name)
Remy Böhmer1469c282020-12-15 18:49:02 +01002604
Gavin Makea2e3302023-03-11 06:46:20 +00002605 if self.manifest.IsMirror and not current_branch_only:
2606 branch = None
Remy Böhmer1469c282020-12-15 18:49:02 +01002607 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002608 branch = self.revisionExpr
2609 if (
2610 not self.manifest.IsMirror
2611 and is_sha1
2612 and depth
2613 and git_require((1, 8, 3))
2614 ):
2615 # Shallow checkout of a specific commit, fetch from that commit and
2616 # not the heads only as the commit might be deeper in the history.
2617 spec.append(branch)
2618 if self.upstream:
2619 spec.append(self.upstream)
David James8d201162013-10-11 17:03:19 -07002620 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002621 if is_sha1:
2622 branch = self.upstream
2623 if branch is not None and branch.strip():
2624 if not branch.startswith("refs/"):
2625 branch = R_HEADS + branch
2626 spec.append(str(("+%s:" % branch) + remote.ToLocal(branch)))
David James8d201162013-10-11 17:03:19 -07002627
Gavin Makea2e3302023-03-11 06:46:20 +00002628 # If mirroring repo and we cannot deduce the tag or branch to fetch,
2629 # fetch whole repo.
2630 if self.manifest.IsMirror and not spec:
2631 spec.append(
2632 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2633 )
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002634
Gavin Makea2e3302023-03-11 06:46:20 +00002635 # If using depth then we should not get all the tags since they may
2636 # be outside of the depth.
2637 if not tags or depth:
2638 cmd.append("--no-tags")
2639 else:
2640 cmd.append("--tags")
2641 spec.append(str(("+refs/tags/*:") + remote.ToLocal("refs/tags/*")))
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002642
Gavin Makea2e3302023-03-11 06:46:20 +00002643 cmd.extend(spec)
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002644
Gavin Makea2e3302023-03-11 06:46:20 +00002645 # At least one retry minimum due to git remote prune.
2646 retry_fetches = max(retry_fetches, 2)
2647 retry_cur_sleep = retry_sleep_initial_sec
2648 ok = prune_tried = False
2649 for try_n in range(retry_fetches):
Jason Chang32b59562023-07-14 16:45:35 -07002650 verify_command = try_n == retry_fetches - 1
Gavin Makea2e3302023-03-11 06:46:20 +00002651 gitcmd = GitCommand(
2652 self,
2653 cmd,
2654 bare=True,
2655 objdir=os.path.join(self.objdir, "objects"),
2656 ssh_proxy=ssh_proxy,
2657 merge_output=True,
2658 capture_stdout=quiet or bool(output_redir),
Jason Chang32b59562023-07-14 16:45:35 -07002659 verify_command=verify_command,
Gavin Makea2e3302023-03-11 06:46:20 +00002660 )
2661 if gitcmd.stdout and not quiet and output_redir:
2662 output_redir.write(gitcmd.stdout)
2663 ret = gitcmd.Wait()
2664 if ret == 0:
2665 ok = True
2666 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002667
Gavin Makea2e3302023-03-11 06:46:20 +00002668 # Retry later due to HTTP 429 Too Many Requests.
2669 elif (
2670 gitcmd.stdout
2671 and "error:" in gitcmd.stdout
2672 and "HTTP 429" in gitcmd.stdout
2673 ):
2674 # Fallthru to sleep+retry logic at the bottom.
2675 pass
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002676
Gavin Makea2e3302023-03-11 06:46:20 +00002677 # Try to prune remote branches once in case there are conflicts.
2678 # For example, if the remote had refs/heads/upstream, but deleted
2679 # that and now has refs/heads/upstream/foo.
2680 elif (
2681 gitcmd.stdout
2682 and "error:" in gitcmd.stdout
2683 and "git remote prune" in gitcmd.stdout
2684 and not prune_tried
2685 ):
2686 prune_tried = True
2687 prunecmd = GitCommand(
2688 self,
2689 ["remote", "prune", name],
2690 bare=True,
2691 ssh_proxy=ssh_proxy,
2692 )
2693 ret = prunecmd.Wait()
2694 if ret:
2695 break
2696 print(
2697 "retrying fetch after pruning remote branches",
2698 file=output_redir,
2699 )
2700 # Continue right away so we don't sleep as we shouldn't need to.
2701 continue
2702 elif current_branch_only and is_sha1 and ret == 128:
2703 # Exit code 128 means "couldn't find the ref you asked for"; if
2704 # we're in sha1 mode, we just tried sync'ing from the upstream
2705 # field; it doesn't exist, thus abort the optimization attempt
2706 # and do a full sync.
2707 break
2708 elif ret < 0:
2709 # Git died with a signal, exit immediately.
2710 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002711
Gavin Makea2e3302023-03-11 06:46:20 +00002712 # Figure out how long to sleep before the next attempt, if there is
2713 # one.
2714 if not verbose and gitcmd.stdout:
2715 print(
2716 "\n%s:\n%s" % (self.name, gitcmd.stdout),
2717 end="",
2718 file=output_redir,
2719 )
2720 if try_n < retry_fetches - 1:
2721 print(
2722 "%s: sleeping %s seconds before retrying"
2723 % (self.name, retry_cur_sleep),
2724 file=output_redir,
2725 )
2726 time.sleep(retry_cur_sleep)
2727 retry_cur_sleep = min(
2728 retry_exp_factor * retry_cur_sleep, MAXIMUM_RETRY_SLEEP_SEC
2729 )
2730 retry_cur_sleep *= 1 - random.uniform(
2731 -RETRY_JITTER_PERCENT, RETRY_JITTER_PERCENT
2732 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002733
Gavin Makea2e3302023-03-11 06:46:20 +00002734 if initial:
2735 if alt_dir:
2736 if old_packed != "":
2737 _lwrite(packed_refs, old_packed)
2738 else:
2739 platform_utils.remove(packed_refs)
2740 self.bare_git.pack_refs("--all", "--prune")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002741
Gavin Makea2e3302023-03-11 06:46:20 +00002742 if is_sha1 and current_branch_only:
2743 # We just synced the upstream given branch; verify we
2744 # got what we wanted, else trigger a second run of all
2745 # refs.
2746 if not self._CheckForImmutableRevision():
2747 # Sync the current branch only with depth set to None.
2748 # We always pass depth=None down to avoid infinite recursion.
2749 return self._RemoteFetch(
2750 name=name,
2751 quiet=quiet,
2752 verbose=verbose,
2753 output_redir=output_redir,
2754 current_branch_only=current_branch_only and depth,
2755 initial=False,
2756 alt_dir=alt_dir,
Nasser Grainawiaafed292023-05-24 12:51:03 -06002757 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00002758 depth=None,
2759 ssh_proxy=ssh_proxy,
2760 clone_filter=clone_filter,
2761 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002762
Gavin Makea2e3302023-03-11 06:46:20 +00002763 return ok
Mike Frysingerf4545122019-11-11 04:34:16 -05002764
Gavin Makea2e3302023-03-11 06:46:20 +00002765 def _ApplyCloneBundle(self, initial=False, quiet=False, verbose=False):
2766 if initial and (
2767 self.manifest.manifestProject.depth or self.clone_depth
2768 ):
2769 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002770
Gavin Makea2e3302023-03-11 06:46:20 +00002771 remote = self.GetRemote()
2772 bundle_url = remote.url + "/clone.bundle"
2773 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
2774 if GetSchemeFromUrl(bundle_url) not in (
2775 "http",
2776 "https",
2777 "persistent-http",
2778 "persistent-https",
2779 ):
2780 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06002781
Gavin Makea2e3302023-03-11 06:46:20 +00002782 bundle_dst = os.path.join(self.gitdir, "clone.bundle")
2783 bundle_tmp = os.path.join(self.gitdir, "clone.bundle.tmp")
2784
2785 exist_dst = os.path.exists(bundle_dst)
2786 exist_tmp = os.path.exists(bundle_tmp)
2787
2788 if not initial and not exist_dst and not exist_tmp:
2789 return False
2790
2791 if not exist_dst:
2792 exist_dst = self._FetchBundle(
2793 bundle_url, bundle_tmp, bundle_dst, quiet, verbose
2794 )
2795 if not exist_dst:
2796 return False
2797
2798 cmd = ["fetch"]
2799 if not verbose:
2800 cmd.append("--quiet")
2801 if not quiet and sys.stdout.isatty():
2802 cmd.append("--progress")
2803 if not self.worktree:
2804 cmd.append("--update-head-ok")
2805 cmd.append(bundle_dst)
2806 for f in remote.fetch:
2807 cmd.append(str(f))
2808 cmd.append("+refs/tags/*:refs/tags/*")
2809
2810 ok = (
2811 GitCommand(
2812 self,
2813 cmd,
2814 bare=True,
2815 objdir=os.path.join(self.objdir, "objects"),
2816 ).Wait()
2817 == 0
2818 )
2819 platform_utils.remove(bundle_dst, missing_ok=True)
2820 platform_utils.remove(bundle_tmp, missing_ok=True)
2821 return ok
2822
2823 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
2824 platform_utils.remove(dstPath, missing_ok=True)
2825
2826 cmd = ["curl", "--fail", "--output", tmpPath, "--netrc", "--location"]
2827 if quiet:
2828 cmd += ["--silent", "--show-error"]
2829 if os.path.exists(tmpPath):
2830 size = os.stat(tmpPath).st_size
2831 if size >= 1024:
2832 cmd += ["--continue-at", "%d" % (size,)]
2833 else:
2834 platform_utils.remove(tmpPath)
2835 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
2836 if cookiefile:
2837 cmd += ["--cookie", cookiefile]
2838 if proxy:
2839 cmd += ["--proxy", proxy]
2840 elif "http_proxy" in os.environ and "darwin" == sys.platform:
2841 cmd += ["--proxy", os.environ["http_proxy"]]
2842 if srcUrl.startswith("persistent-https"):
2843 srcUrl = "http" + srcUrl[len("persistent-https") :]
2844 elif srcUrl.startswith("persistent-http"):
2845 srcUrl = "http" + srcUrl[len("persistent-http") :]
2846 cmd += [srcUrl]
2847
2848 proc = None
2849 with Trace("Fetching bundle: %s", " ".join(cmd)):
2850 if verbose:
2851 print("%s: Downloading bundle: %s" % (self.name, srcUrl))
2852 stdout = None if verbose else subprocess.PIPE
2853 stderr = None if verbose else subprocess.STDOUT
2854 try:
2855 proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
2856 except OSError:
2857 return False
2858
2859 (output, _) = proc.communicate()
2860 curlret = proc.returncode
2861
2862 if curlret == 22:
2863 # From curl man page:
2864 # 22: HTTP page not retrieved. The requested url was not found
2865 # or returned another error with the HTTP error code being 400
2866 # or above. This return code only appears if -f, --fail is used.
2867 if verbose:
2868 print(
2869 "%s: Unable to retrieve clone.bundle; ignoring."
2870 % self.name
2871 )
2872 if output:
2873 print("Curl output:\n%s" % output)
2874 return False
2875 elif curlret and not verbose and output:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002876 logger.error("%s", output)
Gavin Makea2e3302023-03-11 06:46:20 +00002877
2878 if os.path.exists(tmpPath):
2879 if curlret == 0 and self._IsValidBundle(tmpPath, quiet):
2880 platform_utils.rename(tmpPath, dstPath)
2881 return True
2882 else:
2883 platform_utils.remove(tmpPath)
2884 return False
2885 else:
2886 return False
2887
2888 def _IsValidBundle(self, path, quiet):
2889 try:
2890 with open(path, "rb") as f:
2891 if f.read(16) == b"# v2 git bundle\n":
2892 return True
2893 else:
2894 if not quiet:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002895 logger.error("Invalid clone.bundle file; ignoring.")
Gavin Makea2e3302023-03-11 06:46:20 +00002896 return False
2897 except OSError:
2898 return False
2899
2900 def _Checkout(self, rev, quiet=False):
2901 cmd = ["checkout"]
2902 if quiet:
2903 cmd.append("-q")
2904 cmd.append(rev)
2905 cmd.append("--")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002906 if GitCommand(self, cmd).Wait() != 0:
Gavin Makea2e3302023-03-11 06:46:20 +00002907 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002908 raise GitError(
2909 "%s checkout %s " % (self.name, rev), project=self.name
2910 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002911
Gavin Makea2e3302023-03-11 06:46:20 +00002912 def _CherryPick(self, rev, ffonly=False, record_origin=False):
2913 cmd = ["cherry-pick"]
2914 if ffonly:
2915 cmd.append("--ff")
2916 if record_origin:
2917 cmd.append("-x")
2918 cmd.append(rev)
2919 cmd.append("--")
2920 if GitCommand(self, cmd).Wait() != 0:
2921 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002922 raise GitError(
2923 "%s cherry-pick %s " % (self.name, rev), project=self.name
2924 )
Victor Boivie0960b5b2010-11-26 13:42:13 +01002925
Gavin Makea2e3302023-03-11 06:46:20 +00002926 def _LsRemote(self, refs):
2927 cmd = ["ls-remote", self.remote.name, refs]
2928 p = GitCommand(self, cmd, capture_stdout=True)
2929 if p.Wait() == 0:
2930 return p.stdout
2931 return None
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002932
Gavin Makea2e3302023-03-11 06:46:20 +00002933 def _Revert(self, rev):
2934 cmd = ["revert"]
2935 cmd.append("--no-edit")
2936 cmd.append(rev)
2937 cmd.append("--")
2938 if GitCommand(self, cmd).Wait() != 0:
2939 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002940 raise GitError(
2941 "%s revert %s " % (self.name, rev), project=self.name
2942 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002943
Gavin Makea2e3302023-03-11 06:46:20 +00002944 def _ResetHard(self, rev, quiet=True):
2945 cmd = ["reset", "--hard"]
2946 if quiet:
2947 cmd.append("-q")
2948 cmd.append(rev)
2949 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002950 raise GitError(
2951 "%s reset --hard %s " % (self.name, rev), project=self.name
2952 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002953
Gavin Makea2e3302023-03-11 06:46:20 +00002954 def _SyncSubmodules(self, quiet=True):
2955 cmd = ["submodule", "update", "--init", "--recursive"]
2956 if quiet:
2957 cmd.append("-q")
2958 if GitCommand(self, cmd).Wait() != 0:
2959 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07002960 "%s submodule update --init --recursive " % self.name,
2961 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00002962 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002963
Gavin Makea2e3302023-03-11 06:46:20 +00002964 def _Rebase(self, upstream, onto=None):
2965 cmd = ["rebase"]
2966 if onto is not None:
2967 cmd.extend(["--onto", onto])
2968 cmd.append(upstream)
2969 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002970 raise GitError(
2971 "%s rebase %s " % (self.name, upstream), project=self.name
2972 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002973
Gavin Makea2e3302023-03-11 06:46:20 +00002974 def _FastForward(self, head, ffonly=False):
2975 cmd = ["merge", "--no-stat", head]
2976 if ffonly:
2977 cmd.append("--ff-only")
2978 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002979 raise GitError(
2980 "%s merge %s " % (self.name, head), project=self.name
2981 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002982
Gavin Makea2e3302023-03-11 06:46:20 +00002983 def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False):
2984 init_git_dir = not os.path.exists(self.gitdir)
2985 init_obj_dir = not os.path.exists(self.objdir)
2986 try:
2987 # Initialize the bare repository, which contains all of the objects.
2988 if init_obj_dir:
2989 os.makedirs(self.objdir)
2990 self.bare_objdir.init()
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002991
Gavin Makea2e3302023-03-11 06:46:20 +00002992 self._UpdateHooks(quiet=quiet)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002993
Gavin Makea2e3302023-03-11 06:46:20 +00002994 if self.use_git_worktrees:
2995 # Enable per-worktree config file support if possible. This
2996 # is more a nice-to-have feature for users rather than a
2997 # hard requirement.
2998 if git_require((2, 20, 0)):
2999 self.EnableRepositoryExtension("worktreeConfig")
Renaud Paquay788e9622017-01-27 11:41:12 -08003000
Gavin Makea2e3302023-03-11 06:46:20 +00003001 # If we have a separate directory to hold refs, initialize it as
3002 # well.
3003 if self.objdir != self.gitdir:
3004 if init_git_dir:
3005 os.makedirs(self.gitdir)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003006
Gavin Makea2e3302023-03-11 06:46:20 +00003007 if init_obj_dir or init_git_dir:
3008 self._ReferenceGitDir(
3009 self.objdir, self.gitdir, copy_all=True
3010 )
3011 try:
3012 self._CheckDirReference(self.objdir, self.gitdir)
3013 except GitError as e:
3014 if force_sync:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00003015 logger.error(
3016 "Retrying clone after deleting %s", self.gitdir
Gavin Makea2e3302023-03-11 06:46:20 +00003017 )
3018 try:
3019 platform_utils.rmtree(
3020 platform_utils.realpath(self.gitdir)
3021 )
3022 if self.worktree and os.path.exists(
3023 platform_utils.realpath(self.worktree)
3024 ):
3025 platform_utils.rmtree(
3026 platform_utils.realpath(self.worktree)
3027 )
3028 return self._InitGitDir(
3029 mirror_git=mirror_git,
3030 force_sync=False,
3031 quiet=quiet,
3032 )
3033 except Exception:
3034 raise e
3035 raise e
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003036
Gavin Makea2e3302023-03-11 06:46:20 +00003037 if init_git_dir:
3038 mp = self.manifest.manifestProject
3039 ref_dir = mp.reference or ""
Julien Camperguedd654222014-01-09 16:21:37 +01003040
Gavin Makea2e3302023-03-11 06:46:20 +00003041 def _expanded_ref_dirs():
3042 """Iterate through possible git reference dir paths."""
3043 name = self.name + ".git"
3044 yield mirror_git or os.path.join(ref_dir, name)
3045 for prefix in "", self.remote.name:
3046 yield os.path.join(
3047 ref_dir, ".repo", "project-objects", prefix, name
3048 )
3049 yield os.path.join(
3050 ref_dir, ".repo", "worktrees", prefix, name
3051 )
3052
3053 if ref_dir or mirror_git:
3054 found_ref_dir = None
3055 for path in _expanded_ref_dirs():
3056 if os.path.exists(path):
3057 found_ref_dir = path
3058 break
3059 ref_dir = found_ref_dir
3060
3061 if ref_dir:
3062 if not os.path.isabs(ref_dir):
3063 # The alternate directory is relative to the object
3064 # database.
3065 ref_dir = os.path.relpath(
3066 ref_dir, os.path.join(self.objdir, "objects")
3067 )
3068 _lwrite(
3069 os.path.join(
3070 self.objdir, "objects/info/alternates"
3071 ),
3072 os.path.join(ref_dir, "objects") + "\n",
3073 )
3074
3075 m = self.manifest.manifestProject.config
3076 for key in ["user.name", "user.email"]:
3077 if m.Has(key, include_defaults=False):
3078 self.config.SetString(key, m.GetString(key))
3079 if not self.manifest.EnableGitLfs:
3080 self.config.SetString(
3081 "filter.lfs.smudge", "git-lfs smudge --skip -- %f"
3082 )
3083 self.config.SetString(
3084 "filter.lfs.process", "git-lfs filter-process --skip"
3085 )
3086 self.config.SetBoolean(
3087 "core.bare", True if self.manifest.IsMirror else None
3088 )
Josip Sokcevic9267d582023-10-19 14:46:11 -07003089
3090 if not init_obj_dir:
3091 # The project might be shared (obj_dir already initialized), but
3092 # such information is not available here. Instead of passing it,
3093 # set it as shared, and rely to be unset down the execution
3094 # path.
3095 if git_require((2, 7, 0)):
3096 self.EnableRepositoryExtension("preciousObjects")
3097 else:
3098 self.config.SetString("gc.pruneExpire", "never")
3099
Gavin Makea2e3302023-03-11 06:46:20 +00003100 except Exception:
3101 if init_obj_dir and os.path.exists(self.objdir):
3102 platform_utils.rmtree(self.objdir)
3103 if init_git_dir and os.path.exists(self.gitdir):
3104 platform_utils.rmtree(self.gitdir)
3105 raise
3106
3107 def _UpdateHooks(self, quiet=False):
3108 if os.path.exists(self.objdir):
3109 self._InitHooks(quiet=quiet)
3110
3111 def _InitHooks(self, quiet=False):
3112 hooks = platform_utils.realpath(os.path.join(self.objdir, "hooks"))
3113 if not os.path.exists(hooks):
3114 os.makedirs(hooks)
3115
3116 # Delete sample hooks. They're noise.
3117 for hook in glob.glob(os.path.join(hooks, "*.sample")):
3118 try:
3119 platform_utils.remove(hook, missing_ok=True)
3120 except PermissionError:
3121 pass
3122
3123 for stock_hook in _ProjectHooks():
3124 name = os.path.basename(stock_hook)
3125
3126 if (
3127 name in ("commit-msg",)
3128 and not self.remote.review
3129 and self is not self.manifest.manifestProject
3130 ):
3131 # Don't install a Gerrit Code Review hook if this
3132 # project does not appear to use it for reviews.
3133 #
3134 # Since the manifest project is one of those, but also
3135 # managed through gerrit, it's excluded.
3136 continue
3137
3138 dst = os.path.join(hooks, name)
3139 if platform_utils.islink(dst):
3140 continue
3141 if os.path.exists(dst):
3142 # If the files are the same, we'll leave it alone. We create
3143 # symlinks below by default but fallback to hardlinks if the OS
3144 # blocks them. So if we're here, it's probably because we made a
3145 # hardlink below.
3146 if not filecmp.cmp(stock_hook, dst, shallow=False):
3147 if not quiet:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00003148 logger.warning(
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00003149 "warn: %s: Not replacing locally modified %s hook",
Gavin Makea2e3302023-03-11 06:46:20 +00003150 self.RelPath(local=False),
3151 name,
3152 )
3153 continue
3154 try:
3155 platform_utils.symlink(
3156 os.path.relpath(stock_hook, os.path.dirname(dst)), dst
3157 )
3158 except OSError as e:
3159 if e.errno == errno.EPERM:
3160 try:
3161 os.link(stock_hook, dst)
3162 except OSError:
Jason Chang32b59562023-07-14 16:45:35 -07003163 raise GitError(
3164 self._get_symlink_error_message(), project=self.name
3165 )
Gavin Makea2e3302023-03-11 06:46:20 +00003166 else:
3167 raise
3168
3169 def _InitRemote(self):
3170 if self.remote.url:
3171 remote = self.GetRemote()
3172 remote.url = self.remote.url
3173 remote.pushUrl = self.remote.pushUrl
3174 remote.review = self.remote.review
3175 remote.projectname = self.name
3176
3177 if self.worktree:
3178 remote.ResetFetch(mirror=False)
3179 else:
3180 remote.ResetFetch(mirror=True)
3181 remote.Save()
3182
3183 def _InitMRef(self):
3184 """Initialize the pseudo m/<manifest branch> ref."""
3185 if self.manifest.branch:
3186 if self.use_git_worktrees:
3187 # Set up the m/ space to point to the worktree-specific ref
3188 # space. We'll update the worktree-specific ref space on each
3189 # checkout.
3190 ref = R_M + self.manifest.branch
3191 if not self.bare_ref.symref(ref):
3192 self.bare_git.symbolic_ref(
3193 "-m",
3194 "redirecting to worktree scope",
3195 ref,
3196 R_WORKTREE_M + self.manifest.branch,
3197 )
3198
3199 # We can't update this ref with git worktrees until it exists.
3200 # We'll wait until the initial checkout to set it.
3201 if not os.path.exists(self.worktree):
3202 return
3203
3204 base = R_WORKTREE_M
3205 active_git = self.work_git
3206
3207 self._InitAnyMRef(HEAD, self.bare_git, detach=True)
3208 else:
3209 base = R_M
3210 active_git = self.bare_git
3211
3212 self._InitAnyMRef(base + self.manifest.branch, active_git)
3213
3214 def _InitMirrorHead(self):
3215 self._InitAnyMRef(HEAD, self.bare_git)
3216
3217 def _InitAnyMRef(self, ref, active_git, detach=False):
3218 """Initialize |ref| in |active_git| to the value in the manifest.
3219
3220 This points |ref| to the <project> setting in the manifest.
3221
3222 Args:
3223 ref: The branch to update.
3224 active_git: The git repository to make updates in.
3225 detach: Whether to update target of symbolic refs, or overwrite the
3226 ref directly (and thus make it non-symbolic).
3227 """
3228 cur = self.bare_ref.symref(ref)
3229
3230 if self.revisionId:
3231 if cur != "" or self.bare_ref.get(ref) != self.revisionId:
3232 msg = "manifest set to %s" % self.revisionId
3233 dst = self.revisionId + "^0"
3234 active_git.UpdateRef(ref, dst, message=msg, detach=True)
Julien Camperguedd654222014-01-09 16:21:37 +01003235 else:
Gavin Makea2e3302023-03-11 06:46:20 +00003236 remote = self.GetRemote()
3237 dst = remote.ToLocal(self.revisionExpr)
3238 if cur != dst:
3239 msg = "manifest set to %s" % self.revisionExpr
3240 if detach:
3241 active_git.UpdateRef(ref, dst, message=msg, detach=True)
3242 else:
3243 active_git.symbolic_ref("-m", msg, ref, dst)
Julien Camperguedd654222014-01-09 16:21:37 +01003244
Gavin Makea2e3302023-03-11 06:46:20 +00003245 def _CheckDirReference(self, srcdir, destdir):
3246 # Git worktrees don't use symlinks to share at all.
3247 if self.use_git_worktrees:
3248 return
Julien Camperguedd654222014-01-09 16:21:37 +01003249
Gavin Makea2e3302023-03-11 06:46:20 +00003250 for name in self.shareable_dirs:
3251 # Try to self-heal a bit in simple cases.
3252 dst_path = os.path.join(destdir, name)
3253 src_path = os.path.join(srcdir, name)
Julien Camperguedd654222014-01-09 16:21:37 +01003254
Gavin Makea2e3302023-03-11 06:46:20 +00003255 dst = platform_utils.realpath(dst_path)
3256 if os.path.lexists(dst):
3257 src = platform_utils.realpath(src_path)
3258 # Fail if the links are pointing to the wrong place.
3259 if src != dst:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00003260 logger.error(
3261 "error: %s is different in %s vs %s",
3262 name,
3263 destdir,
3264 srcdir,
3265 )
Gavin Makea2e3302023-03-11 06:46:20 +00003266 raise GitError(
3267 "--force-sync not enabled; cannot overwrite a local "
3268 "work tree. If you're comfortable with the "
3269 "possibility of losing the work tree's git metadata,"
3270 " use `repo sync --force-sync {0}` to "
Jason Chang32b59562023-07-14 16:45:35 -07003271 "proceed.".format(self.RelPath(local=False)),
3272 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003273 )
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003274
Gavin Makea2e3302023-03-11 06:46:20 +00003275 def _ReferenceGitDir(self, gitdir, dotgit, copy_all):
3276 """Update |dotgit| to reference |gitdir|, using symlinks where possible.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003277
Gavin Makea2e3302023-03-11 06:46:20 +00003278 Args:
3279 gitdir: The bare git repository. Must already be initialized.
3280 dotgit: The repository you would like to initialize.
3281 copy_all: If true, copy all remaining files from |gitdir| ->
3282 |dotgit|. This saves you the effort of initializing |dotgit|
3283 yourself.
3284 """
3285 symlink_dirs = self.shareable_dirs[:]
3286 to_symlink = symlink_dirs
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003287
Gavin Makea2e3302023-03-11 06:46:20 +00003288 to_copy = []
3289 if copy_all:
3290 to_copy = platform_utils.listdir(gitdir)
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003291
Gavin Makea2e3302023-03-11 06:46:20 +00003292 dotgit = platform_utils.realpath(dotgit)
3293 for name in set(to_copy).union(to_symlink):
3294 try:
3295 src = platform_utils.realpath(os.path.join(gitdir, name))
3296 dst = os.path.join(dotgit, name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003297
Gavin Makea2e3302023-03-11 06:46:20 +00003298 if os.path.lexists(dst):
3299 continue
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003300
Gavin Makea2e3302023-03-11 06:46:20 +00003301 # If the source dir doesn't exist, create an empty dir.
3302 if name in symlink_dirs and not os.path.lexists(src):
3303 os.makedirs(src)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003304
Gavin Makea2e3302023-03-11 06:46:20 +00003305 if name in to_symlink:
3306 platform_utils.symlink(
3307 os.path.relpath(src, os.path.dirname(dst)), dst
3308 )
3309 elif copy_all and not platform_utils.islink(dst):
3310 if platform_utils.isdir(src):
3311 shutil.copytree(src, dst)
3312 elif os.path.isfile(src):
3313 shutil.copy(src, dst)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003314
Gavin Makea2e3302023-03-11 06:46:20 +00003315 except OSError as e:
3316 if e.errno == errno.EPERM:
3317 raise DownloadError(self._get_symlink_error_message())
3318 else:
3319 raise
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003320
Gavin Makea2e3302023-03-11 06:46:20 +00003321 def _InitGitWorktree(self):
3322 """Init the project using git worktrees."""
3323 self.bare_git.worktree("prune")
3324 self.bare_git.worktree(
3325 "add",
3326 "-ff",
3327 "--checkout",
3328 "--detach",
3329 "--lock",
3330 self.worktree,
3331 self.GetRevisionId(),
3332 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003333
Gavin Makea2e3302023-03-11 06:46:20 +00003334 # Rewrite the internal state files to use relative paths between the
3335 # checkouts & worktrees.
3336 dotgit = os.path.join(self.worktree, ".git")
Jason R. Coombs034950b2023-10-20 23:32:02 +05453337 with open(dotgit) as fp:
Gavin Makea2e3302023-03-11 06:46:20 +00003338 # Figure out the checkout->worktree path.
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003339 setting = fp.read()
Gavin Makea2e3302023-03-11 06:46:20 +00003340 assert setting.startswith("gitdir:")
3341 git_worktree_path = setting.split(":", 1)[1].strip()
3342 # Some platforms (e.g. Windows) won't let us update dotgit in situ
3343 # because of file permissions. Delete it and recreate it from scratch
3344 # to avoid.
3345 platform_utils.remove(dotgit)
3346 # Use relative path from checkout->worktree & maintain Unix line endings
3347 # on all OS's to match git behavior.
3348 with open(dotgit, "w", newline="\n") as fp:
3349 print(
3350 "gitdir:",
3351 os.path.relpath(git_worktree_path, self.worktree),
3352 file=fp,
3353 )
3354 # Use relative path from worktree->checkout & maintain Unix line endings
3355 # on all OS's to match git behavior.
3356 with open(
3357 os.path.join(git_worktree_path, "gitdir"), "w", newline="\n"
3358 ) as fp:
3359 print(os.path.relpath(dotgit, git_worktree_path), file=fp)
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003360
Gavin Makea2e3302023-03-11 06:46:20 +00003361 self._InitMRef()
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003362
Gavin Makea2e3302023-03-11 06:46:20 +00003363 def _InitWorkTree(self, force_sync=False, submodules=False):
3364 """Setup the worktree .git path.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003365
Gavin Makea2e3302023-03-11 06:46:20 +00003366 This is the user-visible path like src/foo/.git/.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003367
Gavin Makea2e3302023-03-11 06:46:20 +00003368 With non-git-worktrees, this will be a symlink to the .repo/projects/
3369 path. With git-worktrees, this will be a .git file using "gitdir: ..."
3370 syntax.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003371
Gavin Makea2e3302023-03-11 06:46:20 +00003372 Older checkouts had .git/ directories. If we see that, migrate it.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003373
Gavin Makea2e3302023-03-11 06:46:20 +00003374 This also handles changes in the manifest. Maybe this project was
3375 backed by "foo/bar" on the server, but now it's "new/foo/bar". We have
3376 to update the path we point to under .repo/projects/ to match.
3377 """
3378 dotgit = os.path.join(self.worktree, ".git")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003379
Gavin Makea2e3302023-03-11 06:46:20 +00003380 # If using an old layout style (a directory), migrate it.
3381 if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
Jason Chang32b59562023-07-14 16:45:35 -07003382 self._MigrateOldWorkTreeGitDir(dotgit, project=self.name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003383
Gavin Makea2e3302023-03-11 06:46:20 +00003384 init_dotgit = not os.path.exists(dotgit)
3385 if self.use_git_worktrees:
3386 if init_dotgit:
3387 self._InitGitWorktree()
3388 self._CopyAndLinkFiles()
3389 else:
3390 if not init_dotgit:
3391 # See if the project has changed.
3392 if platform_utils.realpath(
3393 self.gitdir
3394 ) != platform_utils.realpath(dotgit):
3395 platform_utils.remove(dotgit)
Doug Anderson37282b42011-03-04 11:54:18 -08003396
Gavin Makea2e3302023-03-11 06:46:20 +00003397 if init_dotgit or not os.path.exists(dotgit):
3398 os.makedirs(self.worktree, exist_ok=True)
3399 platform_utils.symlink(
3400 os.path.relpath(self.gitdir, self.worktree), dotgit
3401 )
Doug Anderson37282b42011-03-04 11:54:18 -08003402
Gavin Makea2e3302023-03-11 06:46:20 +00003403 if init_dotgit:
3404 _lwrite(
3405 os.path.join(dotgit, HEAD), "%s\n" % self.GetRevisionId()
3406 )
Doug Anderson37282b42011-03-04 11:54:18 -08003407
Gavin Makea2e3302023-03-11 06:46:20 +00003408 # Finish checking out the worktree.
3409 cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
3410 if GitCommand(self, cmd).Wait() != 0:
3411 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07003412 "Cannot initialize work tree for " + self.name,
3413 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003414 )
Doug Anderson37282b42011-03-04 11:54:18 -08003415
Gavin Makea2e3302023-03-11 06:46:20 +00003416 if submodules:
3417 self._SyncSubmodules(quiet=True)
3418 self._CopyAndLinkFiles()
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003419
Gavin Makea2e3302023-03-11 06:46:20 +00003420 @classmethod
Jason Chang32b59562023-07-14 16:45:35 -07003421 def _MigrateOldWorkTreeGitDir(cls, dotgit, project=None):
Gavin Makea2e3302023-03-11 06:46:20 +00003422 """Migrate the old worktree .git/ dir style to a symlink.
3423
3424 This logic specifically only uses state from |dotgit| to figure out
3425 where to move content and not |self|. This way if the backing project
3426 also changed places, we only do the .git/ dir to .git symlink migration
3427 here. The path updates will happen independently.
3428 """
3429 # Figure out where in .repo/projects/ it's pointing to.
3430 if not os.path.islink(os.path.join(dotgit, "refs")):
Jason Chang32b59562023-07-14 16:45:35 -07003431 raise GitError(
3432 f"{dotgit}: unsupported checkout state", project=project
3433 )
Gavin Makea2e3302023-03-11 06:46:20 +00003434 gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, "refs")))
3435
3436 # Remove known symlink paths that exist in .repo/projects/.
3437 KNOWN_LINKS = {
3438 "config",
3439 "description",
3440 "hooks",
3441 "info",
3442 "logs",
3443 "objects",
3444 "packed-refs",
3445 "refs",
3446 "rr-cache",
3447 "shallow",
3448 "svn",
3449 }
3450 # Paths that we know will be in both, but are safe to clobber in
3451 # .repo/projects/.
3452 SAFE_TO_CLOBBER = {
3453 "COMMIT_EDITMSG",
3454 "FETCH_HEAD",
3455 "HEAD",
3456 "gc.log",
3457 "gitk.cache",
3458 "index",
3459 "ORIG_HEAD",
3460 }
3461
3462 # First see if we'd succeed before starting the migration.
3463 unknown_paths = []
3464 for name in platform_utils.listdir(dotgit):
3465 # Ignore all temporary/backup names. These are common with vim &
3466 # emacs.
3467 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3468 continue
3469
3470 dotgit_path = os.path.join(dotgit, name)
3471 if name in KNOWN_LINKS:
3472 if not platform_utils.islink(dotgit_path):
3473 unknown_paths.append(f"{dotgit_path}: should be a symlink")
3474 else:
3475 gitdir_path = os.path.join(gitdir, name)
3476 if name not in SAFE_TO_CLOBBER and os.path.exists(gitdir_path):
3477 unknown_paths.append(
3478 f"{dotgit_path}: unknown file; please file a bug"
3479 )
3480 if unknown_paths:
Jason Chang32b59562023-07-14 16:45:35 -07003481 raise GitError(
3482 "Aborting migration: " + "\n".join(unknown_paths),
3483 project=project,
3484 )
Gavin Makea2e3302023-03-11 06:46:20 +00003485
3486 # Now walk the paths and sync the .git/ to .repo/projects/.
3487 for name in platform_utils.listdir(dotgit):
3488 dotgit_path = os.path.join(dotgit, name)
3489
3490 # Ignore all temporary/backup names. These are common with vim &
3491 # emacs.
3492 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3493 platform_utils.remove(dotgit_path)
3494 elif name in KNOWN_LINKS:
3495 platform_utils.remove(dotgit_path)
3496 else:
3497 gitdir_path = os.path.join(gitdir, name)
3498 platform_utils.remove(gitdir_path, missing_ok=True)
3499 platform_utils.rename(dotgit_path, gitdir_path)
3500
3501 # Now that the dir should be empty, clear it out, and symlink it over.
3502 platform_utils.rmdir(dotgit)
3503 platform_utils.symlink(
Jason R. Coombs47944bb2023-09-29 12:42:22 -04003504 os.path.relpath(gitdir, os.path.dirname(os.path.realpath(dotgit))),
3505 dotgit,
Gavin Makea2e3302023-03-11 06:46:20 +00003506 )
3507
3508 def _get_symlink_error_message(self):
3509 if platform_utils.isWindows():
3510 return (
3511 "Unable to create symbolic link. Please re-run the command as "
3512 "Administrator, or see "
3513 "https://github.com/git-for-windows/git/wiki/Symbolic-Links "
3514 "for other options."
3515 )
3516 return "filesystem must support symlinks"
3517
3518 def _revlist(self, *args, **kw):
3519 a = []
3520 a.extend(args)
3521 a.append("--")
3522 return self.work_git.rev_list(*a, **kw)
3523
3524 @property
3525 def _allrefs(self):
3526 return self.bare_ref.all
3527
3528 def _getLogs(
3529 self, rev1, rev2, oneline=False, color=True, pretty_format=None
3530 ):
3531 """Get logs between two revisions of this project."""
3532 comp = ".."
3533 if rev1:
3534 revs = [rev1]
3535 if rev2:
3536 revs.extend([comp, rev2])
3537 cmd = ["log", "".join(revs)]
3538 out = DiffColoring(self.config)
3539 if out.is_on and color:
3540 cmd.append("--color")
3541 if pretty_format is not None:
3542 cmd.append("--pretty=format:%s" % pretty_format)
3543 if oneline:
3544 cmd.append("--oneline")
3545
3546 try:
3547 log = GitCommand(
3548 self, cmd, capture_stdout=True, capture_stderr=True
3549 )
3550 if log.Wait() == 0:
3551 return log.stdout
3552 except GitError:
3553 # worktree may not exist if groups changed for example. In that
3554 # case, try in gitdir instead.
3555 if not os.path.exists(self.worktree):
3556 return self.bare_git.log(*cmd[1:])
3557 else:
3558 raise
3559 return None
3560
3561 def getAddedAndRemovedLogs(
3562 self, toProject, oneline=False, color=True, pretty_format=None
3563 ):
3564 """Get the list of logs from this revision to given revisionId"""
3565 logs = {}
3566 selfId = self.GetRevisionId(self._allrefs)
3567 toId = toProject.GetRevisionId(toProject._allrefs)
3568
3569 logs["added"] = self._getLogs(
3570 selfId,
3571 toId,
3572 oneline=oneline,
3573 color=color,
3574 pretty_format=pretty_format,
3575 )
3576 logs["removed"] = self._getLogs(
3577 toId,
3578 selfId,
3579 oneline=oneline,
3580 color=color,
3581 pretty_format=pretty_format,
3582 )
3583 return logs
3584
Mike Frysingerd4aee652023-10-19 05:13:32 -04003585 class _GitGetByExec:
Gavin Makea2e3302023-03-11 06:46:20 +00003586 def __init__(self, project, bare, gitdir):
3587 self._project = project
3588 self._bare = bare
3589 self._gitdir = gitdir
3590
3591 # __getstate__ and __setstate__ are required for pickling because
3592 # __getattr__ exists.
3593 def __getstate__(self):
3594 return (self._project, self._bare, self._gitdir)
3595
3596 def __setstate__(self, state):
3597 self._project, self._bare, self._gitdir = state
3598
3599 def LsOthers(self):
3600 p = GitCommand(
3601 self._project,
3602 ["ls-files", "-z", "--others", "--exclude-standard"],
3603 bare=False,
3604 gitdir=self._gitdir,
3605 capture_stdout=True,
3606 capture_stderr=True,
3607 )
3608 if p.Wait() == 0:
3609 out = p.stdout
3610 if out:
3611 # Backslash is not anomalous.
3612 return out[:-1].split("\0")
3613 return []
3614
3615 def DiffZ(self, name, *args):
3616 cmd = [name]
3617 cmd.append("-z")
3618 cmd.append("--ignore-submodules")
3619 cmd.extend(args)
3620 p = GitCommand(
3621 self._project,
3622 cmd,
3623 gitdir=self._gitdir,
3624 bare=False,
3625 capture_stdout=True,
3626 capture_stderr=True,
3627 )
3628 p.Wait()
3629 r = {}
3630 out = p.stdout
3631 if out:
3632 out = iter(out[:-1].split("\0"))
3633 while out:
3634 try:
3635 info = next(out)
3636 path = next(out)
3637 except StopIteration:
3638 break
3639
Mike Frysingerd4aee652023-10-19 05:13:32 -04003640 class _Info:
Gavin Makea2e3302023-03-11 06:46:20 +00003641 def __init__(self, path, omode, nmode, oid, nid, state):
3642 self.path = path
3643 self.src_path = None
3644 self.old_mode = omode
3645 self.new_mode = nmode
3646 self.old_id = oid
3647 self.new_id = nid
3648
3649 if len(state) == 1:
3650 self.status = state
3651 self.level = None
3652 else:
3653 self.status = state[:1]
3654 self.level = state[1:]
3655 while self.level.startswith("0"):
3656 self.level = self.level[1:]
3657
3658 info = info[1:].split(" ")
3659 info = _Info(path, *info)
3660 if info.status in ("R", "C"):
3661 info.src_path = info.path
3662 info.path = next(out)
3663 r[info.path] = info
3664 return r
3665
3666 def GetDotgitPath(self, subpath=None):
3667 """Return the full path to the .git dir.
3668
3669 As a convenience, append |subpath| if provided.
3670 """
3671 if self._bare:
3672 dotgit = self._gitdir
3673 else:
3674 dotgit = os.path.join(self._project.worktree, ".git")
3675 if os.path.isfile(dotgit):
3676 # Git worktrees use a "gitdir:" syntax to point to the
3677 # scratch space.
3678 with open(dotgit) as fp:
3679 setting = fp.read()
3680 assert setting.startswith("gitdir:")
3681 gitdir = setting.split(":", 1)[1].strip()
3682 dotgit = os.path.normpath(
3683 os.path.join(self._project.worktree, gitdir)
3684 )
3685
3686 return dotgit if subpath is None else os.path.join(dotgit, subpath)
3687
3688 def GetHead(self):
3689 """Return the ref that HEAD points to."""
3690 path = self.GetDotgitPath(subpath=HEAD)
3691 try:
3692 with open(path) as fd:
3693 line = fd.readline()
Jason R. Coombsae824fb2023-10-20 23:32:40 +05453694 except OSError as e:
Gavin Makea2e3302023-03-11 06:46:20 +00003695 raise NoManifestException(path, str(e))
3696 try:
3697 line = line.decode()
3698 except AttributeError:
3699 pass
3700 if line.startswith("ref: "):
3701 return line[5:-1]
3702 return line[:-1]
3703
3704 def SetHead(self, ref, message=None):
3705 cmdv = []
3706 if message is not None:
3707 cmdv.extend(["-m", message])
3708 cmdv.append(HEAD)
3709 cmdv.append(ref)
3710 self.symbolic_ref(*cmdv)
3711
3712 def DetachHead(self, new, message=None):
3713 cmdv = ["--no-deref"]
3714 if message is not None:
3715 cmdv.extend(["-m", message])
3716 cmdv.append(HEAD)
3717 cmdv.append(new)
3718 self.update_ref(*cmdv)
3719
3720 def UpdateRef(self, name, new, old=None, message=None, detach=False):
3721 cmdv = []
3722 if message is not None:
3723 cmdv.extend(["-m", message])
3724 if detach:
3725 cmdv.append("--no-deref")
3726 cmdv.append(name)
3727 cmdv.append(new)
3728 if old is not None:
3729 cmdv.append(old)
3730 self.update_ref(*cmdv)
3731
3732 def DeleteRef(self, name, old=None):
3733 if not old:
3734 old = self.rev_parse(name)
3735 self.update_ref("-d", name, old)
3736 self._project.bare_ref.deleted(name)
3737
Jason Chang87058c62023-09-27 11:34:43 -07003738 def rev_list(self, *args, log_as_error=True, **kw):
Gavin Makea2e3302023-03-11 06:46:20 +00003739 if "format" in kw:
3740 cmdv = ["log", "--pretty=format:%s" % kw["format"]]
3741 else:
3742 cmdv = ["rev-list"]
3743 cmdv.extend(args)
3744 p = GitCommand(
3745 self._project,
3746 cmdv,
3747 bare=self._bare,
3748 gitdir=self._gitdir,
3749 capture_stdout=True,
3750 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003751 verify_command=True,
Jason Chang87058c62023-09-27 11:34:43 -07003752 log_as_error=log_as_error,
Gavin Makea2e3302023-03-11 06:46:20 +00003753 )
Jason Chang32b59562023-07-14 16:45:35 -07003754 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003755 return p.stdout.splitlines()
3756
3757 def __getattr__(self, name):
3758 """Allow arbitrary git commands using pythonic syntax.
3759
3760 This allows you to do things like:
3761 git_obj.rev_parse('HEAD')
3762
3763 Since we don't have a 'rev_parse' method defined, the __getattr__
3764 will run. We'll replace the '_' with a '-' and try to run a git
3765 command. Any other positional arguments will be passed to the git
3766 command, and the following keyword arguments are supported:
3767 config: An optional dict of git config options to be passed with
3768 '-c'.
3769
3770 Args:
3771 name: The name of the git command to call. Any '_' characters
3772 will be replaced with '-'.
3773
3774 Returns:
3775 A callable object that will try to call git with the named
3776 command.
3777 """
3778 name = name.replace("_", "-")
3779
Jason Chang87058c62023-09-27 11:34:43 -07003780 def runner(*args, log_as_error=True, **kwargs):
Gavin Makea2e3302023-03-11 06:46:20 +00003781 cmdv = []
3782 config = kwargs.pop("config", None)
3783 for k in kwargs:
3784 raise TypeError(
3785 "%s() got an unexpected keyword argument %r" % (name, k)
3786 )
3787 if config is not None:
3788 for k, v in config.items():
3789 cmdv.append("-c")
3790 cmdv.append("%s=%s" % (k, v))
3791 cmdv.append(name)
3792 cmdv.extend(args)
3793 p = GitCommand(
3794 self._project,
3795 cmdv,
3796 bare=self._bare,
3797 gitdir=self._gitdir,
3798 capture_stdout=True,
3799 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003800 verify_command=True,
Jason Chang87058c62023-09-27 11:34:43 -07003801 log_as_error=log_as_error,
Gavin Makea2e3302023-03-11 06:46:20 +00003802 )
Jason Chang32b59562023-07-14 16:45:35 -07003803 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003804 r = p.stdout
3805 if r.endswith("\n") and r.index("\n") == len(r) - 1:
3806 return r[:-1]
3807 return r
3808
3809 return runner
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003810
3811
Jason Chang32b59562023-07-14 16:45:35 -07003812class LocalSyncFail(RepoError):
3813 """Default error when there is an Sync_LocalHalf error."""
3814
3815
3816class _PriorSyncFailedError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003817 def __str__(self):
3818 return "prior sync failed; rebase still in progress"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003819
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003820
Jason Chang32b59562023-07-14 16:45:35 -07003821class _DirtyError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003822 def __str__(self):
3823 return "contains uncommitted changes"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003824
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003825
Mike Frysingerd4aee652023-10-19 05:13:32 -04003826class _InfoMessage:
Gavin Makea2e3302023-03-11 06:46:20 +00003827 def __init__(self, project, text):
3828 self.project = project
3829 self.text = text
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003830
Gavin Makea2e3302023-03-11 06:46:20 +00003831 def Print(self, syncbuf):
3832 syncbuf.out.info(
3833 "%s/: %s", self.project.RelPath(local=False), self.text
3834 )
3835 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003836
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003837
Mike Frysingerd4aee652023-10-19 05:13:32 -04003838class _Failure:
Gavin Makea2e3302023-03-11 06:46:20 +00003839 def __init__(self, project, why):
3840 self.project = project
3841 self.why = why
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003842
Gavin Makea2e3302023-03-11 06:46:20 +00003843 def Print(self, syncbuf):
3844 syncbuf.out.fail(
3845 "error: %s/: %s", self.project.RelPath(local=False), str(self.why)
3846 )
3847 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003848
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003849
Mike Frysingerd4aee652023-10-19 05:13:32 -04003850class _Later:
Gavin Makea2e3302023-03-11 06:46:20 +00003851 def __init__(self, project, action):
3852 self.project = project
3853 self.action = action
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003854
Gavin Makea2e3302023-03-11 06:46:20 +00003855 def Run(self, syncbuf):
3856 out = syncbuf.out
3857 out.project("project %s/", self.project.RelPath(local=False))
3858 out.nl()
3859 try:
3860 self.action()
3861 out.nl()
3862 return True
3863 except GitError:
3864 out.nl()
3865 return False
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003866
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003867
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003868class _SyncColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +00003869 def __init__(self, config):
3870 super().__init__(config, "reposync")
3871 self.project = self.printer("header", attr="bold")
3872 self.info = self.printer("info")
3873 self.fail = self.printer("fail", fg="red")
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003874
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003875
Mike Frysingerd4aee652023-10-19 05:13:32 -04003876class SyncBuffer:
Gavin Makea2e3302023-03-11 06:46:20 +00003877 def __init__(self, config, detach_head=False):
3878 self._messages = []
3879 self._failures = []
3880 self._later_queue1 = []
3881 self._later_queue2 = []
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003882
Gavin Makea2e3302023-03-11 06:46:20 +00003883 self.out = _SyncColoring(config)
3884 self.out.redirect(sys.stderr)
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003885
Gavin Makea2e3302023-03-11 06:46:20 +00003886 self.detach_head = detach_head
3887 self.clean = True
3888 self.recent_clean = True
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003889
Gavin Makea2e3302023-03-11 06:46:20 +00003890 def info(self, project, fmt, *args):
3891 self._messages.append(_InfoMessage(project, fmt % args))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003892
Gavin Makea2e3302023-03-11 06:46:20 +00003893 def fail(self, project, err=None):
3894 self._failures.append(_Failure(project, err))
David Rileye0684ad2017-04-05 00:02:59 -07003895 self._MarkUnclean()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003896
Gavin Makea2e3302023-03-11 06:46:20 +00003897 def later1(self, project, what):
3898 self._later_queue1.append(_Later(project, what))
Mike Frysinger70d861f2019-08-26 15:22:36 -04003899
Gavin Makea2e3302023-03-11 06:46:20 +00003900 def later2(self, project, what):
3901 self._later_queue2.append(_Later(project, what))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003902
Gavin Makea2e3302023-03-11 06:46:20 +00003903 def Finish(self):
3904 self._PrintMessages()
3905 self._RunLater()
3906 self._PrintMessages()
3907 return self.clean
3908
3909 def Recently(self):
3910 recent_clean = self.recent_clean
3911 self.recent_clean = True
3912 return recent_clean
3913
3914 def _MarkUnclean(self):
3915 self.clean = False
3916 self.recent_clean = False
3917
3918 def _RunLater(self):
3919 for q in ["_later_queue1", "_later_queue2"]:
3920 if not self._RunQueue(q):
3921 return
3922
3923 def _RunQueue(self, queue):
3924 for m in getattr(self, queue):
3925 if not m.Run(self):
3926 self._MarkUnclean()
3927 return False
3928 setattr(self, queue, [])
3929 return True
3930
3931 def _PrintMessages(self):
3932 if self._messages or self._failures:
3933 if os.isatty(2):
3934 self.out.write(progress.CSI_ERASE_LINE)
3935 self.out.write("\r")
3936
3937 for m in self._messages:
3938 m.Print(self)
3939 for m in self._failures:
3940 m.Print(self)
3941
3942 self._messages = []
3943 self._failures = []
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003944
3945
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003946class MetaProject(Project):
Gavin Makea2e3302023-03-11 06:46:20 +00003947 """A special project housed under .repo."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003948
Gavin Makea2e3302023-03-11 06:46:20 +00003949 def __init__(self, manifest, name, gitdir, worktree):
3950 Project.__init__(
3951 self,
3952 manifest=manifest,
3953 name=name,
3954 gitdir=gitdir,
3955 objdir=gitdir,
3956 worktree=worktree,
3957 remote=RemoteSpec("origin"),
3958 relpath=".repo/%s" % name,
3959 revisionExpr="refs/heads/master",
3960 revisionId=None,
3961 groups=None,
3962 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003963
Gavin Makea2e3302023-03-11 06:46:20 +00003964 def PreSync(self):
3965 if self.Exists:
3966 cb = self.CurrentBranch
3967 if cb:
3968 base = self.GetBranch(cb).merge
3969 if base:
3970 self.revisionExpr = base
3971 self.revisionId = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003972
Gavin Makea2e3302023-03-11 06:46:20 +00003973 @property
3974 def HasChanges(self):
3975 """Has the remote received new commits not yet checked out?"""
3976 if not self.remote or not self.revisionExpr:
3977 return False
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003978
Gavin Makea2e3302023-03-11 06:46:20 +00003979 all_refs = self.bare_ref.all
3980 revid = self.GetRevisionId(all_refs)
3981 head = self.work_git.GetHead()
3982 if head.startswith(R_HEADS):
3983 try:
3984 head = all_refs[head]
3985 except KeyError:
3986 head = None
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003987
Gavin Makea2e3302023-03-11 06:46:20 +00003988 if revid == head:
3989 return False
3990 elif self._revlist(not_rev(HEAD), revid):
3991 return True
3992 return False
LaMont Jones9b72cf22022-03-29 21:54:22 +00003993
3994
3995class RepoProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003996 """The MetaProject for repo itself."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003997
Gavin Makea2e3302023-03-11 06:46:20 +00003998 @property
3999 def LastFetch(self):
4000 try:
4001 fh = os.path.join(self.gitdir, "FETCH_HEAD")
4002 return os.path.getmtime(fh)
4003 except OSError:
4004 return 0
LaMont Jones9b72cf22022-03-29 21:54:22 +00004005
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +01004006
LaMont Jones9b72cf22022-03-29 21:54:22 +00004007class ManifestProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00004008 """The MetaProject for manifests."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00004009
Gavin Makea2e3302023-03-11 06:46:20 +00004010 def MetaBranchSwitch(self, submodules=False):
4011 """Prepare for manifest branch switch."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00004012
Gavin Makea2e3302023-03-11 06:46:20 +00004013 # detach and delete manifest branch, allowing a new
4014 # branch to take over
4015 syncbuf = SyncBuffer(self.config, detach_head=True)
4016 self.Sync_LocalHalf(syncbuf, submodules=submodules)
4017 syncbuf.Finish()
LaMont Jones9b72cf22022-03-29 21:54:22 +00004018
Gavin Makea2e3302023-03-11 06:46:20 +00004019 return (
4020 GitCommand(
4021 self,
4022 ["update-ref", "-d", "refs/heads/default"],
4023 capture_stdout=True,
4024 capture_stderr=True,
4025 ).Wait()
4026 == 0
4027 )
LaMont Jones9b03f152022-03-29 23:01:18 +00004028
Gavin Makea2e3302023-03-11 06:46:20 +00004029 @property
4030 def standalone_manifest_url(self):
4031 """The URL of the standalone manifest, or None."""
4032 return self.config.GetString("manifest.standalone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004033
Gavin Makea2e3302023-03-11 06:46:20 +00004034 @property
4035 def manifest_groups(self):
4036 """The manifest groups string."""
4037 return self.config.GetString("manifest.groups")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004038
Gavin Makea2e3302023-03-11 06:46:20 +00004039 @property
4040 def reference(self):
4041 """The --reference for this manifest."""
4042 return self.config.GetString("repo.reference")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004043
Gavin Makea2e3302023-03-11 06:46:20 +00004044 @property
4045 def dissociate(self):
4046 """Whether to dissociate."""
4047 return self.config.GetBoolean("repo.dissociate")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004048
Gavin Makea2e3302023-03-11 06:46:20 +00004049 @property
4050 def archive(self):
4051 """Whether we use archive."""
4052 return self.config.GetBoolean("repo.archive")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004053
Gavin Makea2e3302023-03-11 06:46:20 +00004054 @property
4055 def mirror(self):
4056 """Whether we use mirror."""
4057 return self.config.GetBoolean("repo.mirror")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004058
Gavin Makea2e3302023-03-11 06:46:20 +00004059 @property
4060 def use_worktree(self):
4061 """Whether we use worktree."""
4062 return self.config.GetBoolean("repo.worktree")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004063
Gavin Makea2e3302023-03-11 06:46:20 +00004064 @property
4065 def clone_bundle(self):
4066 """Whether we use clone_bundle."""
4067 return self.config.GetBoolean("repo.clonebundle")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004068
Gavin Makea2e3302023-03-11 06:46:20 +00004069 @property
4070 def submodules(self):
4071 """Whether we use submodules."""
4072 return self.config.GetBoolean("repo.submodules")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004073
Gavin Makea2e3302023-03-11 06:46:20 +00004074 @property
4075 def git_lfs(self):
4076 """Whether we use git_lfs."""
4077 return self.config.GetBoolean("repo.git-lfs")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004078
Gavin Makea2e3302023-03-11 06:46:20 +00004079 @property
4080 def use_superproject(self):
4081 """Whether we use superproject."""
4082 return self.config.GetBoolean("repo.superproject")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004083
Gavin Makea2e3302023-03-11 06:46:20 +00004084 @property
4085 def partial_clone(self):
4086 """Whether this is a partial clone."""
4087 return self.config.GetBoolean("repo.partialclone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004088
Gavin Makea2e3302023-03-11 06:46:20 +00004089 @property
4090 def depth(self):
4091 """Partial clone depth."""
Roberto Vladimir Prado Carranza3d58d212023-09-13 10:27:26 +02004092 return self.config.GetInt("repo.depth")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004093
Gavin Makea2e3302023-03-11 06:46:20 +00004094 @property
4095 def clone_filter(self):
4096 """The clone filter."""
4097 return self.config.GetString("repo.clonefilter")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004098
Gavin Makea2e3302023-03-11 06:46:20 +00004099 @property
4100 def partial_clone_exclude(self):
4101 """Partial clone exclude string"""
4102 return self.config.GetString("repo.partialcloneexclude")
LaMont Jones4ada0432022-04-14 15:10:43 +00004103
Gavin Makea2e3302023-03-11 06:46:20 +00004104 @property
Jason Chang17833322023-05-23 13:06:55 -07004105 def clone_filter_for_depth(self):
4106 """Replace shallow clone with partial clone."""
4107 return self.config.GetString("repo.clonefilterfordepth")
4108
4109 @property
Gavin Makea2e3302023-03-11 06:46:20 +00004110 def manifest_platform(self):
4111 """The --platform argument from `repo init`."""
4112 return self.config.GetString("manifest.platform")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004113
Gavin Makea2e3302023-03-11 06:46:20 +00004114 @property
4115 def _platform_name(self):
4116 """Return the name of the platform."""
4117 return platform.system().lower()
LaMont Jones9b03f152022-03-29 23:01:18 +00004118
Gavin Makea2e3302023-03-11 06:46:20 +00004119 def SyncWithPossibleInit(
4120 self,
4121 submanifest,
4122 verbose=False,
4123 current_branch_only=False,
4124 tags="",
4125 git_event_log=None,
4126 ):
4127 """Sync a manifestProject, possibly for the first time.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004128
Gavin Makea2e3302023-03-11 06:46:20 +00004129 Call Sync() with arguments from the most recent `repo init`. If this is
4130 a new sub manifest, then inherit options from the parent's
4131 manifestProject.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004132
Gavin Makea2e3302023-03-11 06:46:20 +00004133 This is used by subcmds.Sync() to do an initial download of new sub
4134 manifests.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004135
Gavin Makea2e3302023-03-11 06:46:20 +00004136 Args:
4137 submanifest: an XmlSubmanifest, the submanifest to re-sync.
4138 verbose: a boolean, whether to show all output, rather than only
4139 errors.
4140 current_branch_only: a boolean, whether to only fetch the current
4141 manifest branch from the server.
4142 tags: a boolean, whether to fetch tags.
4143 git_event_log: an EventLog, for git tracing.
4144 """
4145 # TODO(lamontjones): when refactoring sync (and init?) consider how to
4146 # better get the init options that we should use for new submanifests
4147 # that are added when syncing an existing workspace.
4148 git_event_log = git_event_log or EventLog()
LaMont Jonesb90a4222022-04-14 15:00:09 +00004149 spec = submanifest.ToSubmanifestSpec()
Gavin Makea2e3302023-03-11 06:46:20 +00004150 # Use the init options from the existing manifestProject, or the parent
4151 # if it doesn't exist.
4152 #
4153 # Today, we only support changing manifest_groups on the sub-manifest,
4154 # with no supported-for-the-user way to change the other arguments from
4155 # those specified by the outermost manifest.
4156 #
4157 # TODO(lamontjones): determine which of these should come from the
4158 # outermost manifest and which should come from the parent manifest.
4159 mp = self if self.Exists else submanifest.parent.manifestProject
4160 return self.Sync(
LaMont Jones55ee3042022-04-06 17:10:21 +00004161 manifest_url=spec.manifestUrl,
4162 manifest_branch=spec.revision,
Gavin Makea2e3302023-03-11 06:46:20 +00004163 standalone_manifest=mp.standalone_manifest_url,
4164 groups=mp.manifest_groups,
4165 platform=mp.manifest_platform,
4166 mirror=mp.mirror,
4167 dissociate=mp.dissociate,
4168 reference=mp.reference,
4169 worktree=mp.use_worktree,
4170 submodules=mp.submodules,
4171 archive=mp.archive,
4172 partial_clone=mp.partial_clone,
4173 clone_filter=mp.clone_filter,
4174 partial_clone_exclude=mp.partial_clone_exclude,
4175 clone_bundle=mp.clone_bundle,
4176 git_lfs=mp.git_lfs,
4177 use_superproject=mp.use_superproject,
LaMont Jones55ee3042022-04-06 17:10:21 +00004178 verbose=verbose,
4179 current_branch_only=current_branch_only,
4180 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00004181 depth=mp.depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004182 git_event_log=git_event_log,
4183 manifest_name=spec.manifestName,
Gavin Makea2e3302023-03-11 06:46:20 +00004184 this_manifest_only=True,
LaMont Jones55ee3042022-04-06 17:10:21 +00004185 outer_manifest=False,
Jason Chang17833322023-05-23 13:06:55 -07004186 clone_filter_for_depth=mp.clone_filter_for_depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004187 )
LaMont Jones409407a2022-04-05 21:21:56 +00004188
Gavin Makea2e3302023-03-11 06:46:20 +00004189 def Sync(
4190 self,
4191 _kwargs_only=(),
4192 manifest_url="",
4193 manifest_branch=None,
4194 standalone_manifest=False,
4195 groups="",
4196 mirror=False,
4197 reference="",
4198 dissociate=False,
4199 worktree=False,
4200 submodules=False,
4201 archive=False,
4202 partial_clone=None,
4203 depth=None,
4204 clone_filter="blob:none",
4205 partial_clone_exclude=None,
4206 clone_bundle=None,
4207 git_lfs=None,
4208 use_superproject=None,
4209 verbose=False,
4210 current_branch_only=False,
4211 git_event_log=None,
4212 platform="",
4213 manifest_name="default.xml",
4214 tags="",
4215 this_manifest_only=False,
4216 outer_manifest=True,
Jason Chang17833322023-05-23 13:06:55 -07004217 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00004218 ):
4219 """Sync the manifest and all submanifests.
LaMont Jones409407a2022-04-05 21:21:56 +00004220
Gavin Makea2e3302023-03-11 06:46:20 +00004221 Args:
4222 manifest_url: a string, the URL of the manifest project.
4223 manifest_branch: a string, the manifest branch to use.
4224 standalone_manifest: a boolean, whether to store the manifest as a
4225 static file.
4226 groups: a string, restricts the checkout to projects with the
4227 specified groups.
4228 mirror: a boolean, whether to create a mirror of the remote
4229 repository.
4230 reference: a string, location of a repo instance to use as a
4231 reference.
4232 dissociate: a boolean, whether to dissociate from reference mirrors
4233 after clone.
4234 worktree: a boolean, whether to use git-worktree to manage projects.
4235 submodules: a boolean, whether sync submodules associated with the
4236 manifest project.
4237 archive: a boolean, whether to checkout each project as an archive.
4238 See git-archive.
4239 partial_clone: a boolean, whether to perform a partial clone.
4240 depth: an int, how deep of a shallow clone to create.
4241 clone_filter: a string, filter to use with partial_clone.
4242 partial_clone_exclude : a string, comma-delimeted list of project
4243 names to exclude from partial clone.
4244 clone_bundle: a boolean, whether to enable /clone.bundle on
4245 HTTP/HTTPS.
4246 git_lfs: a boolean, whether to enable git LFS support.
4247 use_superproject: a boolean, whether to use the manifest
4248 superproject to sync projects.
4249 verbose: a boolean, whether to show all output, rather than only
4250 errors.
4251 current_branch_only: a boolean, whether to only fetch the current
4252 manifest branch from the server.
4253 platform: a string, restrict the checkout to projects with the
4254 specified platform group.
4255 git_event_log: an EventLog, for git tracing.
4256 tags: a boolean, whether to fetch tags.
4257 manifest_name: a string, the name of the manifest file to use.
4258 this_manifest_only: a boolean, whether to only operate on the
4259 current sub manifest.
4260 outer_manifest: a boolean, whether to start at the outermost
4261 manifest.
Jason Chang17833322023-05-23 13:06:55 -07004262 clone_filter_for_depth: a string, when specified replaces shallow
4263 clones with partial.
LaMont Jones9b03f152022-03-29 23:01:18 +00004264
Gavin Makea2e3302023-03-11 06:46:20 +00004265 Returns:
4266 a boolean, whether the sync was successful.
4267 """
4268 assert _kwargs_only == (), "Sync only accepts keyword arguments."
LaMont Jones9b03f152022-03-29 23:01:18 +00004269
Gavin Makea2e3302023-03-11 06:46:20 +00004270 groups = groups or self.manifest.GetDefaultGroupsStr(
4271 with_platform=False
4272 )
4273 platform = platform or "auto"
4274 git_event_log = git_event_log or EventLog()
4275 if outer_manifest and self.manifest.is_submanifest:
4276 # In a multi-manifest checkout, use the outer manifest unless we are
4277 # told not to.
4278 return self.client.outer_manifest.manifestProject.Sync(
4279 manifest_url=manifest_url,
4280 manifest_branch=manifest_branch,
4281 standalone_manifest=standalone_manifest,
4282 groups=groups,
4283 platform=platform,
4284 mirror=mirror,
4285 dissociate=dissociate,
4286 reference=reference,
4287 worktree=worktree,
4288 submodules=submodules,
4289 archive=archive,
4290 partial_clone=partial_clone,
4291 clone_filter=clone_filter,
4292 partial_clone_exclude=partial_clone_exclude,
4293 clone_bundle=clone_bundle,
4294 git_lfs=git_lfs,
4295 use_superproject=use_superproject,
4296 verbose=verbose,
4297 current_branch_only=current_branch_only,
4298 tags=tags,
4299 depth=depth,
4300 git_event_log=git_event_log,
4301 manifest_name=manifest_name,
4302 this_manifest_only=this_manifest_only,
4303 outer_manifest=False,
4304 )
LaMont Jones9b03f152022-03-29 23:01:18 +00004305
Gavin Makea2e3302023-03-11 06:46:20 +00004306 # If repo has already been initialized, we take -u with the absence of
4307 # --standalone-manifest to mean "transition to a standard repo set up",
4308 # which necessitates starting fresh.
4309 # If --standalone-manifest is set, we always tear everything down and
4310 # start anew.
4311 if self.Exists:
4312 was_standalone_manifest = self.config.GetString(
4313 "manifest.standalone"
4314 )
4315 if was_standalone_manifest and not manifest_url:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004316 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004317 "fatal: repo was initialized with a standlone manifest, "
4318 "cannot be re-initialized without --manifest-url/-u"
4319 )
4320 return False
4321
4322 if standalone_manifest or (
4323 was_standalone_manifest and manifest_url
4324 ):
4325 self.config.ClearCache()
4326 if self.gitdir and os.path.exists(self.gitdir):
4327 platform_utils.rmtree(self.gitdir)
4328 if self.worktree and os.path.exists(self.worktree):
4329 platform_utils.rmtree(self.worktree)
4330
4331 is_new = not self.Exists
4332 if is_new:
4333 if not manifest_url:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004334 logger.error("fatal: manifest url is required.")
Gavin Makea2e3302023-03-11 06:46:20 +00004335 return False
4336
4337 if verbose:
4338 print(
4339 "Downloading manifest from %s"
4340 % (GitConfig.ForUser().UrlInsteadOf(manifest_url),),
4341 file=sys.stderr,
4342 )
4343
4344 # The manifest project object doesn't keep track of the path on the
4345 # server where this git is located, so let's save that here.
4346 mirrored_manifest_git = None
4347 if reference:
4348 manifest_git_path = urllib.parse.urlparse(manifest_url).path[1:]
4349 mirrored_manifest_git = os.path.join(
4350 reference, manifest_git_path
4351 )
4352 if not mirrored_manifest_git.endswith(".git"):
4353 mirrored_manifest_git += ".git"
4354 if not os.path.exists(mirrored_manifest_git):
4355 mirrored_manifest_git = os.path.join(
4356 reference, ".repo/manifests.git"
4357 )
4358
4359 self._InitGitDir(mirror_git=mirrored_manifest_git)
4360
4361 # If standalone_manifest is set, mark the project as "standalone" --
4362 # we'll still do much of the manifests.git set up, but will avoid actual
4363 # syncs to a remote.
4364 if standalone_manifest:
4365 self.config.SetString("manifest.standalone", manifest_url)
4366 elif not manifest_url and not manifest_branch:
4367 # If -u is set and --standalone-manifest is not, then we're not in
4368 # standalone mode. Otherwise, use config to infer what we were in
4369 # the last init.
4370 standalone_manifest = bool(
4371 self.config.GetString("manifest.standalone")
4372 )
4373 if not standalone_manifest:
4374 self.config.SetString("manifest.standalone", None)
4375
4376 self._ConfigureDepth(depth)
4377
4378 # Set the remote URL before the remote branch as we might need it below.
4379 if manifest_url:
4380 r = self.GetRemote()
4381 r.url = manifest_url
4382 r.ResetFetch()
4383 r.Save()
4384
4385 if not standalone_manifest:
4386 if manifest_branch:
4387 if manifest_branch == "HEAD":
4388 manifest_branch = self.ResolveRemoteHead()
4389 if manifest_branch is None:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004390 logger.error("fatal: unable to resolve HEAD")
Gavin Makea2e3302023-03-11 06:46:20 +00004391 return False
4392 self.revisionExpr = manifest_branch
4393 else:
4394 if is_new:
4395 default_branch = self.ResolveRemoteHead()
4396 if default_branch is None:
4397 # If the remote doesn't have HEAD configured, default to
4398 # master.
4399 default_branch = "refs/heads/master"
4400 self.revisionExpr = default_branch
4401 else:
4402 self.PreSync()
4403
4404 groups = re.split(r"[,\s]+", groups or "")
4405 all_platforms = ["linux", "darwin", "windows"]
4406 platformize = lambda x: "platform-" + x
4407 if platform == "auto":
4408 if not mirror and not self.mirror:
4409 groups.append(platformize(self._platform_name))
4410 elif platform == "all":
4411 groups.extend(map(platformize, all_platforms))
4412 elif platform in all_platforms:
4413 groups.append(platformize(platform))
4414 elif platform != "none":
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004415 logger.error("fatal: invalid platform flag", file=sys.stderr)
Gavin Makea2e3302023-03-11 06:46:20 +00004416 return False
4417 self.config.SetString("manifest.platform", platform)
4418
4419 groups = [x for x in groups if x]
4420 groupstr = ",".join(groups)
4421 if (
4422 platform == "auto"
4423 and groupstr == self.manifest.GetDefaultGroupsStr()
4424 ):
4425 groupstr = None
4426 self.config.SetString("manifest.groups", groupstr)
4427
4428 if reference:
4429 self.config.SetString("repo.reference", reference)
4430
4431 if dissociate:
4432 self.config.SetBoolean("repo.dissociate", dissociate)
4433
4434 if worktree:
4435 if mirror:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004436 logger.error("fatal: --mirror and --worktree are incompatible")
Gavin Makea2e3302023-03-11 06:46:20 +00004437 return False
4438 if submodules:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004439 logger.error(
4440 "fatal: --submodules and --worktree are incompatible"
Gavin Makea2e3302023-03-11 06:46:20 +00004441 )
4442 return False
4443 self.config.SetBoolean("repo.worktree", worktree)
4444 if is_new:
4445 self.use_git_worktrees = True
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004446 logger.warning("warning: --worktree is experimental!")
Gavin Makea2e3302023-03-11 06:46:20 +00004447
4448 if archive:
4449 if is_new:
4450 self.config.SetBoolean("repo.archive", archive)
4451 else:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004452 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004453 "fatal: --archive is only supported when initializing a "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004454 "new workspace."
Gavin Makea2e3302023-03-11 06:46:20 +00004455 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004456 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004457 "Either delete the .repo folder in this workspace, or "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004458 "initialize in another location."
Gavin Makea2e3302023-03-11 06:46:20 +00004459 )
4460 return False
4461
4462 if mirror:
4463 if is_new:
4464 self.config.SetBoolean("repo.mirror", mirror)
4465 else:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004466 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004467 "fatal: --mirror is only supported when initializing a new "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004468 "workspace."
Gavin Makea2e3302023-03-11 06:46:20 +00004469 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004470 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004471 "Either delete the .repo folder in this workspace, or "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004472 "initialize in another location."
Gavin Makea2e3302023-03-11 06:46:20 +00004473 )
4474 return False
4475
4476 if partial_clone is not None:
4477 if mirror:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004478 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004479 "fatal: --mirror and --partial-clone are mutually "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004480 "exclusive"
Gavin Makea2e3302023-03-11 06:46:20 +00004481 )
4482 return False
4483 self.config.SetBoolean("repo.partialclone", partial_clone)
4484 if clone_filter:
4485 self.config.SetString("repo.clonefilter", clone_filter)
4486 elif self.partial_clone:
4487 clone_filter = self.clone_filter
4488 else:
4489 clone_filter = None
4490
4491 if partial_clone_exclude is not None:
4492 self.config.SetString(
4493 "repo.partialcloneexclude", partial_clone_exclude
4494 )
4495
4496 if clone_bundle is None:
4497 clone_bundle = False if partial_clone else True
4498 else:
4499 self.config.SetBoolean("repo.clonebundle", clone_bundle)
4500
4501 if submodules:
4502 self.config.SetBoolean("repo.submodules", submodules)
4503
4504 if git_lfs is not None:
4505 if git_lfs:
4506 git_require((2, 17, 0), fail=True, msg="Git LFS support")
4507
4508 self.config.SetBoolean("repo.git-lfs", git_lfs)
4509 if not is_new:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004510 logger.warning(
Gavin Makea2e3302023-03-11 06:46:20 +00004511 "warning: Changing --git-lfs settings will only affect new "
4512 "project checkouts.\n"
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004513 " Existing projects will require manual updates.\n"
Gavin Makea2e3302023-03-11 06:46:20 +00004514 )
4515
Jason Chang17833322023-05-23 13:06:55 -07004516 if clone_filter_for_depth is not None:
4517 self.ConfigureCloneFilterForDepth(clone_filter_for_depth)
4518
Gavin Makea2e3302023-03-11 06:46:20 +00004519 if use_superproject is not None:
4520 self.config.SetBoolean("repo.superproject", use_superproject)
4521
4522 if not standalone_manifest:
4523 success = self.Sync_NetworkHalf(
4524 is_new=is_new,
4525 quiet=not verbose,
4526 verbose=verbose,
4527 clone_bundle=clone_bundle,
4528 current_branch_only=current_branch_only,
4529 tags=tags,
4530 submodules=submodules,
4531 clone_filter=clone_filter,
4532 partial_clone_exclude=self.manifest.PartialCloneExclude,
Jason Chang17833322023-05-23 13:06:55 -07004533 clone_filter_for_depth=self.manifest.CloneFilterForDepth,
Gavin Makea2e3302023-03-11 06:46:20 +00004534 ).success
4535 if not success:
4536 r = self.GetRemote()
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004537 logger.error("fatal: cannot obtain manifest %s", r.url)
Gavin Makea2e3302023-03-11 06:46:20 +00004538
4539 # Better delete the manifest git dir if we created it; otherwise
4540 # next time (when user fixes problems) we won't go through the
4541 # "is_new" logic.
4542 if is_new:
4543 platform_utils.rmtree(self.gitdir)
4544 return False
4545
4546 if manifest_branch:
4547 self.MetaBranchSwitch(submodules=submodules)
4548
4549 syncbuf = SyncBuffer(self.config)
4550 self.Sync_LocalHalf(syncbuf, submodules=submodules)
4551 syncbuf.Finish()
4552
4553 if is_new or self.CurrentBranch is None:
Jason Chang1a3612f2023-08-08 14:12:53 -07004554 try:
4555 self.StartBranch("default")
4556 except GitError as e:
4557 msg = str(e)
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004558 logger.error(
4559 "fatal: cannot create default in manifest %s", msg
Gavin Makea2e3302023-03-11 06:46:20 +00004560 )
4561 return False
4562
4563 if not manifest_name:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004564 logger.error("fatal: manifest name (-m) is required.")
Gavin Makea2e3302023-03-11 06:46:20 +00004565 return False
4566
4567 elif is_new:
4568 # This is a new standalone manifest.
4569 manifest_name = "default.xml"
4570 manifest_data = fetch.fetch_file(manifest_url, verbose=verbose)
4571 dest = os.path.join(self.worktree, manifest_name)
4572 os.makedirs(os.path.dirname(dest), exist_ok=True)
4573 with open(dest, "wb") as f:
4574 f.write(manifest_data)
4575
4576 try:
4577 self.manifest.Link(manifest_name)
4578 except ManifestParseError as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004579 logger.error("fatal: manifest '%s' not available", manifest_name)
4580 logger.error("fatal: %s", e)
Gavin Makea2e3302023-03-11 06:46:20 +00004581 return False
4582
4583 if not this_manifest_only:
4584 for submanifest in self.manifest.submanifests.values():
4585 spec = submanifest.ToSubmanifestSpec()
4586 submanifest.repo_client.manifestProject.Sync(
4587 manifest_url=spec.manifestUrl,
4588 manifest_branch=spec.revision,
4589 standalone_manifest=standalone_manifest,
4590 groups=self.manifest_groups,
4591 platform=platform,
4592 mirror=mirror,
4593 dissociate=dissociate,
4594 reference=reference,
4595 worktree=worktree,
4596 submodules=submodules,
4597 archive=archive,
4598 partial_clone=partial_clone,
4599 clone_filter=clone_filter,
4600 partial_clone_exclude=partial_clone_exclude,
4601 clone_bundle=clone_bundle,
4602 git_lfs=git_lfs,
4603 use_superproject=use_superproject,
4604 verbose=verbose,
4605 current_branch_only=current_branch_only,
4606 tags=tags,
4607 depth=depth,
4608 git_event_log=git_event_log,
4609 manifest_name=spec.manifestName,
4610 this_manifest_only=False,
4611 outer_manifest=False,
4612 )
4613
4614 # Lastly, if the manifest has a <superproject> then have the
4615 # superproject sync it (if it will be used).
4616 if git_superproject.UseSuperproject(use_superproject, self.manifest):
4617 sync_result = self.manifest.superproject.Sync(git_event_log)
4618 if not sync_result.success:
4619 submanifest = ""
4620 if self.manifest.path_prefix:
4621 submanifest = f"for {self.manifest.path_prefix} "
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004622 logger.warning(
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004623 "warning: git update of superproject %s failed, "
Gavin Makea2e3302023-03-11 06:46:20 +00004624 "repo sync will not use superproject to fetch source; "
4625 "while this error is not fatal, and you can continue to "
4626 "run repo sync, please run repo init with the "
4627 "--no-use-superproject option to stop seeing this warning",
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004628 submanifest,
Gavin Makea2e3302023-03-11 06:46:20 +00004629 )
4630 if sync_result.fatal and use_superproject is not None:
4631 return False
4632
4633 return True
4634
Jason Chang17833322023-05-23 13:06:55 -07004635 def ConfigureCloneFilterForDepth(self, clone_filter_for_depth):
4636 """Configure clone filter to replace shallow clones.
4637
4638 Args:
4639 clone_filter_for_depth: a string or None, e.g. 'blob:none' will
4640 disable shallow clones and replace with partial clone. None will
4641 enable shallow clones.
4642 """
4643 self.config.SetString(
4644 "repo.clonefilterfordepth", clone_filter_for_depth
4645 )
4646
Gavin Makea2e3302023-03-11 06:46:20 +00004647 def _ConfigureDepth(self, depth):
4648 """Configure the depth we'll sync down.
4649
4650 Args:
4651 depth: an int, how deep of a partial clone to create.
4652 """
4653 # Opt.depth will be non-None if user actually passed --depth to repo
4654 # init.
4655 if depth is not None:
4656 if depth > 0:
4657 # Positive values will set the depth.
4658 depth = str(depth)
4659 else:
4660 # Negative numbers will clear the depth; passing None to
4661 # SetString will do that.
4662 depth = None
4663
4664 # We store the depth in the main manifest project.
4665 self.config.SetString("repo.depth", depth)