blob: 8726af88ddc2d5a1ca7a81cb8702d204b0044eb4 [file] [log] [blame]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001# Copyright (C) 2008 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
Shawn O. Pearce438ee1c2008-11-03 09:59:36 -080015import errno
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070016import filecmp
Wink Saville4c426ef2015-06-03 08:05:17 -070017import glob
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070018import os
LaMont Jonesd82be3e2022-04-05 19:30:46 +000019import platform
Shawn O. Pearcec325dc32011-10-03 08:30:24 -070020import random
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070021import re
22import shutil
23import stat
Sergiy Belozorov96edb9b2024-03-04 19:48:52 +010024import string
Shawn O. Pearce5e7127d2012-08-02 14:57:37 -070025import subprocess
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070026import sys
Julien Campergue335f5ef2013-10-16 11:02:35 +020027import tarfile
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +080028import tempfile
Shawn O. Pearcec325dc32011-10-03 08:30:24 -070029import time
Mike Frysinger64477332023-08-21 21:20:32 -040030from typing import List, NamedTuple
Mike Frysingeracf63b22019-06-13 02:24:21 -040031import urllib.parse
Shawn O. Pearcedf5ee522011-10-11 14:05:21 -070032
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070033from color import Coloring
Scott Leef1d916b2023-10-30 18:11:37 +000034from error import CacheApplyError
Mike Frysinger64477332023-08-21 21:20:32 -040035from error import DownloadError
36from error import GitError
37from error import ManifestInvalidPathError
38from error import ManifestInvalidRevisionError
39from error import ManifestParseError
40from error import NoManifestException
41from error import RepoError
42from error import UploadError
LaMont Jones0de4fc32022-04-21 17:18:35 +000043import fetch
Mike Frysinger64477332023-08-21 21:20:32 -040044from git_command import git_require
45from git_command import GitCommand
46from git_config import GetSchemeFromUrl
47from git_config import GetUrlCookieFile
48from git_config import GitConfig
Scott Leef1d916b2023-10-30 18:11:37 +000049from git_config import ID_RE
Mike Frysinger64477332023-08-21 21:20:32 -040050from git_config import IsId
Scott Leef1d916b2023-10-30 18:11:37 +000051from git_config import RefSpec
Mike Frysinger64477332023-08-21 21:20:32 -040052from git_refs import GitRefs
53from git_refs import HEAD
54from git_refs import R_HEADS
55from git_refs import R_M
56from git_refs import R_PUB
57from git_refs import R_TAGS
58from git_refs import R_WORKTREE_M
LaMont Jonesff6b1da2022-06-01 21:03:34 +000059import git_superproject
LaMont Jones55ee3042022-04-06 17:10:21 +000060from git_trace2_event_log import EventLog
Renaud Paquayd5cec5e2016-11-01 11:24:03 -070061import platform_utils
Mike Frysinger70d861f2019-08-26 15:22:36 -040062import progress
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +000063from repo_logging import RepoLogger
Joanna Wanga6c52f52022-11-03 16:51:19 -040064from repo_trace import Trace
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070065
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -070066
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +000067logger = RepoLogger(__file__)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070068
Chirayu Desai217ea7d2013-03-01 19:14:38 +053069
LaMont Jones1eddca82022-09-01 15:15:04 +000070class SyncNetworkHalfResult(NamedTuple):
Gavin Makea2e3302023-03-11 06:46:20 +000071 """Sync_NetworkHalf return value."""
72
Gavin Makea2e3302023-03-11 06:46:20 +000073 # Did we query the remote? False when optimized_fetch is True and we have
74 # the commit already present.
75 remote_fetched: bool
Jason Chang32b59562023-07-14 16:45:35 -070076 # Error from SyncNetworkHalf
77 error: Exception = None
78
79 @property
80 def success(self) -> bool:
81 return not self.error
82
83
84class SyncNetworkHalfError(RepoError):
85 """Failure trying to sync."""
86
87
88class DeleteWorktreeError(RepoError):
89 """Failure to delete worktree."""
90
91 def __init__(
92 self, *args, aggregate_errors: List[Exception] = None, **kwargs
93 ) -> None:
94 super().__init__(*args, **kwargs)
95 self.aggregate_errors = aggregate_errors or []
96
97
98class DeleteDirtyWorktreeError(DeleteWorktreeError):
99 """Failure to delete worktree due to uncommitted changes."""
LaMont Jones1eddca82022-09-01 15:15:04 +0000100
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +0100101
George Engelbrecht9bc283e2020-04-02 12:36:09 -0600102# Maximum sleep time allowed during retries.
103MAXIMUM_RETRY_SLEEP_SEC = 3600.0
104# +-10% random jitter is added to each Fetches retry sleep duration.
105RETRY_JITTER_PERCENT = 0.1
106
LaMont Jonesfa8d9392022-11-02 22:01:29 +0000107# Whether to use alternates. Switching back and forth is *NOT* supported.
Mike Frysinger1d00a7e2021-12-21 00:40:31 -0500108# TODO(vapier): Remove knob once behavior is verified.
Gavin Makea2e3302023-03-11 06:46:20 +0000109_ALTERNATES = os.environ.get("REPO_USE_ALTERNATES") == "1"
George Engelbrecht9bc283e2020-04-02 12:36:09 -0600110
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +0100111
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700112def _lwrite(path, content):
Gavin Makea2e3302023-03-11 06:46:20 +0000113 lock = "%s.lock" % path
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700114
Gavin Makea2e3302023-03-11 06:46:20 +0000115 # Maintain Unix line endings on all OS's to match git behavior.
116 with open(lock, "w", newline="\n") as fd:
117 fd.write(content)
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700118
Gavin Makea2e3302023-03-11 06:46:20 +0000119 try:
120 platform_utils.rename(lock, path)
121 except OSError:
122 platform_utils.remove(lock)
123 raise
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700124
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700125
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700126def not_rev(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000127 return "^" + r
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700128
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700129
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800130def sq(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000131 return "'" + r.replace("'", "'''") + "'"
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -0800132
David Pursehouse819827a2020-02-12 15:20:19 +0900133
Jonathan Nieder93719792015-03-17 11:29:58 -0700134_project_hook_list = None
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700135
136
Jonathan Nieder93719792015-03-17 11:29:58 -0700137def _ProjectHooks():
Gavin Makea2e3302023-03-11 06:46:20 +0000138 """List the hooks present in the 'hooks' directory.
Jonathan Nieder93719792015-03-17 11:29:58 -0700139
Gavin Makea2e3302023-03-11 06:46:20 +0000140 These hooks are project hooks and are copied to the '.git/hooks' directory
141 of all subprojects.
Jonathan Nieder93719792015-03-17 11:29:58 -0700142
Gavin Makea2e3302023-03-11 06:46:20 +0000143 This function caches the list of hooks (based on the contents of the
144 'repo/hooks' directory) on the first call.
Jonathan Nieder93719792015-03-17 11:29:58 -0700145
Gavin Makea2e3302023-03-11 06:46:20 +0000146 Returns:
147 A list of absolute paths to all of the files in the hooks directory.
148 """
149 global _project_hook_list
150 if _project_hook_list is None:
151 d = platform_utils.realpath(os.path.abspath(os.path.dirname(__file__)))
152 d = os.path.join(d, "hooks")
153 _project_hook_list = [
154 os.path.join(d, x) for x in platform_utils.listdir(d)
155 ]
156 return _project_hook_list
Jonathan Nieder93719792015-03-17 11:29:58 -0700157
158
Mike Frysingerd4aee652023-10-19 05:13:32 -0400159class DownloadedChange:
Gavin Makea2e3302023-03-11 06:46:20 +0000160 _commit_cache = None
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700161
Gavin Makea2e3302023-03-11 06:46:20 +0000162 def __init__(self, project, base, change_id, ps_id, commit):
163 self.project = project
164 self.base = base
165 self.change_id = change_id
166 self.ps_id = ps_id
167 self.commit = commit
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700168
Gavin Makea2e3302023-03-11 06:46:20 +0000169 @property
170 def commits(self):
171 if self._commit_cache is None:
172 self._commit_cache = self.project.bare_git.rev_list(
173 "--abbrev=8",
174 "--abbrev-commit",
175 "--pretty=oneline",
176 "--reverse",
177 "--date-order",
178 not_rev(self.base),
179 self.commit,
180 "--",
181 )
182 return self._commit_cache
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700183
184
Mike Frysingerd4aee652023-10-19 05:13:32 -0400185class ReviewableBranch:
Gavin Makea2e3302023-03-11 06:46:20 +0000186 _commit_cache = None
187 _base_exists = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700188
Gavin Makea2e3302023-03-11 06:46:20 +0000189 def __init__(self, project, branch, base):
190 self.project = project
191 self.branch = branch
192 self.base = base
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700193
Gavin Makea2e3302023-03-11 06:46:20 +0000194 @property
195 def name(self):
196 return self.branch.name
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700197
Gavin Makea2e3302023-03-11 06:46:20 +0000198 @property
199 def commits(self):
200 if self._commit_cache is None:
201 args = (
202 "--abbrev=8",
203 "--abbrev-commit",
204 "--pretty=oneline",
205 "--reverse",
206 "--date-order",
207 not_rev(self.base),
208 R_HEADS + self.name,
209 "--",
210 )
211 try:
Jason Chang87058c62023-09-27 11:34:43 -0700212 self._commit_cache = self.project.bare_git.rev_list(
213 *args, log_as_error=self.base_exists
214 )
Gavin Makea2e3302023-03-11 06:46:20 +0000215 except GitError:
216 # We weren't able to probe the commits for this branch. Was it
217 # tracking a branch that no longer exists? If so, return no
218 # commits. Otherwise, rethrow the error as we don't know what's
219 # going on.
220 if self.base_exists:
221 raise
Mike Frysinger6da17752019-09-11 18:43:17 -0400222
Gavin Makea2e3302023-03-11 06:46:20 +0000223 self._commit_cache = []
Mike Frysinger6da17752019-09-11 18:43:17 -0400224
Gavin Makea2e3302023-03-11 06:46:20 +0000225 return self._commit_cache
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700226
Gavin Makea2e3302023-03-11 06:46:20 +0000227 @property
228 def unabbrev_commits(self):
229 r = dict()
230 for commit in self.project.bare_git.rev_list(
231 not_rev(self.base), R_HEADS + self.name, "--"
232 ):
233 r[commit[0:8]] = commit
234 return r
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800235
Gavin Makea2e3302023-03-11 06:46:20 +0000236 @property
237 def date(self):
238 return self.project.bare_git.log(
239 "--pretty=format:%cd", "-n", "1", R_HEADS + self.name, "--"
240 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700241
Gavin Makea2e3302023-03-11 06:46:20 +0000242 @property
243 def base_exists(self):
244 """Whether the branch we're tracking exists.
Mike Frysinger6da17752019-09-11 18:43:17 -0400245
Gavin Makea2e3302023-03-11 06:46:20 +0000246 Normally it should, but sometimes branches we track can get deleted.
247 """
248 if self._base_exists is None:
249 try:
250 self.project.bare_git.rev_parse("--verify", not_rev(self.base))
251 # If we're still here, the base branch exists.
252 self._base_exists = True
253 except GitError:
254 # If we failed to verify, the base branch doesn't exist.
255 self._base_exists = False
Mike Frysinger6da17752019-09-11 18:43:17 -0400256
Gavin Makea2e3302023-03-11 06:46:20 +0000257 return self._base_exists
Mike Frysinger6da17752019-09-11 18:43:17 -0400258
Gavin Makea2e3302023-03-11 06:46:20 +0000259 def UploadForReview(
260 self,
261 people,
262 dryrun=False,
263 auto_topic=False,
264 hashtags=(),
265 labels=(),
266 private=False,
267 notify=None,
268 wip=False,
269 ready=False,
270 dest_branch=None,
271 validate_certs=True,
272 push_options=None,
Sergiy Belozorov96edb9b2024-03-04 19:48:52 +0100273 patchset_description=None,
Gavin Makea2e3302023-03-11 06:46:20 +0000274 ):
275 self.project.UploadForReview(
276 branch=self.name,
277 people=people,
278 dryrun=dryrun,
279 auto_topic=auto_topic,
280 hashtags=hashtags,
281 labels=labels,
282 private=private,
283 notify=notify,
284 wip=wip,
285 ready=ready,
286 dest_branch=dest_branch,
287 validate_certs=validate_certs,
288 push_options=push_options,
Sergiy Belozorov96edb9b2024-03-04 19:48:52 +0100289 patchset_description=patchset_description,
Gavin Makea2e3302023-03-11 06:46:20 +0000290 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700291
Gavin Makea2e3302023-03-11 06:46:20 +0000292 def GetPublishedRefs(self):
293 refs = {}
294 output = self.project.bare_git.ls_remote(
295 self.branch.remote.SshReviewUrl(self.project.UserEmail),
296 "refs/changes/*",
297 )
298 for line in output.split("\n"):
299 try:
300 (sha, ref) = line.split()
301 refs[sha] = ref
302 except ValueError:
303 pass
Ficus Kirkpatrickbc7ef672009-05-04 12:45:11 -0700304
Gavin Makea2e3302023-03-11 06:46:20 +0000305 return refs
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700306
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700307
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700308class StatusColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000309 def __init__(self, config):
310 super().__init__(config, "status")
311 self.project = self.printer("header", attr="bold")
312 self.branch = self.printer("header", attr="bold")
313 self.nobranch = self.printer("nobranch", fg="red")
314 self.important = self.printer("important", fg="red")
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700315
Gavin Makea2e3302023-03-11 06:46:20 +0000316 self.added = self.printer("added", fg="green")
317 self.changed = self.printer("changed", fg="red")
318 self.untracked = self.printer("untracked", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700319
320
321class DiffColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000322 def __init__(self, config):
323 super().__init__(config, "diff")
324 self.project = self.printer("header", attr="bold")
325 self.fail = self.printer("fail", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700326
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700327
Mike Frysingerd4aee652023-10-19 05:13:32 -0400328class Annotation:
Gavin Makea2e3302023-03-11 06:46:20 +0000329 def __init__(self, name, value, keep):
330 self.name = name
331 self.value = value
332 self.keep = keep
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700333
Gavin Makea2e3302023-03-11 06:46:20 +0000334 def __eq__(self, other):
335 if not isinstance(other, Annotation):
336 return False
337 return self.__dict__ == other.__dict__
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700338
Gavin Makea2e3302023-03-11 06:46:20 +0000339 def __lt__(self, other):
340 # This exists just so that lists of Annotation objects can be sorted,
341 # for use in comparisons.
342 if not isinstance(other, Annotation):
343 raise ValueError("comparison is not between two Annotation objects")
344 if self.name == other.name:
345 if self.value == other.value:
346 return self.keep < other.keep
347 return self.value < other.value
348 return self.name < other.name
Jack Neus6ea0cae2021-07-20 20:52:33 +0000349
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700350
Mike Frysingere6a202f2019-08-02 15:57:57 -0400351def _SafeExpandPath(base, subpath, skipfinal=False):
Gavin Makea2e3302023-03-11 06:46:20 +0000352 """Make sure |subpath| is completely safe under |base|.
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700353
Gavin Makea2e3302023-03-11 06:46:20 +0000354 We make sure no intermediate symlinks are traversed, and that the final path
355 is not a special file (e.g. not a socket or fifo).
Mike Frysingere6a202f2019-08-02 15:57:57 -0400356
Gavin Makea2e3302023-03-11 06:46:20 +0000357 NB: We rely on a number of paths already being filtered out while parsing
358 the manifest. See the validation logic in manifest_xml.py for more details.
359 """
360 # Split up the path by its components. We can't use os.path.sep exclusively
361 # as some platforms (like Windows) will convert / to \ and that bypasses all
362 # our constructed logic here. Especially since manifest authors only use
363 # / in their paths.
364 resep = re.compile(r"[/%s]" % re.escape(os.path.sep))
365 components = resep.split(subpath)
366 if skipfinal:
367 # Whether the caller handles the final component itself.
368 finalpart = components.pop()
Mike Frysingere6a202f2019-08-02 15:57:57 -0400369
Gavin Makea2e3302023-03-11 06:46:20 +0000370 path = base
371 for part in components:
372 if part in {".", ".."}:
373 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400374 f'{subpath}: "{part}" not allowed in paths'
Gavin Makea2e3302023-03-11 06:46:20 +0000375 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400376
Gavin Makea2e3302023-03-11 06:46:20 +0000377 path = os.path.join(path, part)
378 if platform_utils.islink(path):
379 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400380 f"{path}: traversing symlinks not allow"
Gavin Makea2e3302023-03-11 06:46:20 +0000381 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400382
Gavin Makea2e3302023-03-11 06:46:20 +0000383 if os.path.exists(path):
384 if not os.path.isfile(path) and not platform_utils.isdir(path):
385 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400386 f"{path}: only regular files & directories allowed"
Gavin Makea2e3302023-03-11 06:46:20 +0000387 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400388
Gavin Makea2e3302023-03-11 06:46:20 +0000389 if skipfinal:
390 path = os.path.join(path, finalpart)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400391
Gavin Makea2e3302023-03-11 06:46:20 +0000392 return path
Mike Frysingere6a202f2019-08-02 15:57:57 -0400393
394
Mike Frysingerd4aee652023-10-19 05:13:32 -0400395class _CopyFile:
Gavin Makea2e3302023-03-11 06:46:20 +0000396 """Container for <copyfile> manifest element."""
Mike Frysingere6a202f2019-08-02 15:57:57 -0400397
Gavin Makea2e3302023-03-11 06:46:20 +0000398 def __init__(self, git_worktree, src, topdir, dest):
399 """Register a <copyfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400400
Gavin Makea2e3302023-03-11 06:46:20 +0000401 Args:
402 git_worktree: Absolute path to the git project checkout.
403 src: Relative path under |git_worktree| of file to read.
404 topdir: Absolute path to the top of the repo client checkout.
405 dest: Relative path under |topdir| of file to write.
406 """
407 self.git_worktree = git_worktree
408 self.topdir = topdir
409 self.src = src
410 self.dest = dest
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700411
Gavin Makea2e3302023-03-11 06:46:20 +0000412 def _Copy(self):
413 src = _SafeExpandPath(self.git_worktree, self.src)
414 dest = _SafeExpandPath(self.topdir, self.dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400415
Gavin Makea2e3302023-03-11 06:46:20 +0000416 if platform_utils.isdir(src):
417 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400418 f"{self.src}: copying from directory not supported"
Gavin Makea2e3302023-03-11 06:46:20 +0000419 )
420 if platform_utils.isdir(dest):
421 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400422 f"{self.dest}: copying to directory not allowed"
Gavin Makea2e3302023-03-11 06:46:20 +0000423 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400424
Gavin Makea2e3302023-03-11 06:46:20 +0000425 # Copy file if it does not exist or is out of date.
426 if not os.path.exists(dest) or not filecmp.cmp(src, dest):
427 try:
428 # Remove existing file first, since it might be read-only.
429 if os.path.exists(dest):
430 platform_utils.remove(dest)
431 else:
432 dest_dir = os.path.dirname(dest)
433 if not platform_utils.isdir(dest_dir):
434 os.makedirs(dest_dir)
435 shutil.copy(src, dest)
436 # Make the file read-only.
437 mode = os.stat(dest)[stat.ST_MODE]
438 mode = mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
439 os.chmod(dest, mode)
Jason R. Coombsae824fb2023-10-20 23:32:40 +0545440 except OSError:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +0000441 logger.error("error: Cannot copy file %s to %s", src, dest)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700442
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700443
Mike Frysingerd4aee652023-10-19 05:13:32 -0400444class _LinkFile:
Gavin Makea2e3302023-03-11 06:46:20 +0000445 """Container for <linkfile> manifest element."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700446
Gavin Makea2e3302023-03-11 06:46:20 +0000447 def __init__(self, git_worktree, src, topdir, dest):
448 """Register a <linkfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400449
Gavin Makea2e3302023-03-11 06:46:20 +0000450 Args:
451 git_worktree: Absolute path to the git project checkout.
452 src: Target of symlink relative to path under |git_worktree|.
453 topdir: Absolute path to the top of the repo client checkout.
454 dest: Relative path under |topdir| of symlink to create.
455 """
456 self.git_worktree = git_worktree
457 self.topdir = topdir
458 self.src = src
459 self.dest = dest
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500460
Gavin Makea2e3302023-03-11 06:46:20 +0000461 def __linkIt(self, relSrc, absDest):
462 # Link file if it does not exist or is out of date.
463 if not platform_utils.islink(absDest) or (
464 platform_utils.readlink(absDest) != relSrc
465 ):
466 try:
467 # Remove existing file first, since it might be read-only.
468 if os.path.lexists(absDest):
469 platform_utils.remove(absDest)
470 else:
471 dest_dir = os.path.dirname(absDest)
472 if not platform_utils.isdir(dest_dir):
473 os.makedirs(dest_dir)
474 platform_utils.symlink(relSrc, absDest)
Jason R. Coombsae824fb2023-10-20 23:32:40 +0545475 except OSError:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +0000476 logger.error(
477 "error: Cannot link file %s to %s", relSrc, absDest
478 )
Gavin Makea2e3302023-03-11 06:46:20 +0000479
480 def _Link(self):
481 """Link the self.src & self.dest paths.
482
483 Handles wild cards on the src linking all of the files in the source in
484 to the destination directory.
485 """
486 # Some people use src="." to create stable links to projects. Let's
487 # allow that but reject all other uses of "." to keep things simple.
488 if self.src == ".":
489 src = self.git_worktree
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500490 else:
Gavin Makea2e3302023-03-11 06:46:20 +0000491 src = _SafeExpandPath(self.git_worktree, self.src)
Wink Saville4c426ef2015-06-03 08:05:17 -0700492
Gavin Makea2e3302023-03-11 06:46:20 +0000493 if not glob.has_magic(src):
494 # Entity does not contain a wild card so just a simple one to one
495 # link operation.
496 dest = _SafeExpandPath(self.topdir, self.dest, skipfinal=True)
497 # dest & src are absolute paths at this point. Make sure the target
498 # of the symlink is relative in the context of the repo client
499 # checkout.
500 relpath = os.path.relpath(src, os.path.dirname(dest))
501 self.__linkIt(relpath, dest)
502 else:
503 dest = _SafeExpandPath(self.topdir, self.dest)
504 # Entity contains a wild card.
505 if os.path.exists(dest) and not platform_utils.isdir(dest):
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +0000506 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +0000507 "Link error: src with wildcard, %s must be a directory",
508 dest,
509 )
510 else:
511 for absSrcFile in glob.glob(src):
512 # Create a releative path from source dir to destination
513 # dir.
514 absSrcDir = os.path.dirname(absSrcFile)
515 relSrcDir = os.path.relpath(absSrcDir, dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400516
Gavin Makea2e3302023-03-11 06:46:20 +0000517 # Get the source file name.
518 srcFile = os.path.basename(absSrcFile)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400519
Gavin Makea2e3302023-03-11 06:46:20 +0000520 # Now form the final full paths to srcFile. They will be
521 # absolute for the desintaiton and relative for the source.
522 absDest = os.path.join(dest, srcFile)
523 relSrc = os.path.join(relSrcDir, srcFile)
524 self.__linkIt(relSrc, absDest)
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500525
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700526
Mike Frysingerd4aee652023-10-19 05:13:32 -0400527class RemoteSpec:
Gavin Makea2e3302023-03-11 06:46:20 +0000528 def __init__(
529 self,
530 name,
531 url=None,
532 pushUrl=None,
533 review=None,
534 revision=None,
535 orig_name=None,
536 fetchUrl=None,
537 ):
538 self.name = name
539 self.url = url
540 self.pushUrl = pushUrl
541 self.review = review
542 self.revision = revision
543 self.orig_name = orig_name
544 self.fetchUrl = fetchUrl
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700545
Ian Kasprzak0286e312021-02-05 10:06:18 -0800546
Mike Frysingerd4aee652023-10-19 05:13:32 -0400547class Project:
Gavin Makea2e3302023-03-11 06:46:20 +0000548 # These objects can be shared between several working trees.
549 @property
550 def shareable_dirs(self):
551 """Return the shareable directories"""
552 if self.UseAlternates:
553 return ["hooks", "rr-cache"]
554 else:
555 return ["hooks", "objects", "rr-cache"]
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700556
Gavin Makea2e3302023-03-11 06:46:20 +0000557 def __init__(
558 self,
559 manifest,
560 name,
561 remote,
562 gitdir,
563 objdir,
564 worktree,
565 relpath,
566 revisionExpr,
567 revisionId,
568 rebase=True,
569 groups=None,
570 sync_c=False,
571 sync_s=False,
572 sync_tags=True,
573 clone_depth=None,
574 upstream=None,
575 parent=None,
576 use_git_worktrees=False,
577 is_derived=False,
578 dest_branch=None,
579 optimized_fetch=False,
580 retry_fetches=0,
581 old_revision=None,
582 ):
583 """Init a Project object.
Doug Anderson3ba5f952011-04-07 12:51:04 -0700584
585 Args:
Gavin Makea2e3302023-03-11 06:46:20 +0000586 manifest: The XmlManifest object.
587 name: The `name` attribute of manifest.xml's project element.
588 remote: RemoteSpec object specifying its remote's properties.
589 gitdir: Absolute path of git directory.
590 objdir: Absolute path of directory to store git objects.
591 worktree: Absolute path of git working tree.
592 relpath: Relative path of git working tree to repo's top directory.
593 revisionExpr: The `revision` attribute of manifest.xml's project
594 element.
595 revisionId: git commit id for checking out.
596 rebase: The `rebase` attribute of manifest.xml's project element.
597 groups: The `groups` attribute of manifest.xml's project element.
598 sync_c: The `sync-c` attribute of manifest.xml's project element.
599 sync_s: The `sync-s` attribute of manifest.xml's project element.
600 sync_tags: The `sync-tags` attribute of manifest.xml's project
601 element.
602 upstream: The `upstream` attribute of manifest.xml's project
603 element.
604 parent: The parent Project object.
605 use_git_worktrees: Whether to use `git worktree` for this project.
606 is_derived: False if the project was explicitly defined in the
607 manifest; True if the project is a discovered submodule.
608 dest_branch: The branch to which to push changes for review by
609 default.
610 optimized_fetch: If True, when a project is set to a sha1 revision,
611 only fetch from the remote if the sha1 is not present locally.
612 retry_fetches: Retry remote fetches n times upon receiving transient
613 error with exponential backoff and jitter.
614 old_revision: saved git commit id for open GITC projects.
615 """
616 self.client = self.manifest = manifest
617 self.name = name
618 self.remote = remote
619 self.UpdatePaths(relpath, worktree, gitdir, objdir)
620 self.SetRevision(revisionExpr, revisionId=revisionId)
621
622 self.rebase = rebase
623 self.groups = groups
624 self.sync_c = sync_c
625 self.sync_s = sync_s
626 self.sync_tags = sync_tags
627 self.clone_depth = clone_depth
628 self.upstream = upstream
629 self.parent = parent
630 # NB: Do not use this setting in __init__ to change behavior so that the
631 # manifest.git checkout can inspect & change it after instantiating.
632 # See the XmlManifest init code for more info.
633 self.use_git_worktrees = use_git_worktrees
634 self.is_derived = is_derived
635 self.optimized_fetch = optimized_fetch
636 self.retry_fetches = max(0, retry_fetches)
637 self.subprojects = []
638
639 self.snapshots = {}
640 self.copyfiles = []
641 self.linkfiles = []
642 self.annotations = []
643 self.dest_branch = dest_branch
644 self.old_revision = old_revision
645
646 # This will be filled in if a project is later identified to be the
647 # project containing repo hooks.
648 self.enabled_repo_hooks = []
649
650 def RelPath(self, local=True):
651 """Return the path for the project relative to a manifest.
652
653 Args:
654 local: a boolean, if True, the path is relative to the local
655 (sub)manifest. If false, the path is relative to the outermost
656 manifest.
657 """
658 if local:
659 return self.relpath
660 return os.path.join(self.manifest.path_prefix, self.relpath)
661
662 def SetRevision(self, revisionExpr, revisionId=None):
663 """Set revisionId based on revision expression and id"""
664 self.revisionExpr = revisionExpr
665 if revisionId is None and revisionExpr and IsId(revisionExpr):
666 self.revisionId = self.revisionExpr
667 else:
668 self.revisionId = revisionId
669
670 def UpdatePaths(self, relpath, worktree, gitdir, objdir):
671 """Update paths used by this project"""
672 self.gitdir = gitdir.replace("\\", "/")
673 self.objdir = objdir.replace("\\", "/")
674 if worktree:
675 self.worktree = os.path.normpath(worktree).replace("\\", "/")
676 else:
677 self.worktree = None
678 self.relpath = relpath
679
680 self.config = GitConfig.ForRepository(
681 gitdir=self.gitdir, defaults=self.manifest.globalConfig
682 )
683
684 if self.worktree:
685 self.work_git = self._GitGetByExec(
686 self, bare=False, gitdir=self.gitdir
687 )
688 else:
689 self.work_git = None
690 self.bare_git = self._GitGetByExec(self, bare=True, gitdir=self.gitdir)
691 self.bare_ref = GitRefs(self.gitdir)
692 self.bare_objdir = self._GitGetByExec(
693 self, bare=True, gitdir=self.objdir
694 )
695
696 @property
697 def UseAlternates(self):
698 """Whether git alternates are in use.
699
700 This will be removed once migration to alternates is complete.
701 """
702 return _ALTERNATES or self.manifest.is_multimanifest
703
704 @property
705 def Derived(self):
706 return self.is_derived
707
708 @property
709 def Exists(self):
710 return platform_utils.isdir(self.gitdir) and platform_utils.isdir(
711 self.objdir
712 )
713
714 @property
715 def CurrentBranch(self):
716 """Obtain the name of the currently checked out branch.
717
718 The branch name omits the 'refs/heads/' prefix.
719 None is returned if the project is on a detached HEAD, or if the
720 work_git is otheriwse inaccessible (e.g. an incomplete sync).
721 """
722 try:
723 b = self.work_git.GetHead()
724 except NoManifestException:
725 # If the local checkout is in a bad state, don't barf. Let the
726 # callers process this like the head is unreadable.
727 return None
728 if b.startswith(R_HEADS):
729 return b[len(R_HEADS) :]
730 return None
731
732 def IsRebaseInProgress(self):
733 return (
734 os.path.exists(self.work_git.GetDotgitPath("rebase-apply"))
735 or os.path.exists(self.work_git.GetDotgitPath("rebase-merge"))
736 or os.path.exists(os.path.join(self.worktree, ".dotest"))
737 )
738
739 def IsDirty(self, consider_untracked=True):
740 """Is the working directory modified in some way?"""
741 self.work_git.update_index(
742 "-q", "--unmerged", "--ignore-missing", "--refresh"
743 )
744 if self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD):
745 return True
746 if self.work_git.DiffZ("diff-files"):
747 return True
748 if consider_untracked and self.UntrackedFiles():
749 return True
750 return False
751
752 _userident_name = None
753 _userident_email = None
754
755 @property
756 def UserName(self):
757 """Obtain the user's personal name."""
758 if self._userident_name is None:
759 self._LoadUserIdentity()
760 return self._userident_name
761
762 @property
763 def UserEmail(self):
764 """Obtain the user's email address. This is very likely
765 to be their Gerrit login.
766 """
767 if self._userident_email is None:
768 self._LoadUserIdentity()
769 return self._userident_email
770
771 def _LoadUserIdentity(self):
772 u = self.bare_git.var("GIT_COMMITTER_IDENT")
773 m = re.compile("^(.*) <([^>]*)> ").match(u)
774 if m:
775 self._userident_name = m.group(1)
776 self._userident_email = m.group(2)
777 else:
778 self._userident_name = ""
779 self._userident_email = ""
780
781 def GetRemote(self, name=None):
782 """Get the configuration for a single remote.
783
784 Defaults to the current project's remote.
785 """
786 if name is None:
787 name = self.remote.name
788 return self.config.GetRemote(name)
789
790 def GetBranch(self, name):
791 """Get the configuration for a single branch."""
792 return self.config.GetBranch(name)
793
794 def GetBranches(self):
795 """Get all existing local branches."""
796 current = self.CurrentBranch
797 all_refs = self._allrefs
798 heads = {}
799
800 for name, ref_id in all_refs.items():
801 if name.startswith(R_HEADS):
802 name = name[len(R_HEADS) :]
803 b = self.GetBranch(name)
804 b.current = name == current
805 b.published = None
806 b.revision = ref_id
807 heads[name] = b
808
809 for name, ref_id in all_refs.items():
810 if name.startswith(R_PUB):
811 name = name[len(R_PUB) :]
812 b = heads.get(name)
813 if b:
814 b.published = ref_id
815
816 return heads
817
818 def MatchesGroups(self, manifest_groups):
819 """Returns true if the manifest groups specified at init should cause
820 this project to be synced.
821 Prefixing a manifest group with "-" inverts the meaning of a group.
822 All projects are implicitly labelled with "all".
823
824 labels are resolved in order. In the example case of
825 project_groups: "all,group1,group2"
826 manifest_groups: "-group1,group2"
827 the project will be matched.
828
829 The special manifest group "default" will match any project that
830 does not have the special project group "notdefault"
831 """
832 default_groups = self.manifest.default_groups or ["default"]
833 expanded_manifest_groups = manifest_groups or default_groups
834 expanded_project_groups = ["all"] + (self.groups or [])
835 if "notdefault" not in expanded_project_groups:
836 expanded_project_groups += ["default"]
837
838 matched = False
839 for group in expanded_manifest_groups:
840 if group.startswith("-") and group[1:] in expanded_project_groups:
841 matched = False
842 elif group in expanded_project_groups:
843 matched = True
844
845 return matched
846
847 def UncommitedFiles(self, get_all=True):
848 """Returns a list of strings, uncommitted files in the git tree.
849
850 Args:
851 get_all: a boolean, if True - get information about all different
852 uncommitted files. If False - return as soon as any kind of
853 uncommitted files is detected.
854 """
855 details = []
856 self.work_git.update_index(
857 "-q", "--unmerged", "--ignore-missing", "--refresh"
858 )
859 if self.IsRebaseInProgress():
860 details.append("rebase in progress")
861 if not get_all:
862 return details
863
864 changes = self.work_git.DiffZ("diff-index", "--cached", HEAD).keys()
865 if changes:
866 details.extend(changes)
867 if not get_all:
868 return details
869
870 changes = self.work_git.DiffZ("diff-files").keys()
871 if changes:
872 details.extend(changes)
873 if not get_all:
874 return details
875
876 changes = self.UntrackedFiles()
877 if changes:
878 details.extend(changes)
879
880 return details
881
882 def UntrackedFiles(self):
883 """Returns a list of strings, untracked files in the git tree."""
884 return self.work_git.LsOthers()
885
886 def HasChanges(self):
887 """Returns true if there are uncommitted changes."""
888 return bool(self.UncommitedFiles(get_all=False))
889
890 def PrintWorkTreeStatus(self, output_redir=None, quiet=False, local=False):
891 """Prints the status of the repository to stdout.
892
893 Args:
894 output_redir: If specified, redirect the output to this object.
895 quiet: If True then only print the project name. Do not print
896 the modified files, branch name, etc.
897 local: a boolean, if True, the path is relative to the local
898 (sub)manifest. If false, the path is relative to the outermost
899 manifest.
900 """
901 if not platform_utils.isdir(self.worktree):
902 if output_redir is None:
903 output_redir = sys.stdout
904 print(file=output_redir)
905 print("project %s/" % self.RelPath(local), file=output_redir)
906 print(' missing (run "repo sync")', file=output_redir)
907 return
908
909 self.work_git.update_index(
910 "-q", "--unmerged", "--ignore-missing", "--refresh"
911 )
912 rb = self.IsRebaseInProgress()
913 di = self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD)
914 df = self.work_git.DiffZ("diff-files")
915 do = self.work_git.LsOthers()
916 if not rb and not di and not df and not do and not self.CurrentBranch:
917 return "CLEAN"
918
919 out = StatusColoring(self.config)
920 if output_redir is not None:
921 out.redirect(output_redir)
922 out.project("project %-40s", self.RelPath(local) + "/ ")
923
924 if quiet:
925 out.nl()
926 return "DIRTY"
927
928 branch = self.CurrentBranch
929 if branch is None:
930 out.nobranch("(*** NO BRANCH ***)")
931 else:
932 out.branch("branch %s", branch)
933 out.nl()
934
935 if rb:
936 out.important("prior sync failed; rebase still in progress")
937 out.nl()
938
939 paths = list()
940 paths.extend(di.keys())
941 paths.extend(df.keys())
942 paths.extend(do)
943
944 for p in sorted(set(paths)):
945 try:
946 i = di[p]
947 except KeyError:
948 i = None
949
950 try:
951 f = df[p]
952 except KeyError:
953 f = None
954
955 if i:
956 i_status = i.status.upper()
957 else:
958 i_status = "-"
959
960 if f:
961 f_status = f.status.lower()
962 else:
963 f_status = "-"
964
965 if i and i.src_path:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400966 line = (
967 f" {i_status}{f_status}\t{i.src_path} => {p} ({i.level}%)"
Gavin Makea2e3302023-03-11 06:46:20 +0000968 )
969 else:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400970 line = f" {i_status}{f_status}\t{p}"
Gavin Makea2e3302023-03-11 06:46:20 +0000971
972 if i and not f:
973 out.added("%s", line)
974 elif (i and f) or (not i and f):
975 out.changed("%s", line)
976 elif not i and not f:
977 out.untracked("%s", line)
978 else:
979 out.write("%s", line)
980 out.nl()
981
982 return "DIRTY"
983
984 def PrintWorkTreeDiff(
985 self, absolute_paths=False, output_redir=None, local=False
986 ):
987 """Prints the status of the repository to stdout."""
988 out = DiffColoring(self.config)
989 if output_redir:
990 out.redirect(output_redir)
991 cmd = ["diff"]
992 if out.is_on:
993 cmd.append("--color")
994 cmd.append(HEAD)
995 if absolute_paths:
996 cmd.append("--src-prefix=a/%s/" % self.RelPath(local))
997 cmd.append("--dst-prefix=b/%s/" % self.RelPath(local))
998 cmd.append("--")
999 try:
1000 p = GitCommand(self, cmd, capture_stdout=True, capture_stderr=True)
1001 p.Wait()
1002 except GitError as e:
1003 out.nl()
1004 out.project("project %s/" % self.RelPath(local))
1005 out.nl()
1006 out.fail("%s", str(e))
1007 out.nl()
1008 return False
1009 if p.stdout:
1010 out.nl()
1011 out.project("project %s/" % self.RelPath(local))
1012 out.nl()
1013 out.write("%s", p.stdout)
1014 return p.Wait() == 0
1015
1016 def WasPublished(self, branch, all_refs=None):
1017 """Was the branch published (uploaded) for code review?
1018 If so, returns the SHA-1 hash of the last published
1019 state for the branch.
1020 """
1021 key = R_PUB + branch
1022 if all_refs is None:
1023 try:
1024 return self.bare_git.rev_parse(key)
1025 except GitError:
1026 return None
1027 else:
1028 try:
1029 return all_refs[key]
1030 except KeyError:
1031 return None
1032
1033 def CleanPublishedCache(self, all_refs=None):
1034 """Prunes any stale published refs."""
1035 if all_refs is None:
1036 all_refs = self._allrefs
1037 heads = set()
1038 canrm = {}
1039 for name, ref_id in all_refs.items():
1040 if name.startswith(R_HEADS):
1041 heads.add(name)
1042 elif name.startswith(R_PUB):
1043 canrm[name] = ref_id
1044
1045 for name, ref_id in canrm.items():
1046 n = name[len(R_PUB) :]
1047 if R_HEADS + n not in heads:
1048 self.bare_git.DeleteRef(name, ref_id)
1049
1050 def GetUploadableBranches(self, selected_branch=None):
1051 """List any branches which can be uploaded for review."""
1052 heads = {}
1053 pubed = {}
1054
1055 for name, ref_id in self._allrefs.items():
1056 if name.startswith(R_HEADS):
1057 heads[name[len(R_HEADS) :]] = ref_id
1058 elif name.startswith(R_PUB):
1059 pubed[name[len(R_PUB) :]] = ref_id
1060
1061 ready = []
1062 for branch, ref_id in heads.items():
1063 if branch in pubed and pubed[branch] == ref_id:
1064 continue
1065 if selected_branch and branch != selected_branch:
1066 continue
1067
1068 rb = self.GetUploadableBranch(branch)
1069 if rb:
1070 ready.append(rb)
1071 return ready
1072
1073 def GetUploadableBranch(self, branch_name):
1074 """Get a single uploadable branch, or None."""
1075 branch = self.GetBranch(branch_name)
1076 base = branch.LocalMerge
1077 if branch.LocalMerge:
1078 rb = ReviewableBranch(self, branch, base)
1079 if rb.commits:
1080 return rb
1081 return None
1082
1083 def UploadForReview(
1084 self,
1085 branch=None,
1086 people=([], []),
1087 dryrun=False,
1088 auto_topic=False,
1089 hashtags=(),
1090 labels=(),
1091 private=False,
1092 notify=None,
1093 wip=False,
1094 ready=False,
1095 dest_branch=None,
1096 validate_certs=True,
1097 push_options=None,
Sergiy Belozorov96edb9b2024-03-04 19:48:52 +01001098 patchset_description=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001099 ):
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
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001163 ref_spec = f"{R_HEADS + branch.name}:refs/for/{dest_branch}"
Gavin Makea2e3302023-03-11 06:46:20 +00001164 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"]
Sergiy Belozorov96edb9b2024-03-04 19:48:52 +01001181 if patchset_description:
1182 opts += [
1183 f"m={self._encode_patchset_description(patchset_description)}"
1184 ]
Gavin Makea2e3302023-03-11 06:46:20 +00001185 if opts:
1186 ref_spec = ref_spec + "%" + ",".join(opts)
1187 cmd.append(ref_spec)
1188
Jason Chang1e9f7b92023-08-25 10:31:04 -07001189 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001190
1191 if not dryrun:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001192 msg = f"posted to {branch.remote.review} for {dest_branch}"
Gavin Makea2e3302023-03-11 06:46:20 +00001193 self.bare_git.UpdateRef(
1194 R_PUB + branch.name, R_HEADS + branch.name, message=msg
1195 )
1196
Sergiy Belozorov96edb9b2024-03-04 19:48:52 +01001197 @staticmethod
1198 def _encode_patchset_description(original):
1199 """Applies percent-encoding for strings sent as patchset description.
1200
1201 The encoding used is based on but stricter than URL encoding (Section
1202 2.1 of RFC 3986). The only non-escaped characters are alphanumerics, and
1203 'SPACE' (U+0020) can be represented as 'LOW LINE' (U+005F) or
1204 'PLUS SIGN' (U+002B).
1205
1206 For more information, see the Gerrit docs here:
1207 https://gerrit-review.googlesource.com/Documentation/user-upload.html#patch_set_description
1208 """
1209 SAFE = {ord(x) for x in string.ascii_letters + string.digits}
1210
1211 def _enc(b):
1212 if b in SAFE:
1213 return chr(b)
1214 elif b == ord(" "):
1215 return "_"
1216 else:
1217 return f"%{b:02x}"
1218
1219 return "".join(_enc(x) for x in original.encode("utf-8"))
1220
Gavin Makea2e3302023-03-11 06:46:20 +00001221 def _ExtractArchive(self, tarpath, path=None):
1222 """Extract the given tar on its current location
1223
1224 Args:
1225 tarpath: The path to the actual tar file
1226
1227 """
1228 try:
1229 with tarfile.open(tarpath, "r") as tar:
1230 tar.extractall(path=path)
1231 return True
Jason R. Coombsae824fb2023-10-20 23:32:40 +05451232 except (OSError, tarfile.TarError) as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001233 logger.error("error: Cannot extract archive %s: %s", tarpath, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001234 return False
1235
Gavin Mak4feff3b2023-05-16 21:31:10 +00001236 def CachePopulate(self, cache_dir, url):
1237 """Populate cache in the cache_dir.
1238
1239 Args:
1240 cache_dir: Directory to cache git files from Google Storage.
1241 url: Git url of current repository.
1242
1243 Raises:
1244 CacheApplyError if it fails to populate the git cache.
1245 """
1246 cmd = [
1247 "cache",
1248 "populate",
1249 "--ignore_locks",
1250 "-v",
1251 "--cache-dir",
1252 cache_dir,
1253 url,
1254 ]
1255
1256 if GitCommand(self, cmd, cwd=cache_dir).Wait() != 0:
1257 raise CacheApplyError(
1258 "Failed to populate cache. cache_dir: %s "
1259 "url: %s" % (cache_dir, url)
1260 )
1261
1262 def CacheExists(self, cache_dir, url):
1263 """Check the existence of the cache files.
1264
1265 Args:
1266 cache_dir: Directory to cache git files.
1267 url: Git url of current repository.
1268
1269 Raises:
1270 CacheApplyError if the cache files do not exist.
1271 """
1272 cmd = ["cache", "exists", "--quiet", "--cache-dir", cache_dir, url]
1273
1274 exist = GitCommand(self, cmd, cwd=self.gitdir, capture_stdout=True)
1275 if exist.Wait() != 0:
1276 raise CacheApplyError(
1277 "Failed to execute git cache exists cmd. "
1278 "cache_dir: %s url: %s" % (cache_dir, url)
1279 )
1280
1281 if not exist.stdout or not exist.stdout.strip():
1282 raise CacheApplyError(
1283 "Failed to find cache. cache_dir: %s "
1284 "url: %s" % (cache_dir, url)
1285 )
1286 return exist.stdout.strip()
1287
1288 def CacheApply(self, cache_dir):
1289 """Apply git cache files populated from Google Storage buckets.
1290
1291 Args:
1292 cache_dir: Directory to cache git files.
1293
1294 Raises:
1295 CacheApplyError if it fails to apply git caches.
1296 """
1297 remote = self.GetRemote(self.remote.name)
1298
1299 self.CachePopulate(cache_dir, remote.url)
1300
1301 mirror_dir = self.CacheExists(cache_dir, remote.url)
1302
1303 refspec = RefSpec(
1304 True, "refs/heads/*", "refs/remotes/%s/*" % remote.name
1305 )
1306
1307 fetch_cache_cmd = ["fetch", mirror_dir, str(refspec)]
1308 if GitCommand(self, fetch_cache_cmd, self.gitdir).Wait() != 0:
1309 raise CacheApplyError(
1310 "Failed to fetch refs %s from %s" % (mirror_dir, str(refspec))
1311 )
1312
Gavin Makea2e3302023-03-11 06:46:20 +00001313 def Sync_NetworkHalf(
1314 self,
1315 quiet=False,
1316 verbose=False,
1317 output_redir=None,
1318 is_new=None,
1319 current_branch_only=None,
1320 force_sync=False,
1321 clone_bundle=True,
1322 tags=None,
1323 archive=False,
1324 optimized_fetch=False,
1325 retry_fetches=0,
1326 prune=False,
1327 submodules=False,
Gavin Mak4feff3b2023-05-16 21:31:10 +00001328 cache_dir=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001329 ssh_proxy=None,
1330 clone_filter=None,
1331 partial_clone_exclude=set(),
Jason Chang17833322023-05-23 13:06:55 -07001332 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001333 ):
1334 """Perform only the network IO portion of the sync process.
1335 Local working directory/branch state is not affected.
1336 """
1337 if archive and not isinstance(self, MetaProject):
1338 if self.remote.url.startswith(("http://", "https://")):
Jason Chang32b59562023-07-14 16:45:35 -07001339 msg_template = (
1340 "%s: Cannot fetch archives from http/https remotes."
Gavin Makea2e3302023-03-11 06:46:20 +00001341 )
Jason Chang32b59562023-07-14 16:45:35 -07001342 msg_args = self.name
1343 msg = msg_template % msg_args
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001344 logger.error(msg_template, msg_args)
Jason Chang32b59562023-07-14 16:45:35 -07001345 return SyncNetworkHalfResult(
1346 False, SyncNetworkHalfError(msg, project=self.name)
1347 )
Gavin Makea2e3302023-03-11 06:46:20 +00001348
1349 name = self.relpath.replace("\\", "/")
1350 name = name.replace("/", "_")
1351 tarpath = "%s.tar" % name
1352 topdir = self.manifest.topdir
1353
1354 try:
1355 self._FetchArchive(tarpath, cwd=topdir)
1356 except GitError as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001357 logger.error("error: %s", e)
Jason Chang32b59562023-07-14 16:45:35 -07001358 return SyncNetworkHalfResult(False, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001359
1360 # From now on, we only need absolute tarpath.
1361 tarpath = os.path.join(topdir, tarpath)
1362
1363 if not self._ExtractArchive(tarpath, path=topdir):
Jason Chang32b59562023-07-14 16:45:35 -07001364 return SyncNetworkHalfResult(
1365 True,
1366 SyncNetworkHalfError(
1367 f"Unable to Extract Archive {tarpath}",
1368 project=self.name,
1369 ),
1370 )
Gavin Makea2e3302023-03-11 06:46:20 +00001371 try:
1372 platform_utils.remove(tarpath)
1373 except OSError as e:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00001374 logger.warning("warn: Cannot remove archive %s: %s", tarpath, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001375 self._CopyAndLinkFiles()
Jason Chang32b59562023-07-14 16:45:35 -07001376 return SyncNetworkHalfResult(True)
Gavin Makea2e3302023-03-11 06:46:20 +00001377
1378 # If the shared object dir already exists, don't try to rebootstrap with
1379 # a clone bundle download. We should have the majority of objects
1380 # already.
1381 if clone_bundle and os.path.exists(self.objdir):
1382 clone_bundle = False
1383
1384 if self.name in partial_clone_exclude:
1385 clone_bundle = True
1386 clone_filter = None
1387
1388 if is_new is None:
1389 is_new = not self.Exists
1390 if is_new:
1391 self._InitGitDir(force_sync=force_sync, quiet=quiet)
1392 else:
Josip Sokcevic9b57aa02023-12-01 23:01:52 +00001393 try:
1394 # At this point, it's possible that gitdir points to an old
1395 # objdir (e.g. name changed, but objdir exists). Check
1396 # references to ensure that's not the case. See
1397 # https://issues.gerritcodereview.com/40013418 for more
1398 # details.
1399 self._CheckDirReference(self.objdir, self.gitdir)
1400
1401 self._UpdateHooks(quiet=quiet)
1402 except GitError as e:
1403 if not force_sync:
1404 raise e
1405 # Let _InitGitDir fix the issue, force_sync is always True here.
1406 self._InitGitDir(force_sync=True, quiet=quiet)
Gavin Makea2e3302023-03-11 06:46:20 +00001407 self._InitRemote()
1408
1409 if self.UseAlternates:
1410 # If gitdir/objects is a symlink, migrate it from the old layout.
1411 gitdir_objects = os.path.join(self.gitdir, "objects")
1412 if platform_utils.islink(gitdir_objects):
1413 platform_utils.remove(gitdir_objects, missing_ok=True)
1414 gitdir_alt = os.path.join(self.gitdir, "objects/info/alternates")
1415 if not os.path.exists(gitdir_alt):
1416 os.makedirs(os.path.dirname(gitdir_alt), exist_ok=True)
1417 _lwrite(
1418 gitdir_alt,
1419 os.path.join(
1420 os.path.relpath(self.objdir, gitdir_objects), "objects"
1421 )
1422 + "\n",
1423 )
1424
1425 if is_new:
1426 alt = os.path.join(self.objdir, "objects/info/alternates")
1427 try:
1428 with open(alt) as fd:
1429 # This works for both absolute and relative alternate
1430 # directories.
1431 alt_dir = os.path.join(
1432 self.objdir, "objects", fd.readline().rstrip()
1433 )
Jason R. Coombsae824fb2023-10-20 23:32:40 +05451434 except OSError:
Gavin Makea2e3302023-03-11 06:46:20 +00001435 alt_dir = None
1436 else:
1437 alt_dir = None
1438
Gavin Mak4feff3b2023-05-16 21:31:10 +00001439 applied_cache = False
1440 # If cache_dir is provided, and it's a new repository without
1441 # alternative_dir, bootstrap this project repo with the git
1442 # cache files.
1443 if cache_dir is not None and is_new and alt_dir is None:
1444 try:
1445 self.CacheApply(cache_dir)
1446 applied_cache = True
1447 is_new = False
1448 except CacheApplyError as e:
1449 _error("Could not apply git cache: %s", e)
1450 _error("Please check if you have the right GS credentials.")
1451 _error("Please check if the cache files exist in GS.")
1452
Gavin Makea2e3302023-03-11 06:46:20 +00001453 if (
1454 clone_bundle
Gavin Mak4feff3b2023-05-16 21:31:10 +00001455 and not applied_cache
Gavin Makea2e3302023-03-11 06:46:20 +00001456 and alt_dir is None
1457 and self._ApplyCloneBundle(
1458 initial=is_new, quiet=quiet, verbose=verbose
1459 )
1460 ):
1461 is_new = False
1462
1463 if current_branch_only is None:
1464 if self.sync_c:
1465 current_branch_only = True
1466 elif not self.manifest._loaded:
1467 # Manifest cannot check defaults until it syncs.
1468 current_branch_only = False
1469 elif self.manifest.default.sync_c:
1470 current_branch_only = True
1471
1472 if tags is None:
1473 tags = self.sync_tags
1474
1475 if self.clone_depth:
1476 depth = self.clone_depth
1477 else:
1478 depth = self.manifest.manifestProject.depth
1479
Jason Chang17833322023-05-23 13:06:55 -07001480 if depth and clone_filter_for_depth:
1481 depth = None
1482 clone_filter = clone_filter_for_depth
1483
Gavin Makea2e3302023-03-11 06:46:20 +00001484 # See if we can skip the network fetch entirely.
1485 remote_fetched = False
1486 if not (
1487 optimized_fetch
Sylvain56a5a012023-09-11 13:38:00 +02001488 and IsId(self.revisionExpr)
1489 and self._CheckForImmutableRevision()
Gavin Makea2e3302023-03-11 06:46:20 +00001490 ):
1491 remote_fetched = True
Jason Chang32b59562023-07-14 16:45:35 -07001492 try:
1493 if not self._RemoteFetch(
1494 initial=is_new,
1495 quiet=quiet,
1496 verbose=verbose,
1497 output_redir=output_redir,
1498 alt_dir=alt_dir,
1499 current_branch_only=current_branch_only,
1500 tags=tags,
1501 prune=prune,
1502 depth=depth,
1503 submodules=submodules,
1504 force_sync=force_sync,
1505 ssh_proxy=ssh_proxy,
1506 clone_filter=clone_filter,
1507 retry_fetches=retry_fetches,
1508 ):
1509 return SyncNetworkHalfResult(
1510 remote_fetched,
1511 SyncNetworkHalfError(
1512 f"Unable to remote fetch project {self.name}",
1513 project=self.name,
1514 ),
1515 )
1516 except RepoError as e:
1517 return SyncNetworkHalfResult(
1518 remote_fetched,
1519 e,
1520 )
Gavin Makea2e3302023-03-11 06:46:20 +00001521
1522 mp = self.manifest.manifestProject
1523 dissociate = mp.dissociate
1524 if dissociate:
1525 alternates_file = os.path.join(
1526 self.objdir, "objects/info/alternates"
1527 )
1528 if os.path.exists(alternates_file):
1529 cmd = ["repack", "-a", "-d"]
1530 p = GitCommand(
1531 self,
1532 cmd,
1533 bare=True,
1534 capture_stdout=bool(output_redir),
1535 merge_output=bool(output_redir),
1536 )
1537 if p.stdout and output_redir:
1538 output_redir.write(p.stdout)
1539 if p.Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07001540 return SyncNetworkHalfResult(
1541 remote_fetched,
1542 GitError(
1543 "Unable to repack alternates", project=self.name
1544 ),
1545 )
Gavin Makea2e3302023-03-11 06:46:20 +00001546 platform_utils.remove(alternates_file)
1547
1548 if self.worktree:
1549 self._InitMRef()
1550 else:
1551 self._InitMirrorHead()
1552 platform_utils.remove(
1553 os.path.join(self.gitdir, "FETCH_HEAD"), missing_ok=True
1554 )
Jason Chang32b59562023-07-14 16:45:35 -07001555 return SyncNetworkHalfResult(remote_fetched)
Gavin Makea2e3302023-03-11 06:46:20 +00001556
1557 def PostRepoUpgrade(self):
1558 self._InitHooks()
1559
1560 def _CopyAndLinkFiles(self):
1561 if self.client.isGitcClient:
1562 return
1563 for copyfile in self.copyfiles:
1564 copyfile._Copy()
1565 for linkfile in self.linkfiles:
1566 linkfile._Link()
1567
1568 def GetCommitRevisionId(self):
1569 """Get revisionId of a commit.
1570
1571 Use this method instead of GetRevisionId to get the id of the commit
1572 rather than the id of the current git object (for example, a tag)
1573
1574 """
Sylvaine9cb3912023-09-10 23:35:01 +02001575 if self.revisionId:
1576 return self.revisionId
Gavin Makea2e3302023-03-11 06:46:20 +00001577 if not self.revisionExpr.startswith(R_TAGS):
1578 return self.GetRevisionId(self._allrefs)
1579
1580 try:
1581 return self.bare_git.rev_list(self.revisionExpr, "-1")[0]
1582 except GitError:
1583 raise ManifestInvalidRevisionError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001584 f"revision {self.revisionExpr} in {self.name} not found"
Gavin Makea2e3302023-03-11 06:46:20 +00001585 )
1586
1587 def GetRevisionId(self, all_refs=None):
1588 if self.revisionId:
1589 return self.revisionId
1590
1591 rem = self.GetRemote()
1592 rev = rem.ToLocal(self.revisionExpr)
1593
1594 if all_refs is not None and rev in all_refs:
1595 return all_refs[rev]
1596
1597 try:
1598 return self.bare_git.rev_parse("--verify", "%s^0" % rev)
1599 except GitError:
1600 raise ManifestInvalidRevisionError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001601 f"revision {self.revisionExpr} in {self.name} not found"
Gavin Makea2e3302023-03-11 06:46:20 +00001602 )
1603
1604 def SetRevisionId(self, revisionId):
1605 if self.revisionExpr:
1606 self.upstream = self.revisionExpr
1607
1608 self.revisionId = revisionId
1609
Jason Chang32b59562023-07-14 16:45:35 -07001610 def Sync_LocalHalf(
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001611 self,
1612 syncbuf,
1613 force_sync=False,
Josip Sokcevicedadb252024-02-29 09:48:37 -08001614 force_checkout=False,
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001615 submodules=False,
1616 errors=None,
1617 verbose=False,
Jason Chang32b59562023-07-14 16:45:35 -07001618 ):
Gavin Makea2e3302023-03-11 06:46:20 +00001619 """Perform only the local IO portion of the sync process.
1620
1621 Network access is not required.
1622 """
Jason Chang32b59562023-07-14 16:45:35 -07001623 if errors is None:
1624 errors = []
1625
1626 def fail(error: Exception):
1627 errors.append(error)
1628 syncbuf.fail(self, error)
1629
Gavin Makea2e3302023-03-11 06:46:20 +00001630 if not os.path.exists(self.gitdir):
Jason Chang32b59562023-07-14 16:45:35 -07001631 fail(
1632 LocalSyncFail(
1633 "Cannot checkout %s due to missing network sync; Run "
1634 "`repo sync -n %s` first." % (self.name, self.name),
1635 project=self.name,
1636 )
Gavin Makea2e3302023-03-11 06:46:20 +00001637 )
1638 return
1639
1640 self._InitWorkTree(force_sync=force_sync, submodules=submodules)
1641 all_refs = self.bare_ref.all
1642 self.CleanPublishedCache(all_refs)
1643 revid = self.GetRevisionId(all_refs)
1644
1645 # Special case the root of the repo client checkout. Make sure it
1646 # doesn't contain files being checked out to dirs we don't allow.
1647 if self.relpath == ".":
1648 PROTECTED_PATHS = {".repo"}
1649 paths = set(
1650 self.work_git.ls_tree("-z", "--name-only", "--", revid).split(
1651 "\0"
1652 )
1653 )
1654 bad_paths = paths & PROTECTED_PATHS
1655 if bad_paths:
Jason Chang32b59562023-07-14 16:45:35 -07001656 fail(
1657 LocalSyncFail(
1658 "Refusing to checkout project that writes to protected "
1659 "paths: %s" % (", ".join(bad_paths),),
1660 project=self.name,
1661 )
Gavin Makea2e3302023-03-11 06:46:20 +00001662 )
1663 return
1664
1665 def _doff():
1666 self._FastForward(revid)
1667 self._CopyAndLinkFiles()
1668
1669 def _dosubmodules():
1670 self._SyncSubmodules(quiet=True)
1671
1672 head = self.work_git.GetHead()
1673 if head.startswith(R_HEADS):
1674 branch = head[len(R_HEADS) :]
1675 try:
1676 head = all_refs[head]
1677 except KeyError:
1678 head = None
1679 else:
1680 branch = None
1681
1682 if branch is None or syncbuf.detach_head:
1683 # Currently on a detached HEAD. The user is assumed to
1684 # not have any local modifications worth worrying about.
1685 if self.IsRebaseInProgress():
Jason Chang32b59562023-07-14 16:45:35 -07001686 fail(_PriorSyncFailedError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001687 return
1688
1689 if head == revid:
1690 # No changes; don't do anything further.
1691 # Except if the head needs to be detached.
1692 if not syncbuf.detach_head:
1693 # The copy/linkfile config may have changed.
1694 self._CopyAndLinkFiles()
1695 return
1696 else:
1697 lost = self._revlist(not_rev(revid), HEAD)
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001698 if lost and verbose:
Gavin Makea2e3302023-03-11 06:46:20 +00001699 syncbuf.info(self, "discarding %d commits", len(lost))
1700
1701 try:
Josip Sokcevicedadb252024-02-29 09:48:37 -08001702 self._Checkout(revid, force_checkout=force_checkout, quiet=True)
Gavin Makea2e3302023-03-11 06:46:20 +00001703 if submodules:
1704 self._SyncSubmodules(quiet=True)
1705 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001706 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001707 return
1708 self._CopyAndLinkFiles()
1709 return
1710
1711 if head == revid:
1712 # No changes; don't do anything further.
1713 #
1714 # The copy/linkfile config may have changed.
1715 self._CopyAndLinkFiles()
1716 return
1717
1718 branch = self.GetBranch(branch)
1719
1720 if not branch.LocalMerge:
1721 # The current branch has no tracking configuration.
1722 # Jump off it to a detached HEAD.
1723 syncbuf.info(
1724 self, "leaving %s; does not track upstream", branch.name
1725 )
1726 try:
1727 self._Checkout(revid, quiet=True)
1728 if submodules:
1729 self._SyncSubmodules(quiet=True)
1730 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001731 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001732 return
1733 self._CopyAndLinkFiles()
1734 return
1735
1736 upstream_gain = self._revlist(not_rev(HEAD), revid)
1737
1738 # See if we can perform a fast forward merge. This can happen if our
1739 # branch isn't in the exact same state as we last published.
1740 try:
Jason Chang87058c62023-09-27 11:34:43 -07001741 self.work_git.merge_base(
1742 "--is-ancestor", HEAD, revid, log_as_error=False
1743 )
Gavin Makea2e3302023-03-11 06:46:20 +00001744 # Skip the published logic.
1745 pub = False
1746 except GitError:
1747 pub = self.WasPublished(branch.name, all_refs)
1748
1749 if pub:
1750 not_merged = self._revlist(not_rev(revid), pub)
1751 if not_merged:
1752 if upstream_gain:
1753 # The user has published this branch and some of those
1754 # commits are not yet merged upstream. We do not want
1755 # to rewrite the published commits so we punt.
Jason Chang32b59562023-07-14 16:45:35 -07001756 fail(
1757 LocalSyncFail(
1758 "branch %s is published (but not merged) and is "
1759 "now %d commits behind"
1760 % (branch.name, len(upstream_gain)),
1761 project=self.name,
1762 )
Gavin Makea2e3302023-03-11 06:46:20 +00001763 )
1764 return
1765 elif pub == head:
1766 # All published commits are merged, and thus we are a
1767 # strict subset. We can fast-forward safely.
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001768 syncbuf.later1(self, _doff, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001769 if submodules:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001770 syncbuf.later1(self, _dosubmodules, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001771 return
1772
1773 # Examine the local commits not in the remote. Find the
1774 # last one attributed to this user, if any.
1775 local_changes = self._revlist(not_rev(revid), HEAD, format="%H %ce")
1776 last_mine = None
1777 cnt_mine = 0
1778 for commit in local_changes:
1779 commit_id, committer_email = commit.split(" ", 1)
1780 if committer_email == self.UserEmail:
1781 last_mine = commit_id
1782 cnt_mine += 1
1783
1784 if not upstream_gain and cnt_mine == len(local_changes):
1785 # The copy/linkfile config may have changed.
1786 self._CopyAndLinkFiles()
1787 return
1788
1789 if self.IsDirty(consider_untracked=False):
Jason Chang32b59562023-07-14 16:45:35 -07001790 fail(_DirtyError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001791 return
1792
1793 # If the upstream switched on us, warn the user.
1794 if branch.merge != self.revisionExpr:
1795 if branch.merge and self.revisionExpr:
1796 syncbuf.info(
1797 self,
1798 "manifest switched %s...%s",
1799 branch.merge,
1800 self.revisionExpr,
1801 )
1802 elif branch.merge:
1803 syncbuf.info(self, "manifest no longer tracks %s", branch.merge)
1804
1805 if cnt_mine < len(local_changes):
1806 # Upstream rebased. Not everything in HEAD was created by this user.
1807 syncbuf.info(
1808 self,
1809 "discarding %d commits removed from upstream",
1810 len(local_changes) - cnt_mine,
1811 )
1812
1813 branch.remote = self.GetRemote()
Sylvain56a5a012023-09-11 13:38:00 +02001814 if not IsId(self.revisionExpr):
Gavin Makea2e3302023-03-11 06:46:20 +00001815 # In case of manifest sync the revisionExpr might be a SHA1.
1816 branch.merge = self.revisionExpr
1817 if not branch.merge.startswith("refs/"):
1818 branch.merge = R_HEADS + branch.merge
1819 branch.Save()
1820
1821 if cnt_mine > 0 and self.rebase:
1822
1823 def _docopyandlink():
1824 self._CopyAndLinkFiles()
1825
1826 def _dorebase():
1827 self._Rebase(upstream="%s^1" % last_mine, onto=revid)
1828
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001829 syncbuf.later2(self, _dorebase, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001830 if submodules:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001831 syncbuf.later2(self, _dosubmodules, not verbose)
1832 syncbuf.later2(self, _docopyandlink, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001833 elif local_changes:
1834 try:
1835 self._ResetHard(revid)
1836 if submodules:
1837 self._SyncSubmodules(quiet=True)
1838 self._CopyAndLinkFiles()
1839 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001840 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001841 return
1842 else:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001843 syncbuf.later1(self, _doff, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001844 if submodules:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08001845 syncbuf.later1(self, _dosubmodules, not verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00001846
1847 def AddCopyFile(self, src, dest, topdir):
1848 """Mark |src| for copying to |dest| (relative to |topdir|).
1849
1850 No filesystem changes occur here. Actual copying happens later on.
1851
1852 Paths should have basic validation run on them before being queued.
1853 Further checking will be handled when the actual copy happens.
1854 """
1855 self.copyfiles.append(_CopyFile(self.worktree, src, topdir, dest))
1856
1857 def AddLinkFile(self, src, dest, topdir):
1858 """Mark |dest| to create a symlink (relative to |topdir|) pointing to
1859 |src|.
1860
1861 No filesystem changes occur here. Actual linking happens later on.
1862
1863 Paths should have basic validation run on them before being queued.
1864 Further checking will be handled when the actual link happens.
1865 """
1866 self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest))
1867
1868 def AddAnnotation(self, name, value, keep):
1869 self.annotations.append(Annotation(name, value, keep))
1870
1871 def DownloadPatchSet(self, change_id, patch_id):
1872 """Download a single patch set of a single change to FETCH_HEAD."""
1873 remote = self.GetRemote()
1874
1875 cmd = ["fetch", remote.name]
1876 cmd.append(
1877 "refs/changes/%2.2d/%d/%d" % (change_id % 100, change_id, patch_id)
1878 )
Jason Chang1a3612f2023-08-08 14:12:53 -07001879 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001880 return DownloadedChange(
1881 self,
1882 self.GetRevisionId(),
1883 change_id,
1884 patch_id,
1885 self.bare_git.rev_parse("FETCH_HEAD"),
1886 )
1887
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001888 def DeleteWorktree(self, verbose=False, force=False):
Gavin Makea2e3302023-03-11 06:46:20 +00001889 """Delete the source checkout and any other housekeeping tasks.
1890
1891 This currently leaves behind the internal .repo/ cache state. This
1892 helps when switching branches or manifest changes get reverted as we
1893 don't have to redownload all the git objects. But we should do some GC
1894 at some point.
1895
1896 Args:
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001897 verbose: Whether to show verbose messages.
Gavin Makea2e3302023-03-11 06:46:20 +00001898 force: Always delete tree even if dirty.
Doug Anderson3ba5f952011-04-07 12:51:04 -07001899
1900 Returns:
Gavin Makea2e3302023-03-11 06:46:20 +00001901 True if the worktree was completely cleaned out.
1902 """
1903 if self.IsDirty():
1904 if force:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00001905 logger.warning(
Gavin Makea2e3302023-03-11 06:46:20 +00001906 "warning: %s: Removing dirty project: uncommitted changes "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001907 "lost.",
1908 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001909 )
1910 else:
Jason Chang32b59562023-07-14 16:45:35 -07001911 msg = (
1912 "error: %s: Cannot remove project: uncommitted"
1913 "changes are present.\n" % self.RelPath(local=False)
Gavin Makea2e3302023-03-11 06:46:20 +00001914 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001915 logger.error(msg)
Jason Chang32b59562023-07-14 16:45:35 -07001916 raise DeleteDirtyWorktreeError(msg, project=self)
Wink Saville02d79452009-04-10 13:01:24 -07001917
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08001918 if verbose:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001919 print(f"{self.RelPath(local=False)}: Deleting obsolete checkout.")
Wink Saville02d79452009-04-10 13:01:24 -07001920
Gavin Makea2e3302023-03-11 06:46:20 +00001921 # Unlock and delink from the main worktree. We don't use git's worktree
1922 # remove because it will recursively delete projects -- we handle that
1923 # ourselves below. https://crbug.com/git/48
1924 if self.use_git_worktrees:
1925 needle = platform_utils.realpath(self.gitdir)
1926 # Find the git worktree commondir under .repo/worktrees/.
1927 output = self.bare_git.worktree("list", "--porcelain").splitlines()[
1928 0
1929 ]
1930 assert output.startswith("worktree "), output
1931 commondir = output[9:]
1932 # Walk each of the git worktrees to see where they point.
1933 configs = os.path.join(commondir, "worktrees")
1934 for name in os.listdir(configs):
1935 gitdir = os.path.join(configs, name, "gitdir")
1936 with open(gitdir) as fp:
1937 relpath = fp.read().strip()
1938 # Resolve the checkout path and see if it matches this project.
1939 fullpath = platform_utils.realpath(
1940 os.path.join(configs, name, relpath)
1941 )
1942 if fullpath == needle:
1943 platform_utils.rmtree(os.path.join(configs, name))
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001944
Gavin Makea2e3302023-03-11 06:46:20 +00001945 # Delete the .git directory first, so we're less likely to have a
1946 # partially working git repository around. There shouldn't be any git
1947 # projects here, so rmtree works.
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001948
Gavin Makea2e3302023-03-11 06:46:20 +00001949 # Try to remove plain files first in case of git worktrees. If this
1950 # fails for any reason, we'll fall back to rmtree, and that'll display
1951 # errors if it can't remove things either.
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001952 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001953 platform_utils.remove(self.gitdir)
1954 except OSError:
1955 pass
1956 try:
1957 platform_utils.rmtree(self.gitdir)
1958 except OSError as e:
1959 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001960 logger.error("error: %s: %s", self.gitdir, e)
1961 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00001962 "error: %s: Failed to delete obsolete checkout; remove "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001963 "manually, then run `repo sync -l`.",
1964 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001965 )
Jason Chang32b59562023-07-14 16:45:35 -07001966 raise DeleteWorktreeError(aggregate_errors=[e])
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001967
Gavin Makea2e3302023-03-11 06:46:20 +00001968 # Delete everything under the worktree, except for directories that
1969 # contain another git project.
1970 dirs_to_remove = []
1971 failed = False
Jason Chang32b59562023-07-14 16:45:35 -07001972 errors = []
Gavin Makea2e3302023-03-11 06:46:20 +00001973 for root, dirs, files in platform_utils.walk(self.worktree):
1974 for f in files:
1975 path = os.path.join(root, f)
1976 try:
1977 platform_utils.remove(path)
1978 except OSError as e:
1979 if e.errno != errno.ENOENT:
Josip Sokcevic4217a822024-01-24 13:54:25 -08001980 logger.warning("%s: Failed to remove: %s", path, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001981 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001982 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001983 dirs[:] = [
1984 d
1985 for d in dirs
1986 if not os.path.lexists(os.path.join(root, d, ".git"))
1987 ]
1988 dirs_to_remove += [
1989 os.path.join(root, d)
1990 for d in dirs
1991 if os.path.join(root, d) not in dirs_to_remove
1992 ]
1993 for d in reversed(dirs_to_remove):
1994 if platform_utils.islink(d):
1995 try:
1996 platform_utils.remove(d)
1997 except OSError as e:
1998 if e.errno != errno.ENOENT:
Josip Sokcevic4217a822024-01-24 13:54:25 -08001999 logger.warning("%s: Failed to remove: %s", d, e)
Gavin Makea2e3302023-03-11 06:46:20 +00002000 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07002001 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00002002 elif not platform_utils.listdir(d):
2003 try:
2004 platform_utils.rmdir(d)
2005 except OSError as e:
2006 if e.errno != errno.ENOENT:
Josip Sokcevic4217a822024-01-24 13:54:25 -08002007 logger.warning("%s: Failed to remove: %s", d, e)
Gavin Makea2e3302023-03-11 06:46:20 +00002008 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07002009 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00002010 if failed:
Josip Sokcevic4217a822024-01-24 13:54:25 -08002011 rename_path = (
2012 f"{self.worktree}_repo_to_be_deleted_{int(time.time())}"
Gavin Makea2e3302023-03-11 06:46:20 +00002013 )
Josip Sokcevic4217a822024-01-24 13:54:25 -08002014 try:
2015 platform_utils.rename(self.worktree, rename_path)
2016 logger.warning(
2017 "warning: renamed %s to %s. You can delete it, but you "
2018 "might need elevated permissions (e.g. root)",
2019 self.worktree,
2020 rename_path,
2021 )
2022 # Rename successful! Clear the errors.
2023 errors = []
2024 except OSError:
2025 logger.error(
2026 "%s: Failed to delete obsolete checkout.\n",
2027 " Remove manually, then run `repo sync -l`.",
2028 self.RelPath(local=False),
2029 )
2030 raise DeleteWorktreeError(aggregate_errors=errors)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07002031
Gavin Makea2e3302023-03-11 06:46:20 +00002032 # Try deleting parent dirs if they are empty.
2033 path = self.worktree
2034 while path != self.manifest.topdir:
2035 try:
2036 platform_utils.rmdir(path)
2037 except OSError as e:
2038 if e.errno != errno.ENOENT:
2039 break
2040 path = os.path.dirname(path)
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08002041
Gavin Makea2e3302023-03-11 06:46:20 +00002042 return True
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08002043
Gavin Makea2e3302023-03-11 06:46:20 +00002044 def StartBranch(self, name, branch_merge="", revision=None):
2045 """Create a new branch off the manifest's revision."""
2046 if not branch_merge:
2047 branch_merge = self.revisionExpr
2048 head = self.work_git.GetHead()
2049 if head == (R_HEADS + name):
2050 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02002051
David Pursehouse8a68ff92012-09-24 12:15:13 +09002052 all_refs = self.bare_ref.all
Gavin Makea2e3302023-03-11 06:46:20 +00002053 if R_HEADS + name in all_refs:
Jason Chang1a3612f2023-08-08 14:12:53 -07002054 GitCommand(
2055 self, ["checkout", "-q", name, "--"], verify_command=True
2056 ).Wait()
2057 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02002058
Gavin Makea2e3302023-03-11 06:46:20 +00002059 branch = self.GetBranch(name)
2060 branch.remote = self.GetRemote()
2061 branch.merge = branch_merge
Sylvain56a5a012023-09-11 13:38:00 +02002062 if not branch.merge.startswith("refs/") and not IsId(branch_merge):
Gavin Makea2e3302023-03-11 06:46:20 +00002063 branch.merge = R_HEADS + branch_merge
Shawn O. Pearce88443382010-10-08 10:02:09 +02002064
Gavin Makea2e3302023-03-11 06:46:20 +00002065 if revision is None:
2066 revid = self.GetRevisionId(all_refs)
Shawn O. Pearce88443382010-10-08 10:02:09 +02002067 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002068 revid = self.work_git.rev_parse(revision)
Brian Harring14a66742012-09-28 20:21:57 -07002069
Gavin Makea2e3302023-03-11 06:46:20 +00002070 if head.startswith(R_HEADS):
Kevin Degiabaa7f32014-11-12 11:27:45 -07002071 try:
Gavin Makea2e3302023-03-11 06:46:20 +00002072 head = all_refs[head]
2073 except KeyError:
2074 head = None
2075 if revid and head and revid == head:
2076 ref = R_HEADS + name
2077 self.work_git.update_ref(ref, revid)
2078 self.work_git.symbolic_ref(HEAD, ref)
2079 branch.Save()
2080 return True
Kevin Degib1a07b82015-07-27 13:33:43 -06002081
Jason Chang1a3612f2023-08-08 14:12:53 -07002082 GitCommand(
2083 self,
2084 ["checkout", "-q", "-b", branch.name, revid],
2085 verify_command=True,
2086 ).Wait()
2087 branch.Save()
2088 return True
Kevin Degi384b3c52014-10-16 16:02:58 -06002089
Gavin Makea2e3302023-03-11 06:46:20 +00002090 def CheckoutBranch(self, name):
2091 """Checkout a local topic branch.
Shawn O. Pearce2816d4f2009-03-03 17:53:18 -08002092
Gavin Makea2e3302023-03-11 06:46:20 +00002093 Args:
2094 name: The name of the branch to checkout.
Shawn O. Pearce88443382010-10-08 10:02:09 +02002095
Gavin Makea2e3302023-03-11 06:46:20 +00002096 Returns:
Jason Chang1a3612f2023-08-08 14:12:53 -07002097 True if the checkout succeeded; False if the
2098 branch doesn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00002099 """
2100 rev = R_HEADS + name
2101 head = self.work_git.GetHead()
2102 if head == rev:
2103 # Already on the branch.
2104 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02002105
Gavin Makea2e3302023-03-11 06:46:20 +00002106 all_refs = self.bare_ref.all
2107 try:
2108 revid = all_refs[rev]
2109 except KeyError:
2110 # Branch does not exist in this project.
Jason Chang1a3612f2023-08-08 14:12:53 -07002111 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002112
Gavin Makea2e3302023-03-11 06:46:20 +00002113 if head.startswith(R_HEADS):
2114 try:
2115 head = all_refs[head]
2116 except KeyError:
2117 head = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002118
Gavin Makea2e3302023-03-11 06:46:20 +00002119 if head == revid:
2120 # Same revision; just update HEAD to point to the new
2121 # target branch, but otherwise take no other action.
2122 _lwrite(
2123 self.work_git.GetDotgitPath(subpath=HEAD),
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002124 f"ref: {R_HEADS}{name}\n",
Gavin Makea2e3302023-03-11 06:46:20 +00002125 )
2126 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05002127
Jason Chang1a3612f2023-08-08 14:12:53 -07002128 GitCommand(
2129 self,
2130 ["checkout", name, "--"],
2131 capture_stdout=True,
2132 capture_stderr=True,
2133 verify_command=True,
2134 ).Wait()
2135 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05002136
Gavin Makea2e3302023-03-11 06:46:20 +00002137 def AbandonBranch(self, name):
2138 """Destroy a local topic branch.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002139
Gavin Makea2e3302023-03-11 06:46:20 +00002140 Args:
2141 name: The name of the branch to abandon.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002142
Gavin Makea2e3302023-03-11 06:46:20 +00002143 Returns:
Jason Changf9aacd42023-08-03 14:38:00 -07002144 True if the abandon succeeded; Raises GitCommandError if it didn't;
2145 None if the branch didn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00002146 """
2147 rev = R_HEADS + name
2148 all_refs = self.bare_ref.all
2149 if rev not in all_refs:
2150 # Doesn't exist
2151 return None
2152
2153 head = self.work_git.GetHead()
2154 if head == rev:
2155 # We can't destroy the branch while we are sitting
2156 # on it. Switch to a detached HEAD.
2157 head = all_refs[head]
2158
2159 revid = self.GetRevisionId(all_refs)
2160 if head == revid:
2161 _lwrite(
2162 self.work_git.GetDotgitPath(subpath=HEAD), "%s\n" % revid
2163 )
2164 else:
2165 self._Checkout(revid, quiet=True)
Jason Changf9aacd42023-08-03 14:38:00 -07002166 GitCommand(
2167 self,
2168 ["branch", "-D", name],
2169 capture_stdout=True,
2170 capture_stderr=True,
2171 verify_command=True,
2172 ).Wait()
2173 return True
Gavin Makea2e3302023-03-11 06:46:20 +00002174
2175 def PruneHeads(self):
2176 """Prune any topic branches already merged into upstream."""
2177 cb = self.CurrentBranch
2178 kill = []
2179 left = self._allrefs
2180 for name in left.keys():
2181 if name.startswith(R_HEADS):
2182 name = name[len(R_HEADS) :]
2183 if cb is None or name != cb:
2184 kill.append(name)
2185
2186 # Minor optimization: If there's nothing to prune, then don't try to
2187 # read any project state.
2188 if not kill and not cb:
2189 return []
2190
2191 rev = self.GetRevisionId(left)
2192 if (
2193 cb is not None
2194 and not self._revlist(HEAD + "..." + rev)
2195 and not self.IsDirty(consider_untracked=False)
2196 ):
2197 self.work_git.DetachHead(HEAD)
2198 kill.append(cb)
2199
2200 if kill:
2201 old = self.bare_git.GetHead()
2202
2203 try:
2204 self.bare_git.DetachHead(rev)
2205
2206 b = ["branch", "-d"]
2207 b.extend(kill)
2208 b = GitCommand(
2209 self, b, bare=True, capture_stdout=True, capture_stderr=True
2210 )
2211 b.Wait()
2212 finally:
Sylvain56a5a012023-09-11 13:38:00 +02002213 if IsId(old):
Gavin Makea2e3302023-03-11 06:46:20 +00002214 self.bare_git.DetachHead(old)
2215 else:
2216 self.bare_git.SetHead(old)
2217 left = self._allrefs
2218
2219 for branch in kill:
2220 if (R_HEADS + branch) not in left:
2221 self.CleanPublishedCache()
2222 break
2223
2224 if cb and cb not in kill:
2225 kill.append(cb)
2226 kill.sort()
2227
2228 kept = []
2229 for branch in kill:
2230 if R_HEADS + branch in left:
2231 branch = self.GetBranch(branch)
2232 base = branch.LocalMerge
2233 if not base:
2234 base = rev
2235 kept.append(ReviewableBranch(self, branch, base))
2236 return kept
2237
2238 def GetRegisteredSubprojects(self):
2239 result = []
2240
2241 def rec(subprojects):
2242 if not subprojects:
2243 return
2244 result.extend(subprojects)
2245 for p in subprojects:
2246 rec(p.subprojects)
2247
2248 rec(self.subprojects)
2249 return result
2250
2251 def _GetSubmodules(self):
2252 # Unfortunately we cannot call `git submodule status --recursive` here
2253 # because the working tree might not exist yet, and it cannot be used
2254 # without a working tree in its current implementation.
2255
2256 def get_submodules(gitdir, rev):
2257 # Parse .gitmodules for submodule sub_paths and sub_urls.
2258 sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
2259 if not sub_paths:
2260 return []
2261 # Run `git ls-tree` to read SHAs of submodule object, which happen
2262 # to be revision of submodule repository.
2263 sub_revs = git_ls_tree(gitdir, rev, sub_paths)
2264 submodules = []
2265 for sub_path, sub_url in zip(sub_paths, sub_urls):
2266 try:
2267 sub_rev = sub_revs[sub_path]
2268 except KeyError:
2269 # Ignore non-exist submodules.
2270 continue
2271 submodules.append((sub_rev, sub_path, sub_url))
2272 return submodules
2273
2274 re_path = re.compile(r"^submodule\.(.+)\.path=(.*)$")
2275 re_url = re.compile(r"^submodule\.(.+)\.url=(.*)$")
2276
2277 def parse_gitmodules(gitdir, rev):
2278 cmd = ["cat-file", "blob", "%s:.gitmodules" % rev]
2279 try:
2280 p = GitCommand(
2281 None,
2282 cmd,
2283 capture_stdout=True,
2284 capture_stderr=True,
2285 bare=True,
2286 gitdir=gitdir,
2287 )
2288 except GitError:
2289 return [], []
2290 if p.Wait() != 0:
2291 return [], []
2292
2293 gitmodules_lines = []
2294 fd, temp_gitmodules_path = tempfile.mkstemp()
2295 try:
2296 os.write(fd, p.stdout.encode("utf-8"))
2297 os.close(fd)
2298 cmd = ["config", "--file", temp_gitmodules_path, "--list"]
2299 p = GitCommand(
2300 None,
2301 cmd,
2302 capture_stdout=True,
2303 capture_stderr=True,
2304 bare=True,
2305 gitdir=gitdir,
2306 )
2307 if p.Wait() != 0:
2308 return [], []
2309 gitmodules_lines = p.stdout.split("\n")
2310 except GitError:
2311 return [], []
2312 finally:
2313 platform_utils.remove(temp_gitmodules_path)
2314
2315 names = set()
2316 paths = {}
2317 urls = {}
2318 for line in gitmodules_lines:
2319 if not line:
2320 continue
2321 m = re_path.match(line)
2322 if m:
2323 names.add(m.group(1))
2324 paths[m.group(1)] = m.group(2)
2325 continue
2326 m = re_url.match(line)
2327 if m:
2328 names.add(m.group(1))
2329 urls[m.group(1)] = m.group(2)
2330 continue
2331 names = sorted(names)
2332 return (
2333 [paths.get(name, "") for name in names],
2334 [urls.get(name, "") for name in names],
2335 )
2336
2337 def git_ls_tree(gitdir, rev, paths):
2338 cmd = ["ls-tree", rev, "--"]
2339 cmd.extend(paths)
2340 try:
2341 p = GitCommand(
2342 None,
2343 cmd,
2344 capture_stdout=True,
2345 capture_stderr=True,
2346 bare=True,
2347 gitdir=gitdir,
2348 )
2349 except GitError:
2350 return []
2351 if p.Wait() != 0:
2352 return []
2353 objects = {}
2354 for line in p.stdout.split("\n"):
2355 if not line.strip():
2356 continue
2357 object_rev, object_path = line.split()[2:4]
2358 objects[object_path] = object_rev
2359 return objects
2360
2361 try:
2362 rev = self.GetRevisionId()
2363 except GitError:
2364 return []
2365 return get_submodules(self.gitdir, rev)
2366
2367 def GetDerivedSubprojects(self):
2368 result = []
2369 if not self.Exists:
2370 # If git repo does not exist yet, querying its submodules will
2371 # mess up its states; so return here.
2372 return result
2373 for rev, path, url in self._GetSubmodules():
2374 name = self.manifest.GetSubprojectName(self, path)
2375 (
2376 relpath,
2377 worktree,
2378 gitdir,
2379 objdir,
2380 ) = self.manifest.GetSubprojectPaths(self, name, path)
2381 project = self.manifest.paths.get(relpath)
2382 if project:
2383 result.extend(project.GetDerivedSubprojects())
2384 continue
2385
2386 if url.startswith(".."):
2387 url = urllib.parse.urljoin("%s/" % self.remote.url, url)
2388 remote = RemoteSpec(
2389 self.remote.name,
2390 url=url,
2391 pushUrl=self.remote.pushUrl,
2392 review=self.remote.review,
2393 revision=self.remote.revision,
2394 )
2395 subproject = Project(
2396 manifest=self.manifest,
2397 name=name,
2398 remote=remote,
2399 gitdir=gitdir,
2400 objdir=objdir,
2401 worktree=worktree,
2402 relpath=relpath,
2403 revisionExpr=rev,
2404 revisionId=rev,
2405 rebase=self.rebase,
2406 groups=self.groups,
2407 sync_c=self.sync_c,
2408 sync_s=self.sync_s,
2409 sync_tags=self.sync_tags,
2410 parent=self,
2411 is_derived=True,
2412 )
2413 result.append(subproject)
2414 result.extend(subproject.GetDerivedSubprojects())
2415 return result
2416
2417 def EnableRepositoryExtension(self, key, value="true", version=1):
2418 """Enable git repository extension |key| with |value|.
2419
2420 Args:
2421 key: The extension to enabled. Omit the "extensions." prefix.
2422 value: The value to use for the extension.
2423 version: The minimum git repository version needed.
2424 """
2425 # Make sure the git repo version is new enough already.
2426 found_version = self.config.GetInt("core.repositoryFormatVersion")
2427 if found_version is None:
2428 found_version = 0
2429 if found_version < version:
2430 self.config.SetString("core.repositoryFormatVersion", str(version))
2431
2432 # Enable the extension!
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002433 self.config.SetString(f"extensions.{key}", value)
Gavin Makea2e3302023-03-11 06:46:20 +00002434
2435 def ResolveRemoteHead(self, name=None):
2436 """Find out what the default branch (HEAD) points to.
2437
2438 Normally this points to refs/heads/master, but projects are moving to
2439 main. Support whatever the server uses rather than hardcoding "master"
2440 ourselves.
2441 """
2442 if name is None:
2443 name = self.remote.name
2444
2445 # The output will look like (NB: tabs are separators):
2446 # ref: refs/heads/master HEAD
2447 # 5f6803b100bb3cd0f534e96e88c91373e8ed1c44 HEAD
2448 output = self.bare_git.ls_remote(
2449 "-q", "--symref", "--exit-code", name, "HEAD"
2450 )
2451
2452 for line in output.splitlines():
2453 lhs, rhs = line.split("\t", 1)
2454 if rhs == "HEAD" and lhs.startswith("ref:"):
2455 return lhs[4:].strip()
2456
2457 return None
2458
2459 def _CheckForImmutableRevision(self):
2460 try:
2461 # if revision (sha or tag) is not present then following function
2462 # throws an error.
2463 self.bare_git.rev_list(
Jason Chang87058c62023-09-27 11:34:43 -07002464 "-1",
2465 "--missing=allow-any",
2466 "%s^0" % self.revisionExpr,
2467 "--",
2468 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002469 )
2470 if self.upstream:
2471 rev = self.GetRemote().ToLocal(self.upstream)
2472 self.bare_git.rev_list(
Jason Chang87058c62023-09-27 11:34:43 -07002473 "-1",
2474 "--missing=allow-any",
2475 "%s^0" % rev,
2476 "--",
2477 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002478 )
2479 self.bare_git.merge_base(
Jason Chang87058c62023-09-27 11:34:43 -07002480 "--is-ancestor",
2481 self.revisionExpr,
2482 rev,
2483 log_as_error=False,
Gavin Makea2e3302023-03-11 06:46:20 +00002484 )
2485 return True
2486 except GitError:
2487 # There is no such persistent revision. We have to fetch it.
2488 return False
2489
2490 def _FetchArchive(self, tarpath, cwd=None):
2491 cmd = ["archive", "-v", "-o", tarpath]
2492 cmd.append("--remote=%s" % self.remote.url)
2493 cmd.append("--prefix=%s/" % self.RelPath(local=False))
2494 cmd.append(self.revisionExpr)
2495
2496 command = GitCommand(
Jason Chang32b59562023-07-14 16:45:35 -07002497 self,
2498 cmd,
2499 cwd=cwd,
2500 capture_stdout=True,
2501 capture_stderr=True,
2502 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00002503 )
Jason Chang32b59562023-07-14 16:45:35 -07002504 command.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00002505
2506 def _RemoteFetch(
2507 self,
2508 name=None,
2509 current_branch_only=False,
2510 initial=False,
2511 quiet=False,
2512 verbose=False,
2513 output_redir=None,
2514 alt_dir=None,
2515 tags=True,
2516 prune=False,
2517 depth=None,
2518 submodules=False,
2519 ssh_proxy=None,
2520 force_sync=False,
2521 clone_filter=None,
2522 retry_fetches=2,
2523 retry_sleep_initial_sec=4.0,
2524 retry_exp_factor=2.0,
Jason Chang32b59562023-07-14 16:45:35 -07002525 ) -> bool:
Gavin Makea2e3302023-03-11 06:46:20 +00002526 tag_name = None
2527 # The depth should not be used when fetching to a mirror because
2528 # it will result in a shallow repository that cannot be cloned or
2529 # fetched from.
2530 # The repo project should also never be synced with partial depth.
2531 if self.manifest.IsMirror or self.relpath == ".repo/repo":
2532 depth = None
2533
2534 if depth:
2535 current_branch_only = True
2536
Sylvain56a5a012023-09-11 13:38:00 +02002537 is_sha1 = bool(IsId(self.revisionExpr))
Gavin Makea2e3302023-03-11 06:46:20 +00002538
2539 if current_branch_only:
2540 if self.revisionExpr.startswith(R_TAGS):
2541 # This is a tag and its commit id should never change.
2542 tag_name = self.revisionExpr[len(R_TAGS) :]
2543 elif self.upstream and self.upstream.startswith(R_TAGS):
2544 # This is a tag and its commit id should never change.
2545 tag_name = self.upstream[len(R_TAGS) :]
2546
2547 if is_sha1 or tag_name is not None:
2548 if self._CheckForImmutableRevision():
2549 if verbose:
2550 print(
2551 "Skipped fetching project %s (already have "
2552 "persistent ref)" % self.name
2553 )
2554 return True
2555 if is_sha1 and not depth:
2556 # When syncing a specific commit and --depth is not set:
2557 # * if upstream is explicitly specified and is not a sha1, fetch
2558 # only upstream as users expect only upstream to be fetch.
2559 # Note: The commit might not be in upstream in which case the
2560 # sync will fail.
2561 # * otherwise, fetch all branches to make sure we end up with
2562 # the specific commit.
2563 if self.upstream:
Sylvain56a5a012023-09-11 13:38:00 +02002564 current_branch_only = not IsId(self.upstream)
Gavin Makea2e3302023-03-11 06:46:20 +00002565 else:
2566 current_branch_only = False
2567
2568 if not name:
2569 name = self.remote.name
2570
2571 remote = self.GetRemote(name)
2572 if not remote.PreConnectFetch(ssh_proxy):
2573 ssh_proxy = None
2574
2575 if initial:
2576 if alt_dir and "objects" == os.path.basename(alt_dir):
2577 ref_dir = os.path.dirname(alt_dir)
2578 packed_refs = os.path.join(self.gitdir, "packed-refs")
2579
2580 all_refs = self.bare_ref.all
2581 ids = set(all_refs.values())
2582 tmp = set()
2583
2584 for r, ref_id in GitRefs(ref_dir).all.items():
2585 if r not in all_refs:
2586 if r.startswith(R_TAGS) or remote.WritesTo(r):
2587 all_refs[r] = ref_id
2588 ids.add(ref_id)
2589 continue
2590
2591 if ref_id in ids:
2592 continue
2593
2594 r = "refs/_alt/%s" % ref_id
2595 all_refs[r] = ref_id
2596 ids.add(ref_id)
2597 tmp.add(r)
2598
2599 tmp_packed_lines = []
2600 old_packed_lines = []
2601
2602 for r in sorted(all_refs):
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002603 line = f"{all_refs[r]} {r}\n"
Gavin Makea2e3302023-03-11 06:46:20 +00002604 tmp_packed_lines.append(line)
2605 if r not in tmp:
2606 old_packed_lines.append(line)
2607
2608 tmp_packed = "".join(tmp_packed_lines)
2609 old_packed = "".join(old_packed_lines)
2610 _lwrite(packed_refs, tmp_packed)
2611 else:
2612 alt_dir = None
2613
2614 cmd = ["fetch"]
2615
2616 if clone_filter:
2617 git_require((2, 19, 0), fail=True, msg="partial clones")
2618 cmd.append("--filter=%s" % clone_filter)
2619 self.EnableRepositoryExtension("partialclone", self.remote.name)
2620
2621 if depth:
2622 cmd.append("--depth=%s" % depth)
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002623 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002624 # If this repo has shallow objects, then we don't know which refs
2625 # have shallow objects or not. Tell git to unshallow all fetched
2626 # refs. Don't do this with projects that don't have shallow
2627 # objects, since it is less efficient.
2628 if os.path.exists(os.path.join(self.gitdir, "shallow")):
2629 cmd.append("--depth=2147483647")
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002630
Gavin Makea2e3302023-03-11 06:46:20 +00002631 if not verbose:
2632 cmd.append("--quiet")
2633 if not quiet and sys.stdout.isatty():
2634 cmd.append("--progress")
2635 if not self.worktree:
2636 cmd.append("--update-head-ok")
2637 cmd.append(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002638
Gavin Makea2e3302023-03-11 06:46:20 +00002639 if force_sync:
2640 cmd.append("--force")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002641
Gavin Makea2e3302023-03-11 06:46:20 +00002642 if prune:
2643 cmd.append("--prune")
Mike Frysinger29626b42021-05-01 09:37:13 -04002644
Gavin Makea2e3302023-03-11 06:46:20 +00002645 # Always pass something for --recurse-submodules, git with GIT_DIR
2646 # behaves incorrectly when not given `--recurse-submodules=no`.
2647 # (b/218891912)
2648 cmd.append(
2649 f'--recurse-submodules={"on-demand" if submodules else "no"}'
2650 )
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002651
Gavin Makea2e3302023-03-11 06:46:20 +00002652 spec = []
2653 if not current_branch_only:
2654 # Fetch whole repo.
2655 spec.append(
2656 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2657 )
2658 elif tag_name is not None:
2659 spec.append("tag")
2660 spec.append(tag_name)
Remy Böhmer1469c282020-12-15 18:49:02 +01002661
Gavin Makea2e3302023-03-11 06:46:20 +00002662 if self.manifest.IsMirror and not current_branch_only:
2663 branch = None
Remy Böhmer1469c282020-12-15 18:49:02 +01002664 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002665 branch = self.revisionExpr
2666 if (
2667 not self.manifest.IsMirror
2668 and is_sha1
2669 and depth
2670 and git_require((1, 8, 3))
2671 ):
2672 # Shallow checkout of a specific commit, fetch from that commit and
2673 # not the heads only as the commit might be deeper in the history.
2674 spec.append(branch)
2675 if self.upstream:
2676 spec.append(self.upstream)
David James8d201162013-10-11 17:03:19 -07002677 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002678 if is_sha1:
2679 branch = self.upstream
2680 if branch is not None and branch.strip():
2681 if not branch.startswith("refs/"):
2682 branch = R_HEADS + branch
2683 spec.append(str(("+%s:" % branch) + remote.ToLocal(branch)))
David James8d201162013-10-11 17:03:19 -07002684
Gavin Makea2e3302023-03-11 06:46:20 +00002685 # If mirroring repo and we cannot deduce the tag or branch to fetch,
2686 # fetch whole repo.
2687 if self.manifest.IsMirror and not spec:
2688 spec.append(
2689 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2690 )
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002691
Gavin Makea2e3302023-03-11 06:46:20 +00002692 # If using depth then we should not get all the tags since they may
2693 # be outside of the depth.
2694 if not tags or depth:
2695 cmd.append("--no-tags")
2696 else:
2697 cmd.append("--tags")
2698 spec.append(str(("+refs/tags/*:") + remote.ToLocal("refs/tags/*")))
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002699
Gavin Makea2e3302023-03-11 06:46:20 +00002700 cmd.extend(spec)
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002701
Gavin Makea2e3302023-03-11 06:46:20 +00002702 # At least one retry minimum due to git remote prune.
2703 retry_fetches = max(retry_fetches, 2)
2704 retry_cur_sleep = retry_sleep_initial_sec
2705 ok = prune_tried = False
2706 for try_n in range(retry_fetches):
Jason Chang32b59562023-07-14 16:45:35 -07002707 verify_command = try_n == retry_fetches - 1
Gavin Makea2e3302023-03-11 06:46:20 +00002708 gitcmd = GitCommand(
2709 self,
2710 cmd,
2711 bare=True,
2712 objdir=os.path.join(self.objdir, "objects"),
2713 ssh_proxy=ssh_proxy,
2714 merge_output=True,
2715 capture_stdout=quiet or bool(output_redir),
Jason Chang32b59562023-07-14 16:45:35 -07002716 verify_command=verify_command,
Gavin Makea2e3302023-03-11 06:46:20 +00002717 )
2718 if gitcmd.stdout and not quiet and output_redir:
2719 output_redir.write(gitcmd.stdout)
2720 ret = gitcmd.Wait()
2721 if ret == 0:
2722 ok = True
2723 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002724
Gavin Makea2e3302023-03-11 06:46:20 +00002725 # Retry later due to HTTP 429 Too Many Requests.
2726 elif (
2727 gitcmd.stdout
2728 and "error:" in gitcmd.stdout
2729 and "HTTP 429" in gitcmd.stdout
2730 ):
2731 # Fallthru to sleep+retry logic at the bottom.
2732 pass
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002733
Gavin Makea2e3302023-03-11 06:46:20 +00002734 # Try to prune remote branches once in case there are conflicts.
2735 # For example, if the remote had refs/heads/upstream, but deleted
2736 # that and now has refs/heads/upstream/foo.
2737 elif (
2738 gitcmd.stdout
2739 and "error:" in gitcmd.stdout
2740 and "git remote prune" in gitcmd.stdout
2741 and not prune_tried
2742 ):
2743 prune_tried = True
2744 prunecmd = GitCommand(
2745 self,
2746 ["remote", "prune", name],
2747 bare=True,
2748 ssh_proxy=ssh_proxy,
2749 )
2750 ret = prunecmd.Wait()
2751 if ret:
2752 break
2753 print(
2754 "retrying fetch after pruning remote branches",
2755 file=output_redir,
2756 )
2757 # Continue right away so we don't sleep as we shouldn't need to.
2758 continue
2759 elif current_branch_only and is_sha1 and ret == 128:
2760 # Exit code 128 means "couldn't find the ref you asked for"; if
2761 # we're in sha1 mode, we just tried sync'ing from the upstream
2762 # field; it doesn't exist, thus abort the optimization attempt
2763 # and do a full sync.
2764 break
2765 elif ret < 0:
2766 # Git died with a signal, exit immediately.
2767 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002768
Gavin Makea2e3302023-03-11 06:46:20 +00002769 # Figure out how long to sleep before the next attempt, if there is
2770 # one.
2771 if not verbose and gitcmd.stdout:
2772 print(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002773 f"\n{self.name}:\n{gitcmd.stdout}",
Gavin Makea2e3302023-03-11 06:46:20 +00002774 end="",
2775 file=output_redir,
2776 )
2777 if try_n < retry_fetches - 1:
2778 print(
2779 "%s: sleeping %s seconds before retrying"
2780 % (self.name, retry_cur_sleep),
2781 file=output_redir,
2782 )
2783 time.sleep(retry_cur_sleep)
2784 retry_cur_sleep = min(
2785 retry_exp_factor * retry_cur_sleep, MAXIMUM_RETRY_SLEEP_SEC
2786 )
2787 retry_cur_sleep *= 1 - random.uniform(
2788 -RETRY_JITTER_PERCENT, RETRY_JITTER_PERCENT
2789 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002790
Gavin Makea2e3302023-03-11 06:46:20 +00002791 if initial:
2792 if alt_dir:
2793 if old_packed != "":
2794 _lwrite(packed_refs, old_packed)
2795 else:
2796 platform_utils.remove(packed_refs)
2797 self.bare_git.pack_refs("--all", "--prune")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002798
Gavin Makea2e3302023-03-11 06:46:20 +00002799 if is_sha1 and current_branch_only:
2800 # We just synced the upstream given branch; verify we
2801 # got what we wanted, else trigger a second run of all
2802 # refs.
2803 if not self._CheckForImmutableRevision():
2804 # Sync the current branch only with depth set to None.
2805 # We always pass depth=None down to avoid infinite recursion.
2806 return self._RemoteFetch(
2807 name=name,
2808 quiet=quiet,
2809 verbose=verbose,
2810 output_redir=output_redir,
2811 current_branch_only=current_branch_only and depth,
2812 initial=False,
2813 alt_dir=alt_dir,
Nasser Grainawiaafed292023-05-24 12:51:03 -06002814 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00002815 depth=None,
2816 ssh_proxy=ssh_proxy,
2817 clone_filter=clone_filter,
2818 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002819
Gavin Makea2e3302023-03-11 06:46:20 +00002820 return ok
Mike Frysingerf4545122019-11-11 04:34:16 -05002821
Gavin Makea2e3302023-03-11 06:46:20 +00002822 def _ApplyCloneBundle(self, initial=False, quiet=False, verbose=False):
2823 if initial and (
2824 self.manifest.manifestProject.depth or self.clone_depth
2825 ):
2826 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002827
Gavin Makea2e3302023-03-11 06:46:20 +00002828 remote = self.GetRemote()
2829 bundle_url = remote.url + "/clone.bundle"
2830 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
2831 if GetSchemeFromUrl(bundle_url) not in (
2832 "http",
2833 "https",
2834 "persistent-http",
2835 "persistent-https",
2836 ):
2837 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06002838
Gavin Makea2e3302023-03-11 06:46:20 +00002839 bundle_dst = os.path.join(self.gitdir, "clone.bundle")
2840 bundle_tmp = os.path.join(self.gitdir, "clone.bundle.tmp")
2841
2842 exist_dst = os.path.exists(bundle_dst)
2843 exist_tmp = os.path.exists(bundle_tmp)
2844
2845 if not initial and not exist_dst and not exist_tmp:
2846 return False
2847
2848 if not exist_dst:
2849 exist_dst = self._FetchBundle(
2850 bundle_url, bundle_tmp, bundle_dst, quiet, verbose
2851 )
2852 if not exist_dst:
2853 return False
2854
2855 cmd = ["fetch"]
2856 if not verbose:
2857 cmd.append("--quiet")
2858 if not quiet and sys.stdout.isatty():
2859 cmd.append("--progress")
2860 if not self.worktree:
2861 cmd.append("--update-head-ok")
2862 cmd.append(bundle_dst)
2863 for f in remote.fetch:
2864 cmd.append(str(f))
2865 cmd.append("+refs/tags/*:refs/tags/*")
2866
2867 ok = (
2868 GitCommand(
2869 self,
2870 cmd,
2871 bare=True,
2872 objdir=os.path.join(self.objdir, "objects"),
2873 ).Wait()
2874 == 0
2875 )
2876 platform_utils.remove(bundle_dst, missing_ok=True)
2877 platform_utils.remove(bundle_tmp, missing_ok=True)
2878 return ok
2879
2880 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
2881 platform_utils.remove(dstPath, missing_ok=True)
2882
2883 cmd = ["curl", "--fail", "--output", tmpPath, "--netrc", "--location"]
2884 if quiet:
2885 cmd += ["--silent", "--show-error"]
2886 if os.path.exists(tmpPath):
2887 size = os.stat(tmpPath).st_size
2888 if size >= 1024:
2889 cmd += ["--continue-at", "%d" % (size,)]
2890 else:
2891 platform_utils.remove(tmpPath)
2892 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
2893 if cookiefile:
2894 cmd += ["--cookie", cookiefile]
2895 if proxy:
2896 cmd += ["--proxy", proxy]
2897 elif "http_proxy" in os.environ and "darwin" == sys.platform:
2898 cmd += ["--proxy", os.environ["http_proxy"]]
2899 if srcUrl.startswith("persistent-https"):
2900 srcUrl = "http" + srcUrl[len("persistent-https") :]
2901 elif srcUrl.startswith("persistent-http"):
2902 srcUrl = "http" + srcUrl[len("persistent-http") :]
2903 cmd += [srcUrl]
2904
2905 proc = None
2906 with Trace("Fetching bundle: %s", " ".join(cmd)):
2907 if verbose:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002908 print(f"{self.name}: Downloading bundle: {srcUrl}")
Gavin Makea2e3302023-03-11 06:46:20 +00002909 stdout = None if verbose else subprocess.PIPE
2910 stderr = None if verbose else subprocess.STDOUT
2911 try:
2912 proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
2913 except OSError:
2914 return False
2915
2916 (output, _) = proc.communicate()
2917 curlret = proc.returncode
2918
2919 if curlret == 22:
2920 # From curl man page:
2921 # 22: HTTP page not retrieved. The requested url was not found
2922 # or returned another error with the HTTP error code being 400
2923 # or above. This return code only appears if -f, --fail is used.
2924 if verbose:
2925 print(
2926 "%s: Unable to retrieve clone.bundle; ignoring."
2927 % self.name
2928 )
2929 if output:
2930 print("Curl output:\n%s" % output)
2931 return False
2932 elif curlret and not verbose and output:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002933 logger.error("%s", output)
Gavin Makea2e3302023-03-11 06:46:20 +00002934
2935 if os.path.exists(tmpPath):
2936 if curlret == 0 and self._IsValidBundle(tmpPath, quiet):
2937 platform_utils.rename(tmpPath, dstPath)
2938 return True
2939 else:
2940 platform_utils.remove(tmpPath)
2941 return False
2942 else:
2943 return False
2944
2945 def _IsValidBundle(self, path, quiet):
2946 try:
2947 with open(path, "rb") as f:
2948 if f.read(16) == b"# v2 git bundle\n":
2949 return True
2950 else:
2951 if not quiet:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002952 logger.error("Invalid clone.bundle file; ignoring.")
Gavin Makea2e3302023-03-11 06:46:20 +00002953 return False
2954 except OSError:
2955 return False
2956
Josip Sokcevicedadb252024-02-29 09:48:37 -08002957 def _Checkout(self, rev, force_checkout=False, quiet=False):
Gavin Makea2e3302023-03-11 06:46:20 +00002958 cmd = ["checkout"]
2959 if quiet:
2960 cmd.append("-q")
Josip Sokcevicedadb252024-02-29 09:48:37 -08002961 if force_checkout:
2962 cmd.append("-f")
Gavin Makea2e3302023-03-11 06:46:20 +00002963 cmd.append(rev)
2964 cmd.append("--")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002965 if GitCommand(self, cmd).Wait() != 0:
Gavin Makea2e3302023-03-11 06:46:20 +00002966 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002967 raise GitError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002968 f"{self.name} checkout {rev} ", project=self.name
Jason Chang32b59562023-07-14 16:45:35 -07002969 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002970
Gavin Makea2e3302023-03-11 06:46:20 +00002971 def _CherryPick(self, rev, ffonly=False, record_origin=False):
2972 cmd = ["cherry-pick"]
2973 if ffonly:
2974 cmd.append("--ff")
2975 if record_origin:
2976 cmd.append("-x")
2977 cmd.append(rev)
2978 cmd.append("--")
2979 if GitCommand(self, cmd).Wait() != 0:
2980 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002981 raise GitError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002982 f"{self.name} cherry-pick {rev} ", project=self.name
Jason Chang32b59562023-07-14 16:45:35 -07002983 )
Victor Boivie0960b5b2010-11-26 13:42:13 +01002984
Gavin Makea2e3302023-03-11 06:46:20 +00002985 def _LsRemote(self, refs):
2986 cmd = ["ls-remote", self.remote.name, refs]
2987 p = GitCommand(self, cmd, capture_stdout=True)
2988 if p.Wait() == 0:
2989 return p.stdout
2990 return None
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002991
Gavin Makea2e3302023-03-11 06:46:20 +00002992 def _Revert(self, rev):
2993 cmd = ["revert"]
2994 cmd.append("--no-edit")
2995 cmd.append(rev)
2996 cmd.append("--")
2997 if GitCommand(self, cmd).Wait() != 0:
2998 if self._allrefs:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002999 raise GitError(f"{self.name} revert {rev} ", project=self.name)
Mike Frysinger2a089cf2021-11-13 23:29:42 -05003000
Gavin Makea2e3302023-03-11 06:46:20 +00003001 def _ResetHard(self, rev, quiet=True):
3002 cmd = ["reset", "--hard"]
3003 if quiet:
3004 cmd.append("-q")
3005 cmd.append(rev)
3006 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07003007 raise GitError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003008 f"{self.name} reset --hard {rev} ", project=self.name
Jason Chang32b59562023-07-14 16:45:35 -07003009 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05003010
Gavin Makea2e3302023-03-11 06:46:20 +00003011 def _SyncSubmodules(self, quiet=True):
3012 cmd = ["submodule", "update", "--init", "--recursive"]
3013 if quiet:
3014 cmd.append("-q")
3015 if GitCommand(self, cmd).Wait() != 0:
3016 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07003017 "%s submodule update --init --recursive " % self.name,
3018 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003019 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05003020
Gavin Makea2e3302023-03-11 06:46:20 +00003021 def _Rebase(self, upstream, onto=None):
3022 cmd = ["rebase"]
3023 if onto is not None:
3024 cmd.extend(["--onto", onto])
3025 cmd.append(upstream)
3026 if GitCommand(self, cmd).Wait() != 0:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003027 raise GitError(f"{self.name} rebase {upstream} ", project=self.name)
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05003028
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003029 def _FastForward(self, head, ffonly=False, quiet=True):
Gavin Makea2e3302023-03-11 06:46:20 +00003030 cmd = ["merge", "--no-stat", head]
3031 if ffonly:
3032 cmd.append("--ff-only")
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003033 if quiet:
3034 cmd.append("-q")
Gavin Makea2e3302023-03-11 06:46:20 +00003035 if GitCommand(self, cmd).Wait() != 0:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003036 raise GitError(f"{self.name} merge {head} ", project=self.name)
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05003037
Gavin Makea2e3302023-03-11 06:46:20 +00003038 def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False):
3039 init_git_dir = not os.path.exists(self.gitdir)
3040 init_obj_dir = not os.path.exists(self.objdir)
3041 try:
3042 # Initialize the bare repository, which contains all of the objects.
3043 if init_obj_dir:
3044 os.makedirs(self.objdir)
3045 self.bare_objdir.init()
Mike Frysinger2a089cf2021-11-13 23:29:42 -05003046
Gavin Makea2e3302023-03-11 06:46:20 +00003047 self._UpdateHooks(quiet=quiet)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003048
Gavin Makea2e3302023-03-11 06:46:20 +00003049 if self.use_git_worktrees:
3050 # Enable per-worktree config file support if possible. This
3051 # is more a nice-to-have feature for users rather than a
3052 # hard requirement.
3053 if git_require((2, 20, 0)):
3054 self.EnableRepositoryExtension("worktreeConfig")
Renaud Paquay788e9622017-01-27 11:41:12 -08003055
Gavin Makea2e3302023-03-11 06:46:20 +00003056 # If we have a separate directory to hold refs, initialize it as
3057 # well.
3058 if self.objdir != self.gitdir:
3059 if init_git_dir:
3060 os.makedirs(self.gitdir)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003061
Gavin Makea2e3302023-03-11 06:46:20 +00003062 if init_obj_dir or init_git_dir:
3063 self._ReferenceGitDir(
3064 self.objdir, self.gitdir, copy_all=True
3065 )
3066 try:
3067 self._CheckDirReference(self.objdir, self.gitdir)
3068 except GitError as e:
3069 if force_sync:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00003070 logger.error(
3071 "Retrying clone after deleting %s", self.gitdir
Gavin Makea2e3302023-03-11 06:46:20 +00003072 )
3073 try:
3074 platform_utils.rmtree(
3075 platform_utils.realpath(self.gitdir)
3076 )
3077 if self.worktree and os.path.exists(
3078 platform_utils.realpath(self.worktree)
3079 ):
3080 platform_utils.rmtree(
3081 platform_utils.realpath(self.worktree)
3082 )
3083 return self._InitGitDir(
3084 mirror_git=mirror_git,
3085 force_sync=False,
3086 quiet=quiet,
3087 )
3088 except Exception:
3089 raise e
3090 raise e
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003091
Gavin Makea2e3302023-03-11 06:46:20 +00003092 if init_git_dir:
3093 mp = self.manifest.manifestProject
3094 ref_dir = mp.reference or ""
Julien Camperguedd654222014-01-09 16:21:37 +01003095
Gavin Makea2e3302023-03-11 06:46:20 +00003096 def _expanded_ref_dirs():
3097 """Iterate through possible git reference dir paths."""
3098 name = self.name + ".git"
3099 yield mirror_git or os.path.join(ref_dir, name)
3100 for prefix in "", self.remote.name:
3101 yield os.path.join(
3102 ref_dir, ".repo", "project-objects", prefix, name
3103 )
3104 yield os.path.join(
3105 ref_dir, ".repo", "worktrees", prefix, name
3106 )
3107
3108 if ref_dir or mirror_git:
3109 found_ref_dir = None
3110 for path in _expanded_ref_dirs():
3111 if os.path.exists(path):
3112 found_ref_dir = path
3113 break
3114 ref_dir = found_ref_dir
3115
3116 if ref_dir:
3117 if not os.path.isabs(ref_dir):
3118 # The alternate directory is relative to the object
3119 # database.
3120 ref_dir = os.path.relpath(
3121 ref_dir, os.path.join(self.objdir, "objects")
3122 )
3123 _lwrite(
3124 os.path.join(
3125 self.objdir, "objects/info/alternates"
3126 ),
3127 os.path.join(ref_dir, "objects") + "\n",
3128 )
3129
3130 m = self.manifest.manifestProject.config
3131 for key in ["user.name", "user.email"]:
3132 if m.Has(key, include_defaults=False):
3133 self.config.SetString(key, m.GetString(key))
3134 if not self.manifest.EnableGitLfs:
3135 self.config.SetString(
3136 "filter.lfs.smudge", "git-lfs smudge --skip -- %f"
3137 )
3138 self.config.SetString(
3139 "filter.lfs.process", "git-lfs filter-process --skip"
3140 )
3141 self.config.SetBoolean(
3142 "core.bare", True if self.manifest.IsMirror else None
3143 )
Josip Sokcevic9267d582023-10-19 14:46:11 -07003144
3145 if not init_obj_dir:
3146 # The project might be shared (obj_dir already initialized), but
3147 # such information is not available here. Instead of passing it,
3148 # set it as shared, and rely to be unset down the execution
3149 # path.
3150 if git_require((2, 7, 0)):
3151 self.EnableRepositoryExtension("preciousObjects")
3152 else:
3153 self.config.SetString("gc.pruneExpire", "never")
3154
Gavin Makea2e3302023-03-11 06:46:20 +00003155 except Exception:
3156 if init_obj_dir and os.path.exists(self.objdir):
3157 platform_utils.rmtree(self.objdir)
3158 if init_git_dir and os.path.exists(self.gitdir):
3159 platform_utils.rmtree(self.gitdir)
3160 raise
3161
3162 def _UpdateHooks(self, quiet=False):
3163 if os.path.exists(self.objdir):
3164 self._InitHooks(quiet=quiet)
3165
3166 def _InitHooks(self, quiet=False):
3167 hooks = platform_utils.realpath(os.path.join(self.objdir, "hooks"))
3168 if not os.path.exists(hooks):
3169 os.makedirs(hooks)
3170
3171 # Delete sample hooks. They're noise.
3172 for hook in glob.glob(os.path.join(hooks, "*.sample")):
3173 try:
3174 platform_utils.remove(hook, missing_ok=True)
3175 except PermissionError:
3176 pass
3177
3178 for stock_hook in _ProjectHooks():
3179 name = os.path.basename(stock_hook)
3180
3181 if (
3182 name in ("commit-msg",)
3183 and not self.remote.review
3184 and self is not self.manifest.manifestProject
3185 ):
3186 # Don't install a Gerrit Code Review hook if this
3187 # project does not appear to use it for reviews.
3188 #
3189 # Since the manifest project is one of those, but also
3190 # managed through gerrit, it's excluded.
3191 continue
3192
3193 dst = os.path.join(hooks, name)
3194 if platform_utils.islink(dst):
3195 continue
3196 if os.path.exists(dst):
3197 # If the files are the same, we'll leave it alone. We create
3198 # symlinks below by default but fallback to hardlinks if the OS
3199 # blocks them. So if we're here, it's probably because we made a
3200 # hardlink below.
3201 if not filecmp.cmp(stock_hook, dst, shallow=False):
3202 if not quiet:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00003203 logger.warning(
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00003204 "warn: %s: Not replacing locally modified %s hook",
Gavin Makea2e3302023-03-11 06:46:20 +00003205 self.RelPath(local=False),
3206 name,
3207 )
3208 continue
3209 try:
3210 platform_utils.symlink(
3211 os.path.relpath(stock_hook, os.path.dirname(dst)), dst
3212 )
3213 except OSError as e:
3214 if e.errno == errno.EPERM:
3215 try:
3216 os.link(stock_hook, dst)
3217 except OSError:
Jason Chang32b59562023-07-14 16:45:35 -07003218 raise GitError(
3219 self._get_symlink_error_message(), project=self.name
3220 )
Gavin Makea2e3302023-03-11 06:46:20 +00003221 else:
3222 raise
3223
3224 def _InitRemote(self):
3225 if self.remote.url:
3226 remote = self.GetRemote()
3227 remote.url = self.remote.url
3228 remote.pushUrl = self.remote.pushUrl
3229 remote.review = self.remote.review
3230 remote.projectname = self.name
3231
3232 if self.worktree:
3233 remote.ResetFetch(mirror=False)
3234 else:
3235 remote.ResetFetch(mirror=True)
3236 remote.Save()
3237
3238 def _InitMRef(self):
3239 """Initialize the pseudo m/<manifest branch> ref."""
3240 if self.manifest.branch:
3241 if self.use_git_worktrees:
3242 # Set up the m/ space to point to the worktree-specific ref
3243 # space. We'll update the worktree-specific ref space on each
3244 # checkout.
3245 ref = R_M + self.manifest.branch
3246 if not self.bare_ref.symref(ref):
3247 self.bare_git.symbolic_ref(
3248 "-m",
3249 "redirecting to worktree scope",
3250 ref,
3251 R_WORKTREE_M + self.manifest.branch,
3252 )
3253
3254 # We can't update this ref with git worktrees until it exists.
3255 # We'll wait until the initial checkout to set it.
3256 if not os.path.exists(self.worktree):
3257 return
3258
3259 base = R_WORKTREE_M
3260 active_git = self.work_git
3261
3262 self._InitAnyMRef(HEAD, self.bare_git, detach=True)
3263 else:
3264 base = R_M
3265 active_git = self.bare_git
3266
3267 self._InitAnyMRef(base + self.manifest.branch, active_git)
3268
3269 def _InitMirrorHead(self):
3270 self._InitAnyMRef(HEAD, self.bare_git)
3271
3272 def _InitAnyMRef(self, ref, active_git, detach=False):
3273 """Initialize |ref| in |active_git| to the value in the manifest.
3274
3275 This points |ref| to the <project> setting in the manifest.
3276
3277 Args:
3278 ref: The branch to update.
3279 active_git: The git repository to make updates in.
3280 detach: Whether to update target of symbolic refs, or overwrite the
3281 ref directly (and thus make it non-symbolic).
3282 """
3283 cur = self.bare_ref.symref(ref)
3284
3285 if self.revisionId:
3286 if cur != "" or self.bare_ref.get(ref) != self.revisionId:
3287 msg = "manifest set to %s" % self.revisionId
3288 dst = self.revisionId + "^0"
3289 active_git.UpdateRef(ref, dst, message=msg, detach=True)
Julien Camperguedd654222014-01-09 16:21:37 +01003290 else:
Gavin Makea2e3302023-03-11 06:46:20 +00003291 remote = self.GetRemote()
3292 dst = remote.ToLocal(self.revisionExpr)
3293 if cur != dst:
3294 msg = "manifest set to %s" % self.revisionExpr
3295 if detach:
3296 active_git.UpdateRef(ref, dst, message=msg, detach=True)
3297 else:
3298 active_git.symbolic_ref("-m", msg, ref, dst)
Julien Camperguedd654222014-01-09 16:21:37 +01003299
Gavin Makea2e3302023-03-11 06:46:20 +00003300 def _CheckDirReference(self, srcdir, destdir):
3301 # Git worktrees don't use symlinks to share at all.
3302 if self.use_git_worktrees:
3303 return
Julien Camperguedd654222014-01-09 16:21:37 +01003304
Gavin Makea2e3302023-03-11 06:46:20 +00003305 for name in self.shareable_dirs:
3306 # Try to self-heal a bit in simple cases.
3307 dst_path = os.path.join(destdir, name)
3308 src_path = os.path.join(srcdir, name)
Julien Camperguedd654222014-01-09 16:21:37 +01003309
Gavin Makea2e3302023-03-11 06:46:20 +00003310 dst = platform_utils.realpath(dst_path)
3311 if os.path.lexists(dst):
3312 src = platform_utils.realpath(src_path)
3313 # Fail if the links are pointing to the wrong place.
3314 if src != dst:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00003315 logger.error(
3316 "error: %s is different in %s vs %s",
3317 name,
3318 destdir,
3319 srcdir,
3320 )
Gavin Makea2e3302023-03-11 06:46:20 +00003321 raise GitError(
3322 "--force-sync not enabled; cannot overwrite a local "
3323 "work tree. If you're comfortable with the "
3324 "possibility of losing the work tree's git metadata,"
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003325 " use "
3326 f"`repo sync --force-sync {self.RelPath(local=False)}` "
3327 "to proceed.",
Jason Chang32b59562023-07-14 16:45:35 -07003328 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003329 )
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003330
Gavin Makea2e3302023-03-11 06:46:20 +00003331 def _ReferenceGitDir(self, gitdir, dotgit, copy_all):
3332 """Update |dotgit| to reference |gitdir|, using symlinks where possible.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003333
Gavin Makea2e3302023-03-11 06:46:20 +00003334 Args:
3335 gitdir: The bare git repository. Must already be initialized.
3336 dotgit: The repository you would like to initialize.
3337 copy_all: If true, copy all remaining files from |gitdir| ->
3338 |dotgit|. This saves you the effort of initializing |dotgit|
3339 yourself.
3340 """
3341 symlink_dirs = self.shareable_dirs[:]
3342 to_symlink = symlink_dirs
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003343
Gavin Makea2e3302023-03-11 06:46:20 +00003344 to_copy = []
3345 if copy_all:
3346 to_copy = platform_utils.listdir(gitdir)
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003347
Gavin Makea2e3302023-03-11 06:46:20 +00003348 dotgit = platform_utils.realpath(dotgit)
3349 for name in set(to_copy).union(to_symlink):
3350 try:
3351 src = platform_utils.realpath(os.path.join(gitdir, name))
3352 dst = os.path.join(dotgit, name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003353
Gavin Makea2e3302023-03-11 06:46:20 +00003354 if os.path.lexists(dst):
3355 continue
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003356
Gavin Makea2e3302023-03-11 06:46:20 +00003357 # If the source dir doesn't exist, create an empty dir.
3358 if name in symlink_dirs and not os.path.lexists(src):
3359 os.makedirs(src)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003360
Gavin Makea2e3302023-03-11 06:46:20 +00003361 if name in to_symlink:
3362 platform_utils.symlink(
3363 os.path.relpath(src, os.path.dirname(dst)), dst
3364 )
3365 elif copy_all and not platform_utils.islink(dst):
3366 if platform_utils.isdir(src):
3367 shutil.copytree(src, dst)
3368 elif os.path.isfile(src):
3369 shutil.copy(src, dst)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003370
Gavin Makea2e3302023-03-11 06:46:20 +00003371 except OSError as e:
3372 if e.errno == errno.EPERM:
3373 raise DownloadError(self._get_symlink_error_message())
3374 else:
3375 raise
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003376
Gavin Makea2e3302023-03-11 06:46:20 +00003377 def _InitGitWorktree(self):
3378 """Init the project using git worktrees."""
3379 self.bare_git.worktree("prune")
3380 self.bare_git.worktree(
3381 "add",
3382 "-ff",
3383 "--checkout",
3384 "--detach",
3385 "--lock",
3386 self.worktree,
3387 self.GetRevisionId(),
3388 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003389
Gavin Makea2e3302023-03-11 06:46:20 +00003390 # Rewrite the internal state files to use relative paths between the
3391 # checkouts & worktrees.
3392 dotgit = os.path.join(self.worktree, ".git")
Jason R. Coombs034950b2023-10-20 23:32:02 +05453393 with open(dotgit) as fp:
Gavin Makea2e3302023-03-11 06:46:20 +00003394 # Figure out the checkout->worktree path.
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003395 setting = fp.read()
Gavin Makea2e3302023-03-11 06:46:20 +00003396 assert setting.startswith("gitdir:")
3397 git_worktree_path = setting.split(":", 1)[1].strip()
3398 # Some platforms (e.g. Windows) won't let us update dotgit in situ
3399 # because of file permissions. Delete it and recreate it from scratch
3400 # to avoid.
3401 platform_utils.remove(dotgit)
3402 # Use relative path from checkout->worktree & maintain Unix line endings
3403 # on all OS's to match git behavior.
3404 with open(dotgit, "w", newline="\n") as fp:
3405 print(
3406 "gitdir:",
3407 os.path.relpath(git_worktree_path, self.worktree),
3408 file=fp,
3409 )
3410 # Use relative path from worktree->checkout & maintain Unix line endings
3411 # on all OS's to match git behavior.
3412 with open(
3413 os.path.join(git_worktree_path, "gitdir"), "w", newline="\n"
3414 ) as fp:
3415 print(os.path.relpath(dotgit, git_worktree_path), file=fp)
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003416
Gavin Makea2e3302023-03-11 06:46:20 +00003417 self._InitMRef()
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003418
Gavin Makea2e3302023-03-11 06:46:20 +00003419 def _InitWorkTree(self, force_sync=False, submodules=False):
3420 """Setup the worktree .git path.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003421
Gavin Makea2e3302023-03-11 06:46:20 +00003422 This is the user-visible path like src/foo/.git/.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003423
Gavin Makea2e3302023-03-11 06:46:20 +00003424 With non-git-worktrees, this will be a symlink to the .repo/projects/
3425 path. With git-worktrees, this will be a .git file using "gitdir: ..."
3426 syntax.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003427
Gavin Makea2e3302023-03-11 06:46:20 +00003428 Older checkouts had .git/ directories. If we see that, migrate it.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003429
Gavin Makea2e3302023-03-11 06:46:20 +00003430 This also handles changes in the manifest. Maybe this project was
3431 backed by "foo/bar" on the server, but now it's "new/foo/bar". We have
3432 to update the path we point to under .repo/projects/ to match.
3433 """
3434 dotgit = os.path.join(self.worktree, ".git")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003435
Gavin Makea2e3302023-03-11 06:46:20 +00003436 # If using an old layout style (a directory), migrate it.
3437 if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
Jason Chang32b59562023-07-14 16:45:35 -07003438 self._MigrateOldWorkTreeGitDir(dotgit, project=self.name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003439
Gavin Makea2e3302023-03-11 06:46:20 +00003440 init_dotgit = not os.path.exists(dotgit)
3441 if self.use_git_worktrees:
3442 if init_dotgit:
3443 self._InitGitWorktree()
3444 self._CopyAndLinkFiles()
3445 else:
3446 if not init_dotgit:
3447 # See if the project has changed.
3448 if platform_utils.realpath(
3449 self.gitdir
3450 ) != platform_utils.realpath(dotgit):
3451 platform_utils.remove(dotgit)
Doug Anderson37282b42011-03-04 11:54:18 -08003452
Gavin Makea2e3302023-03-11 06:46:20 +00003453 if init_dotgit or not os.path.exists(dotgit):
3454 os.makedirs(self.worktree, exist_ok=True)
3455 platform_utils.symlink(
3456 os.path.relpath(self.gitdir, self.worktree), dotgit
3457 )
Doug Anderson37282b42011-03-04 11:54:18 -08003458
Gavin Makea2e3302023-03-11 06:46:20 +00003459 if init_dotgit:
3460 _lwrite(
3461 os.path.join(dotgit, HEAD), "%s\n" % self.GetRevisionId()
3462 )
Doug Anderson37282b42011-03-04 11:54:18 -08003463
Gavin Makea2e3302023-03-11 06:46:20 +00003464 # Finish checking out the worktree.
3465 cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
3466 if GitCommand(self, cmd).Wait() != 0:
3467 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07003468 "Cannot initialize work tree for " + self.name,
3469 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003470 )
Doug Anderson37282b42011-03-04 11:54:18 -08003471
Gavin Makea2e3302023-03-11 06:46:20 +00003472 if submodules:
3473 self._SyncSubmodules(quiet=True)
3474 self._CopyAndLinkFiles()
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003475
Gavin Makea2e3302023-03-11 06:46:20 +00003476 @classmethod
Jason Chang32b59562023-07-14 16:45:35 -07003477 def _MigrateOldWorkTreeGitDir(cls, dotgit, project=None):
Gavin Makea2e3302023-03-11 06:46:20 +00003478 """Migrate the old worktree .git/ dir style to a symlink.
3479
3480 This logic specifically only uses state from |dotgit| to figure out
3481 where to move content and not |self|. This way if the backing project
3482 also changed places, we only do the .git/ dir to .git symlink migration
3483 here. The path updates will happen independently.
3484 """
3485 # Figure out where in .repo/projects/ it's pointing to.
3486 if not os.path.islink(os.path.join(dotgit, "refs")):
Jason Chang32b59562023-07-14 16:45:35 -07003487 raise GitError(
3488 f"{dotgit}: unsupported checkout state", project=project
3489 )
Gavin Makea2e3302023-03-11 06:46:20 +00003490 gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, "refs")))
3491
3492 # Remove known symlink paths that exist in .repo/projects/.
3493 KNOWN_LINKS = {
3494 "config",
3495 "description",
3496 "hooks",
3497 "info",
3498 "logs",
3499 "objects",
3500 "packed-refs",
3501 "refs",
3502 "rr-cache",
3503 "shallow",
3504 "svn",
3505 }
3506 # Paths that we know will be in both, but are safe to clobber in
3507 # .repo/projects/.
3508 SAFE_TO_CLOBBER = {
3509 "COMMIT_EDITMSG",
3510 "FETCH_HEAD",
3511 "HEAD",
3512 "gc.log",
3513 "gitk.cache",
3514 "index",
3515 "ORIG_HEAD",
3516 }
3517
3518 # First see if we'd succeed before starting the migration.
3519 unknown_paths = []
3520 for name in platform_utils.listdir(dotgit):
3521 # Ignore all temporary/backup names. These are common with vim &
3522 # emacs.
3523 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3524 continue
3525
3526 dotgit_path = os.path.join(dotgit, name)
3527 if name in KNOWN_LINKS:
3528 if not platform_utils.islink(dotgit_path):
3529 unknown_paths.append(f"{dotgit_path}: should be a symlink")
3530 else:
3531 gitdir_path = os.path.join(gitdir, name)
3532 if name not in SAFE_TO_CLOBBER and os.path.exists(gitdir_path):
3533 unknown_paths.append(
3534 f"{dotgit_path}: unknown file; please file a bug"
3535 )
3536 if unknown_paths:
Jason Chang32b59562023-07-14 16:45:35 -07003537 raise GitError(
3538 "Aborting migration: " + "\n".join(unknown_paths),
3539 project=project,
3540 )
Gavin Makea2e3302023-03-11 06:46:20 +00003541
3542 # Now walk the paths and sync the .git/ to .repo/projects/.
3543 for name in platform_utils.listdir(dotgit):
3544 dotgit_path = os.path.join(dotgit, name)
3545
3546 # Ignore all temporary/backup names. These are common with vim &
3547 # emacs.
3548 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3549 platform_utils.remove(dotgit_path)
3550 elif name in KNOWN_LINKS:
3551 platform_utils.remove(dotgit_path)
3552 else:
3553 gitdir_path = os.path.join(gitdir, name)
3554 platform_utils.remove(gitdir_path, missing_ok=True)
3555 platform_utils.rename(dotgit_path, gitdir_path)
3556
3557 # Now that the dir should be empty, clear it out, and symlink it over.
3558 platform_utils.rmdir(dotgit)
3559 platform_utils.symlink(
Jason R. Coombs47944bb2023-09-29 12:42:22 -04003560 os.path.relpath(gitdir, os.path.dirname(os.path.realpath(dotgit))),
3561 dotgit,
Gavin Makea2e3302023-03-11 06:46:20 +00003562 )
3563
3564 def _get_symlink_error_message(self):
3565 if platform_utils.isWindows():
3566 return (
3567 "Unable to create symbolic link. Please re-run the command as "
3568 "Administrator, or see "
3569 "https://github.com/git-for-windows/git/wiki/Symbolic-Links "
3570 "for other options."
3571 )
3572 return "filesystem must support symlinks"
3573
3574 def _revlist(self, *args, **kw):
3575 a = []
3576 a.extend(args)
3577 a.append("--")
3578 return self.work_git.rev_list(*a, **kw)
3579
3580 @property
3581 def _allrefs(self):
3582 return self.bare_ref.all
3583
3584 def _getLogs(
3585 self, rev1, rev2, oneline=False, color=True, pretty_format=None
3586 ):
3587 """Get logs between two revisions of this project."""
3588 comp = ".."
3589 if rev1:
3590 revs = [rev1]
3591 if rev2:
3592 revs.extend([comp, rev2])
3593 cmd = ["log", "".join(revs)]
3594 out = DiffColoring(self.config)
3595 if out.is_on and color:
3596 cmd.append("--color")
3597 if pretty_format is not None:
3598 cmd.append("--pretty=format:%s" % pretty_format)
3599 if oneline:
3600 cmd.append("--oneline")
3601
3602 try:
3603 log = GitCommand(
3604 self, cmd, capture_stdout=True, capture_stderr=True
3605 )
3606 if log.Wait() == 0:
3607 return log.stdout
3608 except GitError:
3609 # worktree may not exist if groups changed for example. In that
3610 # case, try in gitdir instead.
3611 if not os.path.exists(self.worktree):
3612 return self.bare_git.log(*cmd[1:])
3613 else:
3614 raise
3615 return None
3616
3617 def getAddedAndRemovedLogs(
3618 self, toProject, oneline=False, color=True, pretty_format=None
3619 ):
3620 """Get the list of logs from this revision to given revisionId"""
3621 logs = {}
3622 selfId = self.GetRevisionId(self._allrefs)
3623 toId = toProject.GetRevisionId(toProject._allrefs)
3624
3625 logs["added"] = self._getLogs(
3626 selfId,
3627 toId,
3628 oneline=oneline,
3629 color=color,
3630 pretty_format=pretty_format,
3631 )
3632 logs["removed"] = self._getLogs(
3633 toId,
3634 selfId,
3635 oneline=oneline,
3636 color=color,
3637 pretty_format=pretty_format,
3638 )
3639 return logs
3640
Mike Frysingerd4aee652023-10-19 05:13:32 -04003641 class _GitGetByExec:
Gavin Makea2e3302023-03-11 06:46:20 +00003642 def __init__(self, project, bare, gitdir):
3643 self._project = project
3644 self._bare = bare
3645 self._gitdir = gitdir
3646
3647 # __getstate__ and __setstate__ are required for pickling because
3648 # __getattr__ exists.
3649 def __getstate__(self):
3650 return (self._project, self._bare, self._gitdir)
3651
3652 def __setstate__(self, state):
3653 self._project, self._bare, self._gitdir = state
3654
3655 def LsOthers(self):
3656 p = GitCommand(
3657 self._project,
3658 ["ls-files", "-z", "--others", "--exclude-standard"],
3659 bare=False,
3660 gitdir=self._gitdir,
3661 capture_stdout=True,
3662 capture_stderr=True,
3663 )
3664 if p.Wait() == 0:
3665 out = p.stdout
3666 if out:
3667 # Backslash is not anomalous.
3668 return out[:-1].split("\0")
3669 return []
3670
3671 def DiffZ(self, name, *args):
3672 cmd = [name]
3673 cmd.append("-z")
3674 cmd.append("--ignore-submodules")
3675 cmd.extend(args)
3676 p = GitCommand(
3677 self._project,
3678 cmd,
3679 gitdir=self._gitdir,
3680 bare=False,
3681 capture_stdout=True,
3682 capture_stderr=True,
3683 )
3684 p.Wait()
3685 r = {}
3686 out = p.stdout
3687 if out:
3688 out = iter(out[:-1].split("\0"))
3689 while out:
3690 try:
3691 info = next(out)
3692 path = next(out)
3693 except StopIteration:
3694 break
3695
Mike Frysingerd4aee652023-10-19 05:13:32 -04003696 class _Info:
Gavin Makea2e3302023-03-11 06:46:20 +00003697 def __init__(self, path, omode, nmode, oid, nid, state):
3698 self.path = path
3699 self.src_path = None
3700 self.old_mode = omode
3701 self.new_mode = nmode
3702 self.old_id = oid
3703 self.new_id = nid
3704
3705 if len(state) == 1:
3706 self.status = state
3707 self.level = None
3708 else:
3709 self.status = state[:1]
3710 self.level = state[1:]
3711 while self.level.startswith("0"):
3712 self.level = self.level[1:]
3713
3714 info = info[1:].split(" ")
3715 info = _Info(path, *info)
3716 if info.status in ("R", "C"):
3717 info.src_path = info.path
3718 info.path = next(out)
3719 r[info.path] = info
3720 return r
3721
3722 def GetDotgitPath(self, subpath=None):
3723 """Return the full path to the .git dir.
3724
3725 As a convenience, append |subpath| if provided.
3726 """
3727 if self._bare:
3728 dotgit = self._gitdir
3729 else:
3730 dotgit = os.path.join(self._project.worktree, ".git")
3731 if os.path.isfile(dotgit):
3732 # Git worktrees use a "gitdir:" syntax to point to the
3733 # scratch space.
3734 with open(dotgit) as fp:
3735 setting = fp.read()
3736 assert setting.startswith("gitdir:")
3737 gitdir = setting.split(":", 1)[1].strip()
3738 dotgit = os.path.normpath(
3739 os.path.join(self._project.worktree, gitdir)
3740 )
3741
3742 return dotgit if subpath is None else os.path.join(dotgit, subpath)
3743
3744 def GetHead(self):
3745 """Return the ref that HEAD points to."""
3746 path = self.GetDotgitPath(subpath=HEAD)
3747 try:
3748 with open(path) as fd:
3749 line = fd.readline()
Jason R. Coombsae824fb2023-10-20 23:32:40 +05453750 except OSError as e:
Gavin Makea2e3302023-03-11 06:46:20 +00003751 raise NoManifestException(path, str(e))
3752 try:
3753 line = line.decode()
3754 except AttributeError:
3755 pass
3756 if line.startswith("ref: "):
3757 return line[5:-1]
3758 return line[:-1]
3759
3760 def SetHead(self, ref, message=None):
3761 cmdv = []
3762 if message is not None:
3763 cmdv.extend(["-m", message])
3764 cmdv.append(HEAD)
3765 cmdv.append(ref)
3766 self.symbolic_ref(*cmdv)
3767
3768 def DetachHead(self, new, message=None):
3769 cmdv = ["--no-deref"]
3770 if message is not None:
3771 cmdv.extend(["-m", message])
3772 cmdv.append(HEAD)
3773 cmdv.append(new)
3774 self.update_ref(*cmdv)
3775
3776 def UpdateRef(self, name, new, old=None, message=None, detach=False):
3777 cmdv = []
3778 if message is not None:
3779 cmdv.extend(["-m", message])
3780 if detach:
3781 cmdv.append("--no-deref")
3782 cmdv.append(name)
3783 cmdv.append(new)
3784 if old is not None:
3785 cmdv.append(old)
3786 self.update_ref(*cmdv)
3787
3788 def DeleteRef(self, name, old=None):
3789 if not old:
3790 old = self.rev_parse(name)
3791 self.update_ref("-d", name, old)
3792 self._project.bare_ref.deleted(name)
3793
Jason Chang87058c62023-09-27 11:34:43 -07003794 def rev_list(self, *args, log_as_error=True, **kw):
Gavin Makea2e3302023-03-11 06:46:20 +00003795 if "format" in kw:
3796 cmdv = ["log", "--pretty=format:%s" % kw["format"]]
3797 else:
3798 cmdv = ["rev-list"]
3799 cmdv.extend(args)
3800 p = GitCommand(
3801 self._project,
3802 cmdv,
3803 bare=self._bare,
3804 gitdir=self._gitdir,
3805 capture_stdout=True,
3806 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003807 verify_command=True,
Jason Chang87058c62023-09-27 11:34:43 -07003808 log_as_error=log_as_error,
Gavin Makea2e3302023-03-11 06:46:20 +00003809 )
Jason Chang32b59562023-07-14 16:45:35 -07003810 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003811 return p.stdout.splitlines()
3812
3813 def __getattr__(self, name):
3814 """Allow arbitrary git commands using pythonic syntax.
3815
3816 This allows you to do things like:
3817 git_obj.rev_parse('HEAD')
3818
3819 Since we don't have a 'rev_parse' method defined, the __getattr__
3820 will run. We'll replace the '_' with a '-' and try to run a git
3821 command. Any other positional arguments will be passed to the git
3822 command, and the following keyword arguments are supported:
3823 config: An optional dict of git config options to be passed with
3824 '-c'.
3825
3826 Args:
3827 name: The name of the git command to call. Any '_' characters
3828 will be replaced with '-'.
3829
3830 Returns:
3831 A callable object that will try to call git with the named
3832 command.
3833 """
3834 name = name.replace("_", "-")
3835
Jason Chang87058c62023-09-27 11:34:43 -07003836 def runner(*args, log_as_error=True, **kwargs):
Gavin Makea2e3302023-03-11 06:46:20 +00003837 cmdv = []
3838 config = kwargs.pop("config", None)
3839 for k in kwargs:
3840 raise TypeError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003841 f"{name}() got an unexpected keyword argument {k!r}"
Gavin Makea2e3302023-03-11 06:46:20 +00003842 )
3843 if config is not None:
3844 for k, v in config.items():
3845 cmdv.append("-c")
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04003846 cmdv.append(f"{k}={v}")
Gavin Makea2e3302023-03-11 06:46:20 +00003847 cmdv.append(name)
3848 cmdv.extend(args)
3849 p = GitCommand(
3850 self._project,
3851 cmdv,
3852 bare=self._bare,
3853 gitdir=self._gitdir,
3854 capture_stdout=True,
3855 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003856 verify_command=True,
Jason Chang87058c62023-09-27 11:34:43 -07003857 log_as_error=log_as_error,
Gavin Makea2e3302023-03-11 06:46:20 +00003858 )
Jason Chang32b59562023-07-14 16:45:35 -07003859 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003860 r = p.stdout
3861 if r.endswith("\n") and r.index("\n") == len(r) - 1:
3862 return r[:-1]
3863 return r
3864
3865 return runner
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003866
3867
Jason Chang32b59562023-07-14 16:45:35 -07003868class LocalSyncFail(RepoError):
3869 """Default error when there is an Sync_LocalHalf error."""
3870
3871
3872class _PriorSyncFailedError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003873 def __str__(self):
3874 return "prior sync failed; rebase still in progress"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003875
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003876
Jason Chang32b59562023-07-14 16:45:35 -07003877class _DirtyError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003878 def __str__(self):
3879 return "contains uncommitted changes"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003880
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003881
Mike Frysingerd4aee652023-10-19 05:13:32 -04003882class _InfoMessage:
Gavin Makea2e3302023-03-11 06:46:20 +00003883 def __init__(self, project, text):
3884 self.project = project
3885 self.text = text
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003886
Gavin Makea2e3302023-03-11 06:46:20 +00003887 def Print(self, syncbuf):
3888 syncbuf.out.info(
3889 "%s/: %s", self.project.RelPath(local=False), self.text
3890 )
3891 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003892
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003893
Mike Frysingerd4aee652023-10-19 05:13:32 -04003894class _Failure:
Gavin Makea2e3302023-03-11 06:46:20 +00003895 def __init__(self, project, why):
3896 self.project = project
3897 self.why = why
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003898
Gavin Makea2e3302023-03-11 06:46:20 +00003899 def Print(self, syncbuf):
3900 syncbuf.out.fail(
3901 "error: %s/: %s", self.project.RelPath(local=False), str(self.why)
3902 )
3903 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003904
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003905
Mike Frysingerd4aee652023-10-19 05:13:32 -04003906class _Later:
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003907 def __init__(self, project, action, quiet):
Gavin Makea2e3302023-03-11 06:46:20 +00003908 self.project = project
3909 self.action = action
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003910 self.quiet = quiet
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003911
Gavin Makea2e3302023-03-11 06:46:20 +00003912 def Run(self, syncbuf):
3913 out = syncbuf.out
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003914 if not self.quiet:
3915 out.project("project %s/", self.project.RelPath(local=False))
3916 out.nl()
Gavin Makea2e3302023-03-11 06:46:20 +00003917 try:
3918 self.action()
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003919 if not self.quiet:
3920 out.nl()
Gavin Makea2e3302023-03-11 06:46:20 +00003921 return True
3922 except GitError:
3923 out.nl()
3924 return False
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003925
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003926
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003927class _SyncColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +00003928 def __init__(self, config):
3929 super().__init__(config, "reposync")
3930 self.project = self.printer("header", attr="bold")
3931 self.info = self.printer("info")
3932 self.fail = self.printer("fail", fg="red")
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003933
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003934
Mike Frysingerd4aee652023-10-19 05:13:32 -04003935class SyncBuffer:
Gavin Makea2e3302023-03-11 06:46:20 +00003936 def __init__(self, config, detach_head=False):
3937 self._messages = []
3938 self._failures = []
3939 self._later_queue1 = []
3940 self._later_queue2 = []
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003941
Gavin Makea2e3302023-03-11 06:46:20 +00003942 self.out = _SyncColoring(config)
3943 self.out.redirect(sys.stderr)
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003944
Gavin Makea2e3302023-03-11 06:46:20 +00003945 self.detach_head = detach_head
3946 self.clean = True
3947 self.recent_clean = True
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003948
Gavin Makea2e3302023-03-11 06:46:20 +00003949 def info(self, project, fmt, *args):
3950 self._messages.append(_InfoMessage(project, fmt % args))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003951
Gavin Makea2e3302023-03-11 06:46:20 +00003952 def fail(self, project, err=None):
3953 self._failures.append(_Failure(project, err))
David Rileye0684ad2017-04-05 00:02:59 -07003954 self._MarkUnclean()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003955
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003956 def later1(self, project, what, quiet):
3957 self._later_queue1.append(_Later(project, what, quiet))
Mike Frysinger70d861f2019-08-26 15:22:36 -04003958
Tomasz Wasilczyk208f3442024-01-05 12:23:10 -08003959 def later2(self, project, what, quiet):
3960 self._later_queue2.append(_Later(project, what, quiet))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003961
Gavin Makea2e3302023-03-11 06:46:20 +00003962 def Finish(self):
3963 self._PrintMessages()
3964 self._RunLater()
3965 self._PrintMessages()
3966 return self.clean
3967
3968 def Recently(self):
3969 recent_clean = self.recent_clean
3970 self.recent_clean = True
3971 return recent_clean
3972
3973 def _MarkUnclean(self):
3974 self.clean = False
3975 self.recent_clean = False
3976
3977 def _RunLater(self):
3978 for q in ["_later_queue1", "_later_queue2"]:
3979 if not self._RunQueue(q):
3980 return
3981
3982 def _RunQueue(self, queue):
3983 for m in getattr(self, queue):
3984 if not m.Run(self):
3985 self._MarkUnclean()
3986 return False
3987 setattr(self, queue, [])
3988 return True
3989
3990 def _PrintMessages(self):
3991 if self._messages or self._failures:
3992 if os.isatty(2):
3993 self.out.write(progress.CSI_ERASE_LINE)
3994 self.out.write("\r")
3995
3996 for m in self._messages:
3997 m.Print(self)
3998 for m in self._failures:
3999 m.Print(self)
4000
4001 self._messages = []
4002 self._failures = []
Shawn O. Pearce350cde42009-04-16 11:21:18 -07004003
4004
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07004005class MetaProject(Project):
Gavin Makea2e3302023-03-11 06:46:20 +00004006 """A special project housed under .repo."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07004007
Gavin Makea2e3302023-03-11 06:46:20 +00004008 def __init__(self, manifest, name, gitdir, worktree):
4009 Project.__init__(
4010 self,
4011 manifest=manifest,
4012 name=name,
4013 gitdir=gitdir,
4014 objdir=gitdir,
4015 worktree=worktree,
4016 remote=RemoteSpec("origin"),
4017 relpath=".repo/%s" % name,
4018 revisionExpr="refs/heads/master",
4019 revisionId=None,
4020 groups=None,
4021 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07004022
Gavin Makea2e3302023-03-11 06:46:20 +00004023 def PreSync(self):
4024 if self.Exists:
4025 cb = self.CurrentBranch
4026 if cb:
4027 base = self.GetBranch(cb).merge
4028 if base:
4029 self.revisionExpr = base
4030 self.revisionId = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07004031
Gavin Makea2e3302023-03-11 06:46:20 +00004032 @property
4033 def HasChanges(self):
4034 """Has the remote received new commits not yet checked out?"""
4035 if not self.remote or not self.revisionExpr:
4036 return False
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07004037
Gavin Makea2e3302023-03-11 06:46:20 +00004038 all_refs = self.bare_ref.all
4039 revid = self.GetRevisionId(all_refs)
4040 head = self.work_git.GetHead()
4041 if head.startswith(R_HEADS):
4042 try:
4043 head = all_refs[head]
4044 except KeyError:
4045 head = None
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07004046
Gavin Makea2e3302023-03-11 06:46:20 +00004047 if revid == head:
4048 return False
4049 elif self._revlist(not_rev(HEAD), revid):
4050 return True
4051 return False
LaMont Jones9b72cf22022-03-29 21:54:22 +00004052
4053
4054class RepoProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00004055 """The MetaProject for repo itself."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00004056
Gavin Makea2e3302023-03-11 06:46:20 +00004057 @property
4058 def LastFetch(self):
4059 try:
4060 fh = os.path.join(self.gitdir, "FETCH_HEAD")
4061 return os.path.getmtime(fh)
4062 except OSError:
4063 return 0
LaMont Jones9b72cf22022-03-29 21:54:22 +00004064
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +01004065
LaMont Jones9b72cf22022-03-29 21:54:22 +00004066class ManifestProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00004067 """The MetaProject for manifests."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00004068
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08004069 def MetaBranchSwitch(self, submodules=False, verbose=False):
Gavin Makea2e3302023-03-11 06:46:20 +00004070 """Prepare for manifest branch switch."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00004071
Gavin Makea2e3302023-03-11 06:46:20 +00004072 # detach and delete manifest branch, allowing a new
4073 # branch to take over
4074 syncbuf = SyncBuffer(self.config, detach_head=True)
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08004075 self.Sync_LocalHalf(syncbuf, submodules=submodules, verbose=verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00004076 syncbuf.Finish()
LaMont Jones9b72cf22022-03-29 21:54:22 +00004077
Gavin Makea2e3302023-03-11 06:46:20 +00004078 return (
4079 GitCommand(
4080 self,
4081 ["update-ref", "-d", "refs/heads/default"],
4082 capture_stdout=True,
4083 capture_stderr=True,
4084 ).Wait()
4085 == 0
4086 )
LaMont Jones9b03f152022-03-29 23:01:18 +00004087
Gavin Makea2e3302023-03-11 06:46:20 +00004088 @property
4089 def standalone_manifest_url(self):
4090 """The URL of the standalone manifest, or None."""
4091 return self.config.GetString("manifest.standalone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004092
Gavin Makea2e3302023-03-11 06:46:20 +00004093 @property
4094 def manifest_groups(self):
4095 """The manifest groups string."""
4096 return self.config.GetString("manifest.groups")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004097
Gavin Makea2e3302023-03-11 06:46:20 +00004098 @property
4099 def reference(self):
4100 """The --reference for this manifest."""
4101 return self.config.GetString("repo.reference")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004102
Gavin Makea2e3302023-03-11 06:46:20 +00004103 @property
4104 def dissociate(self):
4105 """Whether to dissociate."""
4106 return self.config.GetBoolean("repo.dissociate")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004107
Gavin Makea2e3302023-03-11 06:46:20 +00004108 @property
4109 def archive(self):
4110 """Whether we use archive."""
4111 return self.config.GetBoolean("repo.archive")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004112
Gavin Makea2e3302023-03-11 06:46:20 +00004113 @property
4114 def mirror(self):
4115 """Whether we use mirror."""
4116 return self.config.GetBoolean("repo.mirror")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004117
Gavin Makea2e3302023-03-11 06:46:20 +00004118 @property
4119 def use_worktree(self):
4120 """Whether we use worktree."""
4121 return self.config.GetBoolean("repo.worktree")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004122
Gavin Makea2e3302023-03-11 06:46:20 +00004123 @property
4124 def clone_bundle(self):
4125 """Whether we use clone_bundle."""
4126 return self.config.GetBoolean("repo.clonebundle")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004127
Gavin Makea2e3302023-03-11 06:46:20 +00004128 @property
4129 def submodules(self):
4130 """Whether we use submodules."""
4131 return self.config.GetBoolean("repo.submodules")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004132
Gavin Makea2e3302023-03-11 06:46:20 +00004133 @property
4134 def git_lfs(self):
4135 """Whether we use git_lfs."""
4136 return self.config.GetBoolean("repo.git-lfs")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004137
Gavin Makea2e3302023-03-11 06:46:20 +00004138 @property
4139 def use_superproject(self):
4140 """Whether we use superproject."""
4141 return self.config.GetBoolean("repo.superproject")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004142
Gavin Makea2e3302023-03-11 06:46:20 +00004143 @property
4144 def partial_clone(self):
4145 """Whether this is a partial clone."""
4146 return self.config.GetBoolean("repo.partialclone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004147
Gavin Makea2e3302023-03-11 06:46:20 +00004148 @property
4149 def depth(self):
4150 """Partial clone depth."""
Roberto Vladimir Prado Carranza3d58d212023-09-13 10:27:26 +02004151 return self.config.GetInt("repo.depth")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004152
Gavin Makea2e3302023-03-11 06:46:20 +00004153 @property
4154 def clone_filter(self):
4155 """The clone filter."""
4156 return self.config.GetString("repo.clonefilter")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004157
Gavin Makea2e3302023-03-11 06:46:20 +00004158 @property
4159 def partial_clone_exclude(self):
4160 """Partial clone exclude string"""
4161 return self.config.GetString("repo.partialcloneexclude")
LaMont Jones4ada0432022-04-14 15:10:43 +00004162
Gavin Makea2e3302023-03-11 06:46:20 +00004163 @property
Jason Chang17833322023-05-23 13:06:55 -07004164 def clone_filter_for_depth(self):
4165 """Replace shallow clone with partial clone."""
4166 return self.config.GetString("repo.clonefilterfordepth")
4167
4168 @property
Gavin Makea2e3302023-03-11 06:46:20 +00004169 def manifest_platform(self):
4170 """The --platform argument from `repo init`."""
4171 return self.config.GetString("manifest.platform")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004172
Gavin Makea2e3302023-03-11 06:46:20 +00004173 @property
4174 def _platform_name(self):
4175 """Return the name of the platform."""
4176 return platform.system().lower()
LaMont Jones9b03f152022-03-29 23:01:18 +00004177
Gavin Makea2e3302023-03-11 06:46:20 +00004178 def SyncWithPossibleInit(
4179 self,
4180 submanifest,
4181 verbose=False,
4182 current_branch_only=False,
4183 tags="",
4184 git_event_log=None,
4185 ):
4186 """Sync a manifestProject, possibly for the first time.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004187
Gavin Makea2e3302023-03-11 06:46:20 +00004188 Call Sync() with arguments from the most recent `repo init`. If this is
4189 a new sub manifest, then inherit options from the parent's
4190 manifestProject.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004191
Gavin Makea2e3302023-03-11 06:46:20 +00004192 This is used by subcmds.Sync() to do an initial download of new sub
4193 manifests.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004194
Gavin Makea2e3302023-03-11 06:46:20 +00004195 Args:
4196 submanifest: an XmlSubmanifest, the submanifest to re-sync.
4197 verbose: a boolean, whether to show all output, rather than only
4198 errors.
4199 current_branch_only: a boolean, whether to only fetch the current
4200 manifest branch from the server.
4201 tags: a boolean, whether to fetch tags.
4202 git_event_log: an EventLog, for git tracing.
4203 """
4204 # TODO(lamontjones): when refactoring sync (and init?) consider how to
4205 # better get the init options that we should use for new submanifests
4206 # that are added when syncing an existing workspace.
4207 git_event_log = git_event_log or EventLog()
LaMont Jonesb90a4222022-04-14 15:00:09 +00004208 spec = submanifest.ToSubmanifestSpec()
Gavin Makea2e3302023-03-11 06:46:20 +00004209 # Use the init options from the existing manifestProject, or the parent
4210 # if it doesn't exist.
4211 #
4212 # Today, we only support changing manifest_groups on the sub-manifest,
4213 # with no supported-for-the-user way to change the other arguments from
4214 # those specified by the outermost manifest.
4215 #
4216 # TODO(lamontjones): determine which of these should come from the
4217 # outermost manifest and which should come from the parent manifest.
4218 mp = self if self.Exists else submanifest.parent.manifestProject
4219 return self.Sync(
LaMont Jones55ee3042022-04-06 17:10:21 +00004220 manifest_url=spec.manifestUrl,
4221 manifest_branch=spec.revision,
Gavin Makea2e3302023-03-11 06:46:20 +00004222 standalone_manifest=mp.standalone_manifest_url,
4223 groups=mp.manifest_groups,
4224 platform=mp.manifest_platform,
4225 mirror=mp.mirror,
4226 dissociate=mp.dissociate,
4227 reference=mp.reference,
4228 worktree=mp.use_worktree,
4229 submodules=mp.submodules,
4230 archive=mp.archive,
4231 partial_clone=mp.partial_clone,
4232 clone_filter=mp.clone_filter,
4233 partial_clone_exclude=mp.partial_clone_exclude,
4234 clone_bundle=mp.clone_bundle,
4235 git_lfs=mp.git_lfs,
4236 use_superproject=mp.use_superproject,
LaMont Jones55ee3042022-04-06 17:10:21 +00004237 verbose=verbose,
4238 current_branch_only=current_branch_only,
4239 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00004240 depth=mp.depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004241 git_event_log=git_event_log,
4242 manifest_name=spec.manifestName,
Gavin Makea2e3302023-03-11 06:46:20 +00004243 this_manifest_only=True,
LaMont Jones55ee3042022-04-06 17:10:21 +00004244 outer_manifest=False,
Jason Chang17833322023-05-23 13:06:55 -07004245 clone_filter_for_depth=mp.clone_filter_for_depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004246 )
LaMont Jones409407a2022-04-05 21:21:56 +00004247
Gavin Makea2e3302023-03-11 06:46:20 +00004248 def Sync(
4249 self,
4250 _kwargs_only=(),
4251 manifest_url="",
4252 manifest_branch=None,
4253 standalone_manifest=False,
4254 groups="",
4255 mirror=False,
4256 reference="",
4257 dissociate=False,
4258 worktree=False,
4259 submodules=False,
4260 archive=False,
4261 partial_clone=None,
4262 depth=None,
4263 clone_filter="blob:none",
4264 partial_clone_exclude=None,
4265 clone_bundle=None,
4266 git_lfs=None,
4267 use_superproject=None,
4268 verbose=False,
4269 current_branch_only=False,
4270 git_event_log=None,
4271 platform="",
4272 manifest_name="default.xml",
4273 tags="",
4274 this_manifest_only=False,
4275 outer_manifest=True,
Jason Chang17833322023-05-23 13:06:55 -07004276 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00004277 ):
4278 """Sync the manifest and all submanifests.
LaMont Jones409407a2022-04-05 21:21:56 +00004279
Gavin Makea2e3302023-03-11 06:46:20 +00004280 Args:
4281 manifest_url: a string, the URL of the manifest project.
4282 manifest_branch: a string, the manifest branch to use.
4283 standalone_manifest: a boolean, whether to store the manifest as a
4284 static file.
4285 groups: a string, restricts the checkout to projects with the
4286 specified groups.
4287 mirror: a boolean, whether to create a mirror of the remote
4288 repository.
4289 reference: a string, location of a repo instance to use as a
4290 reference.
4291 dissociate: a boolean, whether to dissociate from reference mirrors
4292 after clone.
4293 worktree: a boolean, whether to use git-worktree to manage projects.
4294 submodules: a boolean, whether sync submodules associated with the
4295 manifest project.
4296 archive: a boolean, whether to checkout each project as an archive.
4297 See git-archive.
4298 partial_clone: a boolean, whether to perform a partial clone.
4299 depth: an int, how deep of a shallow clone to create.
4300 clone_filter: a string, filter to use with partial_clone.
4301 partial_clone_exclude : a string, comma-delimeted list of project
4302 names to exclude from partial clone.
4303 clone_bundle: a boolean, whether to enable /clone.bundle on
4304 HTTP/HTTPS.
4305 git_lfs: a boolean, whether to enable git LFS support.
4306 use_superproject: a boolean, whether to use the manifest
4307 superproject to sync projects.
4308 verbose: a boolean, whether to show all output, rather than only
4309 errors.
4310 current_branch_only: a boolean, whether to only fetch the current
4311 manifest branch from the server.
4312 platform: a string, restrict the checkout to projects with the
4313 specified platform group.
4314 git_event_log: an EventLog, for git tracing.
4315 tags: a boolean, whether to fetch tags.
4316 manifest_name: a string, the name of the manifest file to use.
4317 this_manifest_only: a boolean, whether to only operate on the
4318 current sub manifest.
4319 outer_manifest: a boolean, whether to start at the outermost
4320 manifest.
Jason Chang17833322023-05-23 13:06:55 -07004321 clone_filter_for_depth: a string, when specified replaces shallow
4322 clones with partial.
LaMont Jones9b03f152022-03-29 23:01:18 +00004323
Gavin Makea2e3302023-03-11 06:46:20 +00004324 Returns:
4325 a boolean, whether the sync was successful.
4326 """
4327 assert _kwargs_only == (), "Sync only accepts keyword arguments."
LaMont Jones9b03f152022-03-29 23:01:18 +00004328
Gavin Makea2e3302023-03-11 06:46:20 +00004329 groups = groups or self.manifest.GetDefaultGroupsStr(
4330 with_platform=False
4331 )
4332 platform = platform or "auto"
4333 git_event_log = git_event_log or EventLog()
4334 if outer_manifest and self.manifest.is_submanifest:
4335 # In a multi-manifest checkout, use the outer manifest unless we are
4336 # told not to.
4337 return self.client.outer_manifest.manifestProject.Sync(
4338 manifest_url=manifest_url,
4339 manifest_branch=manifest_branch,
4340 standalone_manifest=standalone_manifest,
4341 groups=groups,
4342 platform=platform,
4343 mirror=mirror,
4344 dissociate=dissociate,
4345 reference=reference,
4346 worktree=worktree,
4347 submodules=submodules,
4348 archive=archive,
4349 partial_clone=partial_clone,
4350 clone_filter=clone_filter,
4351 partial_clone_exclude=partial_clone_exclude,
4352 clone_bundle=clone_bundle,
4353 git_lfs=git_lfs,
4354 use_superproject=use_superproject,
4355 verbose=verbose,
4356 current_branch_only=current_branch_only,
4357 tags=tags,
4358 depth=depth,
4359 git_event_log=git_event_log,
4360 manifest_name=manifest_name,
4361 this_manifest_only=this_manifest_only,
4362 outer_manifest=False,
4363 )
LaMont Jones9b03f152022-03-29 23:01:18 +00004364
Gavin Makea2e3302023-03-11 06:46:20 +00004365 # If repo has already been initialized, we take -u with the absence of
4366 # --standalone-manifest to mean "transition to a standard repo set up",
4367 # which necessitates starting fresh.
4368 # If --standalone-manifest is set, we always tear everything down and
4369 # start anew.
4370 if self.Exists:
4371 was_standalone_manifest = self.config.GetString(
4372 "manifest.standalone"
4373 )
4374 if was_standalone_manifest and not manifest_url:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004375 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004376 "fatal: repo was initialized with a standlone manifest, "
4377 "cannot be re-initialized without --manifest-url/-u"
4378 )
4379 return False
4380
4381 if standalone_manifest or (
4382 was_standalone_manifest and manifest_url
4383 ):
4384 self.config.ClearCache()
4385 if self.gitdir and os.path.exists(self.gitdir):
4386 platform_utils.rmtree(self.gitdir)
4387 if self.worktree and os.path.exists(self.worktree):
4388 platform_utils.rmtree(self.worktree)
4389
4390 is_new = not self.Exists
4391 if is_new:
4392 if not manifest_url:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004393 logger.error("fatal: manifest url is required.")
Gavin Makea2e3302023-03-11 06:46:20 +00004394 return False
4395
4396 if verbose:
4397 print(
4398 "Downloading manifest from %s"
4399 % (GitConfig.ForUser().UrlInsteadOf(manifest_url),),
4400 file=sys.stderr,
4401 )
4402
4403 # The manifest project object doesn't keep track of the path on the
4404 # server where this git is located, so let's save that here.
4405 mirrored_manifest_git = None
4406 if reference:
4407 manifest_git_path = urllib.parse.urlparse(manifest_url).path[1:]
4408 mirrored_manifest_git = os.path.join(
4409 reference, manifest_git_path
4410 )
4411 if not mirrored_manifest_git.endswith(".git"):
4412 mirrored_manifest_git += ".git"
4413 if not os.path.exists(mirrored_manifest_git):
4414 mirrored_manifest_git = os.path.join(
4415 reference, ".repo/manifests.git"
4416 )
4417
4418 self._InitGitDir(mirror_git=mirrored_manifest_git)
4419
4420 # If standalone_manifest is set, mark the project as "standalone" --
4421 # we'll still do much of the manifests.git set up, but will avoid actual
4422 # syncs to a remote.
4423 if standalone_manifest:
4424 self.config.SetString("manifest.standalone", manifest_url)
4425 elif not manifest_url and not manifest_branch:
4426 # If -u is set and --standalone-manifest is not, then we're not in
4427 # standalone mode. Otherwise, use config to infer what we were in
4428 # the last init.
4429 standalone_manifest = bool(
4430 self.config.GetString("manifest.standalone")
4431 )
4432 if not standalone_manifest:
4433 self.config.SetString("manifest.standalone", None)
4434
4435 self._ConfigureDepth(depth)
4436
4437 # Set the remote URL before the remote branch as we might need it below.
4438 if manifest_url:
4439 r = self.GetRemote()
4440 r.url = manifest_url
4441 r.ResetFetch()
4442 r.Save()
4443
4444 if not standalone_manifest:
4445 if manifest_branch:
4446 if manifest_branch == "HEAD":
4447 manifest_branch = self.ResolveRemoteHead()
4448 if manifest_branch is None:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004449 logger.error("fatal: unable to resolve HEAD")
Gavin Makea2e3302023-03-11 06:46:20 +00004450 return False
4451 self.revisionExpr = manifest_branch
4452 else:
4453 if is_new:
4454 default_branch = self.ResolveRemoteHead()
4455 if default_branch is None:
4456 # If the remote doesn't have HEAD configured, default to
4457 # master.
4458 default_branch = "refs/heads/master"
4459 self.revisionExpr = default_branch
4460 else:
4461 self.PreSync()
4462
4463 groups = re.split(r"[,\s]+", groups or "")
4464 all_platforms = ["linux", "darwin", "windows"]
4465 platformize = lambda x: "platform-" + x
4466 if platform == "auto":
4467 if not mirror and not self.mirror:
4468 groups.append(platformize(self._platform_name))
4469 elif platform == "all":
4470 groups.extend(map(platformize, all_platforms))
4471 elif platform in all_platforms:
4472 groups.append(platformize(platform))
4473 elif platform != "none":
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004474 logger.error("fatal: invalid platform flag", file=sys.stderr)
Gavin Makea2e3302023-03-11 06:46:20 +00004475 return False
4476 self.config.SetString("manifest.platform", platform)
4477
4478 groups = [x for x in groups if x]
4479 groupstr = ",".join(groups)
4480 if (
4481 platform == "auto"
4482 and groupstr == self.manifest.GetDefaultGroupsStr()
4483 ):
4484 groupstr = None
4485 self.config.SetString("manifest.groups", groupstr)
4486
4487 if reference:
4488 self.config.SetString("repo.reference", reference)
4489
4490 if dissociate:
4491 self.config.SetBoolean("repo.dissociate", dissociate)
4492
4493 if worktree:
4494 if mirror:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004495 logger.error("fatal: --mirror and --worktree are incompatible")
Gavin Makea2e3302023-03-11 06:46:20 +00004496 return False
4497 if submodules:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004498 logger.error(
4499 "fatal: --submodules and --worktree are incompatible"
Gavin Makea2e3302023-03-11 06:46:20 +00004500 )
4501 return False
4502 self.config.SetBoolean("repo.worktree", worktree)
4503 if is_new:
4504 self.use_git_worktrees = True
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004505 logger.warning("warning: --worktree is experimental!")
Gavin Makea2e3302023-03-11 06:46:20 +00004506
4507 if archive:
4508 if is_new:
4509 self.config.SetBoolean("repo.archive", archive)
4510 else:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004511 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004512 "fatal: --archive is only supported when initializing a "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004513 "new workspace."
Gavin Makea2e3302023-03-11 06:46:20 +00004514 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004515 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004516 "Either delete the .repo folder in this workspace, or "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004517 "initialize in another location."
Gavin Makea2e3302023-03-11 06:46:20 +00004518 )
4519 return False
4520
4521 if mirror:
4522 if is_new:
4523 self.config.SetBoolean("repo.mirror", mirror)
4524 else:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004525 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004526 "fatal: --mirror is only supported when initializing a new "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004527 "workspace."
Gavin Makea2e3302023-03-11 06:46:20 +00004528 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004529 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004530 "Either delete the .repo folder in this workspace, or "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004531 "initialize in another location."
Gavin Makea2e3302023-03-11 06:46:20 +00004532 )
4533 return False
4534
4535 if partial_clone is not None:
4536 if mirror:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004537 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004538 "fatal: --mirror and --partial-clone are mutually "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004539 "exclusive"
Gavin Makea2e3302023-03-11 06:46:20 +00004540 )
4541 return False
4542 self.config.SetBoolean("repo.partialclone", partial_clone)
4543 if clone_filter:
4544 self.config.SetString("repo.clonefilter", clone_filter)
4545 elif self.partial_clone:
4546 clone_filter = self.clone_filter
4547 else:
4548 clone_filter = None
4549
4550 if partial_clone_exclude is not None:
4551 self.config.SetString(
4552 "repo.partialcloneexclude", partial_clone_exclude
4553 )
4554
4555 if clone_bundle is None:
4556 clone_bundle = False if partial_clone else True
4557 else:
4558 self.config.SetBoolean("repo.clonebundle", clone_bundle)
4559
4560 if submodules:
4561 self.config.SetBoolean("repo.submodules", submodules)
4562
4563 if git_lfs is not None:
4564 if git_lfs:
4565 git_require((2, 17, 0), fail=True, msg="Git LFS support")
4566
4567 self.config.SetBoolean("repo.git-lfs", git_lfs)
4568 if not is_new:
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004569 logger.warning(
Gavin Makea2e3302023-03-11 06:46:20 +00004570 "warning: Changing --git-lfs settings will only affect new "
4571 "project checkouts.\n"
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004572 " Existing projects will require manual updates.\n"
Gavin Makea2e3302023-03-11 06:46:20 +00004573 )
4574
Jason Chang17833322023-05-23 13:06:55 -07004575 if clone_filter_for_depth is not None:
4576 self.ConfigureCloneFilterForDepth(clone_filter_for_depth)
4577
Gavin Makea2e3302023-03-11 06:46:20 +00004578 if use_superproject is not None:
4579 self.config.SetBoolean("repo.superproject", use_superproject)
4580
4581 if not standalone_manifest:
4582 success = self.Sync_NetworkHalf(
4583 is_new=is_new,
4584 quiet=not verbose,
4585 verbose=verbose,
4586 clone_bundle=clone_bundle,
4587 current_branch_only=current_branch_only,
4588 tags=tags,
4589 submodules=submodules,
4590 clone_filter=clone_filter,
4591 partial_clone_exclude=self.manifest.PartialCloneExclude,
Jason Chang17833322023-05-23 13:06:55 -07004592 clone_filter_for_depth=self.manifest.CloneFilterForDepth,
Gavin Makea2e3302023-03-11 06:46:20 +00004593 ).success
4594 if not success:
4595 r = self.GetRemote()
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004596 logger.error("fatal: cannot obtain manifest %s", r.url)
Gavin Makea2e3302023-03-11 06:46:20 +00004597
4598 # Better delete the manifest git dir if we created it; otherwise
4599 # next time (when user fixes problems) we won't go through the
4600 # "is_new" logic.
4601 if is_new:
4602 platform_utils.rmtree(self.gitdir)
4603 return False
4604
4605 if manifest_branch:
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08004606 self.MetaBranchSwitch(submodules=submodules, verbose=verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00004607
4608 syncbuf = SyncBuffer(self.config)
Tomasz Wasilczyk4c809212023-12-08 13:42:17 -08004609 self.Sync_LocalHalf(syncbuf, submodules=submodules, verbose=verbose)
Gavin Makea2e3302023-03-11 06:46:20 +00004610 syncbuf.Finish()
4611
4612 if is_new or self.CurrentBranch is None:
Jason Chang1a3612f2023-08-08 14:12:53 -07004613 try:
4614 self.StartBranch("default")
4615 except GitError as e:
4616 msg = str(e)
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004617 logger.error(
4618 "fatal: cannot create default in manifest %s", msg
Gavin Makea2e3302023-03-11 06:46:20 +00004619 )
4620 return False
4621
4622 if not manifest_name:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004623 logger.error("fatal: manifest name (-m) is required.")
Gavin Makea2e3302023-03-11 06:46:20 +00004624 return False
4625
4626 elif is_new:
4627 # This is a new standalone manifest.
4628 manifest_name = "default.xml"
4629 manifest_data = fetch.fetch_file(manifest_url, verbose=verbose)
4630 dest = os.path.join(self.worktree, manifest_name)
4631 os.makedirs(os.path.dirname(dest), exist_ok=True)
4632 with open(dest, "wb") as f:
4633 f.write(manifest_data)
4634
4635 try:
4636 self.manifest.Link(manifest_name)
4637 except ManifestParseError as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004638 logger.error("fatal: manifest '%s' not available", manifest_name)
4639 logger.error("fatal: %s", e)
Gavin Makea2e3302023-03-11 06:46:20 +00004640 return False
4641
4642 if not this_manifest_only:
4643 for submanifest in self.manifest.submanifests.values():
4644 spec = submanifest.ToSubmanifestSpec()
4645 submanifest.repo_client.manifestProject.Sync(
4646 manifest_url=spec.manifestUrl,
4647 manifest_branch=spec.revision,
4648 standalone_manifest=standalone_manifest,
4649 groups=self.manifest_groups,
4650 platform=platform,
4651 mirror=mirror,
4652 dissociate=dissociate,
4653 reference=reference,
4654 worktree=worktree,
4655 submodules=submodules,
4656 archive=archive,
4657 partial_clone=partial_clone,
4658 clone_filter=clone_filter,
4659 partial_clone_exclude=partial_clone_exclude,
4660 clone_bundle=clone_bundle,
4661 git_lfs=git_lfs,
4662 use_superproject=use_superproject,
4663 verbose=verbose,
4664 current_branch_only=current_branch_only,
4665 tags=tags,
4666 depth=depth,
4667 git_event_log=git_event_log,
4668 manifest_name=spec.manifestName,
4669 this_manifest_only=False,
4670 outer_manifest=False,
4671 )
4672
4673 # Lastly, if the manifest has a <superproject> then have the
4674 # superproject sync it (if it will be used).
4675 if git_superproject.UseSuperproject(use_superproject, self.manifest):
4676 sync_result = self.manifest.superproject.Sync(git_event_log)
4677 if not sync_result.success:
4678 submanifest = ""
4679 if self.manifest.path_prefix:
4680 submanifest = f"for {self.manifest.path_prefix} "
Aravind Vasudevan8bc50002023-10-13 19:22:47 +00004681 logger.warning(
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004682 "warning: git update of superproject %s failed, "
Gavin Makea2e3302023-03-11 06:46:20 +00004683 "repo sync will not use superproject to fetch source; "
4684 "while this error is not fatal, and you can continue to "
4685 "run repo sync, please run repo init with the "
4686 "--no-use-superproject option to stop seeing this warning",
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004687 submanifest,
Gavin Makea2e3302023-03-11 06:46:20 +00004688 )
4689 if sync_result.fatal and use_superproject is not None:
4690 return False
4691
4692 return True
4693
Jason Chang17833322023-05-23 13:06:55 -07004694 def ConfigureCloneFilterForDepth(self, clone_filter_for_depth):
4695 """Configure clone filter to replace shallow clones.
4696
4697 Args:
4698 clone_filter_for_depth: a string or None, e.g. 'blob:none' will
4699 disable shallow clones and replace with partial clone. None will
4700 enable shallow clones.
4701 """
4702 self.config.SetString(
4703 "repo.clonefilterfordepth", clone_filter_for_depth
4704 )
4705
Gavin Makea2e3302023-03-11 06:46:20 +00004706 def _ConfigureDepth(self, depth):
4707 """Configure the depth we'll sync down.
4708
4709 Args:
4710 depth: an int, how deep of a partial clone to create.
4711 """
4712 # Opt.depth will be non-None if user actually passed --depth to repo
4713 # init.
4714 if depth is not None:
4715 if depth > 0:
4716 # Positive values will set the depth.
4717 depth = str(depth)
4718 else:
4719 # Negative numbers will clear the depth; passing None to
4720 # SetString will do that.
4721 depth = None
4722
4723 # We store the depth in the main manifest project.
4724 self.config.SetString("repo.depth", depth)