blob: 987ba5fe6378329eed9b961c0c3cfd5b71c6cfe2 [file] [log] [blame]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001# Copyright (C) 2008 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
Shawn O. Pearce438ee1c2008-11-03 09:59:36 -080015import errno
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070016import filecmp
Wink Saville4c426ef2015-06-03 08:05:17 -070017import glob
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070018import os
LaMont Jonesd82be3e2022-04-05 19:30:46 +000019import platform
Shawn O. Pearcec325dc32011-10-03 08:30:24 -070020import random
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070021import re
22import shutil
23import stat
Shawn O. Pearce5e7127d2012-08-02 14:57:37 -070024import subprocess
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070025import sys
Julien Campergue335f5ef2013-10-16 11:02:35 +020026import tarfile
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +080027import tempfile
Shawn O. Pearcec325dc32011-10-03 08:30:24 -070028import time
Mike Frysinger64477332023-08-21 21:20:32 -040029from typing import List, NamedTuple
Mike Frysingeracf63b22019-06-13 02:24:21 -040030import urllib.parse
Shawn O. Pearcedf5ee522011-10-11 14:05:21 -070031
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070032from color import Coloring
Mike Frysinger64477332023-08-21 21:20:32 -040033from error import DownloadError
34from error import GitError
35from error import ManifestInvalidPathError
36from error import ManifestInvalidRevisionError
37from error import ManifestParseError
38from error import NoManifestException
39from error import RepoError
40from error import UploadError
LaMont Jones0de4fc32022-04-21 17:18:35 +000041import fetch
Mike Frysinger64477332023-08-21 21:20:32 -040042from git_command import git_require
43from git_command import GitCommand
44from git_config import GetSchemeFromUrl
45from git_config import GetUrlCookieFile
46from git_config import GitConfig
Mike Frysinger64477332023-08-21 21:20:32 -040047from git_config import IsId
48from git_refs import GitRefs
49from git_refs import HEAD
50from git_refs import R_HEADS
51from git_refs import R_M
52from git_refs import R_PUB
53from git_refs import R_TAGS
54from git_refs import R_WORKTREE_M
LaMont Jonesff6b1da2022-06-01 21:03:34 +000055import git_superproject
LaMont Jones55ee3042022-04-06 17:10:21 +000056from git_trace2_event_log import EventLog
Renaud Paquayd5cec5e2016-11-01 11:24:03 -070057import platform_utils
Mike Frysinger70d861f2019-08-26 15:22:36 -040058import progress
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +000059from repo_logging import RepoLogger
Joanna Wanga6c52f52022-11-03 16:51:19 -040060from repo_trace import Trace
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070061
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -070062
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +000063logger = RepoLogger(__file__)
64
65
LaMont Jones1eddca82022-09-01 15:15:04 +000066class SyncNetworkHalfResult(NamedTuple):
Gavin Makea2e3302023-03-11 06:46:20 +000067 """Sync_NetworkHalf return value."""
68
Gavin Makea2e3302023-03-11 06:46:20 +000069 # Did we query the remote? False when optimized_fetch is True and we have
70 # the commit already present.
71 remote_fetched: bool
Jason Chang32b59562023-07-14 16:45:35 -070072 # Error from SyncNetworkHalf
73 error: Exception = None
74
75 @property
76 def success(self) -> bool:
77 return not self.error
78
79
80class SyncNetworkHalfError(RepoError):
81 """Failure trying to sync."""
82
83
84class DeleteWorktreeError(RepoError):
85 """Failure to delete worktree."""
86
87 def __init__(
88 self, *args, aggregate_errors: List[Exception] = None, **kwargs
89 ) -> None:
90 super().__init__(*args, **kwargs)
91 self.aggregate_errors = aggregate_errors or []
92
93
94class DeleteDirtyWorktreeError(DeleteWorktreeError):
95 """Failure to delete worktree due to uncommitted changes."""
LaMont Jones1eddca82022-09-01 15:15:04 +000096
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +010097
George Engelbrecht9bc283e2020-04-02 12:36:09 -060098# Maximum sleep time allowed during retries.
99MAXIMUM_RETRY_SLEEP_SEC = 3600.0
100# +-10% random jitter is added to each Fetches retry sleep duration.
101RETRY_JITTER_PERCENT = 0.1
102
LaMont Jonesfa8d9392022-11-02 22:01:29 +0000103# Whether to use alternates. Switching back and forth is *NOT* supported.
Mike Frysinger1d00a7e2021-12-21 00:40:31 -0500104# TODO(vapier): Remove knob once behavior is verified.
Gavin Makea2e3302023-03-11 06:46:20 +0000105_ALTERNATES = os.environ.get("REPO_USE_ALTERNATES") == "1"
George Engelbrecht9bc283e2020-04-02 12:36:09 -0600106
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +0100107
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700108def _lwrite(path, content):
Gavin Makea2e3302023-03-11 06:46:20 +0000109 lock = "%s.lock" % path
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700110
Gavin Makea2e3302023-03-11 06:46:20 +0000111 # Maintain Unix line endings on all OS's to match git behavior.
112 with open(lock, "w", newline="\n") as fd:
113 fd.write(content)
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700114
Gavin Makea2e3302023-03-11 06:46:20 +0000115 try:
116 platform_utils.rename(lock, path)
117 except OSError:
118 platform_utils.remove(lock)
119 raise
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700120
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700121
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700122def not_rev(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000123 return "^" + r
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700124
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700125
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800126def sq(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000127 return "'" + r.replace("'", "'''") + "'"
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -0800128
David Pursehouse819827a2020-02-12 15:20:19 +0900129
Jonathan Nieder93719792015-03-17 11:29:58 -0700130_project_hook_list = None
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700131
132
Jonathan Nieder93719792015-03-17 11:29:58 -0700133def _ProjectHooks():
Gavin Makea2e3302023-03-11 06:46:20 +0000134 """List the hooks present in the 'hooks' directory.
Jonathan Nieder93719792015-03-17 11:29:58 -0700135
Gavin Makea2e3302023-03-11 06:46:20 +0000136 These hooks are project hooks and are copied to the '.git/hooks' directory
137 of all subprojects.
Jonathan Nieder93719792015-03-17 11:29:58 -0700138
Gavin Makea2e3302023-03-11 06:46:20 +0000139 This function caches the list of hooks (based on the contents of the
140 'repo/hooks' directory) on the first call.
Jonathan Nieder93719792015-03-17 11:29:58 -0700141
Gavin Makea2e3302023-03-11 06:46:20 +0000142 Returns:
143 A list of absolute paths to all of the files in the hooks directory.
144 """
145 global _project_hook_list
146 if _project_hook_list is None:
147 d = platform_utils.realpath(os.path.abspath(os.path.dirname(__file__)))
148 d = os.path.join(d, "hooks")
149 _project_hook_list = [
150 os.path.join(d, x) for x in platform_utils.listdir(d)
151 ]
152 return _project_hook_list
Jonathan Nieder93719792015-03-17 11:29:58 -0700153
154
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700155class DownloadedChange(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000156 _commit_cache = None
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700157
Gavin Makea2e3302023-03-11 06:46:20 +0000158 def __init__(self, project, base, change_id, ps_id, commit):
159 self.project = project
160 self.base = base
161 self.change_id = change_id
162 self.ps_id = ps_id
163 self.commit = commit
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700164
Gavin Makea2e3302023-03-11 06:46:20 +0000165 @property
166 def commits(self):
167 if self._commit_cache is None:
168 self._commit_cache = self.project.bare_git.rev_list(
169 "--abbrev=8",
170 "--abbrev-commit",
171 "--pretty=oneline",
172 "--reverse",
173 "--date-order",
174 not_rev(self.base),
175 self.commit,
176 "--",
177 )
178 return self._commit_cache
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700179
180
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700181class ReviewableBranch(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000182 _commit_cache = None
183 _base_exists = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700184
Gavin Makea2e3302023-03-11 06:46:20 +0000185 def __init__(self, project, branch, base):
186 self.project = project
187 self.branch = branch
188 self.base = base
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700189
Gavin Makea2e3302023-03-11 06:46:20 +0000190 @property
191 def name(self):
192 return self.branch.name
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700193
Gavin Makea2e3302023-03-11 06:46:20 +0000194 @property
195 def commits(self):
196 if self._commit_cache is None:
197 args = (
198 "--abbrev=8",
199 "--abbrev-commit",
200 "--pretty=oneline",
201 "--reverse",
202 "--date-order",
203 not_rev(self.base),
204 R_HEADS + self.name,
205 "--",
206 )
207 try:
208 self._commit_cache = self.project.bare_git.rev_list(*args)
209 except GitError:
210 # We weren't able to probe the commits for this branch. Was it
211 # tracking a branch that no longer exists? If so, return no
212 # commits. Otherwise, rethrow the error as we don't know what's
213 # going on.
214 if self.base_exists:
215 raise
Mike Frysinger6da17752019-09-11 18:43:17 -0400216
Gavin Makea2e3302023-03-11 06:46:20 +0000217 self._commit_cache = []
Mike Frysinger6da17752019-09-11 18:43:17 -0400218
Gavin Makea2e3302023-03-11 06:46:20 +0000219 return self._commit_cache
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700220
Gavin Makea2e3302023-03-11 06:46:20 +0000221 @property
222 def unabbrev_commits(self):
223 r = dict()
224 for commit in self.project.bare_git.rev_list(
225 not_rev(self.base), R_HEADS + self.name, "--"
226 ):
227 r[commit[0:8]] = commit
228 return r
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800229
Gavin Makea2e3302023-03-11 06:46:20 +0000230 @property
231 def date(self):
232 return self.project.bare_git.log(
233 "--pretty=format:%cd", "-n", "1", R_HEADS + self.name, "--"
234 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700235
Gavin Makea2e3302023-03-11 06:46:20 +0000236 @property
237 def base_exists(self):
238 """Whether the branch we're tracking exists.
Mike Frysinger6da17752019-09-11 18:43:17 -0400239
Gavin Makea2e3302023-03-11 06:46:20 +0000240 Normally it should, but sometimes branches we track can get deleted.
241 """
242 if self._base_exists is None:
243 try:
244 self.project.bare_git.rev_parse("--verify", not_rev(self.base))
245 # If we're still here, the base branch exists.
246 self._base_exists = True
247 except GitError:
248 # If we failed to verify, the base branch doesn't exist.
249 self._base_exists = False
Mike Frysinger6da17752019-09-11 18:43:17 -0400250
Gavin Makea2e3302023-03-11 06:46:20 +0000251 return self._base_exists
Mike Frysinger6da17752019-09-11 18:43:17 -0400252
Gavin Makea2e3302023-03-11 06:46:20 +0000253 def UploadForReview(
254 self,
255 people,
256 dryrun=False,
257 auto_topic=False,
258 hashtags=(),
259 labels=(),
260 private=False,
261 notify=None,
262 wip=False,
263 ready=False,
264 dest_branch=None,
265 validate_certs=True,
266 push_options=None,
267 ):
268 self.project.UploadForReview(
269 branch=self.name,
270 people=people,
271 dryrun=dryrun,
272 auto_topic=auto_topic,
273 hashtags=hashtags,
274 labels=labels,
275 private=private,
276 notify=notify,
277 wip=wip,
278 ready=ready,
279 dest_branch=dest_branch,
280 validate_certs=validate_certs,
281 push_options=push_options,
282 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700283
Gavin Makea2e3302023-03-11 06:46:20 +0000284 def GetPublishedRefs(self):
285 refs = {}
286 output = self.project.bare_git.ls_remote(
287 self.branch.remote.SshReviewUrl(self.project.UserEmail),
288 "refs/changes/*",
289 )
290 for line in output.split("\n"):
291 try:
292 (sha, ref) = line.split()
293 refs[sha] = ref
294 except ValueError:
295 pass
Ficus Kirkpatrickbc7ef672009-05-04 12:45:11 -0700296
Gavin Makea2e3302023-03-11 06:46:20 +0000297 return refs
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700298
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700299
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700300class StatusColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000301 def __init__(self, config):
302 super().__init__(config, "status")
303 self.project = self.printer("header", attr="bold")
304 self.branch = self.printer("header", attr="bold")
305 self.nobranch = self.printer("nobranch", fg="red")
306 self.important = self.printer("important", fg="red")
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700307
Gavin Makea2e3302023-03-11 06:46:20 +0000308 self.added = self.printer("added", fg="green")
309 self.changed = self.printer("changed", fg="red")
310 self.untracked = self.printer("untracked", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700311
312
313class DiffColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000314 def __init__(self, config):
315 super().__init__(config, "diff")
316 self.project = self.printer("header", attr="bold")
317 self.fail = self.printer("fail", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700318
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700319
Jack Neus6ea0cae2021-07-20 20:52:33 +0000320class Annotation(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000321 def __init__(self, name, value, keep):
322 self.name = name
323 self.value = value
324 self.keep = keep
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700325
Gavin Makea2e3302023-03-11 06:46:20 +0000326 def __eq__(self, other):
327 if not isinstance(other, Annotation):
328 return False
329 return self.__dict__ == other.__dict__
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700330
Gavin Makea2e3302023-03-11 06:46:20 +0000331 def __lt__(self, other):
332 # This exists just so that lists of Annotation objects can be sorted,
333 # for use in comparisons.
334 if not isinstance(other, Annotation):
335 raise ValueError("comparison is not between two Annotation objects")
336 if self.name == other.name:
337 if self.value == other.value:
338 return self.keep < other.keep
339 return self.value < other.value
340 return self.name < other.name
Jack Neus6ea0cae2021-07-20 20:52:33 +0000341
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700342
Mike Frysingere6a202f2019-08-02 15:57:57 -0400343def _SafeExpandPath(base, subpath, skipfinal=False):
Gavin Makea2e3302023-03-11 06:46:20 +0000344 """Make sure |subpath| is completely safe under |base|.
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700345
Gavin Makea2e3302023-03-11 06:46:20 +0000346 We make sure no intermediate symlinks are traversed, and that the final path
347 is not a special file (e.g. not a socket or fifo).
Mike Frysingere6a202f2019-08-02 15:57:57 -0400348
Gavin Makea2e3302023-03-11 06:46:20 +0000349 NB: We rely on a number of paths already being filtered out while parsing
350 the manifest. See the validation logic in manifest_xml.py for more details.
351 """
352 # Split up the path by its components. We can't use os.path.sep exclusively
353 # as some platforms (like Windows) will convert / to \ and that bypasses all
354 # our constructed logic here. Especially since manifest authors only use
355 # / in their paths.
356 resep = re.compile(r"[/%s]" % re.escape(os.path.sep))
357 components = resep.split(subpath)
358 if skipfinal:
359 # Whether the caller handles the final component itself.
360 finalpart = components.pop()
Mike Frysingere6a202f2019-08-02 15:57:57 -0400361
Gavin Makea2e3302023-03-11 06:46:20 +0000362 path = base
363 for part in components:
364 if part in {".", ".."}:
365 raise ManifestInvalidPathError(
366 '%s: "%s" not allowed in paths' % (subpath, part)
367 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400368
Gavin Makea2e3302023-03-11 06:46:20 +0000369 path = os.path.join(path, part)
370 if platform_utils.islink(path):
371 raise ManifestInvalidPathError(
372 "%s: traversing symlinks not allow" % (path,)
373 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400374
Gavin Makea2e3302023-03-11 06:46:20 +0000375 if os.path.exists(path):
376 if not os.path.isfile(path) and not platform_utils.isdir(path):
377 raise ManifestInvalidPathError(
378 "%s: only regular files & directories allowed" % (path,)
379 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400380
Gavin Makea2e3302023-03-11 06:46:20 +0000381 if skipfinal:
382 path = os.path.join(path, finalpart)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400383
Gavin Makea2e3302023-03-11 06:46:20 +0000384 return path
Mike Frysingere6a202f2019-08-02 15:57:57 -0400385
386
387class _CopyFile(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000388 """Container for <copyfile> manifest element."""
Mike Frysingere6a202f2019-08-02 15:57:57 -0400389
Gavin Makea2e3302023-03-11 06:46:20 +0000390 def __init__(self, git_worktree, src, topdir, dest):
391 """Register a <copyfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400392
Gavin Makea2e3302023-03-11 06:46:20 +0000393 Args:
394 git_worktree: Absolute path to the git project checkout.
395 src: Relative path under |git_worktree| of file to read.
396 topdir: Absolute path to the top of the repo client checkout.
397 dest: Relative path under |topdir| of file to write.
398 """
399 self.git_worktree = git_worktree
400 self.topdir = topdir
401 self.src = src
402 self.dest = dest
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700403
Gavin Makea2e3302023-03-11 06:46:20 +0000404 def _Copy(self):
405 src = _SafeExpandPath(self.git_worktree, self.src)
406 dest = _SafeExpandPath(self.topdir, self.dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400407
Gavin Makea2e3302023-03-11 06:46:20 +0000408 if platform_utils.isdir(src):
409 raise ManifestInvalidPathError(
410 "%s: copying from directory not supported" % (self.src,)
411 )
412 if platform_utils.isdir(dest):
413 raise ManifestInvalidPathError(
414 "%s: copying to directory not allowed" % (self.dest,)
415 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400416
Gavin Makea2e3302023-03-11 06:46:20 +0000417 # Copy file if it does not exist or is out of date.
418 if not os.path.exists(dest) or not filecmp.cmp(src, dest):
419 try:
420 # Remove existing file first, since it might be read-only.
421 if os.path.exists(dest):
422 platform_utils.remove(dest)
423 else:
424 dest_dir = os.path.dirname(dest)
425 if not platform_utils.isdir(dest_dir):
426 os.makedirs(dest_dir)
427 shutil.copy(src, dest)
428 # Make the file read-only.
429 mode = os.stat(dest)[stat.ST_MODE]
430 mode = mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
431 os.chmod(dest, mode)
432 except IOError:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +0000433 logger.error("error: Cannot copy file %s to %s", src, dest)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700434
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700435
Anthony King7bdac712014-07-16 12:56:40 +0100436class _LinkFile(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000437 """Container for <linkfile> manifest element."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700438
Gavin Makea2e3302023-03-11 06:46:20 +0000439 def __init__(self, git_worktree, src, topdir, dest):
440 """Register a <linkfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400441
Gavin Makea2e3302023-03-11 06:46:20 +0000442 Args:
443 git_worktree: Absolute path to the git project checkout.
444 src: Target of symlink relative to path under |git_worktree|.
445 topdir: Absolute path to the top of the repo client checkout.
446 dest: Relative path under |topdir| of symlink to create.
447 """
448 self.git_worktree = git_worktree
449 self.topdir = topdir
450 self.src = src
451 self.dest = dest
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500452
Gavin Makea2e3302023-03-11 06:46:20 +0000453 def __linkIt(self, relSrc, absDest):
454 # Link file if it does not exist or is out of date.
455 if not platform_utils.islink(absDest) or (
456 platform_utils.readlink(absDest) != relSrc
457 ):
458 try:
459 # Remove existing file first, since it might be read-only.
460 if os.path.lexists(absDest):
461 platform_utils.remove(absDest)
462 else:
463 dest_dir = os.path.dirname(absDest)
464 if not platform_utils.isdir(dest_dir):
465 os.makedirs(dest_dir)
466 platform_utils.symlink(relSrc, absDest)
467 except IOError:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +0000468 logger.error(
469 "error: Cannot link file %s to %s", relSrc, absDest
470 )
Gavin Makea2e3302023-03-11 06:46:20 +0000471
472 def _Link(self):
473 """Link the self.src & self.dest paths.
474
475 Handles wild cards on the src linking all of the files in the source in
476 to the destination directory.
477 """
478 # Some people use src="." to create stable links to projects. Let's
479 # allow that but reject all other uses of "." to keep things simple.
480 if self.src == ".":
481 src = self.git_worktree
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500482 else:
Gavin Makea2e3302023-03-11 06:46:20 +0000483 src = _SafeExpandPath(self.git_worktree, self.src)
Wink Saville4c426ef2015-06-03 08:05:17 -0700484
Gavin Makea2e3302023-03-11 06:46:20 +0000485 if not glob.has_magic(src):
486 # Entity does not contain a wild card so just a simple one to one
487 # link operation.
488 dest = _SafeExpandPath(self.topdir, self.dest, skipfinal=True)
489 # dest & src are absolute paths at this point. Make sure the target
490 # of the symlink is relative in the context of the repo client
491 # checkout.
492 relpath = os.path.relpath(src, os.path.dirname(dest))
493 self.__linkIt(relpath, dest)
494 else:
495 dest = _SafeExpandPath(self.topdir, self.dest)
496 # Entity contains a wild card.
497 if os.path.exists(dest) and not platform_utils.isdir(dest):
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +0000498 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +0000499 "Link error: src with wildcard, %s must be a directory",
500 dest,
501 )
502 else:
503 for absSrcFile in glob.glob(src):
504 # Create a releative path from source dir to destination
505 # dir.
506 absSrcDir = os.path.dirname(absSrcFile)
507 relSrcDir = os.path.relpath(absSrcDir, dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400508
Gavin Makea2e3302023-03-11 06:46:20 +0000509 # Get the source file name.
510 srcFile = os.path.basename(absSrcFile)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400511
Gavin Makea2e3302023-03-11 06:46:20 +0000512 # Now form the final full paths to srcFile. They will be
513 # absolute for the desintaiton and relative for the source.
514 absDest = os.path.join(dest, srcFile)
515 relSrc = os.path.join(relSrcDir, srcFile)
516 self.__linkIt(relSrc, absDest)
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500517
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700518
Shawn O. Pearced1f70d92009-05-19 14:58:02 -0700519class RemoteSpec(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000520 def __init__(
521 self,
522 name,
523 url=None,
524 pushUrl=None,
525 review=None,
526 revision=None,
527 orig_name=None,
528 fetchUrl=None,
529 ):
530 self.name = name
531 self.url = url
532 self.pushUrl = pushUrl
533 self.review = review
534 self.revision = revision
535 self.orig_name = orig_name
536 self.fetchUrl = fetchUrl
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700537
Ian Kasprzak0286e312021-02-05 10:06:18 -0800538
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700539class Project(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000540 # These objects can be shared between several working trees.
541 @property
542 def shareable_dirs(self):
543 """Return the shareable directories"""
544 if self.UseAlternates:
545 return ["hooks", "rr-cache"]
546 else:
547 return ["hooks", "objects", "rr-cache"]
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700548
Gavin Makea2e3302023-03-11 06:46:20 +0000549 def __init__(
550 self,
551 manifest,
552 name,
553 remote,
554 gitdir,
555 objdir,
556 worktree,
557 relpath,
558 revisionExpr,
559 revisionId,
560 rebase=True,
561 groups=None,
562 sync_c=False,
563 sync_s=False,
564 sync_tags=True,
565 clone_depth=None,
566 upstream=None,
567 parent=None,
568 use_git_worktrees=False,
569 is_derived=False,
570 dest_branch=None,
571 optimized_fetch=False,
572 retry_fetches=0,
573 old_revision=None,
574 ):
575 """Init a Project object.
Doug Anderson3ba5f952011-04-07 12:51:04 -0700576
577 Args:
Gavin Makea2e3302023-03-11 06:46:20 +0000578 manifest: The XmlManifest object.
579 name: The `name` attribute of manifest.xml's project element.
580 remote: RemoteSpec object specifying its remote's properties.
581 gitdir: Absolute path of git directory.
582 objdir: Absolute path of directory to store git objects.
583 worktree: Absolute path of git working tree.
584 relpath: Relative path of git working tree to repo's top directory.
585 revisionExpr: The `revision` attribute of manifest.xml's project
586 element.
587 revisionId: git commit id for checking out.
588 rebase: The `rebase` attribute of manifest.xml's project element.
589 groups: The `groups` attribute of manifest.xml's project element.
590 sync_c: The `sync-c` attribute of manifest.xml's project element.
591 sync_s: The `sync-s` attribute of manifest.xml's project element.
592 sync_tags: The `sync-tags` attribute of manifest.xml's project
593 element.
594 upstream: The `upstream` attribute of manifest.xml's project
595 element.
596 parent: The parent Project object.
597 use_git_worktrees: Whether to use `git worktree` for this project.
598 is_derived: False if the project was explicitly defined in the
599 manifest; True if the project is a discovered submodule.
600 dest_branch: The branch to which to push changes for review by
601 default.
602 optimized_fetch: If True, when a project is set to a sha1 revision,
603 only fetch from the remote if the sha1 is not present locally.
604 retry_fetches: Retry remote fetches n times upon receiving transient
605 error with exponential backoff and jitter.
606 old_revision: saved git commit id for open GITC projects.
607 """
608 self.client = self.manifest = manifest
609 self.name = name
610 self.remote = remote
611 self.UpdatePaths(relpath, worktree, gitdir, objdir)
612 self.SetRevision(revisionExpr, revisionId=revisionId)
613
614 self.rebase = rebase
615 self.groups = groups
616 self.sync_c = sync_c
617 self.sync_s = sync_s
618 self.sync_tags = sync_tags
619 self.clone_depth = clone_depth
620 self.upstream = upstream
621 self.parent = parent
622 # NB: Do not use this setting in __init__ to change behavior so that the
623 # manifest.git checkout can inspect & change it after instantiating.
624 # See the XmlManifest init code for more info.
625 self.use_git_worktrees = use_git_worktrees
626 self.is_derived = is_derived
627 self.optimized_fetch = optimized_fetch
628 self.retry_fetches = max(0, retry_fetches)
629 self.subprojects = []
630
631 self.snapshots = {}
632 self.copyfiles = []
633 self.linkfiles = []
634 self.annotations = []
635 self.dest_branch = dest_branch
636 self.old_revision = old_revision
637
638 # This will be filled in if a project is later identified to be the
639 # project containing repo hooks.
640 self.enabled_repo_hooks = []
641
642 def RelPath(self, local=True):
643 """Return the path for the project relative to a manifest.
644
645 Args:
646 local: a boolean, if True, the path is relative to the local
647 (sub)manifest. If false, the path is relative to the outermost
648 manifest.
649 """
650 if local:
651 return self.relpath
652 return os.path.join(self.manifest.path_prefix, self.relpath)
653
654 def SetRevision(self, revisionExpr, revisionId=None):
655 """Set revisionId based on revision expression and id"""
656 self.revisionExpr = revisionExpr
657 if revisionId is None and revisionExpr and IsId(revisionExpr):
658 self.revisionId = self.revisionExpr
659 else:
660 self.revisionId = revisionId
661
662 def UpdatePaths(self, relpath, worktree, gitdir, objdir):
663 """Update paths used by this project"""
664 self.gitdir = gitdir.replace("\\", "/")
665 self.objdir = objdir.replace("\\", "/")
666 if worktree:
667 self.worktree = os.path.normpath(worktree).replace("\\", "/")
668 else:
669 self.worktree = None
670 self.relpath = relpath
671
672 self.config = GitConfig.ForRepository(
673 gitdir=self.gitdir, defaults=self.manifest.globalConfig
674 )
675
676 if self.worktree:
677 self.work_git = self._GitGetByExec(
678 self, bare=False, gitdir=self.gitdir
679 )
680 else:
681 self.work_git = None
682 self.bare_git = self._GitGetByExec(self, bare=True, gitdir=self.gitdir)
683 self.bare_ref = GitRefs(self.gitdir)
684 self.bare_objdir = self._GitGetByExec(
685 self, bare=True, gitdir=self.objdir
686 )
687
688 @property
689 def UseAlternates(self):
690 """Whether git alternates are in use.
691
692 This will be removed once migration to alternates is complete.
693 """
694 return _ALTERNATES or self.manifest.is_multimanifest
695
696 @property
697 def Derived(self):
698 return self.is_derived
699
700 @property
701 def Exists(self):
702 return platform_utils.isdir(self.gitdir) and platform_utils.isdir(
703 self.objdir
704 )
705
706 @property
707 def CurrentBranch(self):
708 """Obtain the name of the currently checked out branch.
709
710 The branch name omits the 'refs/heads/' prefix.
711 None is returned if the project is on a detached HEAD, or if the
712 work_git is otheriwse inaccessible (e.g. an incomplete sync).
713 """
714 try:
715 b = self.work_git.GetHead()
716 except NoManifestException:
717 # If the local checkout is in a bad state, don't barf. Let the
718 # callers process this like the head is unreadable.
719 return None
720 if b.startswith(R_HEADS):
721 return b[len(R_HEADS) :]
722 return None
723
724 def IsRebaseInProgress(self):
725 return (
726 os.path.exists(self.work_git.GetDotgitPath("rebase-apply"))
727 or os.path.exists(self.work_git.GetDotgitPath("rebase-merge"))
728 or os.path.exists(os.path.join(self.worktree, ".dotest"))
729 )
730
731 def IsDirty(self, consider_untracked=True):
732 """Is the working directory modified in some way?"""
733 self.work_git.update_index(
734 "-q", "--unmerged", "--ignore-missing", "--refresh"
735 )
736 if self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD):
737 return True
738 if self.work_git.DiffZ("diff-files"):
739 return True
740 if consider_untracked and self.UntrackedFiles():
741 return True
742 return False
743
744 _userident_name = None
745 _userident_email = None
746
747 @property
748 def UserName(self):
749 """Obtain the user's personal name."""
750 if self._userident_name is None:
751 self._LoadUserIdentity()
752 return self._userident_name
753
754 @property
755 def UserEmail(self):
756 """Obtain the user's email address. This is very likely
757 to be their Gerrit login.
758 """
759 if self._userident_email is None:
760 self._LoadUserIdentity()
761 return self._userident_email
762
763 def _LoadUserIdentity(self):
764 u = self.bare_git.var("GIT_COMMITTER_IDENT")
765 m = re.compile("^(.*) <([^>]*)> ").match(u)
766 if m:
767 self._userident_name = m.group(1)
768 self._userident_email = m.group(2)
769 else:
770 self._userident_name = ""
771 self._userident_email = ""
772
773 def GetRemote(self, name=None):
774 """Get the configuration for a single remote.
775
776 Defaults to the current project's remote.
777 """
778 if name is None:
779 name = self.remote.name
780 return self.config.GetRemote(name)
781
782 def GetBranch(self, name):
783 """Get the configuration for a single branch."""
784 return self.config.GetBranch(name)
785
786 def GetBranches(self):
787 """Get all existing local branches."""
788 current = self.CurrentBranch
789 all_refs = self._allrefs
790 heads = {}
791
792 for name, ref_id in all_refs.items():
793 if name.startswith(R_HEADS):
794 name = name[len(R_HEADS) :]
795 b = self.GetBranch(name)
796 b.current = name == current
797 b.published = None
798 b.revision = ref_id
799 heads[name] = b
800
801 for name, ref_id in all_refs.items():
802 if name.startswith(R_PUB):
803 name = name[len(R_PUB) :]
804 b = heads.get(name)
805 if b:
806 b.published = ref_id
807
808 return heads
809
810 def MatchesGroups(self, manifest_groups):
811 """Returns true if the manifest groups specified at init should cause
812 this project to be synced.
813 Prefixing a manifest group with "-" inverts the meaning of a group.
814 All projects are implicitly labelled with "all".
815
816 labels are resolved in order. In the example case of
817 project_groups: "all,group1,group2"
818 manifest_groups: "-group1,group2"
819 the project will be matched.
820
821 The special manifest group "default" will match any project that
822 does not have the special project group "notdefault"
823 """
824 default_groups = self.manifest.default_groups or ["default"]
825 expanded_manifest_groups = manifest_groups or default_groups
826 expanded_project_groups = ["all"] + (self.groups or [])
827 if "notdefault" not in expanded_project_groups:
828 expanded_project_groups += ["default"]
829
830 matched = False
831 for group in expanded_manifest_groups:
832 if group.startswith("-") and group[1:] in expanded_project_groups:
833 matched = False
834 elif group in expanded_project_groups:
835 matched = True
836
837 return matched
838
839 def UncommitedFiles(self, get_all=True):
840 """Returns a list of strings, uncommitted files in the git tree.
841
842 Args:
843 get_all: a boolean, if True - get information about all different
844 uncommitted files. If False - return as soon as any kind of
845 uncommitted files is detected.
846 """
847 details = []
848 self.work_git.update_index(
849 "-q", "--unmerged", "--ignore-missing", "--refresh"
850 )
851 if self.IsRebaseInProgress():
852 details.append("rebase in progress")
853 if not get_all:
854 return details
855
856 changes = self.work_git.DiffZ("diff-index", "--cached", HEAD).keys()
857 if changes:
858 details.extend(changes)
859 if not get_all:
860 return details
861
862 changes = self.work_git.DiffZ("diff-files").keys()
863 if changes:
864 details.extend(changes)
865 if not get_all:
866 return details
867
868 changes = self.UntrackedFiles()
869 if changes:
870 details.extend(changes)
871
872 return details
873
874 def UntrackedFiles(self):
875 """Returns a list of strings, untracked files in the git tree."""
876 return self.work_git.LsOthers()
877
878 def HasChanges(self):
879 """Returns true if there are uncommitted changes."""
880 return bool(self.UncommitedFiles(get_all=False))
881
882 def PrintWorkTreeStatus(self, output_redir=None, quiet=False, local=False):
883 """Prints the status of the repository to stdout.
884
885 Args:
886 output_redir: If specified, redirect the output to this object.
887 quiet: If True then only print the project name. Do not print
888 the modified files, branch name, etc.
889 local: a boolean, if True, the path is relative to the local
890 (sub)manifest. If false, the path is relative to the outermost
891 manifest.
892 """
893 if not platform_utils.isdir(self.worktree):
894 if output_redir is None:
895 output_redir = sys.stdout
896 print(file=output_redir)
897 print("project %s/" % self.RelPath(local), file=output_redir)
898 print(' missing (run "repo sync")', file=output_redir)
899 return
900
901 self.work_git.update_index(
902 "-q", "--unmerged", "--ignore-missing", "--refresh"
903 )
904 rb = self.IsRebaseInProgress()
905 di = self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD)
906 df = self.work_git.DiffZ("diff-files")
907 do = self.work_git.LsOthers()
908 if not rb and not di and not df and not do and not self.CurrentBranch:
909 return "CLEAN"
910
911 out = StatusColoring(self.config)
912 if output_redir is not None:
913 out.redirect(output_redir)
914 out.project("project %-40s", self.RelPath(local) + "/ ")
915
916 if quiet:
917 out.nl()
918 return "DIRTY"
919
920 branch = self.CurrentBranch
921 if branch is None:
922 out.nobranch("(*** NO BRANCH ***)")
923 else:
924 out.branch("branch %s", branch)
925 out.nl()
926
927 if rb:
928 out.important("prior sync failed; rebase still in progress")
929 out.nl()
930
931 paths = list()
932 paths.extend(di.keys())
933 paths.extend(df.keys())
934 paths.extend(do)
935
936 for p in sorted(set(paths)):
937 try:
938 i = di[p]
939 except KeyError:
940 i = None
941
942 try:
943 f = df[p]
944 except KeyError:
945 f = None
946
947 if i:
948 i_status = i.status.upper()
949 else:
950 i_status = "-"
951
952 if f:
953 f_status = f.status.lower()
954 else:
955 f_status = "-"
956
957 if i and i.src_path:
958 line = " %s%s\t%s => %s (%s%%)" % (
959 i_status,
960 f_status,
961 i.src_path,
962 p,
963 i.level,
964 )
965 else:
966 line = " %s%s\t%s" % (i_status, f_status, p)
967
968 if i and not f:
969 out.added("%s", line)
970 elif (i and f) or (not i and f):
971 out.changed("%s", line)
972 elif not i and not f:
973 out.untracked("%s", line)
974 else:
975 out.write("%s", line)
976 out.nl()
977
978 return "DIRTY"
979
980 def PrintWorkTreeDiff(
981 self, absolute_paths=False, output_redir=None, local=False
982 ):
983 """Prints the status of the repository to stdout."""
984 out = DiffColoring(self.config)
985 if output_redir:
986 out.redirect(output_redir)
987 cmd = ["diff"]
988 if out.is_on:
989 cmd.append("--color")
990 cmd.append(HEAD)
991 if absolute_paths:
992 cmd.append("--src-prefix=a/%s/" % self.RelPath(local))
993 cmd.append("--dst-prefix=b/%s/" % self.RelPath(local))
994 cmd.append("--")
995 try:
996 p = GitCommand(self, cmd, capture_stdout=True, capture_stderr=True)
997 p.Wait()
998 except GitError as e:
999 out.nl()
1000 out.project("project %s/" % self.RelPath(local))
1001 out.nl()
1002 out.fail("%s", str(e))
1003 out.nl()
1004 return False
1005 if p.stdout:
1006 out.nl()
1007 out.project("project %s/" % self.RelPath(local))
1008 out.nl()
1009 out.write("%s", p.stdout)
1010 return p.Wait() == 0
1011
1012 def WasPublished(self, branch, all_refs=None):
1013 """Was the branch published (uploaded) for code review?
1014 If so, returns the SHA-1 hash of the last published
1015 state for the branch.
1016 """
1017 key = R_PUB + branch
1018 if all_refs is None:
1019 try:
1020 return self.bare_git.rev_parse(key)
1021 except GitError:
1022 return None
1023 else:
1024 try:
1025 return all_refs[key]
1026 except KeyError:
1027 return None
1028
1029 def CleanPublishedCache(self, all_refs=None):
1030 """Prunes any stale published refs."""
1031 if all_refs is None:
1032 all_refs = self._allrefs
1033 heads = set()
1034 canrm = {}
1035 for name, ref_id in all_refs.items():
1036 if name.startswith(R_HEADS):
1037 heads.add(name)
1038 elif name.startswith(R_PUB):
1039 canrm[name] = ref_id
1040
1041 for name, ref_id in canrm.items():
1042 n = name[len(R_PUB) :]
1043 if R_HEADS + n not in heads:
1044 self.bare_git.DeleteRef(name, ref_id)
1045
1046 def GetUploadableBranches(self, selected_branch=None):
1047 """List any branches which can be uploaded for review."""
1048 heads = {}
1049 pubed = {}
1050
1051 for name, ref_id in self._allrefs.items():
1052 if name.startswith(R_HEADS):
1053 heads[name[len(R_HEADS) :]] = ref_id
1054 elif name.startswith(R_PUB):
1055 pubed[name[len(R_PUB) :]] = ref_id
1056
1057 ready = []
1058 for branch, ref_id in heads.items():
1059 if branch in pubed and pubed[branch] == ref_id:
1060 continue
1061 if selected_branch and branch != selected_branch:
1062 continue
1063
1064 rb = self.GetUploadableBranch(branch)
1065 if rb:
1066 ready.append(rb)
1067 return ready
1068
1069 def GetUploadableBranch(self, branch_name):
1070 """Get a single uploadable branch, or None."""
1071 branch = self.GetBranch(branch_name)
1072 base = branch.LocalMerge
1073 if branch.LocalMerge:
1074 rb = ReviewableBranch(self, branch, base)
1075 if rb.commits:
1076 return rb
1077 return None
1078
1079 def UploadForReview(
1080 self,
1081 branch=None,
1082 people=([], []),
1083 dryrun=False,
1084 auto_topic=False,
1085 hashtags=(),
1086 labels=(),
1087 private=False,
1088 notify=None,
1089 wip=False,
1090 ready=False,
1091 dest_branch=None,
1092 validate_certs=True,
1093 push_options=None,
1094 ):
1095 """Uploads the named branch for code review."""
1096 if branch is None:
1097 branch = self.CurrentBranch
1098 if branch is None:
Jason Chang32b59562023-07-14 16:45:35 -07001099 raise GitError("not currently on a branch", project=self.name)
Gavin Makea2e3302023-03-11 06:46:20 +00001100
1101 branch = self.GetBranch(branch)
1102 if not branch.LocalMerge:
Jason Chang32b59562023-07-14 16:45:35 -07001103 raise GitError(
1104 "branch %s does not track a remote" % branch.name,
1105 project=self.name,
1106 )
Gavin Makea2e3302023-03-11 06:46:20 +00001107 if not branch.remote.review:
Jason Chang32b59562023-07-14 16:45:35 -07001108 raise GitError(
1109 "remote %s has no review url" % branch.remote.name,
1110 project=self.name,
1111 )
Gavin Makea2e3302023-03-11 06:46:20 +00001112
1113 # Basic validity check on label syntax.
1114 for label in labels:
1115 if not re.match(r"^.+[+-][0-9]+$", label):
1116 raise UploadError(
1117 f'invalid label syntax "{label}": labels use forms like '
Jason Chang5a3a5f72023-08-17 11:36:41 -07001118 "CodeReview+1 or Verified-1",
1119 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00001120 )
1121
1122 if dest_branch is None:
1123 dest_branch = self.dest_branch
1124 if dest_branch is None:
1125 dest_branch = branch.merge
1126 if not dest_branch.startswith(R_HEADS):
1127 dest_branch = R_HEADS + dest_branch
1128
1129 if not branch.remote.projectname:
1130 branch.remote.projectname = self.name
1131 branch.remote.Save()
1132
1133 url = branch.remote.ReviewUrl(self.UserEmail, validate_certs)
1134 if url is None:
Jason Chang5a3a5f72023-08-17 11:36:41 -07001135 raise UploadError("review not configured", project=self.name)
Gavin Makea2e3302023-03-11 06:46:20 +00001136 cmd = ["push"]
1137 if dryrun:
1138 cmd.append("-n")
1139
1140 if url.startswith("ssh://"):
1141 cmd.append("--receive-pack=gerrit receive-pack")
1142
Aravind Vasudevan99ebf622023-04-04 23:44:37 +00001143 # This stops git from pushing all reachable annotated tags when
1144 # push.followTags is configured. Gerrit does not accept any tags
1145 # pushed to a CL.
1146 if git_require((1, 8, 3)):
1147 cmd.append("--no-follow-tags")
1148
Gavin Makea2e3302023-03-11 06:46:20 +00001149 for push_option in push_options or []:
1150 cmd.append("-o")
1151 cmd.append(push_option)
1152
1153 cmd.append(url)
1154
1155 if dest_branch.startswith(R_HEADS):
1156 dest_branch = dest_branch[len(R_HEADS) :]
1157
1158 ref_spec = "%s:refs/for/%s" % (R_HEADS + branch.name, dest_branch)
1159 opts = []
1160 if auto_topic:
1161 opts += ["topic=" + branch.name]
1162 opts += ["t=%s" % p for p in hashtags]
1163 # NB: No need to encode labels as they've been validated above.
1164 opts += ["l=%s" % p for p in labels]
1165
1166 opts += ["r=%s" % p for p in people[0]]
1167 opts += ["cc=%s" % p for p in people[1]]
1168 if notify:
1169 opts += ["notify=" + notify]
1170 if private:
1171 opts += ["private"]
1172 if wip:
1173 opts += ["wip"]
1174 if ready:
1175 opts += ["ready"]
1176 if opts:
1177 ref_spec = ref_spec + "%" + ",".join(opts)
1178 cmd.append(ref_spec)
1179
Jason Chang1e9f7b92023-08-25 10:31:04 -07001180 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001181
1182 if not dryrun:
1183 msg = "posted to %s for %s" % (branch.remote.review, dest_branch)
1184 self.bare_git.UpdateRef(
1185 R_PUB + branch.name, R_HEADS + branch.name, message=msg
1186 )
1187
1188 def _ExtractArchive(self, tarpath, path=None):
1189 """Extract the given tar on its current location
1190
1191 Args:
1192 tarpath: The path to the actual tar file
1193
1194 """
1195 try:
1196 with tarfile.open(tarpath, "r") as tar:
1197 tar.extractall(path=path)
1198 return True
1199 except (IOError, tarfile.TarError) as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001200 logger.error("error: Cannot extract archive %s: %s", tarpath, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001201 return False
1202
1203 def Sync_NetworkHalf(
1204 self,
1205 quiet=False,
1206 verbose=False,
1207 output_redir=None,
1208 is_new=None,
1209 current_branch_only=None,
1210 force_sync=False,
1211 clone_bundle=True,
1212 tags=None,
1213 archive=False,
1214 optimized_fetch=False,
1215 retry_fetches=0,
1216 prune=False,
1217 submodules=False,
1218 ssh_proxy=None,
1219 clone_filter=None,
1220 partial_clone_exclude=set(),
Jason Chang17833322023-05-23 13:06:55 -07001221 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001222 ):
1223 """Perform only the network IO portion of the sync process.
1224 Local working directory/branch state is not affected.
1225 """
1226 if archive and not isinstance(self, MetaProject):
1227 if self.remote.url.startswith(("http://", "https://")):
Jason Chang32b59562023-07-14 16:45:35 -07001228 msg_template = (
1229 "%s: Cannot fetch archives from http/https remotes."
Gavin Makea2e3302023-03-11 06:46:20 +00001230 )
Jason Chang32b59562023-07-14 16:45:35 -07001231 msg_args = self.name
1232 msg = msg_template % msg_args
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001233 logger.error(msg_template, msg_args)
Jason Chang32b59562023-07-14 16:45:35 -07001234 return SyncNetworkHalfResult(
1235 False, SyncNetworkHalfError(msg, project=self.name)
1236 )
Gavin Makea2e3302023-03-11 06:46:20 +00001237
1238 name = self.relpath.replace("\\", "/")
1239 name = name.replace("/", "_")
1240 tarpath = "%s.tar" % name
1241 topdir = self.manifest.topdir
1242
1243 try:
1244 self._FetchArchive(tarpath, cwd=topdir)
1245 except GitError as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001246 logger.error("error: %s", e)
Jason Chang32b59562023-07-14 16:45:35 -07001247 return SyncNetworkHalfResult(False, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001248
1249 # From now on, we only need absolute tarpath.
1250 tarpath = os.path.join(topdir, tarpath)
1251
1252 if not self._ExtractArchive(tarpath, path=topdir):
Jason Chang32b59562023-07-14 16:45:35 -07001253 return SyncNetworkHalfResult(
1254 True,
1255 SyncNetworkHalfError(
1256 f"Unable to Extract Archive {tarpath}",
1257 project=self.name,
1258 ),
1259 )
Gavin Makea2e3302023-03-11 06:46:20 +00001260 try:
1261 platform_utils.remove(tarpath)
1262 except OSError as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001263 logger.warn("warn: Cannot remove archive %s: %s", tarpath, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001264 self._CopyAndLinkFiles()
Jason Chang32b59562023-07-14 16:45:35 -07001265 return SyncNetworkHalfResult(True)
Gavin Makea2e3302023-03-11 06:46:20 +00001266
1267 # If the shared object dir already exists, don't try to rebootstrap with
1268 # a clone bundle download. We should have the majority of objects
1269 # already.
1270 if clone_bundle and os.path.exists(self.objdir):
1271 clone_bundle = False
1272
1273 if self.name in partial_clone_exclude:
1274 clone_bundle = True
1275 clone_filter = None
1276
1277 if is_new is None:
1278 is_new = not self.Exists
1279 if is_new:
1280 self._InitGitDir(force_sync=force_sync, quiet=quiet)
1281 else:
1282 self._UpdateHooks(quiet=quiet)
1283 self._InitRemote()
1284
1285 if self.UseAlternates:
1286 # If gitdir/objects is a symlink, migrate it from the old layout.
1287 gitdir_objects = os.path.join(self.gitdir, "objects")
1288 if platform_utils.islink(gitdir_objects):
1289 platform_utils.remove(gitdir_objects, missing_ok=True)
1290 gitdir_alt = os.path.join(self.gitdir, "objects/info/alternates")
1291 if not os.path.exists(gitdir_alt):
1292 os.makedirs(os.path.dirname(gitdir_alt), exist_ok=True)
1293 _lwrite(
1294 gitdir_alt,
1295 os.path.join(
1296 os.path.relpath(self.objdir, gitdir_objects), "objects"
1297 )
1298 + "\n",
1299 )
1300
1301 if is_new:
1302 alt = os.path.join(self.objdir, "objects/info/alternates")
1303 try:
1304 with open(alt) as fd:
1305 # This works for both absolute and relative alternate
1306 # directories.
1307 alt_dir = os.path.join(
1308 self.objdir, "objects", fd.readline().rstrip()
1309 )
1310 except IOError:
1311 alt_dir = None
1312 else:
1313 alt_dir = None
1314
1315 if (
1316 clone_bundle
1317 and alt_dir is None
1318 and self._ApplyCloneBundle(
1319 initial=is_new, quiet=quiet, verbose=verbose
1320 )
1321 ):
1322 is_new = False
1323
1324 if current_branch_only is None:
1325 if self.sync_c:
1326 current_branch_only = True
1327 elif not self.manifest._loaded:
1328 # Manifest cannot check defaults until it syncs.
1329 current_branch_only = False
1330 elif self.manifest.default.sync_c:
1331 current_branch_only = True
1332
1333 if tags is None:
1334 tags = self.sync_tags
1335
1336 if self.clone_depth:
1337 depth = self.clone_depth
1338 else:
1339 depth = self.manifest.manifestProject.depth
1340
Jason Chang17833322023-05-23 13:06:55 -07001341 if depth and clone_filter_for_depth:
1342 depth = None
1343 clone_filter = clone_filter_for_depth
1344
Gavin Makea2e3302023-03-11 06:46:20 +00001345 # See if we can skip the network fetch entirely.
1346 remote_fetched = False
1347 if not (
1348 optimized_fetch
Sylvain56a5a012023-09-11 13:38:00 +02001349 and IsId(self.revisionExpr)
1350 and self._CheckForImmutableRevision()
Gavin Makea2e3302023-03-11 06:46:20 +00001351 ):
1352 remote_fetched = True
Jason Chang32b59562023-07-14 16:45:35 -07001353 try:
1354 if not self._RemoteFetch(
1355 initial=is_new,
1356 quiet=quiet,
1357 verbose=verbose,
1358 output_redir=output_redir,
1359 alt_dir=alt_dir,
1360 current_branch_only=current_branch_only,
1361 tags=tags,
1362 prune=prune,
1363 depth=depth,
1364 submodules=submodules,
1365 force_sync=force_sync,
1366 ssh_proxy=ssh_proxy,
1367 clone_filter=clone_filter,
1368 retry_fetches=retry_fetches,
1369 ):
1370 return SyncNetworkHalfResult(
1371 remote_fetched,
1372 SyncNetworkHalfError(
1373 f"Unable to remote fetch project {self.name}",
1374 project=self.name,
1375 ),
1376 )
1377 except RepoError as e:
1378 return SyncNetworkHalfResult(
1379 remote_fetched,
1380 e,
1381 )
Gavin Makea2e3302023-03-11 06:46:20 +00001382
1383 mp = self.manifest.manifestProject
1384 dissociate = mp.dissociate
1385 if dissociate:
1386 alternates_file = os.path.join(
1387 self.objdir, "objects/info/alternates"
1388 )
1389 if os.path.exists(alternates_file):
1390 cmd = ["repack", "-a", "-d"]
1391 p = GitCommand(
1392 self,
1393 cmd,
1394 bare=True,
1395 capture_stdout=bool(output_redir),
1396 merge_output=bool(output_redir),
1397 )
1398 if p.stdout and output_redir:
1399 output_redir.write(p.stdout)
1400 if p.Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07001401 return SyncNetworkHalfResult(
1402 remote_fetched,
1403 GitError(
1404 "Unable to repack alternates", project=self.name
1405 ),
1406 )
Gavin Makea2e3302023-03-11 06:46:20 +00001407 platform_utils.remove(alternates_file)
1408
1409 if self.worktree:
1410 self._InitMRef()
1411 else:
1412 self._InitMirrorHead()
1413 platform_utils.remove(
1414 os.path.join(self.gitdir, "FETCH_HEAD"), missing_ok=True
1415 )
Jason Chang32b59562023-07-14 16:45:35 -07001416 return SyncNetworkHalfResult(remote_fetched)
Gavin Makea2e3302023-03-11 06:46:20 +00001417
1418 def PostRepoUpgrade(self):
1419 self._InitHooks()
1420
1421 def _CopyAndLinkFiles(self):
1422 if self.client.isGitcClient:
1423 return
1424 for copyfile in self.copyfiles:
1425 copyfile._Copy()
1426 for linkfile in self.linkfiles:
1427 linkfile._Link()
1428
1429 def GetCommitRevisionId(self):
1430 """Get revisionId of a commit.
1431
1432 Use this method instead of GetRevisionId to get the id of the commit
1433 rather than the id of the current git object (for example, a tag)
1434
1435 """
Sylvaine9cb3912023-09-10 23:35:01 +02001436 if self.revisionId:
1437 return self.revisionId
Gavin Makea2e3302023-03-11 06:46:20 +00001438 if not self.revisionExpr.startswith(R_TAGS):
1439 return self.GetRevisionId(self._allrefs)
1440
1441 try:
1442 return self.bare_git.rev_list(self.revisionExpr, "-1")[0]
1443 except GitError:
1444 raise ManifestInvalidRevisionError(
1445 "revision %s in %s not found" % (self.revisionExpr, self.name)
1446 )
1447
1448 def GetRevisionId(self, all_refs=None):
1449 if self.revisionId:
1450 return self.revisionId
1451
1452 rem = self.GetRemote()
1453 rev = rem.ToLocal(self.revisionExpr)
1454
1455 if all_refs is not None and rev in all_refs:
1456 return all_refs[rev]
1457
1458 try:
1459 return self.bare_git.rev_parse("--verify", "%s^0" % rev)
1460 except GitError:
1461 raise ManifestInvalidRevisionError(
1462 "revision %s in %s not found" % (self.revisionExpr, self.name)
1463 )
1464
1465 def SetRevisionId(self, revisionId):
1466 if self.revisionExpr:
1467 self.upstream = self.revisionExpr
1468
1469 self.revisionId = revisionId
1470
Jason Chang32b59562023-07-14 16:45:35 -07001471 def Sync_LocalHalf(
1472 self, syncbuf, force_sync=False, submodules=False, errors=None
1473 ):
Gavin Makea2e3302023-03-11 06:46:20 +00001474 """Perform only the local IO portion of the sync process.
1475
1476 Network access is not required.
1477 """
Jason Chang32b59562023-07-14 16:45:35 -07001478 if errors is None:
1479 errors = []
1480
1481 def fail(error: Exception):
1482 errors.append(error)
1483 syncbuf.fail(self, error)
1484
Gavin Makea2e3302023-03-11 06:46:20 +00001485 if not os.path.exists(self.gitdir):
Jason Chang32b59562023-07-14 16:45:35 -07001486 fail(
1487 LocalSyncFail(
1488 "Cannot checkout %s due to missing network sync; Run "
1489 "`repo sync -n %s` first." % (self.name, self.name),
1490 project=self.name,
1491 )
Gavin Makea2e3302023-03-11 06:46:20 +00001492 )
1493 return
1494
1495 self._InitWorkTree(force_sync=force_sync, submodules=submodules)
1496 all_refs = self.bare_ref.all
1497 self.CleanPublishedCache(all_refs)
1498 revid = self.GetRevisionId(all_refs)
1499
1500 # Special case the root of the repo client checkout. Make sure it
1501 # doesn't contain files being checked out to dirs we don't allow.
1502 if self.relpath == ".":
1503 PROTECTED_PATHS = {".repo"}
1504 paths = set(
1505 self.work_git.ls_tree("-z", "--name-only", "--", revid).split(
1506 "\0"
1507 )
1508 )
1509 bad_paths = paths & PROTECTED_PATHS
1510 if bad_paths:
Jason Chang32b59562023-07-14 16:45:35 -07001511 fail(
1512 LocalSyncFail(
1513 "Refusing to checkout project that writes to protected "
1514 "paths: %s" % (", ".join(bad_paths),),
1515 project=self.name,
1516 )
Gavin Makea2e3302023-03-11 06:46:20 +00001517 )
1518 return
1519
1520 def _doff():
1521 self._FastForward(revid)
1522 self._CopyAndLinkFiles()
1523
1524 def _dosubmodules():
1525 self._SyncSubmodules(quiet=True)
1526
1527 head = self.work_git.GetHead()
1528 if head.startswith(R_HEADS):
1529 branch = head[len(R_HEADS) :]
1530 try:
1531 head = all_refs[head]
1532 except KeyError:
1533 head = None
1534 else:
1535 branch = None
1536
1537 if branch is None or syncbuf.detach_head:
1538 # Currently on a detached HEAD. The user is assumed to
1539 # not have any local modifications worth worrying about.
1540 if self.IsRebaseInProgress():
Jason Chang32b59562023-07-14 16:45:35 -07001541 fail(_PriorSyncFailedError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001542 return
1543
1544 if head == revid:
1545 # No changes; don't do anything further.
1546 # Except if the head needs to be detached.
1547 if not syncbuf.detach_head:
1548 # The copy/linkfile config may have changed.
1549 self._CopyAndLinkFiles()
1550 return
1551 else:
1552 lost = self._revlist(not_rev(revid), HEAD)
1553 if lost:
1554 syncbuf.info(self, "discarding %d commits", len(lost))
1555
1556 try:
1557 self._Checkout(revid, quiet=True)
1558 if submodules:
1559 self._SyncSubmodules(quiet=True)
1560 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001561 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001562 return
1563 self._CopyAndLinkFiles()
1564 return
1565
1566 if head == revid:
1567 # No changes; don't do anything further.
1568 #
1569 # The copy/linkfile config may have changed.
1570 self._CopyAndLinkFiles()
1571 return
1572
1573 branch = self.GetBranch(branch)
1574
1575 if not branch.LocalMerge:
1576 # The current branch has no tracking configuration.
1577 # Jump off it to a detached HEAD.
1578 syncbuf.info(
1579 self, "leaving %s; does not track upstream", branch.name
1580 )
1581 try:
1582 self._Checkout(revid, quiet=True)
1583 if submodules:
1584 self._SyncSubmodules(quiet=True)
1585 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001586 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001587 return
1588 self._CopyAndLinkFiles()
1589 return
1590
1591 upstream_gain = self._revlist(not_rev(HEAD), revid)
1592
1593 # See if we can perform a fast forward merge. This can happen if our
1594 # branch isn't in the exact same state as we last published.
1595 try:
1596 self.work_git.merge_base("--is-ancestor", HEAD, revid)
1597 # Skip the published logic.
1598 pub = False
1599 except GitError:
1600 pub = self.WasPublished(branch.name, all_refs)
1601
1602 if pub:
1603 not_merged = self._revlist(not_rev(revid), pub)
1604 if not_merged:
1605 if upstream_gain:
1606 # The user has published this branch and some of those
1607 # commits are not yet merged upstream. We do not want
1608 # to rewrite the published commits so we punt.
Jason Chang32b59562023-07-14 16:45:35 -07001609 fail(
1610 LocalSyncFail(
1611 "branch %s is published (but not merged) and is "
1612 "now %d commits behind"
1613 % (branch.name, len(upstream_gain)),
1614 project=self.name,
1615 )
Gavin Makea2e3302023-03-11 06:46:20 +00001616 )
1617 return
1618 elif pub == head:
1619 # All published commits are merged, and thus we are a
1620 # strict subset. We can fast-forward safely.
1621 syncbuf.later1(self, _doff)
1622 if submodules:
1623 syncbuf.later1(self, _dosubmodules)
1624 return
1625
1626 # Examine the local commits not in the remote. Find the
1627 # last one attributed to this user, if any.
1628 local_changes = self._revlist(not_rev(revid), HEAD, format="%H %ce")
1629 last_mine = None
1630 cnt_mine = 0
1631 for commit in local_changes:
1632 commit_id, committer_email = commit.split(" ", 1)
1633 if committer_email == self.UserEmail:
1634 last_mine = commit_id
1635 cnt_mine += 1
1636
1637 if not upstream_gain and cnt_mine == len(local_changes):
1638 # The copy/linkfile config may have changed.
1639 self._CopyAndLinkFiles()
1640 return
1641
1642 if self.IsDirty(consider_untracked=False):
Jason Chang32b59562023-07-14 16:45:35 -07001643 fail(_DirtyError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001644 return
1645
1646 # If the upstream switched on us, warn the user.
1647 if branch.merge != self.revisionExpr:
1648 if branch.merge and self.revisionExpr:
1649 syncbuf.info(
1650 self,
1651 "manifest switched %s...%s",
1652 branch.merge,
1653 self.revisionExpr,
1654 )
1655 elif branch.merge:
1656 syncbuf.info(self, "manifest no longer tracks %s", branch.merge)
1657
1658 if cnt_mine < len(local_changes):
1659 # Upstream rebased. Not everything in HEAD was created by this user.
1660 syncbuf.info(
1661 self,
1662 "discarding %d commits removed from upstream",
1663 len(local_changes) - cnt_mine,
1664 )
1665
1666 branch.remote = self.GetRemote()
Sylvain56a5a012023-09-11 13:38:00 +02001667 if not IsId(self.revisionExpr):
Gavin Makea2e3302023-03-11 06:46:20 +00001668 # In case of manifest sync the revisionExpr might be a SHA1.
1669 branch.merge = self.revisionExpr
1670 if not branch.merge.startswith("refs/"):
1671 branch.merge = R_HEADS + branch.merge
1672 branch.Save()
1673
1674 if cnt_mine > 0 and self.rebase:
1675
1676 def _docopyandlink():
1677 self._CopyAndLinkFiles()
1678
1679 def _dorebase():
1680 self._Rebase(upstream="%s^1" % last_mine, onto=revid)
1681
1682 syncbuf.later2(self, _dorebase)
1683 if submodules:
1684 syncbuf.later2(self, _dosubmodules)
1685 syncbuf.later2(self, _docopyandlink)
1686 elif local_changes:
1687 try:
1688 self._ResetHard(revid)
1689 if submodules:
1690 self._SyncSubmodules(quiet=True)
1691 self._CopyAndLinkFiles()
1692 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001693 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001694 return
1695 else:
1696 syncbuf.later1(self, _doff)
1697 if submodules:
1698 syncbuf.later1(self, _dosubmodules)
1699
1700 def AddCopyFile(self, src, dest, topdir):
1701 """Mark |src| for copying to |dest| (relative to |topdir|).
1702
1703 No filesystem changes occur here. Actual copying happens later on.
1704
1705 Paths should have basic validation run on them before being queued.
1706 Further checking will be handled when the actual copy happens.
1707 """
1708 self.copyfiles.append(_CopyFile(self.worktree, src, topdir, dest))
1709
1710 def AddLinkFile(self, src, dest, topdir):
1711 """Mark |dest| to create a symlink (relative to |topdir|) pointing to
1712 |src|.
1713
1714 No filesystem changes occur here. Actual linking happens later on.
1715
1716 Paths should have basic validation run on them before being queued.
1717 Further checking will be handled when the actual link happens.
1718 """
1719 self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest))
1720
1721 def AddAnnotation(self, name, value, keep):
1722 self.annotations.append(Annotation(name, value, keep))
1723
1724 def DownloadPatchSet(self, change_id, patch_id):
1725 """Download a single patch set of a single change to FETCH_HEAD."""
1726 remote = self.GetRemote()
1727
1728 cmd = ["fetch", remote.name]
1729 cmd.append(
1730 "refs/changes/%2.2d/%d/%d" % (change_id % 100, change_id, patch_id)
1731 )
Jason Chang1a3612f2023-08-08 14:12:53 -07001732 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001733 return DownloadedChange(
1734 self,
1735 self.GetRevisionId(),
1736 change_id,
1737 patch_id,
1738 self.bare_git.rev_parse("FETCH_HEAD"),
1739 )
1740
1741 def DeleteWorktree(self, quiet=False, force=False):
1742 """Delete the source checkout and any other housekeeping tasks.
1743
1744 This currently leaves behind the internal .repo/ cache state. This
1745 helps when switching branches or manifest changes get reverted as we
1746 don't have to redownload all the git objects. But we should do some GC
1747 at some point.
1748
1749 Args:
1750 quiet: Whether to hide normal messages.
1751 force: Always delete tree even if dirty.
Doug Anderson3ba5f952011-04-07 12:51:04 -07001752
1753 Returns:
Gavin Makea2e3302023-03-11 06:46:20 +00001754 True if the worktree was completely cleaned out.
1755 """
1756 if self.IsDirty():
1757 if force:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001758 logger.warn(
Gavin Makea2e3302023-03-11 06:46:20 +00001759 "warning: %s: Removing dirty project: uncommitted changes "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001760 "lost.",
1761 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001762 )
1763 else:
Jason Chang32b59562023-07-14 16:45:35 -07001764 msg = (
1765 "error: %s: Cannot remove project: uncommitted"
1766 "changes are present.\n" % self.RelPath(local=False)
Gavin Makea2e3302023-03-11 06:46:20 +00001767 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001768 logger.error(msg)
Jason Chang32b59562023-07-14 16:45:35 -07001769 raise DeleteDirtyWorktreeError(msg, project=self)
Wink Saville02d79452009-04-10 13:01:24 -07001770
Gavin Makea2e3302023-03-11 06:46:20 +00001771 if not quiet:
1772 print(
1773 "%s: Deleting obsolete checkout." % (self.RelPath(local=False),)
1774 )
Wink Saville02d79452009-04-10 13:01:24 -07001775
Gavin Makea2e3302023-03-11 06:46:20 +00001776 # Unlock and delink from the main worktree. We don't use git's worktree
1777 # remove because it will recursively delete projects -- we handle that
1778 # ourselves below. https://crbug.com/git/48
1779 if self.use_git_worktrees:
1780 needle = platform_utils.realpath(self.gitdir)
1781 # Find the git worktree commondir under .repo/worktrees/.
1782 output = self.bare_git.worktree("list", "--porcelain").splitlines()[
1783 0
1784 ]
1785 assert output.startswith("worktree "), output
1786 commondir = output[9:]
1787 # Walk each of the git worktrees to see where they point.
1788 configs = os.path.join(commondir, "worktrees")
1789 for name in os.listdir(configs):
1790 gitdir = os.path.join(configs, name, "gitdir")
1791 with open(gitdir) as fp:
1792 relpath = fp.read().strip()
1793 # Resolve the checkout path and see if it matches this project.
1794 fullpath = platform_utils.realpath(
1795 os.path.join(configs, name, relpath)
1796 )
1797 if fullpath == needle:
1798 platform_utils.rmtree(os.path.join(configs, name))
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001799
Gavin Makea2e3302023-03-11 06:46:20 +00001800 # Delete the .git directory first, so we're less likely to have a
1801 # partially working git repository around. There shouldn't be any git
1802 # projects here, so rmtree works.
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001803
Gavin Makea2e3302023-03-11 06:46:20 +00001804 # Try to remove plain files first in case of git worktrees. If this
1805 # fails for any reason, we'll fall back to rmtree, and that'll display
1806 # errors if it can't remove things either.
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001807 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001808 platform_utils.remove(self.gitdir)
1809 except OSError:
1810 pass
1811 try:
1812 platform_utils.rmtree(self.gitdir)
1813 except OSError as e:
1814 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001815 logger.error("error: %s: %s", self.gitdir, e)
1816 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00001817 "error: %s: Failed to delete obsolete checkout; remove "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001818 "manually, then run `repo sync -l`.",
1819 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001820 )
Jason Chang32b59562023-07-14 16:45:35 -07001821 raise DeleteWorktreeError(aggregate_errors=[e])
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001822
Gavin Makea2e3302023-03-11 06:46:20 +00001823 # Delete everything under the worktree, except for directories that
1824 # contain another git project.
1825 dirs_to_remove = []
1826 failed = False
Jason Chang32b59562023-07-14 16:45:35 -07001827 errors = []
Gavin Makea2e3302023-03-11 06:46:20 +00001828 for root, dirs, files in platform_utils.walk(self.worktree):
1829 for f in files:
1830 path = os.path.join(root, f)
1831 try:
1832 platform_utils.remove(path)
1833 except OSError as e:
1834 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001835 logger.error("error: %s: Failed to remove: %s", path, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001836 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001837 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001838 dirs[:] = [
1839 d
1840 for d in dirs
1841 if not os.path.lexists(os.path.join(root, d, ".git"))
1842 ]
1843 dirs_to_remove += [
1844 os.path.join(root, d)
1845 for d in dirs
1846 if os.path.join(root, d) not in dirs_to_remove
1847 ]
1848 for d in reversed(dirs_to_remove):
1849 if platform_utils.islink(d):
1850 try:
1851 platform_utils.remove(d)
1852 except OSError as e:
1853 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001854 logger.error("error: %s: Failed to remove: %s", d, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001855 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001856 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001857 elif not platform_utils.listdir(d):
1858 try:
1859 platform_utils.rmdir(d)
1860 except OSError as e:
1861 if e.errno != errno.ENOENT:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001862 logger.error("error: %s: Failed to remove: %s", d, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001863 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001864 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001865 if failed:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001866 logger.error(
1867 "error: %s: Failed to delete obsolete checkout.",
1868 self.RelPath(local=False),
Gavin Makea2e3302023-03-11 06:46:20 +00001869 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00001870 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00001871 " Remove manually, then run `repo sync -l`.",
Gavin Makea2e3302023-03-11 06:46:20 +00001872 )
Jason Chang32b59562023-07-14 16:45:35 -07001873 raise DeleteWorktreeError(aggregate_errors=errors)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07001874
Gavin Makea2e3302023-03-11 06:46:20 +00001875 # Try deleting parent dirs if they are empty.
1876 path = self.worktree
1877 while path != self.manifest.topdir:
1878 try:
1879 platform_utils.rmdir(path)
1880 except OSError as e:
1881 if e.errno != errno.ENOENT:
1882 break
1883 path = os.path.dirname(path)
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001884
Gavin Makea2e3302023-03-11 06:46:20 +00001885 return True
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001886
Gavin Makea2e3302023-03-11 06:46:20 +00001887 def StartBranch(self, name, branch_merge="", revision=None):
1888 """Create a new branch off the manifest's revision."""
1889 if not branch_merge:
1890 branch_merge = self.revisionExpr
1891 head = self.work_git.GetHead()
1892 if head == (R_HEADS + name):
1893 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001894
David Pursehouse8a68ff92012-09-24 12:15:13 +09001895 all_refs = self.bare_ref.all
Gavin Makea2e3302023-03-11 06:46:20 +00001896 if R_HEADS + name in all_refs:
Jason Chang1a3612f2023-08-08 14:12:53 -07001897 GitCommand(
1898 self, ["checkout", "-q", name, "--"], verify_command=True
1899 ).Wait()
1900 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001901
Gavin Makea2e3302023-03-11 06:46:20 +00001902 branch = self.GetBranch(name)
1903 branch.remote = self.GetRemote()
1904 branch.merge = branch_merge
Sylvain56a5a012023-09-11 13:38:00 +02001905 if not branch.merge.startswith("refs/") and not IsId(branch_merge):
Gavin Makea2e3302023-03-11 06:46:20 +00001906 branch.merge = R_HEADS + branch_merge
Shawn O. Pearce88443382010-10-08 10:02:09 +02001907
Gavin Makea2e3302023-03-11 06:46:20 +00001908 if revision is None:
1909 revid = self.GetRevisionId(all_refs)
Shawn O. Pearce88443382010-10-08 10:02:09 +02001910 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001911 revid = self.work_git.rev_parse(revision)
Brian Harring14a66742012-09-28 20:21:57 -07001912
Gavin Makea2e3302023-03-11 06:46:20 +00001913 if head.startswith(R_HEADS):
Kevin Degiabaa7f32014-11-12 11:27:45 -07001914 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001915 head = all_refs[head]
1916 except KeyError:
1917 head = None
1918 if revid and head and revid == head:
1919 ref = R_HEADS + name
1920 self.work_git.update_ref(ref, revid)
1921 self.work_git.symbolic_ref(HEAD, ref)
1922 branch.Save()
1923 return True
Kevin Degib1a07b82015-07-27 13:33:43 -06001924
Jason Chang1a3612f2023-08-08 14:12:53 -07001925 GitCommand(
1926 self,
1927 ["checkout", "-q", "-b", branch.name, revid],
1928 verify_command=True,
1929 ).Wait()
1930 branch.Save()
1931 return True
Kevin Degi384b3c52014-10-16 16:02:58 -06001932
Gavin Makea2e3302023-03-11 06:46:20 +00001933 def CheckoutBranch(self, name):
1934 """Checkout a local topic branch.
Shawn O. Pearce2816d4f2009-03-03 17:53:18 -08001935
Gavin Makea2e3302023-03-11 06:46:20 +00001936 Args:
1937 name: The name of the branch to checkout.
Shawn O. Pearce88443382010-10-08 10:02:09 +02001938
Gavin Makea2e3302023-03-11 06:46:20 +00001939 Returns:
Jason Chang1a3612f2023-08-08 14:12:53 -07001940 True if the checkout succeeded; False if the
1941 branch doesn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00001942 """
1943 rev = R_HEADS + name
1944 head = self.work_git.GetHead()
1945 if head == rev:
1946 # Already on the branch.
1947 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001948
Gavin Makea2e3302023-03-11 06:46:20 +00001949 all_refs = self.bare_ref.all
1950 try:
1951 revid = all_refs[rev]
1952 except KeyError:
1953 # Branch does not exist in this project.
Jason Chang1a3612f2023-08-08 14:12:53 -07001954 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001955
Gavin Makea2e3302023-03-11 06:46:20 +00001956 if head.startswith(R_HEADS):
1957 try:
1958 head = all_refs[head]
1959 except KeyError:
1960 head = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001961
Gavin Makea2e3302023-03-11 06:46:20 +00001962 if head == revid:
1963 # Same revision; just update HEAD to point to the new
1964 # target branch, but otherwise take no other action.
1965 _lwrite(
1966 self.work_git.GetDotgitPath(subpath=HEAD),
1967 "ref: %s%s\n" % (R_HEADS, name),
1968 )
1969 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05001970
Jason Chang1a3612f2023-08-08 14:12:53 -07001971 GitCommand(
1972 self,
1973 ["checkout", name, "--"],
1974 capture_stdout=True,
1975 capture_stderr=True,
1976 verify_command=True,
1977 ).Wait()
1978 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05001979
Gavin Makea2e3302023-03-11 06:46:20 +00001980 def AbandonBranch(self, name):
1981 """Destroy a local topic branch.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07001982
Gavin Makea2e3302023-03-11 06:46:20 +00001983 Args:
1984 name: The name of the branch to abandon.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07001985
Gavin Makea2e3302023-03-11 06:46:20 +00001986 Returns:
Jason Changf9aacd42023-08-03 14:38:00 -07001987 True if the abandon succeeded; Raises GitCommandError if it didn't;
1988 None if the branch didn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00001989 """
1990 rev = R_HEADS + name
1991 all_refs = self.bare_ref.all
1992 if rev not in all_refs:
1993 # Doesn't exist
1994 return None
1995
1996 head = self.work_git.GetHead()
1997 if head == rev:
1998 # We can't destroy the branch while we are sitting
1999 # on it. Switch to a detached HEAD.
2000 head = all_refs[head]
2001
2002 revid = self.GetRevisionId(all_refs)
2003 if head == revid:
2004 _lwrite(
2005 self.work_git.GetDotgitPath(subpath=HEAD), "%s\n" % revid
2006 )
2007 else:
2008 self._Checkout(revid, quiet=True)
Jason Changf9aacd42023-08-03 14:38:00 -07002009 GitCommand(
2010 self,
2011 ["branch", "-D", name],
2012 capture_stdout=True,
2013 capture_stderr=True,
2014 verify_command=True,
2015 ).Wait()
2016 return True
Gavin Makea2e3302023-03-11 06:46:20 +00002017
2018 def PruneHeads(self):
2019 """Prune any topic branches already merged into upstream."""
2020 cb = self.CurrentBranch
2021 kill = []
2022 left = self._allrefs
2023 for name in left.keys():
2024 if name.startswith(R_HEADS):
2025 name = name[len(R_HEADS) :]
2026 if cb is None or name != cb:
2027 kill.append(name)
2028
2029 # Minor optimization: If there's nothing to prune, then don't try to
2030 # read any project state.
2031 if not kill and not cb:
2032 return []
2033
2034 rev = self.GetRevisionId(left)
2035 if (
2036 cb is not None
2037 and not self._revlist(HEAD + "..." + rev)
2038 and not self.IsDirty(consider_untracked=False)
2039 ):
2040 self.work_git.DetachHead(HEAD)
2041 kill.append(cb)
2042
2043 if kill:
2044 old = self.bare_git.GetHead()
2045
2046 try:
2047 self.bare_git.DetachHead(rev)
2048
2049 b = ["branch", "-d"]
2050 b.extend(kill)
2051 b = GitCommand(
2052 self, b, bare=True, capture_stdout=True, capture_stderr=True
2053 )
2054 b.Wait()
2055 finally:
Sylvain56a5a012023-09-11 13:38:00 +02002056 if IsId(old):
Gavin Makea2e3302023-03-11 06:46:20 +00002057 self.bare_git.DetachHead(old)
2058 else:
2059 self.bare_git.SetHead(old)
2060 left = self._allrefs
2061
2062 for branch in kill:
2063 if (R_HEADS + branch) not in left:
2064 self.CleanPublishedCache()
2065 break
2066
2067 if cb and cb not in kill:
2068 kill.append(cb)
2069 kill.sort()
2070
2071 kept = []
2072 for branch in kill:
2073 if R_HEADS + branch in left:
2074 branch = self.GetBranch(branch)
2075 base = branch.LocalMerge
2076 if not base:
2077 base = rev
2078 kept.append(ReviewableBranch(self, branch, base))
2079 return kept
2080
2081 def GetRegisteredSubprojects(self):
2082 result = []
2083
2084 def rec(subprojects):
2085 if not subprojects:
2086 return
2087 result.extend(subprojects)
2088 for p in subprojects:
2089 rec(p.subprojects)
2090
2091 rec(self.subprojects)
2092 return result
2093
2094 def _GetSubmodules(self):
2095 # Unfortunately we cannot call `git submodule status --recursive` here
2096 # because the working tree might not exist yet, and it cannot be used
2097 # without a working tree in its current implementation.
2098
2099 def get_submodules(gitdir, rev):
2100 # Parse .gitmodules for submodule sub_paths and sub_urls.
2101 sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
2102 if not sub_paths:
2103 return []
2104 # Run `git ls-tree` to read SHAs of submodule object, which happen
2105 # to be revision of submodule repository.
2106 sub_revs = git_ls_tree(gitdir, rev, sub_paths)
2107 submodules = []
2108 for sub_path, sub_url in zip(sub_paths, sub_urls):
2109 try:
2110 sub_rev = sub_revs[sub_path]
2111 except KeyError:
2112 # Ignore non-exist submodules.
2113 continue
2114 submodules.append((sub_rev, sub_path, sub_url))
2115 return submodules
2116
2117 re_path = re.compile(r"^submodule\.(.+)\.path=(.*)$")
2118 re_url = re.compile(r"^submodule\.(.+)\.url=(.*)$")
2119
2120 def parse_gitmodules(gitdir, rev):
2121 cmd = ["cat-file", "blob", "%s:.gitmodules" % rev]
2122 try:
2123 p = GitCommand(
2124 None,
2125 cmd,
2126 capture_stdout=True,
2127 capture_stderr=True,
2128 bare=True,
2129 gitdir=gitdir,
2130 )
2131 except GitError:
2132 return [], []
2133 if p.Wait() != 0:
2134 return [], []
2135
2136 gitmodules_lines = []
2137 fd, temp_gitmodules_path = tempfile.mkstemp()
2138 try:
2139 os.write(fd, p.stdout.encode("utf-8"))
2140 os.close(fd)
2141 cmd = ["config", "--file", temp_gitmodules_path, "--list"]
2142 p = GitCommand(
2143 None,
2144 cmd,
2145 capture_stdout=True,
2146 capture_stderr=True,
2147 bare=True,
2148 gitdir=gitdir,
2149 )
2150 if p.Wait() != 0:
2151 return [], []
2152 gitmodules_lines = p.stdout.split("\n")
2153 except GitError:
2154 return [], []
2155 finally:
2156 platform_utils.remove(temp_gitmodules_path)
2157
2158 names = set()
2159 paths = {}
2160 urls = {}
2161 for line in gitmodules_lines:
2162 if not line:
2163 continue
2164 m = re_path.match(line)
2165 if m:
2166 names.add(m.group(1))
2167 paths[m.group(1)] = m.group(2)
2168 continue
2169 m = re_url.match(line)
2170 if m:
2171 names.add(m.group(1))
2172 urls[m.group(1)] = m.group(2)
2173 continue
2174 names = sorted(names)
2175 return (
2176 [paths.get(name, "") for name in names],
2177 [urls.get(name, "") for name in names],
2178 )
2179
2180 def git_ls_tree(gitdir, rev, paths):
2181 cmd = ["ls-tree", rev, "--"]
2182 cmd.extend(paths)
2183 try:
2184 p = GitCommand(
2185 None,
2186 cmd,
2187 capture_stdout=True,
2188 capture_stderr=True,
2189 bare=True,
2190 gitdir=gitdir,
2191 )
2192 except GitError:
2193 return []
2194 if p.Wait() != 0:
2195 return []
2196 objects = {}
2197 for line in p.stdout.split("\n"):
2198 if not line.strip():
2199 continue
2200 object_rev, object_path = line.split()[2:4]
2201 objects[object_path] = object_rev
2202 return objects
2203
2204 try:
2205 rev = self.GetRevisionId()
2206 except GitError:
2207 return []
2208 return get_submodules(self.gitdir, rev)
2209
2210 def GetDerivedSubprojects(self):
2211 result = []
2212 if not self.Exists:
2213 # If git repo does not exist yet, querying its submodules will
2214 # mess up its states; so return here.
2215 return result
2216 for rev, path, url in self._GetSubmodules():
2217 name = self.manifest.GetSubprojectName(self, path)
2218 (
2219 relpath,
2220 worktree,
2221 gitdir,
2222 objdir,
2223 ) = self.manifest.GetSubprojectPaths(self, name, path)
2224 project = self.manifest.paths.get(relpath)
2225 if project:
2226 result.extend(project.GetDerivedSubprojects())
2227 continue
2228
2229 if url.startswith(".."):
2230 url = urllib.parse.urljoin("%s/" % self.remote.url, url)
2231 remote = RemoteSpec(
2232 self.remote.name,
2233 url=url,
2234 pushUrl=self.remote.pushUrl,
2235 review=self.remote.review,
2236 revision=self.remote.revision,
2237 )
2238 subproject = Project(
2239 manifest=self.manifest,
2240 name=name,
2241 remote=remote,
2242 gitdir=gitdir,
2243 objdir=objdir,
2244 worktree=worktree,
2245 relpath=relpath,
2246 revisionExpr=rev,
2247 revisionId=rev,
2248 rebase=self.rebase,
2249 groups=self.groups,
2250 sync_c=self.sync_c,
2251 sync_s=self.sync_s,
2252 sync_tags=self.sync_tags,
2253 parent=self,
2254 is_derived=True,
2255 )
2256 result.append(subproject)
2257 result.extend(subproject.GetDerivedSubprojects())
2258 return result
2259
2260 def EnableRepositoryExtension(self, key, value="true", version=1):
2261 """Enable git repository extension |key| with |value|.
2262
2263 Args:
2264 key: The extension to enabled. Omit the "extensions." prefix.
2265 value: The value to use for the extension.
2266 version: The minimum git repository version needed.
2267 """
2268 # Make sure the git repo version is new enough already.
2269 found_version = self.config.GetInt("core.repositoryFormatVersion")
2270 if found_version is None:
2271 found_version = 0
2272 if found_version < version:
2273 self.config.SetString("core.repositoryFormatVersion", str(version))
2274
2275 # Enable the extension!
2276 self.config.SetString("extensions.%s" % (key,), value)
2277
2278 def ResolveRemoteHead(self, name=None):
2279 """Find out what the default branch (HEAD) points to.
2280
2281 Normally this points to refs/heads/master, but projects are moving to
2282 main. Support whatever the server uses rather than hardcoding "master"
2283 ourselves.
2284 """
2285 if name is None:
2286 name = self.remote.name
2287
2288 # The output will look like (NB: tabs are separators):
2289 # ref: refs/heads/master HEAD
2290 # 5f6803b100bb3cd0f534e96e88c91373e8ed1c44 HEAD
2291 output = self.bare_git.ls_remote(
2292 "-q", "--symref", "--exit-code", name, "HEAD"
2293 )
2294
2295 for line in output.splitlines():
2296 lhs, rhs = line.split("\t", 1)
2297 if rhs == "HEAD" and lhs.startswith("ref:"):
2298 return lhs[4:].strip()
2299
2300 return None
2301
2302 def _CheckForImmutableRevision(self):
2303 try:
2304 # if revision (sha or tag) is not present then following function
2305 # throws an error.
2306 self.bare_git.rev_list(
2307 "-1", "--missing=allow-any", "%s^0" % self.revisionExpr, "--"
2308 )
2309 if self.upstream:
2310 rev = self.GetRemote().ToLocal(self.upstream)
2311 self.bare_git.rev_list(
2312 "-1", "--missing=allow-any", "%s^0" % rev, "--"
2313 )
2314 self.bare_git.merge_base(
2315 "--is-ancestor", self.revisionExpr, rev
2316 )
2317 return True
2318 except GitError:
2319 # There is no such persistent revision. We have to fetch it.
2320 return False
2321
2322 def _FetchArchive(self, tarpath, cwd=None):
2323 cmd = ["archive", "-v", "-o", tarpath]
2324 cmd.append("--remote=%s" % self.remote.url)
2325 cmd.append("--prefix=%s/" % self.RelPath(local=False))
2326 cmd.append(self.revisionExpr)
2327
2328 command = GitCommand(
Jason Chang32b59562023-07-14 16:45:35 -07002329 self,
2330 cmd,
2331 cwd=cwd,
2332 capture_stdout=True,
2333 capture_stderr=True,
2334 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00002335 )
Jason Chang32b59562023-07-14 16:45:35 -07002336 command.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00002337
2338 def _RemoteFetch(
2339 self,
2340 name=None,
2341 current_branch_only=False,
2342 initial=False,
2343 quiet=False,
2344 verbose=False,
2345 output_redir=None,
2346 alt_dir=None,
2347 tags=True,
2348 prune=False,
2349 depth=None,
2350 submodules=False,
2351 ssh_proxy=None,
2352 force_sync=False,
2353 clone_filter=None,
2354 retry_fetches=2,
2355 retry_sleep_initial_sec=4.0,
2356 retry_exp_factor=2.0,
Jason Chang32b59562023-07-14 16:45:35 -07002357 ) -> bool:
Gavin Makea2e3302023-03-11 06:46:20 +00002358 tag_name = None
2359 # The depth should not be used when fetching to a mirror because
2360 # it will result in a shallow repository that cannot be cloned or
2361 # fetched from.
2362 # The repo project should also never be synced with partial depth.
2363 if self.manifest.IsMirror or self.relpath == ".repo/repo":
2364 depth = None
2365
2366 if depth:
2367 current_branch_only = True
2368
Sylvain56a5a012023-09-11 13:38:00 +02002369 is_sha1 = bool(IsId(self.revisionExpr))
Gavin Makea2e3302023-03-11 06:46:20 +00002370
2371 if current_branch_only:
2372 if self.revisionExpr.startswith(R_TAGS):
2373 # This is a tag and its commit id should never change.
2374 tag_name = self.revisionExpr[len(R_TAGS) :]
2375 elif self.upstream and self.upstream.startswith(R_TAGS):
2376 # This is a tag and its commit id should never change.
2377 tag_name = self.upstream[len(R_TAGS) :]
2378
2379 if is_sha1 or tag_name is not None:
2380 if self._CheckForImmutableRevision():
2381 if verbose:
2382 print(
2383 "Skipped fetching project %s (already have "
2384 "persistent ref)" % self.name
2385 )
2386 return True
2387 if is_sha1 and not depth:
2388 # When syncing a specific commit and --depth is not set:
2389 # * if upstream is explicitly specified and is not a sha1, fetch
2390 # only upstream as users expect only upstream to be fetch.
2391 # Note: The commit might not be in upstream in which case the
2392 # sync will fail.
2393 # * otherwise, fetch all branches to make sure we end up with
2394 # the specific commit.
2395 if self.upstream:
Sylvain56a5a012023-09-11 13:38:00 +02002396 current_branch_only = not IsId(self.upstream)
Gavin Makea2e3302023-03-11 06:46:20 +00002397 else:
2398 current_branch_only = False
2399
2400 if not name:
2401 name = self.remote.name
2402
2403 remote = self.GetRemote(name)
2404 if not remote.PreConnectFetch(ssh_proxy):
2405 ssh_proxy = None
2406
2407 if initial:
2408 if alt_dir and "objects" == os.path.basename(alt_dir):
2409 ref_dir = os.path.dirname(alt_dir)
2410 packed_refs = os.path.join(self.gitdir, "packed-refs")
2411
2412 all_refs = self.bare_ref.all
2413 ids = set(all_refs.values())
2414 tmp = set()
2415
2416 for r, ref_id in GitRefs(ref_dir).all.items():
2417 if r not in all_refs:
2418 if r.startswith(R_TAGS) or remote.WritesTo(r):
2419 all_refs[r] = ref_id
2420 ids.add(ref_id)
2421 continue
2422
2423 if ref_id in ids:
2424 continue
2425
2426 r = "refs/_alt/%s" % ref_id
2427 all_refs[r] = ref_id
2428 ids.add(ref_id)
2429 tmp.add(r)
2430
2431 tmp_packed_lines = []
2432 old_packed_lines = []
2433
2434 for r in sorted(all_refs):
2435 line = "%s %s\n" % (all_refs[r], r)
2436 tmp_packed_lines.append(line)
2437 if r not in tmp:
2438 old_packed_lines.append(line)
2439
2440 tmp_packed = "".join(tmp_packed_lines)
2441 old_packed = "".join(old_packed_lines)
2442 _lwrite(packed_refs, tmp_packed)
2443 else:
2444 alt_dir = None
2445
2446 cmd = ["fetch"]
2447
2448 if clone_filter:
2449 git_require((2, 19, 0), fail=True, msg="partial clones")
2450 cmd.append("--filter=%s" % clone_filter)
2451 self.EnableRepositoryExtension("partialclone", self.remote.name)
2452
2453 if depth:
2454 cmd.append("--depth=%s" % depth)
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002455 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002456 # If this repo has shallow objects, then we don't know which refs
2457 # have shallow objects or not. Tell git to unshallow all fetched
2458 # refs. Don't do this with projects that don't have shallow
2459 # objects, since it is less efficient.
2460 if os.path.exists(os.path.join(self.gitdir, "shallow")):
2461 cmd.append("--depth=2147483647")
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002462
Gavin Makea2e3302023-03-11 06:46:20 +00002463 if not verbose:
2464 cmd.append("--quiet")
2465 if not quiet and sys.stdout.isatty():
2466 cmd.append("--progress")
2467 if not self.worktree:
2468 cmd.append("--update-head-ok")
2469 cmd.append(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002470
Gavin Makea2e3302023-03-11 06:46:20 +00002471 if force_sync:
2472 cmd.append("--force")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002473
Gavin Makea2e3302023-03-11 06:46:20 +00002474 if prune:
2475 cmd.append("--prune")
Mike Frysinger29626b42021-05-01 09:37:13 -04002476
Gavin Makea2e3302023-03-11 06:46:20 +00002477 # Always pass something for --recurse-submodules, git with GIT_DIR
2478 # behaves incorrectly when not given `--recurse-submodules=no`.
2479 # (b/218891912)
2480 cmd.append(
2481 f'--recurse-submodules={"on-demand" if submodules else "no"}'
2482 )
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002483
Gavin Makea2e3302023-03-11 06:46:20 +00002484 spec = []
2485 if not current_branch_only:
2486 # Fetch whole repo.
2487 spec.append(
2488 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2489 )
2490 elif tag_name is not None:
2491 spec.append("tag")
2492 spec.append(tag_name)
Remy Böhmer1469c282020-12-15 18:49:02 +01002493
Gavin Makea2e3302023-03-11 06:46:20 +00002494 if self.manifest.IsMirror and not current_branch_only:
2495 branch = None
Remy Böhmer1469c282020-12-15 18:49:02 +01002496 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002497 branch = self.revisionExpr
2498 if (
2499 not self.manifest.IsMirror
2500 and is_sha1
2501 and depth
2502 and git_require((1, 8, 3))
2503 ):
2504 # Shallow checkout of a specific commit, fetch from that commit and
2505 # not the heads only as the commit might be deeper in the history.
2506 spec.append(branch)
2507 if self.upstream:
2508 spec.append(self.upstream)
David James8d201162013-10-11 17:03:19 -07002509 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002510 if is_sha1:
2511 branch = self.upstream
2512 if branch is not None and branch.strip():
2513 if not branch.startswith("refs/"):
2514 branch = R_HEADS + branch
2515 spec.append(str(("+%s:" % branch) + remote.ToLocal(branch)))
David James8d201162013-10-11 17:03:19 -07002516
Gavin Makea2e3302023-03-11 06:46:20 +00002517 # If mirroring repo and we cannot deduce the tag or branch to fetch,
2518 # fetch whole repo.
2519 if self.manifest.IsMirror and not spec:
2520 spec.append(
2521 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2522 )
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002523
Gavin Makea2e3302023-03-11 06:46:20 +00002524 # If using depth then we should not get all the tags since they may
2525 # be outside of the depth.
2526 if not tags or depth:
2527 cmd.append("--no-tags")
2528 else:
2529 cmd.append("--tags")
2530 spec.append(str(("+refs/tags/*:") + remote.ToLocal("refs/tags/*")))
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002531
Gavin Makea2e3302023-03-11 06:46:20 +00002532 cmd.extend(spec)
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002533
Gavin Makea2e3302023-03-11 06:46:20 +00002534 # At least one retry minimum due to git remote prune.
2535 retry_fetches = max(retry_fetches, 2)
2536 retry_cur_sleep = retry_sleep_initial_sec
2537 ok = prune_tried = False
2538 for try_n in range(retry_fetches):
Jason Chang32b59562023-07-14 16:45:35 -07002539 verify_command = try_n == retry_fetches - 1
Gavin Makea2e3302023-03-11 06:46:20 +00002540 gitcmd = GitCommand(
2541 self,
2542 cmd,
2543 bare=True,
2544 objdir=os.path.join(self.objdir, "objects"),
2545 ssh_proxy=ssh_proxy,
2546 merge_output=True,
2547 capture_stdout=quiet or bool(output_redir),
Jason Chang32b59562023-07-14 16:45:35 -07002548 verify_command=verify_command,
Gavin Makea2e3302023-03-11 06:46:20 +00002549 )
2550 if gitcmd.stdout and not quiet and output_redir:
2551 output_redir.write(gitcmd.stdout)
2552 ret = gitcmd.Wait()
2553 if ret == 0:
2554 ok = True
2555 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002556
Gavin Makea2e3302023-03-11 06:46:20 +00002557 # Retry later due to HTTP 429 Too Many Requests.
2558 elif (
2559 gitcmd.stdout
2560 and "error:" in gitcmd.stdout
2561 and "HTTP 429" in gitcmd.stdout
2562 ):
2563 # Fallthru to sleep+retry logic at the bottom.
2564 pass
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002565
Gavin Makea2e3302023-03-11 06:46:20 +00002566 # Try to prune remote branches once in case there are conflicts.
2567 # For example, if the remote had refs/heads/upstream, but deleted
2568 # that and now has refs/heads/upstream/foo.
2569 elif (
2570 gitcmd.stdout
2571 and "error:" in gitcmd.stdout
2572 and "git remote prune" in gitcmd.stdout
2573 and not prune_tried
2574 ):
2575 prune_tried = True
2576 prunecmd = GitCommand(
2577 self,
2578 ["remote", "prune", name],
2579 bare=True,
2580 ssh_proxy=ssh_proxy,
2581 )
2582 ret = prunecmd.Wait()
2583 if ret:
2584 break
2585 print(
2586 "retrying fetch after pruning remote branches",
2587 file=output_redir,
2588 )
2589 # Continue right away so we don't sleep as we shouldn't need to.
2590 continue
2591 elif current_branch_only and is_sha1 and ret == 128:
2592 # Exit code 128 means "couldn't find the ref you asked for"; if
2593 # we're in sha1 mode, we just tried sync'ing from the upstream
2594 # field; it doesn't exist, thus abort the optimization attempt
2595 # and do a full sync.
2596 break
2597 elif ret < 0:
2598 # Git died with a signal, exit immediately.
2599 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002600
Gavin Makea2e3302023-03-11 06:46:20 +00002601 # Figure out how long to sleep before the next attempt, if there is
2602 # one.
2603 if not verbose and gitcmd.stdout:
2604 print(
2605 "\n%s:\n%s" % (self.name, gitcmd.stdout),
2606 end="",
2607 file=output_redir,
2608 )
2609 if try_n < retry_fetches - 1:
2610 print(
2611 "%s: sleeping %s seconds before retrying"
2612 % (self.name, retry_cur_sleep),
2613 file=output_redir,
2614 )
2615 time.sleep(retry_cur_sleep)
2616 retry_cur_sleep = min(
2617 retry_exp_factor * retry_cur_sleep, MAXIMUM_RETRY_SLEEP_SEC
2618 )
2619 retry_cur_sleep *= 1 - random.uniform(
2620 -RETRY_JITTER_PERCENT, RETRY_JITTER_PERCENT
2621 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002622
Gavin Makea2e3302023-03-11 06:46:20 +00002623 if initial:
2624 if alt_dir:
2625 if old_packed != "":
2626 _lwrite(packed_refs, old_packed)
2627 else:
2628 platform_utils.remove(packed_refs)
2629 self.bare_git.pack_refs("--all", "--prune")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002630
Gavin Makea2e3302023-03-11 06:46:20 +00002631 if is_sha1 and current_branch_only:
2632 # We just synced the upstream given branch; verify we
2633 # got what we wanted, else trigger a second run of all
2634 # refs.
2635 if not self._CheckForImmutableRevision():
2636 # Sync the current branch only with depth set to None.
2637 # We always pass depth=None down to avoid infinite recursion.
2638 return self._RemoteFetch(
2639 name=name,
2640 quiet=quiet,
2641 verbose=verbose,
2642 output_redir=output_redir,
2643 current_branch_only=current_branch_only and depth,
2644 initial=False,
2645 alt_dir=alt_dir,
Nasser Grainawiaafed292023-05-24 12:51:03 -06002646 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00002647 depth=None,
2648 ssh_proxy=ssh_proxy,
2649 clone_filter=clone_filter,
2650 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002651
Gavin Makea2e3302023-03-11 06:46:20 +00002652 return ok
Mike Frysingerf4545122019-11-11 04:34:16 -05002653
Gavin Makea2e3302023-03-11 06:46:20 +00002654 def _ApplyCloneBundle(self, initial=False, quiet=False, verbose=False):
2655 if initial and (
2656 self.manifest.manifestProject.depth or self.clone_depth
2657 ):
2658 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002659
Gavin Makea2e3302023-03-11 06:46:20 +00002660 remote = self.GetRemote()
2661 bundle_url = remote.url + "/clone.bundle"
2662 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
2663 if GetSchemeFromUrl(bundle_url) not in (
2664 "http",
2665 "https",
2666 "persistent-http",
2667 "persistent-https",
2668 ):
2669 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06002670
Gavin Makea2e3302023-03-11 06:46:20 +00002671 bundle_dst = os.path.join(self.gitdir, "clone.bundle")
2672 bundle_tmp = os.path.join(self.gitdir, "clone.bundle.tmp")
2673
2674 exist_dst = os.path.exists(bundle_dst)
2675 exist_tmp = os.path.exists(bundle_tmp)
2676
2677 if not initial and not exist_dst and not exist_tmp:
2678 return False
2679
2680 if not exist_dst:
2681 exist_dst = self._FetchBundle(
2682 bundle_url, bundle_tmp, bundle_dst, quiet, verbose
2683 )
2684 if not exist_dst:
2685 return False
2686
2687 cmd = ["fetch"]
2688 if not verbose:
2689 cmd.append("--quiet")
2690 if not quiet and sys.stdout.isatty():
2691 cmd.append("--progress")
2692 if not self.worktree:
2693 cmd.append("--update-head-ok")
2694 cmd.append(bundle_dst)
2695 for f in remote.fetch:
2696 cmd.append(str(f))
2697 cmd.append("+refs/tags/*:refs/tags/*")
2698
2699 ok = (
2700 GitCommand(
2701 self,
2702 cmd,
2703 bare=True,
2704 objdir=os.path.join(self.objdir, "objects"),
2705 ).Wait()
2706 == 0
2707 )
2708 platform_utils.remove(bundle_dst, missing_ok=True)
2709 platform_utils.remove(bundle_tmp, missing_ok=True)
2710 return ok
2711
2712 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
2713 platform_utils.remove(dstPath, missing_ok=True)
2714
2715 cmd = ["curl", "--fail", "--output", tmpPath, "--netrc", "--location"]
2716 if quiet:
2717 cmd += ["--silent", "--show-error"]
2718 if os.path.exists(tmpPath):
2719 size = os.stat(tmpPath).st_size
2720 if size >= 1024:
2721 cmd += ["--continue-at", "%d" % (size,)]
2722 else:
2723 platform_utils.remove(tmpPath)
2724 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
2725 if cookiefile:
2726 cmd += ["--cookie", cookiefile]
2727 if proxy:
2728 cmd += ["--proxy", proxy]
2729 elif "http_proxy" in os.environ and "darwin" == sys.platform:
2730 cmd += ["--proxy", os.environ["http_proxy"]]
2731 if srcUrl.startswith("persistent-https"):
2732 srcUrl = "http" + srcUrl[len("persistent-https") :]
2733 elif srcUrl.startswith("persistent-http"):
2734 srcUrl = "http" + srcUrl[len("persistent-http") :]
2735 cmd += [srcUrl]
2736
2737 proc = None
2738 with Trace("Fetching bundle: %s", " ".join(cmd)):
2739 if verbose:
2740 print("%s: Downloading bundle: %s" % (self.name, srcUrl))
2741 stdout = None if verbose else subprocess.PIPE
2742 stderr = None if verbose else subprocess.STDOUT
2743 try:
2744 proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
2745 except OSError:
2746 return False
2747
2748 (output, _) = proc.communicate()
2749 curlret = proc.returncode
2750
2751 if curlret == 22:
2752 # From curl man page:
2753 # 22: HTTP page not retrieved. The requested url was not found
2754 # or returned another error with the HTTP error code being 400
2755 # or above. This return code only appears if -f, --fail is used.
2756 if verbose:
2757 print(
2758 "%s: Unable to retrieve clone.bundle; ignoring."
2759 % self.name
2760 )
2761 if output:
2762 print("Curl output:\n%s" % output)
2763 return False
2764 elif curlret and not verbose and output:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002765 logger.error("%s", output)
Gavin Makea2e3302023-03-11 06:46:20 +00002766
2767 if os.path.exists(tmpPath):
2768 if curlret == 0 and self._IsValidBundle(tmpPath, quiet):
2769 platform_utils.rename(tmpPath, dstPath)
2770 return True
2771 else:
2772 platform_utils.remove(tmpPath)
2773 return False
2774 else:
2775 return False
2776
2777 def _IsValidBundle(self, path, quiet):
2778 try:
2779 with open(path, "rb") as f:
2780 if f.read(16) == b"# v2 git bundle\n":
2781 return True
2782 else:
2783 if not quiet:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002784 logger.error("Invalid clone.bundle file; ignoring.")
Gavin Makea2e3302023-03-11 06:46:20 +00002785 return False
2786 except OSError:
2787 return False
2788
2789 def _Checkout(self, rev, quiet=False):
2790 cmd = ["checkout"]
2791 if quiet:
2792 cmd.append("-q")
2793 cmd.append(rev)
2794 cmd.append("--")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002795 if GitCommand(self, cmd).Wait() != 0:
Gavin Makea2e3302023-03-11 06:46:20 +00002796 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002797 raise GitError(
2798 "%s checkout %s " % (self.name, rev), project=self.name
2799 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002800
Gavin Makea2e3302023-03-11 06:46:20 +00002801 def _CherryPick(self, rev, ffonly=False, record_origin=False):
2802 cmd = ["cherry-pick"]
2803 if ffonly:
2804 cmd.append("--ff")
2805 if record_origin:
2806 cmd.append("-x")
2807 cmd.append(rev)
2808 cmd.append("--")
2809 if GitCommand(self, cmd).Wait() != 0:
2810 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002811 raise GitError(
2812 "%s cherry-pick %s " % (self.name, rev), project=self.name
2813 )
Victor Boivie0960b5b2010-11-26 13:42:13 +01002814
Gavin Makea2e3302023-03-11 06:46:20 +00002815 def _LsRemote(self, refs):
2816 cmd = ["ls-remote", self.remote.name, refs]
2817 p = GitCommand(self, cmd, capture_stdout=True)
2818 if p.Wait() == 0:
2819 return p.stdout
2820 return None
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002821
Gavin Makea2e3302023-03-11 06:46:20 +00002822 def _Revert(self, rev):
2823 cmd = ["revert"]
2824 cmd.append("--no-edit")
2825 cmd.append(rev)
2826 cmd.append("--")
2827 if GitCommand(self, cmd).Wait() != 0:
2828 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002829 raise GitError(
2830 "%s revert %s " % (self.name, rev), project=self.name
2831 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002832
Gavin Makea2e3302023-03-11 06:46:20 +00002833 def _ResetHard(self, rev, quiet=True):
2834 cmd = ["reset", "--hard"]
2835 if quiet:
2836 cmd.append("-q")
2837 cmd.append(rev)
2838 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002839 raise GitError(
2840 "%s reset --hard %s " % (self.name, rev), project=self.name
2841 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002842
Gavin Makea2e3302023-03-11 06:46:20 +00002843 def _SyncSubmodules(self, quiet=True):
2844 cmd = ["submodule", "update", "--init", "--recursive"]
2845 if quiet:
2846 cmd.append("-q")
2847 if GitCommand(self, cmd).Wait() != 0:
2848 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07002849 "%s submodule update --init --recursive " % self.name,
2850 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00002851 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002852
Gavin Makea2e3302023-03-11 06:46:20 +00002853 def _Rebase(self, upstream, onto=None):
2854 cmd = ["rebase"]
2855 if onto is not None:
2856 cmd.extend(["--onto", onto])
2857 cmd.append(upstream)
2858 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002859 raise GitError(
2860 "%s rebase %s " % (self.name, upstream), project=self.name
2861 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002862
Gavin Makea2e3302023-03-11 06:46:20 +00002863 def _FastForward(self, head, ffonly=False):
2864 cmd = ["merge", "--no-stat", head]
2865 if ffonly:
2866 cmd.append("--ff-only")
2867 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002868 raise GitError(
2869 "%s merge %s " % (self.name, head), project=self.name
2870 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002871
Gavin Makea2e3302023-03-11 06:46:20 +00002872 def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False):
2873 init_git_dir = not os.path.exists(self.gitdir)
2874 init_obj_dir = not os.path.exists(self.objdir)
2875 try:
2876 # Initialize the bare repository, which contains all of the objects.
2877 if init_obj_dir:
2878 os.makedirs(self.objdir)
2879 self.bare_objdir.init()
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002880
Gavin Makea2e3302023-03-11 06:46:20 +00002881 self._UpdateHooks(quiet=quiet)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002882
Gavin Makea2e3302023-03-11 06:46:20 +00002883 if self.use_git_worktrees:
2884 # Enable per-worktree config file support if possible. This
2885 # is more a nice-to-have feature for users rather than a
2886 # hard requirement.
2887 if git_require((2, 20, 0)):
2888 self.EnableRepositoryExtension("worktreeConfig")
Renaud Paquay788e9622017-01-27 11:41:12 -08002889
Gavin Makea2e3302023-03-11 06:46:20 +00002890 # If we have a separate directory to hold refs, initialize it as
2891 # well.
2892 if self.objdir != self.gitdir:
2893 if init_git_dir:
2894 os.makedirs(self.gitdir)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002895
Gavin Makea2e3302023-03-11 06:46:20 +00002896 if init_obj_dir or init_git_dir:
2897 self._ReferenceGitDir(
2898 self.objdir, self.gitdir, copy_all=True
2899 )
2900 try:
2901 self._CheckDirReference(self.objdir, self.gitdir)
2902 except GitError as e:
2903 if force_sync:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00002904 logger.error(
2905 "Retrying clone after deleting %s", self.gitdir
Gavin Makea2e3302023-03-11 06:46:20 +00002906 )
2907 try:
2908 platform_utils.rmtree(
2909 platform_utils.realpath(self.gitdir)
2910 )
2911 if self.worktree and os.path.exists(
2912 platform_utils.realpath(self.worktree)
2913 ):
2914 platform_utils.rmtree(
2915 platform_utils.realpath(self.worktree)
2916 )
2917 return self._InitGitDir(
2918 mirror_git=mirror_git,
2919 force_sync=False,
2920 quiet=quiet,
2921 )
2922 except Exception:
2923 raise e
2924 raise e
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002925
Gavin Makea2e3302023-03-11 06:46:20 +00002926 if init_git_dir:
2927 mp = self.manifest.manifestProject
2928 ref_dir = mp.reference or ""
Julien Camperguedd654222014-01-09 16:21:37 +01002929
Gavin Makea2e3302023-03-11 06:46:20 +00002930 def _expanded_ref_dirs():
2931 """Iterate through possible git reference dir paths."""
2932 name = self.name + ".git"
2933 yield mirror_git or os.path.join(ref_dir, name)
2934 for prefix in "", self.remote.name:
2935 yield os.path.join(
2936 ref_dir, ".repo", "project-objects", prefix, name
2937 )
2938 yield os.path.join(
2939 ref_dir, ".repo", "worktrees", prefix, name
2940 )
2941
2942 if ref_dir or mirror_git:
2943 found_ref_dir = None
2944 for path in _expanded_ref_dirs():
2945 if os.path.exists(path):
2946 found_ref_dir = path
2947 break
2948 ref_dir = found_ref_dir
2949
2950 if ref_dir:
2951 if not os.path.isabs(ref_dir):
2952 # The alternate directory is relative to the object
2953 # database.
2954 ref_dir = os.path.relpath(
2955 ref_dir, os.path.join(self.objdir, "objects")
2956 )
2957 _lwrite(
2958 os.path.join(
2959 self.objdir, "objects/info/alternates"
2960 ),
2961 os.path.join(ref_dir, "objects") + "\n",
2962 )
2963
2964 m = self.manifest.manifestProject.config
2965 for key in ["user.name", "user.email"]:
2966 if m.Has(key, include_defaults=False):
2967 self.config.SetString(key, m.GetString(key))
2968 if not self.manifest.EnableGitLfs:
2969 self.config.SetString(
2970 "filter.lfs.smudge", "git-lfs smudge --skip -- %f"
2971 )
2972 self.config.SetString(
2973 "filter.lfs.process", "git-lfs filter-process --skip"
2974 )
2975 self.config.SetBoolean(
2976 "core.bare", True if self.manifest.IsMirror else None
2977 )
2978 except Exception:
2979 if init_obj_dir and os.path.exists(self.objdir):
2980 platform_utils.rmtree(self.objdir)
2981 if init_git_dir and os.path.exists(self.gitdir):
2982 platform_utils.rmtree(self.gitdir)
2983 raise
2984
2985 def _UpdateHooks(self, quiet=False):
2986 if os.path.exists(self.objdir):
2987 self._InitHooks(quiet=quiet)
2988
2989 def _InitHooks(self, quiet=False):
2990 hooks = platform_utils.realpath(os.path.join(self.objdir, "hooks"))
2991 if not os.path.exists(hooks):
2992 os.makedirs(hooks)
2993
2994 # Delete sample hooks. They're noise.
2995 for hook in glob.glob(os.path.join(hooks, "*.sample")):
2996 try:
2997 platform_utils.remove(hook, missing_ok=True)
2998 except PermissionError:
2999 pass
3000
3001 for stock_hook in _ProjectHooks():
3002 name = os.path.basename(stock_hook)
3003
3004 if (
3005 name in ("commit-msg",)
3006 and not self.remote.review
3007 and self is not self.manifest.manifestProject
3008 ):
3009 # Don't install a Gerrit Code Review hook if this
3010 # project does not appear to use it for reviews.
3011 #
3012 # Since the manifest project is one of those, but also
3013 # managed through gerrit, it's excluded.
3014 continue
3015
3016 dst = os.path.join(hooks, name)
3017 if platform_utils.islink(dst):
3018 continue
3019 if os.path.exists(dst):
3020 # If the files are the same, we'll leave it alone. We create
3021 # symlinks below by default but fallback to hardlinks if the OS
3022 # blocks them. So if we're here, it's probably because we made a
3023 # hardlink below.
3024 if not filecmp.cmp(stock_hook, dst, shallow=False):
3025 if not quiet:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00003026 logger.warn(
3027 "warn: %s: Not replacing locally modified %s hook",
Gavin Makea2e3302023-03-11 06:46:20 +00003028 self.RelPath(local=False),
3029 name,
3030 )
3031 continue
3032 try:
3033 platform_utils.symlink(
3034 os.path.relpath(stock_hook, os.path.dirname(dst)), dst
3035 )
3036 except OSError as e:
3037 if e.errno == errno.EPERM:
3038 try:
3039 os.link(stock_hook, dst)
3040 except OSError:
Jason Chang32b59562023-07-14 16:45:35 -07003041 raise GitError(
3042 self._get_symlink_error_message(), project=self.name
3043 )
Gavin Makea2e3302023-03-11 06:46:20 +00003044 else:
3045 raise
3046
3047 def _InitRemote(self):
3048 if self.remote.url:
3049 remote = self.GetRemote()
3050 remote.url = self.remote.url
3051 remote.pushUrl = self.remote.pushUrl
3052 remote.review = self.remote.review
3053 remote.projectname = self.name
3054
3055 if self.worktree:
3056 remote.ResetFetch(mirror=False)
3057 else:
3058 remote.ResetFetch(mirror=True)
3059 remote.Save()
3060
3061 def _InitMRef(self):
3062 """Initialize the pseudo m/<manifest branch> ref."""
3063 if self.manifest.branch:
3064 if self.use_git_worktrees:
3065 # Set up the m/ space to point to the worktree-specific ref
3066 # space. We'll update the worktree-specific ref space on each
3067 # checkout.
3068 ref = R_M + self.manifest.branch
3069 if not self.bare_ref.symref(ref):
3070 self.bare_git.symbolic_ref(
3071 "-m",
3072 "redirecting to worktree scope",
3073 ref,
3074 R_WORKTREE_M + self.manifest.branch,
3075 )
3076
3077 # We can't update this ref with git worktrees until it exists.
3078 # We'll wait until the initial checkout to set it.
3079 if not os.path.exists(self.worktree):
3080 return
3081
3082 base = R_WORKTREE_M
3083 active_git = self.work_git
3084
3085 self._InitAnyMRef(HEAD, self.bare_git, detach=True)
3086 else:
3087 base = R_M
3088 active_git = self.bare_git
3089
3090 self._InitAnyMRef(base + self.manifest.branch, active_git)
3091
3092 def _InitMirrorHead(self):
3093 self._InitAnyMRef(HEAD, self.bare_git)
3094
3095 def _InitAnyMRef(self, ref, active_git, detach=False):
3096 """Initialize |ref| in |active_git| to the value in the manifest.
3097
3098 This points |ref| to the <project> setting in the manifest.
3099
3100 Args:
3101 ref: The branch to update.
3102 active_git: The git repository to make updates in.
3103 detach: Whether to update target of symbolic refs, or overwrite the
3104 ref directly (and thus make it non-symbolic).
3105 """
3106 cur = self.bare_ref.symref(ref)
3107
3108 if self.revisionId:
3109 if cur != "" or self.bare_ref.get(ref) != self.revisionId:
3110 msg = "manifest set to %s" % self.revisionId
3111 dst = self.revisionId + "^0"
3112 active_git.UpdateRef(ref, dst, message=msg, detach=True)
Julien Camperguedd654222014-01-09 16:21:37 +01003113 else:
Gavin Makea2e3302023-03-11 06:46:20 +00003114 remote = self.GetRemote()
3115 dst = remote.ToLocal(self.revisionExpr)
3116 if cur != dst:
3117 msg = "manifest set to %s" % self.revisionExpr
3118 if detach:
3119 active_git.UpdateRef(ref, dst, message=msg, detach=True)
3120 else:
3121 active_git.symbolic_ref("-m", msg, ref, dst)
Julien Camperguedd654222014-01-09 16:21:37 +01003122
Gavin Makea2e3302023-03-11 06:46:20 +00003123 def _CheckDirReference(self, srcdir, destdir):
3124 # Git worktrees don't use symlinks to share at all.
3125 if self.use_git_worktrees:
3126 return
Julien Camperguedd654222014-01-09 16:21:37 +01003127
Gavin Makea2e3302023-03-11 06:46:20 +00003128 for name in self.shareable_dirs:
3129 # Try to self-heal a bit in simple cases.
3130 dst_path = os.path.join(destdir, name)
3131 src_path = os.path.join(srcdir, name)
Julien Camperguedd654222014-01-09 16:21:37 +01003132
Gavin Makea2e3302023-03-11 06:46:20 +00003133 dst = platform_utils.realpath(dst_path)
3134 if os.path.lexists(dst):
3135 src = platform_utils.realpath(src_path)
3136 # Fail if the links are pointing to the wrong place.
3137 if src != dst:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00003138 logger.error(
3139 "error: %s is different in %s vs %s",
3140 name,
3141 destdir,
3142 srcdir,
3143 )
Gavin Makea2e3302023-03-11 06:46:20 +00003144 raise GitError(
3145 "--force-sync not enabled; cannot overwrite a local "
3146 "work tree. If you're comfortable with the "
3147 "possibility of losing the work tree's git metadata,"
3148 " use `repo sync --force-sync {0}` to "
Jason Chang32b59562023-07-14 16:45:35 -07003149 "proceed.".format(self.RelPath(local=False)),
3150 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003151 )
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003152
Gavin Makea2e3302023-03-11 06:46:20 +00003153 def _ReferenceGitDir(self, gitdir, dotgit, copy_all):
3154 """Update |dotgit| to reference |gitdir|, using symlinks where possible.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003155
Gavin Makea2e3302023-03-11 06:46:20 +00003156 Args:
3157 gitdir: The bare git repository. Must already be initialized.
3158 dotgit: The repository you would like to initialize.
3159 copy_all: If true, copy all remaining files from |gitdir| ->
3160 |dotgit|. This saves you the effort of initializing |dotgit|
3161 yourself.
3162 """
3163 symlink_dirs = self.shareable_dirs[:]
3164 to_symlink = symlink_dirs
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003165
Gavin Makea2e3302023-03-11 06:46:20 +00003166 to_copy = []
3167 if copy_all:
3168 to_copy = platform_utils.listdir(gitdir)
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003169
Gavin Makea2e3302023-03-11 06:46:20 +00003170 dotgit = platform_utils.realpath(dotgit)
3171 for name in set(to_copy).union(to_symlink):
3172 try:
3173 src = platform_utils.realpath(os.path.join(gitdir, name))
3174 dst = os.path.join(dotgit, name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003175
Gavin Makea2e3302023-03-11 06:46:20 +00003176 if os.path.lexists(dst):
3177 continue
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003178
Gavin Makea2e3302023-03-11 06:46:20 +00003179 # If the source dir doesn't exist, create an empty dir.
3180 if name in symlink_dirs and not os.path.lexists(src):
3181 os.makedirs(src)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003182
Gavin Makea2e3302023-03-11 06:46:20 +00003183 if name in to_symlink:
3184 platform_utils.symlink(
3185 os.path.relpath(src, os.path.dirname(dst)), dst
3186 )
3187 elif copy_all and not platform_utils.islink(dst):
3188 if platform_utils.isdir(src):
3189 shutil.copytree(src, dst)
3190 elif os.path.isfile(src):
3191 shutil.copy(src, dst)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003192
Gavin Makea2e3302023-03-11 06:46:20 +00003193 except OSError as e:
3194 if e.errno == errno.EPERM:
3195 raise DownloadError(self._get_symlink_error_message())
3196 else:
3197 raise
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003198
Gavin Makea2e3302023-03-11 06:46:20 +00003199 def _InitGitWorktree(self):
3200 """Init the project using git worktrees."""
3201 self.bare_git.worktree("prune")
3202 self.bare_git.worktree(
3203 "add",
3204 "-ff",
3205 "--checkout",
3206 "--detach",
3207 "--lock",
3208 self.worktree,
3209 self.GetRevisionId(),
3210 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003211
Gavin Makea2e3302023-03-11 06:46:20 +00003212 # Rewrite the internal state files to use relative paths between the
3213 # checkouts & worktrees.
3214 dotgit = os.path.join(self.worktree, ".git")
3215 with open(dotgit, "r") as fp:
3216 # Figure out the checkout->worktree path.
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003217 setting = fp.read()
Gavin Makea2e3302023-03-11 06:46:20 +00003218 assert setting.startswith("gitdir:")
3219 git_worktree_path = setting.split(":", 1)[1].strip()
3220 # Some platforms (e.g. Windows) won't let us update dotgit in situ
3221 # because of file permissions. Delete it and recreate it from scratch
3222 # to avoid.
3223 platform_utils.remove(dotgit)
3224 # Use relative path from checkout->worktree & maintain Unix line endings
3225 # on all OS's to match git behavior.
3226 with open(dotgit, "w", newline="\n") as fp:
3227 print(
3228 "gitdir:",
3229 os.path.relpath(git_worktree_path, self.worktree),
3230 file=fp,
3231 )
3232 # Use relative path from worktree->checkout & maintain Unix line endings
3233 # on all OS's to match git behavior.
3234 with open(
3235 os.path.join(git_worktree_path, "gitdir"), "w", newline="\n"
3236 ) as fp:
3237 print(os.path.relpath(dotgit, git_worktree_path), file=fp)
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003238
Gavin Makea2e3302023-03-11 06:46:20 +00003239 self._InitMRef()
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003240
Gavin Makea2e3302023-03-11 06:46:20 +00003241 def _InitWorkTree(self, force_sync=False, submodules=False):
3242 """Setup the worktree .git path.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003243
Gavin Makea2e3302023-03-11 06:46:20 +00003244 This is the user-visible path like src/foo/.git/.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003245
Gavin Makea2e3302023-03-11 06:46:20 +00003246 With non-git-worktrees, this will be a symlink to the .repo/projects/
3247 path. With git-worktrees, this will be a .git file using "gitdir: ..."
3248 syntax.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003249
Gavin Makea2e3302023-03-11 06:46:20 +00003250 Older checkouts had .git/ directories. If we see that, migrate it.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003251
Gavin Makea2e3302023-03-11 06:46:20 +00003252 This also handles changes in the manifest. Maybe this project was
3253 backed by "foo/bar" on the server, but now it's "new/foo/bar". We have
3254 to update the path we point to under .repo/projects/ to match.
3255 """
3256 dotgit = os.path.join(self.worktree, ".git")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003257
Gavin Makea2e3302023-03-11 06:46:20 +00003258 # If using an old layout style (a directory), migrate it.
3259 if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
Jason Chang32b59562023-07-14 16:45:35 -07003260 self._MigrateOldWorkTreeGitDir(dotgit, project=self.name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003261
Gavin Makea2e3302023-03-11 06:46:20 +00003262 init_dotgit = not os.path.exists(dotgit)
3263 if self.use_git_worktrees:
3264 if init_dotgit:
3265 self._InitGitWorktree()
3266 self._CopyAndLinkFiles()
3267 else:
3268 if not init_dotgit:
3269 # See if the project has changed.
3270 if platform_utils.realpath(
3271 self.gitdir
3272 ) != platform_utils.realpath(dotgit):
3273 platform_utils.remove(dotgit)
Doug Anderson37282b42011-03-04 11:54:18 -08003274
Gavin Makea2e3302023-03-11 06:46:20 +00003275 if init_dotgit or not os.path.exists(dotgit):
3276 os.makedirs(self.worktree, exist_ok=True)
3277 platform_utils.symlink(
3278 os.path.relpath(self.gitdir, self.worktree), dotgit
3279 )
Doug Anderson37282b42011-03-04 11:54:18 -08003280
Gavin Makea2e3302023-03-11 06:46:20 +00003281 if init_dotgit:
3282 _lwrite(
3283 os.path.join(dotgit, HEAD), "%s\n" % self.GetRevisionId()
3284 )
Doug Anderson37282b42011-03-04 11:54:18 -08003285
Gavin Makea2e3302023-03-11 06:46:20 +00003286 # Finish checking out the worktree.
3287 cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
3288 if GitCommand(self, cmd).Wait() != 0:
3289 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07003290 "Cannot initialize work tree for " + self.name,
3291 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003292 )
Doug Anderson37282b42011-03-04 11:54:18 -08003293
Gavin Makea2e3302023-03-11 06:46:20 +00003294 if submodules:
3295 self._SyncSubmodules(quiet=True)
3296 self._CopyAndLinkFiles()
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003297
Gavin Makea2e3302023-03-11 06:46:20 +00003298 @classmethod
Jason Chang32b59562023-07-14 16:45:35 -07003299 def _MigrateOldWorkTreeGitDir(cls, dotgit, project=None):
Gavin Makea2e3302023-03-11 06:46:20 +00003300 """Migrate the old worktree .git/ dir style to a symlink.
3301
3302 This logic specifically only uses state from |dotgit| to figure out
3303 where to move content and not |self|. This way if the backing project
3304 also changed places, we only do the .git/ dir to .git symlink migration
3305 here. The path updates will happen independently.
3306 """
3307 # Figure out where in .repo/projects/ it's pointing to.
3308 if not os.path.islink(os.path.join(dotgit, "refs")):
Jason Chang32b59562023-07-14 16:45:35 -07003309 raise GitError(
3310 f"{dotgit}: unsupported checkout state", project=project
3311 )
Gavin Makea2e3302023-03-11 06:46:20 +00003312 gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, "refs")))
3313
3314 # Remove known symlink paths that exist in .repo/projects/.
3315 KNOWN_LINKS = {
3316 "config",
3317 "description",
3318 "hooks",
3319 "info",
3320 "logs",
3321 "objects",
3322 "packed-refs",
3323 "refs",
3324 "rr-cache",
3325 "shallow",
3326 "svn",
3327 }
3328 # Paths that we know will be in both, but are safe to clobber in
3329 # .repo/projects/.
3330 SAFE_TO_CLOBBER = {
3331 "COMMIT_EDITMSG",
3332 "FETCH_HEAD",
3333 "HEAD",
3334 "gc.log",
3335 "gitk.cache",
3336 "index",
3337 "ORIG_HEAD",
3338 }
3339
3340 # First see if we'd succeed before starting the migration.
3341 unknown_paths = []
3342 for name in platform_utils.listdir(dotgit):
3343 # Ignore all temporary/backup names. These are common with vim &
3344 # emacs.
3345 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3346 continue
3347
3348 dotgit_path = os.path.join(dotgit, name)
3349 if name in KNOWN_LINKS:
3350 if not platform_utils.islink(dotgit_path):
3351 unknown_paths.append(f"{dotgit_path}: should be a symlink")
3352 else:
3353 gitdir_path = os.path.join(gitdir, name)
3354 if name not in SAFE_TO_CLOBBER and os.path.exists(gitdir_path):
3355 unknown_paths.append(
3356 f"{dotgit_path}: unknown file; please file a bug"
3357 )
3358 if unknown_paths:
Jason Chang32b59562023-07-14 16:45:35 -07003359 raise GitError(
3360 "Aborting migration: " + "\n".join(unknown_paths),
3361 project=project,
3362 )
Gavin Makea2e3302023-03-11 06:46:20 +00003363
3364 # Now walk the paths and sync the .git/ to .repo/projects/.
3365 for name in platform_utils.listdir(dotgit):
3366 dotgit_path = os.path.join(dotgit, name)
3367
3368 # Ignore all temporary/backup names. These are common with vim &
3369 # emacs.
3370 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3371 platform_utils.remove(dotgit_path)
3372 elif name in KNOWN_LINKS:
3373 platform_utils.remove(dotgit_path)
3374 else:
3375 gitdir_path = os.path.join(gitdir, name)
3376 platform_utils.remove(gitdir_path, missing_ok=True)
3377 platform_utils.rename(dotgit_path, gitdir_path)
3378
3379 # Now that the dir should be empty, clear it out, and symlink it over.
3380 platform_utils.rmdir(dotgit)
3381 platform_utils.symlink(
3382 os.path.relpath(gitdir, os.path.dirname(dotgit)), dotgit
3383 )
3384
3385 def _get_symlink_error_message(self):
3386 if platform_utils.isWindows():
3387 return (
3388 "Unable to create symbolic link. Please re-run the command as "
3389 "Administrator, or see "
3390 "https://github.com/git-for-windows/git/wiki/Symbolic-Links "
3391 "for other options."
3392 )
3393 return "filesystem must support symlinks"
3394
3395 def _revlist(self, *args, **kw):
3396 a = []
3397 a.extend(args)
3398 a.append("--")
3399 return self.work_git.rev_list(*a, **kw)
3400
3401 @property
3402 def _allrefs(self):
3403 return self.bare_ref.all
3404
3405 def _getLogs(
3406 self, rev1, rev2, oneline=False, color=True, pretty_format=None
3407 ):
3408 """Get logs between two revisions of this project."""
3409 comp = ".."
3410 if rev1:
3411 revs = [rev1]
3412 if rev2:
3413 revs.extend([comp, rev2])
3414 cmd = ["log", "".join(revs)]
3415 out = DiffColoring(self.config)
3416 if out.is_on and color:
3417 cmd.append("--color")
3418 if pretty_format is not None:
3419 cmd.append("--pretty=format:%s" % pretty_format)
3420 if oneline:
3421 cmd.append("--oneline")
3422
3423 try:
3424 log = GitCommand(
3425 self, cmd, capture_stdout=True, capture_stderr=True
3426 )
3427 if log.Wait() == 0:
3428 return log.stdout
3429 except GitError:
3430 # worktree may not exist if groups changed for example. In that
3431 # case, try in gitdir instead.
3432 if not os.path.exists(self.worktree):
3433 return self.bare_git.log(*cmd[1:])
3434 else:
3435 raise
3436 return None
3437
3438 def getAddedAndRemovedLogs(
3439 self, toProject, oneline=False, color=True, pretty_format=None
3440 ):
3441 """Get the list of logs from this revision to given revisionId"""
3442 logs = {}
3443 selfId = self.GetRevisionId(self._allrefs)
3444 toId = toProject.GetRevisionId(toProject._allrefs)
3445
3446 logs["added"] = self._getLogs(
3447 selfId,
3448 toId,
3449 oneline=oneline,
3450 color=color,
3451 pretty_format=pretty_format,
3452 )
3453 logs["removed"] = self._getLogs(
3454 toId,
3455 selfId,
3456 oneline=oneline,
3457 color=color,
3458 pretty_format=pretty_format,
3459 )
3460 return logs
3461
3462 class _GitGetByExec(object):
3463 def __init__(self, project, bare, gitdir):
3464 self._project = project
3465 self._bare = bare
3466 self._gitdir = gitdir
3467
3468 # __getstate__ and __setstate__ are required for pickling because
3469 # __getattr__ exists.
3470 def __getstate__(self):
3471 return (self._project, self._bare, self._gitdir)
3472
3473 def __setstate__(self, state):
3474 self._project, self._bare, self._gitdir = state
3475
3476 def LsOthers(self):
3477 p = GitCommand(
3478 self._project,
3479 ["ls-files", "-z", "--others", "--exclude-standard"],
3480 bare=False,
3481 gitdir=self._gitdir,
3482 capture_stdout=True,
3483 capture_stderr=True,
3484 )
3485 if p.Wait() == 0:
3486 out = p.stdout
3487 if out:
3488 # Backslash is not anomalous.
3489 return out[:-1].split("\0")
3490 return []
3491
3492 def DiffZ(self, name, *args):
3493 cmd = [name]
3494 cmd.append("-z")
3495 cmd.append("--ignore-submodules")
3496 cmd.extend(args)
3497 p = GitCommand(
3498 self._project,
3499 cmd,
3500 gitdir=self._gitdir,
3501 bare=False,
3502 capture_stdout=True,
3503 capture_stderr=True,
3504 )
3505 p.Wait()
3506 r = {}
3507 out = p.stdout
3508 if out:
3509 out = iter(out[:-1].split("\0"))
3510 while out:
3511 try:
3512 info = next(out)
3513 path = next(out)
3514 except StopIteration:
3515 break
3516
3517 class _Info(object):
3518 def __init__(self, path, omode, nmode, oid, nid, state):
3519 self.path = path
3520 self.src_path = None
3521 self.old_mode = omode
3522 self.new_mode = nmode
3523 self.old_id = oid
3524 self.new_id = nid
3525
3526 if len(state) == 1:
3527 self.status = state
3528 self.level = None
3529 else:
3530 self.status = state[:1]
3531 self.level = state[1:]
3532 while self.level.startswith("0"):
3533 self.level = self.level[1:]
3534
3535 info = info[1:].split(" ")
3536 info = _Info(path, *info)
3537 if info.status in ("R", "C"):
3538 info.src_path = info.path
3539 info.path = next(out)
3540 r[info.path] = info
3541 return r
3542
3543 def GetDotgitPath(self, subpath=None):
3544 """Return the full path to the .git dir.
3545
3546 As a convenience, append |subpath| if provided.
3547 """
3548 if self._bare:
3549 dotgit = self._gitdir
3550 else:
3551 dotgit = os.path.join(self._project.worktree, ".git")
3552 if os.path.isfile(dotgit):
3553 # Git worktrees use a "gitdir:" syntax to point to the
3554 # scratch space.
3555 with open(dotgit) as fp:
3556 setting = fp.read()
3557 assert setting.startswith("gitdir:")
3558 gitdir = setting.split(":", 1)[1].strip()
3559 dotgit = os.path.normpath(
3560 os.path.join(self._project.worktree, gitdir)
3561 )
3562
3563 return dotgit if subpath is None else os.path.join(dotgit, subpath)
3564
3565 def GetHead(self):
3566 """Return the ref that HEAD points to."""
3567 path = self.GetDotgitPath(subpath=HEAD)
3568 try:
3569 with open(path) as fd:
3570 line = fd.readline()
3571 except IOError as e:
3572 raise NoManifestException(path, str(e))
3573 try:
3574 line = line.decode()
3575 except AttributeError:
3576 pass
3577 if line.startswith("ref: "):
3578 return line[5:-1]
3579 return line[:-1]
3580
3581 def SetHead(self, ref, message=None):
3582 cmdv = []
3583 if message is not None:
3584 cmdv.extend(["-m", message])
3585 cmdv.append(HEAD)
3586 cmdv.append(ref)
3587 self.symbolic_ref(*cmdv)
3588
3589 def DetachHead(self, new, message=None):
3590 cmdv = ["--no-deref"]
3591 if message is not None:
3592 cmdv.extend(["-m", message])
3593 cmdv.append(HEAD)
3594 cmdv.append(new)
3595 self.update_ref(*cmdv)
3596
3597 def UpdateRef(self, name, new, old=None, message=None, detach=False):
3598 cmdv = []
3599 if message is not None:
3600 cmdv.extend(["-m", message])
3601 if detach:
3602 cmdv.append("--no-deref")
3603 cmdv.append(name)
3604 cmdv.append(new)
3605 if old is not None:
3606 cmdv.append(old)
3607 self.update_ref(*cmdv)
3608
3609 def DeleteRef(self, name, old=None):
3610 if not old:
3611 old = self.rev_parse(name)
3612 self.update_ref("-d", name, old)
3613 self._project.bare_ref.deleted(name)
3614
3615 def rev_list(self, *args, **kw):
3616 if "format" in kw:
3617 cmdv = ["log", "--pretty=format:%s" % kw["format"]]
3618 else:
3619 cmdv = ["rev-list"]
3620 cmdv.extend(args)
3621 p = GitCommand(
3622 self._project,
3623 cmdv,
3624 bare=self._bare,
3625 gitdir=self._gitdir,
3626 capture_stdout=True,
3627 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003628 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00003629 )
Jason Chang32b59562023-07-14 16:45:35 -07003630 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003631 return p.stdout.splitlines()
3632
3633 def __getattr__(self, name):
3634 """Allow arbitrary git commands using pythonic syntax.
3635
3636 This allows you to do things like:
3637 git_obj.rev_parse('HEAD')
3638
3639 Since we don't have a 'rev_parse' method defined, the __getattr__
3640 will run. We'll replace the '_' with a '-' and try to run a git
3641 command. Any other positional arguments will be passed to the git
3642 command, and the following keyword arguments are supported:
3643 config: An optional dict of git config options to be passed with
3644 '-c'.
3645
3646 Args:
3647 name: The name of the git command to call. Any '_' characters
3648 will be replaced with '-'.
3649
3650 Returns:
3651 A callable object that will try to call git with the named
3652 command.
3653 """
3654 name = name.replace("_", "-")
3655
3656 def runner(*args, **kwargs):
3657 cmdv = []
3658 config = kwargs.pop("config", None)
3659 for k in kwargs:
3660 raise TypeError(
3661 "%s() got an unexpected keyword argument %r" % (name, k)
3662 )
3663 if config is not None:
3664 for k, v in config.items():
3665 cmdv.append("-c")
3666 cmdv.append("%s=%s" % (k, v))
3667 cmdv.append(name)
3668 cmdv.extend(args)
3669 p = GitCommand(
3670 self._project,
3671 cmdv,
3672 bare=self._bare,
3673 gitdir=self._gitdir,
3674 capture_stdout=True,
3675 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003676 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00003677 )
Jason Chang32b59562023-07-14 16:45:35 -07003678 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003679 r = p.stdout
3680 if r.endswith("\n") and r.index("\n") == len(r) - 1:
3681 return r[:-1]
3682 return r
3683
3684 return runner
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003685
3686
Jason Chang32b59562023-07-14 16:45:35 -07003687class LocalSyncFail(RepoError):
3688 """Default error when there is an Sync_LocalHalf error."""
3689
3690
3691class _PriorSyncFailedError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003692 def __str__(self):
3693 return "prior sync failed; rebase still in progress"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003694
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003695
Jason Chang32b59562023-07-14 16:45:35 -07003696class _DirtyError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003697 def __str__(self):
3698 return "contains uncommitted changes"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003699
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003700
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003701class _InfoMessage(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003702 def __init__(self, project, text):
3703 self.project = project
3704 self.text = text
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003705
Gavin Makea2e3302023-03-11 06:46:20 +00003706 def Print(self, syncbuf):
3707 syncbuf.out.info(
3708 "%s/: %s", self.project.RelPath(local=False), self.text
3709 )
3710 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003711
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003712
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003713class _Failure(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003714 def __init__(self, project, why):
3715 self.project = project
3716 self.why = why
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003717
Gavin Makea2e3302023-03-11 06:46:20 +00003718 def Print(self, syncbuf):
3719 syncbuf.out.fail(
3720 "error: %s/: %s", self.project.RelPath(local=False), str(self.why)
3721 )
3722 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003723
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003724
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003725class _Later(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003726 def __init__(self, project, action):
3727 self.project = project
3728 self.action = action
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003729
Gavin Makea2e3302023-03-11 06:46:20 +00003730 def Run(self, syncbuf):
3731 out = syncbuf.out
3732 out.project("project %s/", self.project.RelPath(local=False))
3733 out.nl()
3734 try:
3735 self.action()
3736 out.nl()
3737 return True
3738 except GitError:
3739 out.nl()
3740 return False
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003741
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003742
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003743class _SyncColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +00003744 def __init__(self, config):
3745 super().__init__(config, "reposync")
3746 self.project = self.printer("header", attr="bold")
3747 self.info = self.printer("info")
3748 self.fail = self.printer("fail", fg="red")
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003749
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003750
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003751class SyncBuffer(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003752 def __init__(self, config, detach_head=False):
3753 self._messages = []
3754 self._failures = []
3755 self._later_queue1 = []
3756 self._later_queue2 = []
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003757
Gavin Makea2e3302023-03-11 06:46:20 +00003758 self.out = _SyncColoring(config)
3759 self.out.redirect(sys.stderr)
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003760
Gavin Makea2e3302023-03-11 06:46:20 +00003761 self.detach_head = detach_head
3762 self.clean = True
3763 self.recent_clean = True
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003764
Gavin Makea2e3302023-03-11 06:46:20 +00003765 def info(self, project, fmt, *args):
3766 self._messages.append(_InfoMessage(project, fmt % args))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003767
Gavin Makea2e3302023-03-11 06:46:20 +00003768 def fail(self, project, err=None):
3769 self._failures.append(_Failure(project, err))
David Rileye0684ad2017-04-05 00:02:59 -07003770 self._MarkUnclean()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003771
Gavin Makea2e3302023-03-11 06:46:20 +00003772 def later1(self, project, what):
3773 self._later_queue1.append(_Later(project, what))
Mike Frysinger70d861f2019-08-26 15:22:36 -04003774
Gavin Makea2e3302023-03-11 06:46:20 +00003775 def later2(self, project, what):
3776 self._later_queue2.append(_Later(project, what))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003777
Gavin Makea2e3302023-03-11 06:46:20 +00003778 def Finish(self):
3779 self._PrintMessages()
3780 self._RunLater()
3781 self._PrintMessages()
3782 return self.clean
3783
3784 def Recently(self):
3785 recent_clean = self.recent_clean
3786 self.recent_clean = True
3787 return recent_clean
3788
3789 def _MarkUnclean(self):
3790 self.clean = False
3791 self.recent_clean = False
3792
3793 def _RunLater(self):
3794 for q in ["_later_queue1", "_later_queue2"]:
3795 if not self._RunQueue(q):
3796 return
3797
3798 def _RunQueue(self, queue):
3799 for m in getattr(self, queue):
3800 if not m.Run(self):
3801 self._MarkUnclean()
3802 return False
3803 setattr(self, queue, [])
3804 return True
3805
3806 def _PrintMessages(self):
3807 if self._messages or self._failures:
3808 if os.isatty(2):
3809 self.out.write(progress.CSI_ERASE_LINE)
3810 self.out.write("\r")
3811
3812 for m in self._messages:
3813 m.Print(self)
3814 for m in self._failures:
3815 m.Print(self)
3816
3817 self._messages = []
3818 self._failures = []
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003819
3820
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003821class MetaProject(Project):
Gavin Makea2e3302023-03-11 06:46:20 +00003822 """A special project housed under .repo."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003823
Gavin Makea2e3302023-03-11 06:46:20 +00003824 def __init__(self, manifest, name, gitdir, worktree):
3825 Project.__init__(
3826 self,
3827 manifest=manifest,
3828 name=name,
3829 gitdir=gitdir,
3830 objdir=gitdir,
3831 worktree=worktree,
3832 remote=RemoteSpec("origin"),
3833 relpath=".repo/%s" % name,
3834 revisionExpr="refs/heads/master",
3835 revisionId=None,
3836 groups=None,
3837 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003838
Gavin Makea2e3302023-03-11 06:46:20 +00003839 def PreSync(self):
3840 if self.Exists:
3841 cb = self.CurrentBranch
3842 if cb:
3843 base = self.GetBranch(cb).merge
3844 if base:
3845 self.revisionExpr = base
3846 self.revisionId = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003847
Gavin Makea2e3302023-03-11 06:46:20 +00003848 @property
3849 def HasChanges(self):
3850 """Has the remote received new commits not yet checked out?"""
3851 if not self.remote or not self.revisionExpr:
3852 return False
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003853
Gavin Makea2e3302023-03-11 06:46:20 +00003854 all_refs = self.bare_ref.all
3855 revid = self.GetRevisionId(all_refs)
3856 head = self.work_git.GetHead()
3857 if head.startswith(R_HEADS):
3858 try:
3859 head = all_refs[head]
3860 except KeyError:
3861 head = None
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003862
Gavin Makea2e3302023-03-11 06:46:20 +00003863 if revid == head:
3864 return False
3865 elif self._revlist(not_rev(HEAD), revid):
3866 return True
3867 return False
LaMont Jones9b72cf22022-03-29 21:54:22 +00003868
3869
3870class RepoProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003871 """The MetaProject for repo itself."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003872
Gavin Makea2e3302023-03-11 06:46:20 +00003873 @property
3874 def LastFetch(self):
3875 try:
3876 fh = os.path.join(self.gitdir, "FETCH_HEAD")
3877 return os.path.getmtime(fh)
3878 except OSError:
3879 return 0
LaMont Jones9b72cf22022-03-29 21:54:22 +00003880
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +01003881
LaMont Jones9b72cf22022-03-29 21:54:22 +00003882class ManifestProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003883 """The MetaProject for manifests."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003884
Gavin Makea2e3302023-03-11 06:46:20 +00003885 def MetaBranchSwitch(self, submodules=False):
3886 """Prepare for manifest branch switch."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003887
Gavin Makea2e3302023-03-11 06:46:20 +00003888 # detach and delete manifest branch, allowing a new
3889 # branch to take over
3890 syncbuf = SyncBuffer(self.config, detach_head=True)
3891 self.Sync_LocalHalf(syncbuf, submodules=submodules)
3892 syncbuf.Finish()
LaMont Jones9b72cf22022-03-29 21:54:22 +00003893
Gavin Makea2e3302023-03-11 06:46:20 +00003894 return (
3895 GitCommand(
3896 self,
3897 ["update-ref", "-d", "refs/heads/default"],
3898 capture_stdout=True,
3899 capture_stderr=True,
3900 ).Wait()
3901 == 0
3902 )
LaMont Jones9b03f152022-03-29 23:01:18 +00003903
Gavin Makea2e3302023-03-11 06:46:20 +00003904 @property
3905 def standalone_manifest_url(self):
3906 """The URL of the standalone manifest, or None."""
3907 return self.config.GetString("manifest.standalone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003908
Gavin Makea2e3302023-03-11 06:46:20 +00003909 @property
3910 def manifest_groups(self):
3911 """The manifest groups string."""
3912 return self.config.GetString("manifest.groups")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003913
Gavin Makea2e3302023-03-11 06:46:20 +00003914 @property
3915 def reference(self):
3916 """The --reference for this manifest."""
3917 return self.config.GetString("repo.reference")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003918
Gavin Makea2e3302023-03-11 06:46:20 +00003919 @property
3920 def dissociate(self):
3921 """Whether to dissociate."""
3922 return self.config.GetBoolean("repo.dissociate")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003923
Gavin Makea2e3302023-03-11 06:46:20 +00003924 @property
3925 def archive(self):
3926 """Whether we use archive."""
3927 return self.config.GetBoolean("repo.archive")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003928
Gavin Makea2e3302023-03-11 06:46:20 +00003929 @property
3930 def mirror(self):
3931 """Whether we use mirror."""
3932 return self.config.GetBoolean("repo.mirror")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003933
Gavin Makea2e3302023-03-11 06:46:20 +00003934 @property
3935 def use_worktree(self):
3936 """Whether we use worktree."""
3937 return self.config.GetBoolean("repo.worktree")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003938
Gavin Makea2e3302023-03-11 06:46:20 +00003939 @property
3940 def clone_bundle(self):
3941 """Whether we use clone_bundle."""
3942 return self.config.GetBoolean("repo.clonebundle")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003943
Gavin Makea2e3302023-03-11 06:46:20 +00003944 @property
3945 def submodules(self):
3946 """Whether we use submodules."""
3947 return self.config.GetBoolean("repo.submodules")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003948
Gavin Makea2e3302023-03-11 06:46:20 +00003949 @property
3950 def git_lfs(self):
3951 """Whether we use git_lfs."""
3952 return self.config.GetBoolean("repo.git-lfs")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003953
Gavin Makea2e3302023-03-11 06:46:20 +00003954 @property
3955 def use_superproject(self):
3956 """Whether we use superproject."""
3957 return self.config.GetBoolean("repo.superproject")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003958
Gavin Makea2e3302023-03-11 06:46:20 +00003959 @property
3960 def partial_clone(self):
3961 """Whether this is a partial clone."""
3962 return self.config.GetBoolean("repo.partialclone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003963
Gavin Makea2e3302023-03-11 06:46:20 +00003964 @property
3965 def depth(self):
3966 """Partial clone depth."""
3967 return self.config.GetString("repo.depth")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003968
Gavin Makea2e3302023-03-11 06:46:20 +00003969 @property
3970 def clone_filter(self):
3971 """The clone filter."""
3972 return self.config.GetString("repo.clonefilter")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003973
Gavin Makea2e3302023-03-11 06:46:20 +00003974 @property
3975 def partial_clone_exclude(self):
3976 """Partial clone exclude string"""
3977 return self.config.GetString("repo.partialcloneexclude")
LaMont Jones4ada0432022-04-14 15:10:43 +00003978
Gavin Makea2e3302023-03-11 06:46:20 +00003979 @property
Jason Chang17833322023-05-23 13:06:55 -07003980 def clone_filter_for_depth(self):
3981 """Replace shallow clone with partial clone."""
3982 return self.config.GetString("repo.clonefilterfordepth")
3983
3984 @property
Gavin Makea2e3302023-03-11 06:46:20 +00003985 def manifest_platform(self):
3986 """The --platform argument from `repo init`."""
3987 return self.config.GetString("manifest.platform")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003988
Gavin Makea2e3302023-03-11 06:46:20 +00003989 @property
3990 def _platform_name(self):
3991 """Return the name of the platform."""
3992 return platform.system().lower()
LaMont Jones9b03f152022-03-29 23:01:18 +00003993
Gavin Makea2e3302023-03-11 06:46:20 +00003994 def SyncWithPossibleInit(
3995 self,
3996 submanifest,
3997 verbose=False,
3998 current_branch_only=False,
3999 tags="",
4000 git_event_log=None,
4001 ):
4002 """Sync a manifestProject, possibly for the first time.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004003
Gavin Makea2e3302023-03-11 06:46:20 +00004004 Call Sync() with arguments from the most recent `repo init`. If this is
4005 a new sub manifest, then inherit options from the parent's
4006 manifestProject.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004007
Gavin Makea2e3302023-03-11 06:46:20 +00004008 This is used by subcmds.Sync() to do an initial download of new sub
4009 manifests.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004010
Gavin Makea2e3302023-03-11 06:46:20 +00004011 Args:
4012 submanifest: an XmlSubmanifest, the submanifest to re-sync.
4013 verbose: a boolean, whether to show all output, rather than only
4014 errors.
4015 current_branch_only: a boolean, whether to only fetch the current
4016 manifest branch from the server.
4017 tags: a boolean, whether to fetch tags.
4018 git_event_log: an EventLog, for git tracing.
4019 """
4020 # TODO(lamontjones): when refactoring sync (and init?) consider how to
4021 # better get the init options that we should use for new submanifests
4022 # that are added when syncing an existing workspace.
4023 git_event_log = git_event_log or EventLog()
LaMont Jonesb90a4222022-04-14 15:00:09 +00004024 spec = submanifest.ToSubmanifestSpec()
Gavin Makea2e3302023-03-11 06:46:20 +00004025 # Use the init options from the existing manifestProject, or the parent
4026 # if it doesn't exist.
4027 #
4028 # Today, we only support changing manifest_groups on the sub-manifest,
4029 # with no supported-for-the-user way to change the other arguments from
4030 # those specified by the outermost manifest.
4031 #
4032 # TODO(lamontjones): determine which of these should come from the
4033 # outermost manifest and which should come from the parent manifest.
4034 mp = self if self.Exists else submanifest.parent.manifestProject
4035 return self.Sync(
LaMont Jones55ee3042022-04-06 17:10:21 +00004036 manifest_url=spec.manifestUrl,
4037 manifest_branch=spec.revision,
Gavin Makea2e3302023-03-11 06:46:20 +00004038 standalone_manifest=mp.standalone_manifest_url,
4039 groups=mp.manifest_groups,
4040 platform=mp.manifest_platform,
4041 mirror=mp.mirror,
4042 dissociate=mp.dissociate,
4043 reference=mp.reference,
4044 worktree=mp.use_worktree,
4045 submodules=mp.submodules,
4046 archive=mp.archive,
4047 partial_clone=mp.partial_clone,
4048 clone_filter=mp.clone_filter,
4049 partial_clone_exclude=mp.partial_clone_exclude,
4050 clone_bundle=mp.clone_bundle,
4051 git_lfs=mp.git_lfs,
4052 use_superproject=mp.use_superproject,
LaMont Jones55ee3042022-04-06 17:10:21 +00004053 verbose=verbose,
4054 current_branch_only=current_branch_only,
4055 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00004056 depth=mp.depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004057 git_event_log=git_event_log,
4058 manifest_name=spec.manifestName,
Gavin Makea2e3302023-03-11 06:46:20 +00004059 this_manifest_only=True,
LaMont Jones55ee3042022-04-06 17:10:21 +00004060 outer_manifest=False,
Jason Chang17833322023-05-23 13:06:55 -07004061 clone_filter_for_depth=mp.clone_filter_for_depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004062 )
LaMont Jones409407a2022-04-05 21:21:56 +00004063
Gavin Makea2e3302023-03-11 06:46:20 +00004064 def Sync(
4065 self,
4066 _kwargs_only=(),
4067 manifest_url="",
4068 manifest_branch=None,
4069 standalone_manifest=False,
4070 groups="",
4071 mirror=False,
4072 reference="",
4073 dissociate=False,
4074 worktree=False,
4075 submodules=False,
4076 archive=False,
4077 partial_clone=None,
4078 depth=None,
4079 clone_filter="blob:none",
4080 partial_clone_exclude=None,
4081 clone_bundle=None,
4082 git_lfs=None,
4083 use_superproject=None,
4084 verbose=False,
4085 current_branch_only=False,
4086 git_event_log=None,
4087 platform="",
4088 manifest_name="default.xml",
4089 tags="",
4090 this_manifest_only=False,
4091 outer_manifest=True,
Jason Chang17833322023-05-23 13:06:55 -07004092 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00004093 ):
4094 """Sync the manifest and all submanifests.
LaMont Jones409407a2022-04-05 21:21:56 +00004095
Gavin Makea2e3302023-03-11 06:46:20 +00004096 Args:
4097 manifest_url: a string, the URL of the manifest project.
4098 manifest_branch: a string, the manifest branch to use.
4099 standalone_manifest: a boolean, whether to store the manifest as a
4100 static file.
4101 groups: a string, restricts the checkout to projects with the
4102 specified groups.
4103 mirror: a boolean, whether to create a mirror of the remote
4104 repository.
4105 reference: a string, location of a repo instance to use as a
4106 reference.
4107 dissociate: a boolean, whether to dissociate from reference mirrors
4108 after clone.
4109 worktree: a boolean, whether to use git-worktree to manage projects.
4110 submodules: a boolean, whether sync submodules associated with the
4111 manifest project.
4112 archive: a boolean, whether to checkout each project as an archive.
4113 See git-archive.
4114 partial_clone: a boolean, whether to perform a partial clone.
4115 depth: an int, how deep of a shallow clone to create.
4116 clone_filter: a string, filter to use with partial_clone.
4117 partial_clone_exclude : a string, comma-delimeted list of project
4118 names to exclude from partial clone.
4119 clone_bundle: a boolean, whether to enable /clone.bundle on
4120 HTTP/HTTPS.
4121 git_lfs: a boolean, whether to enable git LFS support.
4122 use_superproject: a boolean, whether to use the manifest
4123 superproject to sync projects.
4124 verbose: a boolean, whether to show all output, rather than only
4125 errors.
4126 current_branch_only: a boolean, whether to only fetch the current
4127 manifest branch from the server.
4128 platform: a string, restrict the checkout to projects with the
4129 specified platform group.
4130 git_event_log: an EventLog, for git tracing.
4131 tags: a boolean, whether to fetch tags.
4132 manifest_name: a string, the name of the manifest file to use.
4133 this_manifest_only: a boolean, whether to only operate on the
4134 current sub manifest.
4135 outer_manifest: a boolean, whether to start at the outermost
4136 manifest.
Jason Chang17833322023-05-23 13:06:55 -07004137 clone_filter_for_depth: a string, when specified replaces shallow
4138 clones with partial.
LaMont Jones9b03f152022-03-29 23:01:18 +00004139
Gavin Makea2e3302023-03-11 06:46:20 +00004140 Returns:
4141 a boolean, whether the sync was successful.
4142 """
4143 assert _kwargs_only == (), "Sync only accepts keyword arguments."
LaMont Jones9b03f152022-03-29 23:01:18 +00004144
Gavin Makea2e3302023-03-11 06:46:20 +00004145 groups = groups or self.manifest.GetDefaultGroupsStr(
4146 with_platform=False
4147 )
4148 platform = platform or "auto"
4149 git_event_log = git_event_log or EventLog()
4150 if outer_manifest and self.manifest.is_submanifest:
4151 # In a multi-manifest checkout, use the outer manifest unless we are
4152 # told not to.
4153 return self.client.outer_manifest.manifestProject.Sync(
4154 manifest_url=manifest_url,
4155 manifest_branch=manifest_branch,
4156 standalone_manifest=standalone_manifest,
4157 groups=groups,
4158 platform=platform,
4159 mirror=mirror,
4160 dissociate=dissociate,
4161 reference=reference,
4162 worktree=worktree,
4163 submodules=submodules,
4164 archive=archive,
4165 partial_clone=partial_clone,
4166 clone_filter=clone_filter,
4167 partial_clone_exclude=partial_clone_exclude,
4168 clone_bundle=clone_bundle,
4169 git_lfs=git_lfs,
4170 use_superproject=use_superproject,
4171 verbose=verbose,
4172 current_branch_only=current_branch_only,
4173 tags=tags,
4174 depth=depth,
4175 git_event_log=git_event_log,
4176 manifest_name=manifest_name,
4177 this_manifest_only=this_manifest_only,
4178 outer_manifest=False,
4179 )
LaMont Jones9b03f152022-03-29 23:01:18 +00004180
Gavin Makea2e3302023-03-11 06:46:20 +00004181 # If repo has already been initialized, we take -u with the absence of
4182 # --standalone-manifest to mean "transition to a standard repo set up",
4183 # which necessitates starting fresh.
4184 # If --standalone-manifest is set, we always tear everything down and
4185 # start anew.
4186 if self.Exists:
4187 was_standalone_manifest = self.config.GetString(
4188 "manifest.standalone"
4189 )
4190 if was_standalone_manifest and not manifest_url:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004191 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004192 "fatal: repo was initialized with a standlone manifest, "
4193 "cannot be re-initialized without --manifest-url/-u"
4194 )
4195 return False
4196
4197 if standalone_manifest or (
4198 was_standalone_manifest and manifest_url
4199 ):
4200 self.config.ClearCache()
4201 if self.gitdir and os.path.exists(self.gitdir):
4202 platform_utils.rmtree(self.gitdir)
4203 if self.worktree and os.path.exists(self.worktree):
4204 platform_utils.rmtree(self.worktree)
4205
4206 is_new = not self.Exists
4207 if is_new:
4208 if not manifest_url:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004209 logger.error("fatal: manifest url is required.")
Gavin Makea2e3302023-03-11 06:46:20 +00004210 return False
4211
4212 if verbose:
4213 print(
4214 "Downloading manifest from %s"
4215 % (GitConfig.ForUser().UrlInsteadOf(manifest_url),),
4216 file=sys.stderr,
4217 )
4218
4219 # The manifest project object doesn't keep track of the path on the
4220 # server where this git is located, so let's save that here.
4221 mirrored_manifest_git = None
4222 if reference:
4223 manifest_git_path = urllib.parse.urlparse(manifest_url).path[1:]
4224 mirrored_manifest_git = os.path.join(
4225 reference, manifest_git_path
4226 )
4227 if not mirrored_manifest_git.endswith(".git"):
4228 mirrored_manifest_git += ".git"
4229 if not os.path.exists(mirrored_manifest_git):
4230 mirrored_manifest_git = os.path.join(
4231 reference, ".repo/manifests.git"
4232 )
4233
4234 self._InitGitDir(mirror_git=mirrored_manifest_git)
4235
4236 # If standalone_manifest is set, mark the project as "standalone" --
4237 # we'll still do much of the manifests.git set up, but will avoid actual
4238 # syncs to a remote.
4239 if standalone_manifest:
4240 self.config.SetString("manifest.standalone", manifest_url)
4241 elif not manifest_url and not manifest_branch:
4242 # If -u is set and --standalone-manifest is not, then we're not in
4243 # standalone mode. Otherwise, use config to infer what we were in
4244 # the last init.
4245 standalone_manifest = bool(
4246 self.config.GetString("manifest.standalone")
4247 )
4248 if not standalone_manifest:
4249 self.config.SetString("manifest.standalone", None)
4250
4251 self._ConfigureDepth(depth)
4252
4253 # Set the remote URL before the remote branch as we might need it below.
4254 if manifest_url:
4255 r = self.GetRemote()
4256 r.url = manifest_url
4257 r.ResetFetch()
4258 r.Save()
4259
4260 if not standalone_manifest:
4261 if manifest_branch:
4262 if manifest_branch == "HEAD":
4263 manifest_branch = self.ResolveRemoteHead()
4264 if manifest_branch is None:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004265 logger.error("fatal: unable to resolve HEAD")
Gavin Makea2e3302023-03-11 06:46:20 +00004266 return False
4267 self.revisionExpr = manifest_branch
4268 else:
4269 if is_new:
4270 default_branch = self.ResolveRemoteHead()
4271 if default_branch is None:
4272 # If the remote doesn't have HEAD configured, default to
4273 # master.
4274 default_branch = "refs/heads/master"
4275 self.revisionExpr = default_branch
4276 else:
4277 self.PreSync()
4278
4279 groups = re.split(r"[,\s]+", groups or "")
4280 all_platforms = ["linux", "darwin", "windows"]
4281 platformize = lambda x: "platform-" + x
4282 if platform == "auto":
4283 if not mirror and not self.mirror:
4284 groups.append(platformize(self._platform_name))
4285 elif platform == "all":
4286 groups.extend(map(platformize, all_platforms))
4287 elif platform in all_platforms:
4288 groups.append(platformize(platform))
4289 elif platform != "none":
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004290 logger.error("fatal: invalid platform flag", file=sys.stderr)
Gavin Makea2e3302023-03-11 06:46:20 +00004291 return False
4292 self.config.SetString("manifest.platform", platform)
4293
4294 groups = [x for x in groups if x]
4295 groupstr = ",".join(groups)
4296 if (
4297 platform == "auto"
4298 and groupstr == self.manifest.GetDefaultGroupsStr()
4299 ):
4300 groupstr = None
4301 self.config.SetString("manifest.groups", groupstr)
4302
4303 if reference:
4304 self.config.SetString("repo.reference", reference)
4305
4306 if dissociate:
4307 self.config.SetBoolean("repo.dissociate", dissociate)
4308
4309 if worktree:
4310 if mirror:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004311 logger.error("fatal: --mirror and --worktree are incompatible")
Gavin Makea2e3302023-03-11 06:46:20 +00004312 return False
4313 if submodules:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004314 logger.error(
4315 "fatal: --submodules and --worktree are incompatible"
Gavin Makea2e3302023-03-11 06:46:20 +00004316 )
4317 return False
4318 self.config.SetBoolean("repo.worktree", worktree)
4319 if is_new:
4320 self.use_git_worktrees = True
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004321 logger.warn("warning: --worktree is experimental!")
Gavin Makea2e3302023-03-11 06:46:20 +00004322
4323 if archive:
4324 if is_new:
4325 self.config.SetBoolean("repo.archive", archive)
4326 else:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004327 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004328 "fatal: --archive is only supported when initializing a "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004329 "new workspace."
Gavin Makea2e3302023-03-11 06:46:20 +00004330 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004331 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004332 "Either delete the .repo folder in this workspace, or "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004333 "initialize in another location."
Gavin Makea2e3302023-03-11 06:46:20 +00004334 )
4335 return False
4336
4337 if mirror:
4338 if is_new:
4339 self.config.SetBoolean("repo.mirror", mirror)
4340 else:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004341 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004342 "fatal: --mirror is only supported when initializing a new "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004343 "workspace."
Gavin Makea2e3302023-03-11 06:46:20 +00004344 )
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004345 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004346 "Either delete the .repo folder in this workspace, or "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004347 "initialize in another location."
Gavin Makea2e3302023-03-11 06:46:20 +00004348 )
4349 return False
4350
4351 if partial_clone is not None:
4352 if mirror:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004353 logger.error(
Gavin Makea2e3302023-03-11 06:46:20 +00004354 "fatal: --mirror and --partial-clone are mutually "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004355 "exclusive"
Gavin Makea2e3302023-03-11 06:46:20 +00004356 )
4357 return False
4358 self.config.SetBoolean("repo.partialclone", partial_clone)
4359 if clone_filter:
4360 self.config.SetString("repo.clonefilter", clone_filter)
4361 elif self.partial_clone:
4362 clone_filter = self.clone_filter
4363 else:
4364 clone_filter = None
4365
4366 if partial_clone_exclude is not None:
4367 self.config.SetString(
4368 "repo.partialcloneexclude", partial_clone_exclude
4369 )
4370
4371 if clone_bundle is None:
4372 clone_bundle = False if partial_clone else True
4373 else:
4374 self.config.SetBoolean("repo.clonebundle", clone_bundle)
4375
4376 if submodules:
4377 self.config.SetBoolean("repo.submodules", submodules)
4378
4379 if git_lfs is not None:
4380 if git_lfs:
4381 git_require((2, 17, 0), fail=True, msg="Git LFS support")
4382
4383 self.config.SetBoolean("repo.git-lfs", git_lfs)
4384 if not is_new:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004385 logger.warn(
Gavin Makea2e3302023-03-11 06:46:20 +00004386 "warning: Changing --git-lfs settings will only affect new "
4387 "project checkouts.\n"
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004388 " Existing projects will require manual updates.\n"
Gavin Makea2e3302023-03-11 06:46:20 +00004389 )
4390
Jason Chang17833322023-05-23 13:06:55 -07004391 if clone_filter_for_depth is not None:
4392 self.ConfigureCloneFilterForDepth(clone_filter_for_depth)
4393
Gavin Makea2e3302023-03-11 06:46:20 +00004394 if use_superproject is not None:
4395 self.config.SetBoolean("repo.superproject", use_superproject)
4396
4397 if not standalone_manifest:
4398 success = self.Sync_NetworkHalf(
4399 is_new=is_new,
4400 quiet=not verbose,
4401 verbose=verbose,
4402 clone_bundle=clone_bundle,
4403 current_branch_only=current_branch_only,
4404 tags=tags,
4405 submodules=submodules,
4406 clone_filter=clone_filter,
4407 partial_clone_exclude=self.manifest.PartialCloneExclude,
Jason Chang17833322023-05-23 13:06:55 -07004408 clone_filter_for_depth=self.manifest.CloneFilterForDepth,
Gavin Makea2e3302023-03-11 06:46:20 +00004409 ).success
4410 if not success:
4411 r = self.GetRemote()
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004412 logger.error("fatal: cannot obtain manifest %s", r.url)
Gavin Makea2e3302023-03-11 06:46:20 +00004413
4414 # Better delete the manifest git dir if we created it; otherwise
4415 # next time (when user fixes problems) we won't go through the
4416 # "is_new" logic.
4417 if is_new:
4418 platform_utils.rmtree(self.gitdir)
4419 return False
4420
4421 if manifest_branch:
4422 self.MetaBranchSwitch(submodules=submodules)
4423
4424 syncbuf = SyncBuffer(self.config)
4425 self.Sync_LocalHalf(syncbuf, submodules=submodules)
4426 syncbuf.Finish()
4427
4428 if is_new or self.CurrentBranch is None:
Jason Chang1a3612f2023-08-08 14:12:53 -07004429 try:
4430 self.StartBranch("default")
4431 except GitError as e:
4432 msg = str(e)
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004433 logger.error(
4434 "fatal: cannot create default in manifest %s", msg
Gavin Makea2e3302023-03-11 06:46:20 +00004435 )
4436 return False
4437
4438 if not manifest_name:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004439 logger.error("fatal: manifest name (-m) is required.")
Gavin Makea2e3302023-03-11 06:46:20 +00004440 return False
4441
4442 elif is_new:
4443 # This is a new standalone manifest.
4444 manifest_name = "default.xml"
4445 manifest_data = fetch.fetch_file(manifest_url, verbose=verbose)
4446 dest = os.path.join(self.worktree, manifest_name)
4447 os.makedirs(os.path.dirname(dest), exist_ok=True)
4448 with open(dest, "wb") as f:
4449 f.write(manifest_data)
4450
4451 try:
4452 self.manifest.Link(manifest_name)
4453 except ManifestParseError as e:
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004454 logger.error("fatal: manifest '%s' not available", manifest_name)
4455 logger.error("fatal: %s", e)
Gavin Makea2e3302023-03-11 06:46:20 +00004456 return False
4457
4458 if not this_manifest_only:
4459 for submanifest in self.manifest.submanifests.values():
4460 spec = submanifest.ToSubmanifestSpec()
4461 submanifest.repo_client.manifestProject.Sync(
4462 manifest_url=spec.manifestUrl,
4463 manifest_branch=spec.revision,
4464 standalone_manifest=standalone_manifest,
4465 groups=self.manifest_groups,
4466 platform=platform,
4467 mirror=mirror,
4468 dissociate=dissociate,
4469 reference=reference,
4470 worktree=worktree,
4471 submodules=submodules,
4472 archive=archive,
4473 partial_clone=partial_clone,
4474 clone_filter=clone_filter,
4475 partial_clone_exclude=partial_clone_exclude,
4476 clone_bundle=clone_bundle,
4477 git_lfs=git_lfs,
4478 use_superproject=use_superproject,
4479 verbose=verbose,
4480 current_branch_only=current_branch_only,
4481 tags=tags,
4482 depth=depth,
4483 git_event_log=git_event_log,
4484 manifest_name=spec.manifestName,
4485 this_manifest_only=False,
4486 outer_manifest=False,
4487 )
4488
4489 # Lastly, if the manifest has a <superproject> then have the
4490 # superproject sync it (if it will be used).
4491 if git_superproject.UseSuperproject(use_superproject, self.manifest):
4492 sync_result = self.manifest.superproject.Sync(git_event_log)
4493 if not sync_result.success:
4494 submanifest = ""
4495 if self.manifest.path_prefix:
4496 submanifest = f"for {self.manifest.path_prefix} "
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004497 logger.warn(
4498 "warning: git update of superproject %s failed, "
Gavin Makea2e3302023-03-11 06:46:20 +00004499 "repo sync will not use superproject to fetch source; "
4500 "while this error is not fatal, and you can continue to "
4501 "run repo sync, please run repo init with the "
4502 "--no-use-superproject option to stop seeing this warning",
Aravind Vasudevan7a1f1f72023-09-14 08:17:20 +00004503 submanifest,
Gavin Makea2e3302023-03-11 06:46:20 +00004504 )
4505 if sync_result.fatal and use_superproject is not None:
4506 return False
4507
4508 return True
4509
Jason Chang17833322023-05-23 13:06:55 -07004510 def ConfigureCloneFilterForDepth(self, clone_filter_for_depth):
4511 """Configure clone filter to replace shallow clones.
4512
4513 Args:
4514 clone_filter_for_depth: a string or None, e.g. 'blob:none' will
4515 disable shallow clones and replace with partial clone. None will
4516 enable shallow clones.
4517 """
4518 self.config.SetString(
4519 "repo.clonefilterfordepth", clone_filter_for_depth
4520 )
4521
Gavin Makea2e3302023-03-11 06:46:20 +00004522 def _ConfigureDepth(self, depth):
4523 """Configure the depth we'll sync down.
4524
4525 Args:
4526 depth: an int, how deep of a partial clone to create.
4527 """
4528 # Opt.depth will be non-None if user actually passed --depth to repo
4529 # init.
4530 if depth is not None:
4531 if depth > 0:
4532 # Positive values will set the depth.
4533 depth = str(depth)
4534 else:
4535 # Negative numbers will clear the depth; passing None to
4536 # SetString will do that.
4537 depth = None
4538
4539 # We store the depth in the main manifest project.
4540 self.config.SetString("repo.depth", depth)