blob: 34e9046dfe8d96b4ba82d500095d19eb20160a68 [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
47from git_config import ID_RE
48from git_config import IsId
49from git_refs import GitRefs
50from git_refs import HEAD
51from git_refs import R_HEADS
52from git_refs import R_M
53from git_refs import R_PUB
54from git_refs import R_TAGS
55from git_refs import R_WORKTREE_M
LaMont Jonesff6b1da2022-06-01 21:03:34 +000056import git_superproject
LaMont Jones55ee3042022-04-06 17:10:21 +000057from git_trace2_event_log import EventLog
Renaud Paquayd5cec5e2016-11-01 11:24:03 -070058import platform_utils
Mike Frysinger70d861f2019-08-26 15:22:36 -040059import progress
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
LaMont Jones1eddca82022-09-01 15:15:04 +000063class SyncNetworkHalfResult(NamedTuple):
Gavin Makea2e3302023-03-11 06:46:20 +000064 """Sync_NetworkHalf return value."""
65
Gavin Makea2e3302023-03-11 06:46:20 +000066 # Did we query the remote? False when optimized_fetch is True and we have
67 # the commit already present.
68 remote_fetched: bool
Jason Chang32b59562023-07-14 16:45:35 -070069 # Error from SyncNetworkHalf
70 error: Exception = None
71
72 @property
73 def success(self) -> bool:
74 return not self.error
75
76
77class SyncNetworkHalfError(RepoError):
78 """Failure trying to sync."""
79
80
81class DeleteWorktreeError(RepoError):
82 """Failure to delete worktree."""
83
84 def __init__(
85 self, *args, aggregate_errors: List[Exception] = None, **kwargs
86 ) -> None:
87 super().__init__(*args, **kwargs)
88 self.aggregate_errors = aggregate_errors or []
89
90
91class DeleteDirtyWorktreeError(DeleteWorktreeError):
92 """Failure to delete worktree due to uncommitted changes."""
LaMont Jones1eddca82022-09-01 15:15:04 +000093
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +010094
George Engelbrecht9bc283e2020-04-02 12:36:09 -060095# Maximum sleep time allowed during retries.
96MAXIMUM_RETRY_SLEEP_SEC = 3600.0
97# +-10% random jitter is added to each Fetches retry sleep duration.
98RETRY_JITTER_PERCENT = 0.1
99
LaMont Jonesfa8d9392022-11-02 22:01:29 +0000100# Whether to use alternates. Switching back and forth is *NOT* supported.
Mike Frysinger1d00a7e2021-12-21 00:40:31 -0500101# TODO(vapier): Remove knob once behavior is verified.
Gavin Makea2e3302023-03-11 06:46:20 +0000102_ALTERNATES = os.environ.get("REPO_USE_ALTERNATES") == "1"
George Engelbrecht9bc283e2020-04-02 12:36:09 -0600103
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +0100104
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700105def _lwrite(path, content):
Gavin Makea2e3302023-03-11 06:46:20 +0000106 lock = "%s.lock" % path
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700107
Gavin Makea2e3302023-03-11 06:46:20 +0000108 # Maintain Unix line endings on all OS's to match git behavior.
109 with open(lock, "w", newline="\n") as fd:
110 fd.write(content)
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700111
Gavin Makea2e3302023-03-11 06:46:20 +0000112 try:
113 platform_utils.rename(lock, path)
114 except OSError:
115 platform_utils.remove(lock)
116 raise
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700117
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700118
Shawn O. Pearce48244782009-04-16 08:25:57 -0700119def _error(fmt, *args):
Gavin Makea2e3302023-03-11 06:46:20 +0000120 msg = fmt % args
121 print("error: %s" % msg, file=sys.stderr)
Shawn O. Pearce48244782009-04-16 08:25:57 -0700122
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700123
David Pursehousef33929d2015-08-24 14:39:14 +0900124def _warn(fmt, *args):
Gavin Makea2e3302023-03-11 06:46:20 +0000125 msg = fmt % args
126 print("warn: %s" % msg, file=sys.stderr)
David Pursehousef33929d2015-08-24 14:39:14 +0900127
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700128
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700129def not_rev(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000130 return "^" + r
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700131
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700132
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800133def sq(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000134 return "'" + r.replace("'", "'''") + "'"
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -0800135
David Pursehouse819827a2020-02-12 15:20:19 +0900136
Jonathan Nieder93719792015-03-17 11:29:58 -0700137_project_hook_list = None
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700138
139
Jonathan Nieder93719792015-03-17 11:29:58 -0700140def _ProjectHooks():
Gavin Makea2e3302023-03-11 06:46:20 +0000141 """List the hooks present in the 'hooks' directory.
Jonathan Nieder93719792015-03-17 11:29:58 -0700142
Gavin Makea2e3302023-03-11 06:46:20 +0000143 These hooks are project hooks and are copied to the '.git/hooks' directory
144 of all subprojects.
Jonathan Nieder93719792015-03-17 11:29:58 -0700145
Gavin Makea2e3302023-03-11 06:46:20 +0000146 This function caches the list of hooks (based on the contents of the
147 'repo/hooks' directory) on the first call.
Jonathan Nieder93719792015-03-17 11:29:58 -0700148
Gavin Makea2e3302023-03-11 06:46:20 +0000149 Returns:
150 A list of absolute paths to all of the files in the hooks directory.
151 """
152 global _project_hook_list
153 if _project_hook_list is None:
154 d = platform_utils.realpath(os.path.abspath(os.path.dirname(__file__)))
155 d = os.path.join(d, "hooks")
156 _project_hook_list = [
157 os.path.join(d, x) for x in platform_utils.listdir(d)
158 ]
159 return _project_hook_list
Jonathan Nieder93719792015-03-17 11:29:58 -0700160
161
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700162class DownloadedChange(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000163 _commit_cache = None
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700164
Gavin Makea2e3302023-03-11 06:46:20 +0000165 def __init__(self, project, base, change_id, ps_id, commit):
166 self.project = project
167 self.base = base
168 self.change_id = change_id
169 self.ps_id = ps_id
170 self.commit = commit
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700171
Gavin Makea2e3302023-03-11 06:46:20 +0000172 @property
173 def commits(self):
174 if self._commit_cache is None:
175 self._commit_cache = self.project.bare_git.rev_list(
176 "--abbrev=8",
177 "--abbrev-commit",
178 "--pretty=oneline",
179 "--reverse",
180 "--date-order",
181 not_rev(self.base),
182 self.commit,
183 "--",
184 )
185 return self._commit_cache
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700186
187
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700188class ReviewableBranch(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000189 _commit_cache = None
190 _base_exists = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700191
Gavin Makea2e3302023-03-11 06:46:20 +0000192 def __init__(self, project, branch, base):
193 self.project = project
194 self.branch = branch
195 self.base = base
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700196
Gavin Makea2e3302023-03-11 06:46:20 +0000197 @property
198 def name(self):
199 return self.branch.name
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700200
Gavin Makea2e3302023-03-11 06:46:20 +0000201 @property
202 def commits(self):
203 if self._commit_cache is None:
204 args = (
205 "--abbrev=8",
206 "--abbrev-commit",
207 "--pretty=oneline",
208 "--reverse",
209 "--date-order",
210 not_rev(self.base),
211 R_HEADS + self.name,
212 "--",
213 )
214 try:
215 self._commit_cache = self.project.bare_git.rev_list(*args)
216 except GitError:
217 # We weren't able to probe the commits for this branch. Was it
218 # tracking a branch that no longer exists? If so, return no
219 # commits. Otherwise, rethrow the error as we don't know what's
220 # going on.
221 if self.base_exists:
222 raise
Mike Frysinger6da17752019-09-11 18:43:17 -0400223
Gavin Makea2e3302023-03-11 06:46:20 +0000224 self._commit_cache = []
Mike Frysinger6da17752019-09-11 18:43:17 -0400225
Gavin Makea2e3302023-03-11 06:46:20 +0000226 return self._commit_cache
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700227
Gavin Makea2e3302023-03-11 06:46:20 +0000228 @property
229 def unabbrev_commits(self):
230 r = dict()
231 for commit in self.project.bare_git.rev_list(
232 not_rev(self.base), R_HEADS + self.name, "--"
233 ):
234 r[commit[0:8]] = commit
235 return r
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800236
Gavin Makea2e3302023-03-11 06:46:20 +0000237 @property
238 def date(self):
239 return self.project.bare_git.log(
240 "--pretty=format:%cd", "-n", "1", R_HEADS + self.name, "--"
241 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700242
Gavin Makea2e3302023-03-11 06:46:20 +0000243 @property
244 def base_exists(self):
245 """Whether the branch we're tracking exists.
Mike Frysinger6da17752019-09-11 18:43:17 -0400246
Gavin Makea2e3302023-03-11 06:46:20 +0000247 Normally it should, but sometimes branches we track can get deleted.
248 """
249 if self._base_exists is None:
250 try:
251 self.project.bare_git.rev_parse("--verify", not_rev(self.base))
252 # If we're still here, the base branch exists.
253 self._base_exists = True
254 except GitError:
255 # If we failed to verify, the base branch doesn't exist.
256 self._base_exists = False
Mike Frysinger6da17752019-09-11 18:43:17 -0400257
Gavin Makea2e3302023-03-11 06:46:20 +0000258 return self._base_exists
Mike Frysinger6da17752019-09-11 18:43:17 -0400259
Gavin Makea2e3302023-03-11 06:46:20 +0000260 def UploadForReview(
261 self,
262 people,
263 dryrun=False,
264 auto_topic=False,
265 hashtags=(),
266 labels=(),
267 private=False,
268 notify=None,
269 wip=False,
270 ready=False,
271 dest_branch=None,
272 validate_certs=True,
273 push_options=None,
274 ):
275 self.project.UploadForReview(
276 branch=self.name,
277 people=people,
278 dryrun=dryrun,
279 auto_topic=auto_topic,
280 hashtags=hashtags,
281 labels=labels,
282 private=private,
283 notify=notify,
284 wip=wip,
285 ready=ready,
286 dest_branch=dest_branch,
287 validate_certs=validate_certs,
288 push_options=push_options,
289 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700290
Gavin Makea2e3302023-03-11 06:46:20 +0000291 def GetPublishedRefs(self):
292 refs = {}
293 output = self.project.bare_git.ls_remote(
294 self.branch.remote.SshReviewUrl(self.project.UserEmail),
295 "refs/changes/*",
296 )
297 for line in output.split("\n"):
298 try:
299 (sha, ref) = line.split()
300 refs[sha] = ref
301 except ValueError:
302 pass
Ficus Kirkpatrickbc7ef672009-05-04 12:45:11 -0700303
Gavin Makea2e3302023-03-11 06:46:20 +0000304 return refs
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700305
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700306
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700307class StatusColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000308 def __init__(self, config):
309 super().__init__(config, "status")
310 self.project = self.printer("header", attr="bold")
311 self.branch = self.printer("header", attr="bold")
312 self.nobranch = self.printer("nobranch", fg="red")
313 self.important = self.printer("important", fg="red")
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700314
Gavin Makea2e3302023-03-11 06:46:20 +0000315 self.added = self.printer("added", fg="green")
316 self.changed = self.printer("changed", fg="red")
317 self.untracked = self.printer("untracked", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700318
319
320class DiffColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000321 def __init__(self, config):
322 super().__init__(config, "diff")
323 self.project = self.printer("header", attr="bold")
324 self.fail = self.printer("fail", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700325
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700326
Jack Neus6ea0cae2021-07-20 20:52:33 +0000327class Annotation(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000328 def __init__(self, name, value, keep):
329 self.name = name
330 self.value = value
331 self.keep = keep
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700332
Gavin Makea2e3302023-03-11 06:46:20 +0000333 def __eq__(self, other):
334 if not isinstance(other, Annotation):
335 return False
336 return self.__dict__ == other.__dict__
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700337
Gavin Makea2e3302023-03-11 06:46:20 +0000338 def __lt__(self, other):
339 # This exists just so that lists of Annotation objects can be sorted,
340 # for use in comparisons.
341 if not isinstance(other, Annotation):
342 raise ValueError("comparison is not between two Annotation objects")
343 if self.name == other.name:
344 if self.value == other.value:
345 return self.keep < other.keep
346 return self.value < other.value
347 return self.name < other.name
Jack Neus6ea0cae2021-07-20 20:52:33 +0000348
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700349
Mike Frysingere6a202f2019-08-02 15:57:57 -0400350def _SafeExpandPath(base, subpath, skipfinal=False):
Gavin Makea2e3302023-03-11 06:46:20 +0000351 """Make sure |subpath| is completely safe under |base|.
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700352
Gavin Makea2e3302023-03-11 06:46:20 +0000353 We make sure no intermediate symlinks are traversed, and that the final path
354 is not a special file (e.g. not a socket or fifo).
Mike Frysingere6a202f2019-08-02 15:57:57 -0400355
Gavin Makea2e3302023-03-11 06:46:20 +0000356 NB: We rely on a number of paths already being filtered out while parsing
357 the manifest. See the validation logic in manifest_xml.py for more details.
358 """
359 # Split up the path by its components. We can't use os.path.sep exclusively
360 # as some platforms (like Windows) will convert / to \ and that bypasses all
361 # our constructed logic here. Especially since manifest authors only use
362 # / in their paths.
363 resep = re.compile(r"[/%s]" % re.escape(os.path.sep))
364 components = resep.split(subpath)
365 if skipfinal:
366 # Whether the caller handles the final component itself.
367 finalpart = components.pop()
Mike Frysingere6a202f2019-08-02 15:57:57 -0400368
Gavin Makea2e3302023-03-11 06:46:20 +0000369 path = base
370 for part in components:
371 if part in {".", ".."}:
372 raise ManifestInvalidPathError(
373 '%s: "%s" not allowed in paths' % (subpath, part)
374 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400375
Gavin Makea2e3302023-03-11 06:46:20 +0000376 path = os.path.join(path, part)
377 if platform_utils.islink(path):
378 raise ManifestInvalidPathError(
379 "%s: traversing symlinks not allow" % (path,)
380 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400381
Gavin Makea2e3302023-03-11 06:46:20 +0000382 if os.path.exists(path):
383 if not os.path.isfile(path) and not platform_utils.isdir(path):
384 raise ManifestInvalidPathError(
385 "%s: only regular files & directories allowed" % (path,)
386 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400387
Gavin Makea2e3302023-03-11 06:46:20 +0000388 if skipfinal:
389 path = os.path.join(path, finalpart)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400390
Gavin Makea2e3302023-03-11 06:46:20 +0000391 return path
Mike Frysingere6a202f2019-08-02 15:57:57 -0400392
393
394class _CopyFile(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000395 """Container for <copyfile> manifest element."""
Mike Frysingere6a202f2019-08-02 15:57:57 -0400396
Gavin Makea2e3302023-03-11 06:46:20 +0000397 def __init__(self, git_worktree, src, topdir, dest):
398 """Register a <copyfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400399
Gavin Makea2e3302023-03-11 06:46:20 +0000400 Args:
401 git_worktree: Absolute path to the git project checkout.
402 src: Relative path under |git_worktree| of file to read.
403 topdir: Absolute path to the top of the repo client checkout.
404 dest: Relative path under |topdir| of file to write.
405 """
406 self.git_worktree = git_worktree
407 self.topdir = topdir
408 self.src = src
409 self.dest = dest
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700410
Gavin Makea2e3302023-03-11 06:46:20 +0000411 def _Copy(self):
412 src = _SafeExpandPath(self.git_worktree, self.src)
413 dest = _SafeExpandPath(self.topdir, self.dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400414
Gavin Makea2e3302023-03-11 06:46:20 +0000415 if platform_utils.isdir(src):
416 raise ManifestInvalidPathError(
417 "%s: copying from directory not supported" % (self.src,)
418 )
419 if platform_utils.isdir(dest):
420 raise ManifestInvalidPathError(
421 "%s: copying to directory not allowed" % (self.dest,)
422 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400423
Gavin Makea2e3302023-03-11 06:46:20 +0000424 # Copy file if it does not exist or is out of date.
425 if not os.path.exists(dest) or not filecmp.cmp(src, dest):
426 try:
427 # Remove existing file first, since it might be read-only.
428 if os.path.exists(dest):
429 platform_utils.remove(dest)
430 else:
431 dest_dir = os.path.dirname(dest)
432 if not platform_utils.isdir(dest_dir):
433 os.makedirs(dest_dir)
434 shutil.copy(src, dest)
435 # Make the file read-only.
436 mode = os.stat(dest)[stat.ST_MODE]
437 mode = mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
438 os.chmod(dest, mode)
439 except IOError:
440 _error("Cannot copy file %s to %s", src, dest)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700441
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700442
Anthony King7bdac712014-07-16 12:56:40 +0100443class _LinkFile(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000444 """Container for <linkfile> manifest element."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700445
Gavin Makea2e3302023-03-11 06:46:20 +0000446 def __init__(self, git_worktree, src, topdir, dest):
447 """Register a <linkfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400448
Gavin Makea2e3302023-03-11 06:46:20 +0000449 Args:
450 git_worktree: Absolute path to the git project checkout.
451 src: Target of symlink relative to path under |git_worktree|.
452 topdir: Absolute path to the top of the repo client checkout.
453 dest: Relative path under |topdir| of symlink to create.
454 """
455 self.git_worktree = git_worktree
456 self.topdir = topdir
457 self.src = src
458 self.dest = dest
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500459
Gavin Makea2e3302023-03-11 06:46:20 +0000460 def __linkIt(self, relSrc, absDest):
461 # Link file if it does not exist or is out of date.
462 if not platform_utils.islink(absDest) or (
463 platform_utils.readlink(absDest) != relSrc
464 ):
465 try:
466 # Remove existing file first, since it might be read-only.
467 if os.path.lexists(absDest):
468 platform_utils.remove(absDest)
469 else:
470 dest_dir = os.path.dirname(absDest)
471 if not platform_utils.isdir(dest_dir):
472 os.makedirs(dest_dir)
473 platform_utils.symlink(relSrc, absDest)
474 except IOError:
475 _error("Cannot link file %s to %s", relSrc, absDest)
476
477 def _Link(self):
478 """Link the self.src & self.dest paths.
479
480 Handles wild cards on the src linking all of the files in the source in
481 to the destination directory.
482 """
483 # Some people use src="." to create stable links to projects. Let's
484 # allow that but reject all other uses of "." to keep things simple.
485 if self.src == ".":
486 src = self.git_worktree
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500487 else:
Gavin Makea2e3302023-03-11 06:46:20 +0000488 src = _SafeExpandPath(self.git_worktree, self.src)
Wink Saville4c426ef2015-06-03 08:05:17 -0700489
Gavin Makea2e3302023-03-11 06:46:20 +0000490 if not glob.has_magic(src):
491 # Entity does not contain a wild card so just a simple one to one
492 # link operation.
493 dest = _SafeExpandPath(self.topdir, self.dest, skipfinal=True)
494 # dest & src are absolute paths at this point. Make sure the target
495 # of the symlink is relative in the context of the repo client
496 # checkout.
497 relpath = os.path.relpath(src, os.path.dirname(dest))
498 self.__linkIt(relpath, dest)
499 else:
500 dest = _SafeExpandPath(self.topdir, self.dest)
501 # Entity contains a wild card.
502 if os.path.exists(dest) and not platform_utils.isdir(dest):
503 _error(
504 "Link error: src with wildcard, %s must be a directory",
505 dest,
506 )
507 else:
508 for absSrcFile in glob.glob(src):
509 # Create a releative path from source dir to destination
510 # dir.
511 absSrcDir = os.path.dirname(absSrcFile)
512 relSrcDir = os.path.relpath(absSrcDir, dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400513
Gavin Makea2e3302023-03-11 06:46:20 +0000514 # Get the source file name.
515 srcFile = os.path.basename(absSrcFile)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400516
Gavin Makea2e3302023-03-11 06:46:20 +0000517 # Now form the final full paths to srcFile. They will be
518 # absolute for the desintaiton and relative for the source.
519 absDest = os.path.join(dest, srcFile)
520 relSrc = os.path.join(relSrcDir, srcFile)
521 self.__linkIt(relSrc, absDest)
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500522
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700523
Shawn O. Pearced1f70d92009-05-19 14:58:02 -0700524class RemoteSpec(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000525 def __init__(
526 self,
527 name,
528 url=None,
529 pushUrl=None,
530 review=None,
531 revision=None,
532 orig_name=None,
533 fetchUrl=None,
534 ):
535 self.name = name
536 self.url = url
537 self.pushUrl = pushUrl
538 self.review = review
539 self.revision = revision
540 self.orig_name = orig_name
541 self.fetchUrl = fetchUrl
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700542
Ian Kasprzak0286e312021-02-05 10:06:18 -0800543
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700544class Project(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000545 # These objects can be shared between several working trees.
546 @property
547 def shareable_dirs(self):
548 """Return the shareable directories"""
549 if self.UseAlternates:
550 return ["hooks", "rr-cache"]
551 else:
552 return ["hooks", "objects", "rr-cache"]
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700553
Gavin Makea2e3302023-03-11 06:46:20 +0000554 def __init__(
555 self,
556 manifest,
557 name,
558 remote,
559 gitdir,
560 objdir,
561 worktree,
562 relpath,
563 revisionExpr,
564 revisionId,
565 rebase=True,
566 groups=None,
567 sync_c=False,
568 sync_s=False,
569 sync_tags=True,
570 clone_depth=None,
571 upstream=None,
572 parent=None,
573 use_git_worktrees=False,
574 is_derived=False,
575 dest_branch=None,
576 optimized_fetch=False,
577 retry_fetches=0,
578 old_revision=None,
579 ):
580 """Init a Project object.
Doug Anderson3ba5f952011-04-07 12:51:04 -0700581
582 Args:
Gavin Makea2e3302023-03-11 06:46:20 +0000583 manifest: The XmlManifest object.
584 name: The `name` attribute of manifest.xml's project element.
585 remote: RemoteSpec object specifying its remote's properties.
586 gitdir: Absolute path of git directory.
587 objdir: Absolute path of directory to store git objects.
588 worktree: Absolute path of git working tree.
589 relpath: Relative path of git working tree to repo's top directory.
590 revisionExpr: The `revision` attribute of manifest.xml's project
591 element.
592 revisionId: git commit id for checking out.
593 rebase: The `rebase` attribute of manifest.xml's project element.
594 groups: The `groups` attribute of manifest.xml's project element.
595 sync_c: The `sync-c` attribute of manifest.xml's project element.
596 sync_s: The `sync-s` attribute of manifest.xml's project element.
597 sync_tags: The `sync-tags` attribute of manifest.xml's project
598 element.
599 upstream: The `upstream` attribute of manifest.xml's project
600 element.
601 parent: The parent Project object.
602 use_git_worktrees: Whether to use `git worktree` for this project.
603 is_derived: False if the project was explicitly defined in the
604 manifest; True if the project is a discovered submodule.
605 dest_branch: The branch to which to push changes for review by
606 default.
607 optimized_fetch: If True, when a project is set to a sha1 revision,
608 only fetch from the remote if the sha1 is not present locally.
609 retry_fetches: Retry remote fetches n times upon receiving transient
610 error with exponential backoff and jitter.
611 old_revision: saved git commit id for open GITC projects.
612 """
613 self.client = self.manifest = manifest
614 self.name = name
615 self.remote = remote
616 self.UpdatePaths(relpath, worktree, gitdir, objdir)
617 self.SetRevision(revisionExpr, revisionId=revisionId)
618
619 self.rebase = rebase
620 self.groups = groups
621 self.sync_c = sync_c
622 self.sync_s = sync_s
623 self.sync_tags = sync_tags
624 self.clone_depth = clone_depth
625 self.upstream = upstream
626 self.parent = parent
627 # NB: Do not use this setting in __init__ to change behavior so that the
628 # manifest.git checkout can inspect & change it after instantiating.
629 # See the XmlManifest init code for more info.
630 self.use_git_worktrees = use_git_worktrees
631 self.is_derived = is_derived
632 self.optimized_fetch = optimized_fetch
633 self.retry_fetches = max(0, retry_fetches)
634 self.subprojects = []
635
636 self.snapshots = {}
637 self.copyfiles = []
638 self.linkfiles = []
639 self.annotations = []
640 self.dest_branch = dest_branch
641 self.old_revision = old_revision
642
643 # This will be filled in if a project is later identified to be the
644 # project containing repo hooks.
645 self.enabled_repo_hooks = []
646
647 def RelPath(self, local=True):
648 """Return the path for the project relative to a manifest.
649
650 Args:
651 local: a boolean, if True, the path is relative to the local
652 (sub)manifest. If false, the path is relative to the outermost
653 manifest.
654 """
655 if local:
656 return self.relpath
657 return os.path.join(self.manifest.path_prefix, self.relpath)
658
659 def SetRevision(self, revisionExpr, revisionId=None):
660 """Set revisionId based on revision expression and id"""
661 self.revisionExpr = revisionExpr
662 if revisionId is None and revisionExpr and IsId(revisionExpr):
663 self.revisionId = self.revisionExpr
664 else:
665 self.revisionId = revisionId
666
667 def UpdatePaths(self, relpath, worktree, gitdir, objdir):
668 """Update paths used by this project"""
669 self.gitdir = gitdir.replace("\\", "/")
670 self.objdir = objdir.replace("\\", "/")
671 if worktree:
672 self.worktree = os.path.normpath(worktree).replace("\\", "/")
673 else:
674 self.worktree = None
675 self.relpath = relpath
676
677 self.config = GitConfig.ForRepository(
678 gitdir=self.gitdir, defaults=self.manifest.globalConfig
679 )
680
681 if self.worktree:
682 self.work_git = self._GitGetByExec(
683 self, bare=False, gitdir=self.gitdir
684 )
685 else:
686 self.work_git = None
687 self.bare_git = self._GitGetByExec(self, bare=True, gitdir=self.gitdir)
688 self.bare_ref = GitRefs(self.gitdir)
689 self.bare_objdir = self._GitGetByExec(
690 self, bare=True, gitdir=self.objdir
691 )
692
693 @property
694 def UseAlternates(self):
695 """Whether git alternates are in use.
696
697 This will be removed once migration to alternates is complete.
698 """
699 return _ALTERNATES or self.manifest.is_multimanifest
700
701 @property
702 def Derived(self):
703 return self.is_derived
704
705 @property
706 def Exists(self):
707 return platform_utils.isdir(self.gitdir) and platform_utils.isdir(
708 self.objdir
709 )
710
711 @property
712 def CurrentBranch(self):
713 """Obtain the name of the currently checked out branch.
714
715 The branch name omits the 'refs/heads/' prefix.
716 None is returned if the project is on a detached HEAD, or if the
717 work_git is otheriwse inaccessible (e.g. an incomplete sync).
718 """
719 try:
720 b = self.work_git.GetHead()
721 except NoManifestException:
722 # If the local checkout is in a bad state, don't barf. Let the
723 # callers process this like the head is unreadable.
724 return None
725 if b.startswith(R_HEADS):
726 return b[len(R_HEADS) :]
727 return None
728
729 def IsRebaseInProgress(self):
730 return (
731 os.path.exists(self.work_git.GetDotgitPath("rebase-apply"))
732 or os.path.exists(self.work_git.GetDotgitPath("rebase-merge"))
733 or os.path.exists(os.path.join(self.worktree, ".dotest"))
734 )
735
736 def IsDirty(self, consider_untracked=True):
737 """Is the working directory modified in some way?"""
738 self.work_git.update_index(
739 "-q", "--unmerged", "--ignore-missing", "--refresh"
740 )
741 if self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD):
742 return True
743 if self.work_git.DiffZ("diff-files"):
744 return True
745 if consider_untracked and self.UntrackedFiles():
746 return True
747 return False
748
749 _userident_name = None
750 _userident_email = None
751
752 @property
753 def UserName(self):
754 """Obtain the user's personal name."""
755 if self._userident_name is None:
756 self._LoadUserIdentity()
757 return self._userident_name
758
759 @property
760 def UserEmail(self):
761 """Obtain the user's email address. This is very likely
762 to be their Gerrit login.
763 """
764 if self._userident_email is None:
765 self._LoadUserIdentity()
766 return self._userident_email
767
768 def _LoadUserIdentity(self):
769 u = self.bare_git.var("GIT_COMMITTER_IDENT")
770 m = re.compile("^(.*) <([^>]*)> ").match(u)
771 if m:
772 self._userident_name = m.group(1)
773 self._userident_email = m.group(2)
774 else:
775 self._userident_name = ""
776 self._userident_email = ""
777
778 def GetRemote(self, name=None):
779 """Get the configuration for a single remote.
780
781 Defaults to the current project's remote.
782 """
783 if name is None:
784 name = self.remote.name
785 return self.config.GetRemote(name)
786
787 def GetBranch(self, name):
788 """Get the configuration for a single branch."""
789 return self.config.GetBranch(name)
790
791 def GetBranches(self):
792 """Get all existing local branches."""
793 current = self.CurrentBranch
794 all_refs = self._allrefs
795 heads = {}
796
797 for name, ref_id in all_refs.items():
798 if name.startswith(R_HEADS):
799 name = name[len(R_HEADS) :]
800 b = self.GetBranch(name)
801 b.current = name == current
802 b.published = None
803 b.revision = ref_id
804 heads[name] = b
805
806 for name, ref_id in all_refs.items():
807 if name.startswith(R_PUB):
808 name = name[len(R_PUB) :]
809 b = heads.get(name)
810 if b:
811 b.published = ref_id
812
813 return heads
814
815 def MatchesGroups(self, manifest_groups):
816 """Returns true if the manifest groups specified at init should cause
817 this project to be synced.
818 Prefixing a manifest group with "-" inverts the meaning of a group.
819 All projects are implicitly labelled with "all".
820
821 labels are resolved in order. In the example case of
822 project_groups: "all,group1,group2"
823 manifest_groups: "-group1,group2"
824 the project will be matched.
825
826 The special manifest group "default" will match any project that
827 does not have the special project group "notdefault"
828 """
829 default_groups = self.manifest.default_groups or ["default"]
830 expanded_manifest_groups = manifest_groups or default_groups
831 expanded_project_groups = ["all"] + (self.groups or [])
832 if "notdefault" not in expanded_project_groups:
833 expanded_project_groups += ["default"]
834
835 matched = False
836 for group in expanded_manifest_groups:
837 if group.startswith("-") and group[1:] in expanded_project_groups:
838 matched = False
839 elif group in expanded_project_groups:
840 matched = True
841
842 return matched
843
844 def UncommitedFiles(self, get_all=True):
845 """Returns a list of strings, uncommitted files in the git tree.
846
847 Args:
848 get_all: a boolean, if True - get information about all different
849 uncommitted files. If False - return as soon as any kind of
850 uncommitted files is detected.
851 """
852 details = []
853 self.work_git.update_index(
854 "-q", "--unmerged", "--ignore-missing", "--refresh"
855 )
856 if self.IsRebaseInProgress():
857 details.append("rebase in progress")
858 if not get_all:
859 return details
860
861 changes = self.work_git.DiffZ("diff-index", "--cached", HEAD).keys()
862 if changes:
863 details.extend(changes)
864 if not get_all:
865 return details
866
867 changes = self.work_git.DiffZ("diff-files").keys()
868 if changes:
869 details.extend(changes)
870 if not get_all:
871 return details
872
873 changes = self.UntrackedFiles()
874 if changes:
875 details.extend(changes)
876
877 return details
878
879 def UntrackedFiles(self):
880 """Returns a list of strings, untracked files in the git tree."""
881 return self.work_git.LsOthers()
882
883 def HasChanges(self):
884 """Returns true if there are uncommitted changes."""
885 return bool(self.UncommitedFiles(get_all=False))
886
887 def PrintWorkTreeStatus(self, output_redir=None, quiet=False, local=False):
888 """Prints the status of the repository to stdout.
889
890 Args:
891 output_redir: If specified, redirect the output to this object.
892 quiet: If True then only print the project name. Do not print
893 the modified files, branch name, etc.
894 local: a boolean, if True, the path is relative to the local
895 (sub)manifest. If false, the path is relative to the outermost
896 manifest.
897 """
898 if not platform_utils.isdir(self.worktree):
899 if output_redir is None:
900 output_redir = sys.stdout
901 print(file=output_redir)
902 print("project %s/" % self.RelPath(local), file=output_redir)
903 print(' missing (run "repo sync")', file=output_redir)
904 return
905
906 self.work_git.update_index(
907 "-q", "--unmerged", "--ignore-missing", "--refresh"
908 )
909 rb = self.IsRebaseInProgress()
910 di = self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD)
911 df = self.work_git.DiffZ("diff-files")
912 do = self.work_git.LsOthers()
913 if not rb and not di and not df and not do and not self.CurrentBranch:
914 return "CLEAN"
915
916 out = StatusColoring(self.config)
917 if output_redir is not None:
918 out.redirect(output_redir)
919 out.project("project %-40s", self.RelPath(local) + "/ ")
920
921 if quiet:
922 out.nl()
923 return "DIRTY"
924
925 branch = self.CurrentBranch
926 if branch is None:
927 out.nobranch("(*** NO BRANCH ***)")
928 else:
929 out.branch("branch %s", branch)
930 out.nl()
931
932 if rb:
933 out.important("prior sync failed; rebase still in progress")
934 out.nl()
935
936 paths = list()
937 paths.extend(di.keys())
938 paths.extend(df.keys())
939 paths.extend(do)
940
941 for p in sorted(set(paths)):
942 try:
943 i = di[p]
944 except KeyError:
945 i = None
946
947 try:
948 f = df[p]
949 except KeyError:
950 f = None
951
952 if i:
953 i_status = i.status.upper()
954 else:
955 i_status = "-"
956
957 if f:
958 f_status = f.status.lower()
959 else:
960 f_status = "-"
961
962 if i and i.src_path:
963 line = " %s%s\t%s => %s (%s%%)" % (
964 i_status,
965 f_status,
966 i.src_path,
967 p,
968 i.level,
969 )
970 else:
971 line = " %s%s\t%s" % (i_status, f_status, p)
972
973 if i and not f:
974 out.added("%s", line)
975 elif (i and f) or (not i and f):
976 out.changed("%s", line)
977 elif not i and not f:
978 out.untracked("%s", line)
979 else:
980 out.write("%s", line)
981 out.nl()
982
983 return "DIRTY"
984
985 def PrintWorkTreeDiff(
986 self, absolute_paths=False, output_redir=None, local=False
987 ):
988 """Prints the status of the repository to stdout."""
989 out = DiffColoring(self.config)
990 if output_redir:
991 out.redirect(output_redir)
992 cmd = ["diff"]
993 if out.is_on:
994 cmd.append("--color")
995 cmd.append(HEAD)
996 if absolute_paths:
997 cmd.append("--src-prefix=a/%s/" % self.RelPath(local))
998 cmd.append("--dst-prefix=b/%s/" % self.RelPath(local))
999 cmd.append("--")
1000 try:
1001 p = GitCommand(self, cmd, capture_stdout=True, capture_stderr=True)
1002 p.Wait()
1003 except GitError as e:
1004 out.nl()
1005 out.project("project %s/" % self.RelPath(local))
1006 out.nl()
1007 out.fail("%s", str(e))
1008 out.nl()
1009 return False
1010 if p.stdout:
1011 out.nl()
1012 out.project("project %s/" % self.RelPath(local))
1013 out.nl()
1014 out.write("%s", p.stdout)
1015 return p.Wait() == 0
1016
1017 def WasPublished(self, branch, all_refs=None):
1018 """Was the branch published (uploaded) for code review?
1019 If so, returns the SHA-1 hash of the last published
1020 state for the branch.
1021 """
1022 key = R_PUB + branch
1023 if all_refs is None:
1024 try:
1025 return self.bare_git.rev_parse(key)
1026 except GitError:
1027 return None
1028 else:
1029 try:
1030 return all_refs[key]
1031 except KeyError:
1032 return None
1033
1034 def CleanPublishedCache(self, all_refs=None):
1035 """Prunes any stale published refs."""
1036 if all_refs is None:
1037 all_refs = self._allrefs
1038 heads = set()
1039 canrm = {}
1040 for name, ref_id in all_refs.items():
1041 if name.startswith(R_HEADS):
1042 heads.add(name)
1043 elif name.startswith(R_PUB):
1044 canrm[name] = ref_id
1045
1046 for name, ref_id in canrm.items():
1047 n = name[len(R_PUB) :]
1048 if R_HEADS + n not in heads:
1049 self.bare_git.DeleteRef(name, ref_id)
1050
1051 def GetUploadableBranches(self, selected_branch=None):
1052 """List any branches which can be uploaded for review."""
1053 heads = {}
1054 pubed = {}
1055
1056 for name, ref_id in self._allrefs.items():
1057 if name.startswith(R_HEADS):
1058 heads[name[len(R_HEADS) :]] = ref_id
1059 elif name.startswith(R_PUB):
1060 pubed[name[len(R_PUB) :]] = ref_id
1061
1062 ready = []
1063 for branch, ref_id in heads.items():
1064 if branch in pubed and pubed[branch] == ref_id:
1065 continue
1066 if selected_branch and branch != selected_branch:
1067 continue
1068
1069 rb = self.GetUploadableBranch(branch)
1070 if rb:
1071 ready.append(rb)
1072 return ready
1073
1074 def GetUploadableBranch(self, branch_name):
1075 """Get a single uploadable branch, or None."""
1076 branch = self.GetBranch(branch_name)
1077 base = branch.LocalMerge
1078 if branch.LocalMerge:
1079 rb = ReviewableBranch(self, branch, base)
1080 if rb.commits:
1081 return rb
1082 return None
1083
1084 def UploadForReview(
1085 self,
1086 branch=None,
1087 people=([], []),
1088 dryrun=False,
1089 auto_topic=False,
1090 hashtags=(),
1091 labels=(),
1092 private=False,
1093 notify=None,
1094 wip=False,
1095 ready=False,
1096 dest_branch=None,
1097 validate_certs=True,
1098 push_options=None,
1099 ):
1100 """Uploads the named branch for code review."""
1101 if branch is None:
1102 branch = self.CurrentBranch
1103 if branch is None:
Jason Chang32b59562023-07-14 16:45:35 -07001104 raise GitError("not currently on a branch", project=self.name)
Gavin Makea2e3302023-03-11 06:46:20 +00001105
1106 branch = self.GetBranch(branch)
1107 if not branch.LocalMerge:
Jason Chang32b59562023-07-14 16:45:35 -07001108 raise GitError(
1109 "branch %s does not track a remote" % branch.name,
1110 project=self.name,
1111 )
Gavin Makea2e3302023-03-11 06:46:20 +00001112 if not branch.remote.review:
Jason Chang32b59562023-07-14 16:45:35 -07001113 raise GitError(
1114 "remote %s has no review url" % branch.remote.name,
1115 project=self.name,
1116 )
Gavin Makea2e3302023-03-11 06:46:20 +00001117
1118 # Basic validity check on label syntax.
1119 for label in labels:
1120 if not re.match(r"^.+[+-][0-9]+$", label):
1121 raise UploadError(
1122 f'invalid label syntax "{label}": labels use forms like '
Jason Chang5a3a5f72023-08-17 11:36:41 -07001123 "CodeReview+1 or Verified-1",
1124 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00001125 )
1126
1127 if dest_branch is None:
1128 dest_branch = self.dest_branch
1129 if dest_branch is None:
1130 dest_branch = branch.merge
1131 if not dest_branch.startswith(R_HEADS):
1132 dest_branch = R_HEADS + dest_branch
1133
1134 if not branch.remote.projectname:
1135 branch.remote.projectname = self.name
1136 branch.remote.Save()
1137
1138 url = branch.remote.ReviewUrl(self.UserEmail, validate_certs)
1139 if url is None:
Jason Chang5a3a5f72023-08-17 11:36:41 -07001140 raise UploadError("review not configured", project=self.name)
Gavin Makea2e3302023-03-11 06:46:20 +00001141 cmd = ["push"]
1142 if dryrun:
1143 cmd.append("-n")
1144
1145 if url.startswith("ssh://"):
1146 cmd.append("--receive-pack=gerrit receive-pack")
1147
Aravind Vasudevan99ebf622023-04-04 23:44:37 +00001148 # This stops git from pushing all reachable annotated tags when
1149 # push.followTags is configured. Gerrit does not accept any tags
1150 # pushed to a CL.
1151 if git_require((1, 8, 3)):
1152 cmd.append("--no-follow-tags")
1153
Gavin Makea2e3302023-03-11 06:46:20 +00001154 for push_option in push_options or []:
1155 cmd.append("-o")
1156 cmd.append(push_option)
1157
1158 cmd.append(url)
1159
1160 if dest_branch.startswith(R_HEADS):
1161 dest_branch = dest_branch[len(R_HEADS) :]
1162
1163 ref_spec = "%s:refs/for/%s" % (R_HEADS + branch.name, dest_branch)
1164 opts = []
1165 if auto_topic:
1166 opts += ["topic=" + branch.name]
1167 opts += ["t=%s" % p for p in hashtags]
1168 # NB: No need to encode labels as they've been validated above.
1169 opts += ["l=%s" % p for p in labels]
1170
1171 opts += ["r=%s" % p for p in people[0]]
1172 opts += ["cc=%s" % p for p in people[1]]
1173 if notify:
1174 opts += ["notify=" + notify]
1175 if private:
1176 opts += ["private"]
1177 if wip:
1178 opts += ["wip"]
1179 if ready:
1180 opts += ["ready"]
1181 if opts:
1182 ref_spec = ref_spec + "%" + ",".join(opts)
1183 cmd.append(ref_spec)
1184
Jason Chang1e9f7b92023-08-25 10:31:04 -07001185 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001186
1187 if not dryrun:
1188 msg = "posted to %s for %s" % (branch.remote.review, dest_branch)
1189 self.bare_git.UpdateRef(
1190 R_PUB + branch.name, R_HEADS + branch.name, message=msg
1191 )
1192
1193 def _ExtractArchive(self, tarpath, path=None):
1194 """Extract the given tar on its current location
1195
1196 Args:
1197 tarpath: The path to the actual tar file
1198
1199 """
1200 try:
1201 with tarfile.open(tarpath, "r") as tar:
1202 tar.extractall(path=path)
1203 return True
1204 except (IOError, tarfile.TarError) as e:
1205 _error("Cannot extract archive %s: %s", tarpath, str(e))
1206 return False
1207
1208 def Sync_NetworkHalf(
1209 self,
1210 quiet=False,
1211 verbose=False,
1212 output_redir=None,
1213 is_new=None,
1214 current_branch_only=None,
1215 force_sync=False,
1216 clone_bundle=True,
1217 tags=None,
1218 archive=False,
1219 optimized_fetch=False,
1220 retry_fetches=0,
1221 prune=False,
1222 submodules=False,
1223 ssh_proxy=None,
1224 clone_filter=None,
1225 partial_clone_exclude=set(),
Jason Chang17833322023-05-23 13:06:55 -07001226 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001227 ):
1228 """Perform only the network IO portion of the sync process.
1229 Local working directory/branch state is not affected.
1230 """
1231 if archive and not isinstance(self, MetaProject):
1232 if self.remote.url.startswith(("http://", "https://")):
Jason Chang32b59562023-07-14 16:45:35 -07001233 msg_template = (
1234 "%s: Cannot fetch archives from http/https remotes."
Gavin Makea2e3302023-03-11 06:46:20 +00001235 )
Jason Chang32b59562023-07-14 16:45:35 -07001236 msg_args = self.name
1237 msg = msg_template % msg_args
1238 _error(
1239 msg_template,
1240 msg_args,
1241 )
1242 return SyncNetworkHalfResult(
1243 False, SyncNetworkHalfError(msg, project=self.name)
1244 )
Gavin Makea2e3302023-03-11 06:46:20 +00001245
1246 name = self.relpath.replace("\\", "/")
1247 name = name.replace("/", "_")
1248 tarpath = "%s.tar" % name
1249 topdir = self.manifest.topdir
1250
1251 try:
1252 self._FetchArchive(tarpath, cwd=topdir)
1253 except GitError as e:
1254 _error("%s", e)
Jason Chang32b59562023-07-14 16:45:35 -07001255 return SyncNetworkHalfResult(False, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001256
1257 # From now on, we only need absolute tarpath.
1258 tarpath = os.path.join(topdir, tarpath)
1259
1260 if not self._ExtractArchive(tarpath, path=topdir):
Jason Chang32b59562023-07-14 16:45:35 -07001261 return SyncNetworkHalfResult(
1262 True,
1263 SyncNetworkHalfError(
1264 f"Unable to Extract Archive {tarpath}",
1265 project=self.name,
1266 ),
1267 )
Gavin Makea2e3302023-03-11 06:46:20 +00001268 try:
1269 platform_utils.remove(tarpath)
1270 except OSError as e:
1271 _warn("Cannot remove archive %s: %s", tarpath, str(e))
1272 self._CopyAndLinkFiles()
Jason Chang32b59562023-07-14 16:45:35 -07001273 return SyncNetworkHalfResult(True)
Gavin Makea2e3302023-03-11 06:46:20 +00001274
1275 # If the shared object dir already exists, don't try to rebootstrap with
1276 # a clone bundle download. We should have the majority of objects
1277 # already.
1278 if clone_bundle and os.path.exists(self.objdir):
1279 clone_bundle = False
1280
1281 if self.name in partial_clone_exclude:
1282 clone_bundle = True
1283 clone_filter = None
1284
1285 if is_new is None:
1286 is_new = not self.Exists
1287 if is_new:
1288 self._InitGitDir(force_sync=force_sync, quiet=quiet)
1289 else:
1290 self._UpdateHooks(quiet=quiet)
1291 self._InitRemote()
1292
1293 if self.UseAlternates:
1294 # If gitdir/objects is a symlink, migrate it from the old layout.
1295 gitdir_objects = os.path.join(self.gitdir, "objects")
1296 if platform_utils.islink(gitdir_objects):
1297 platform_utils.remove(gitdir_objects, missing_ok=True)
1298 gitdir_alt = os.path.join(self.gitdir, "objects/info/alternates")
1299 if not os.path.exists(gitdir_alt):
1300 os.makedirs(os.path.dirname(gitdir_alt), exist_ok=True)
1301 _lwrite(
1302 gitdir_alt,
1303 os.path.join(
1304 os.path.relpath(self.objdir, gitdir_objects), "objects"
1305 )
1306 + "\n",
1307 )
1308
1309 if is_new:
1310 alt = os.path.join(self.objdir, "objects/info/alternates")
1311 try:
1312 with open(alt) as fd:
1313 # This works for both absolute and relative alternate
1314 # directories.
1315 alt_dir = os.path.join(
1316 self.objdir, "objects", fd.readline().rstrip()
1317 )
1318 except IOError:
1319 alt_dir = None
1320 else:
1321 alt_dir = None
1322
1323 if (
1324 clone_bundle
1325 and alt_dir is None
1326 and self._ApplyCloneBundle(
1327 initial=is_new, quiet=quiet, verbose=verbose
1328 )
1329 ):
1330 is_new = False
1331
1332 if current_branch_only is None:
1333 if self.sync_c:
1334 current_branch_only = True
1335 elif not self.manifest._loaded:
1336 # Manifest cannot check defaults until it syncs.
1337 current_branch_only = False
1338 elif self.manifest.default.sync_c:
1339 current_branch_only = True
1340
1341 if tags is None:
1342 tags = self.sync_tags
1343
1344 if self.clone_depth:
1345 depth = self.clone_depth
1346 else:
1347 depth = self.manifest.manifestProject.depth
1348
Jason Chang17833322023-05-23 13:06:55 -07001349 if depth and clone_filter_for_depth:
1350 depth = None
1351 clone_filter = clone_filter_for_depth
1352
Gavin Makea2e3302023-03-11 06:46:20 +00001353 # See if we can skip the network fetch entirely.
1354 remote_fetched = False
1355 if not (
1356 optimized_fetch
1357 and (
1358 ID_RE.match(self.revisionExpr)
1359 and self._CheckForImmutableRevision()
1360 )
1361 ):
1362 remote_fetched = True
Jason Chang32b59562023-07-14 16:45:35 -07001363 try:
1364 if not self._RemoteFetch(
1365 initial=is_new,
1366 quiet=quiet,
1367 verbose=verbose,
1368 output_redir=output_redir,
1369 alt_dir=alt_dir,
1370 current_branch_only=current_branch_only,
1371 tags=tags,
1372 prune=prune,
1373 depth=depth,
1374 submodules=submodules,
1375 force_sync=force_sync,
1376 ssh_proxy=ssh_proxy,
1377 clone_filter=clone_filter,
1378 retry_fetches=retry_fetches,
1379 ):
1380 return SyncNetworkHalfResult(
1381 remote_fetched,
1382 SyncNetworkHalfError(
1383 f"Unable to remote fetch project {self.name}",
1384 project=self.name,
1385 ),
1386 )
1387 except RepoError as e:
1388 return SyncNetworkHalfResult(
1389 remote_fetched,
1390 e,
1391 )
Gavin Makea2e3302023-03-11 06:46:20 +00001392
1393 mp = self.manifest.manifestProject
1394 dissociate = mp.dissociate
1395 if dissociate:
1396 alternates_file = os.path.join(
1397 self.objdir, "objects/info/alternates"
1398 )
1399 if os.path.exists(alternates_file):
1400 cmd = ["repack", "-a", "-d"]
1401 p = GitCommand(
1402 self,
1403 cmd,
1404 bare=True,
1405 capture_stdout=bool(output_redir),
1406 merge_output=bool(output_redir),
1407 )
1408 if p.stdout and output_redir:
1409 output_redir.write(p.stdout)
1410 if p.Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07001411 return SyncNetworkHalfResult(
1412 remote_fetched,
1413 GitError(
1414 "Unable to repack alternates", project=self.name
1415 ),
1416 )
Gavin Makea2e3302023-03-11 06:46:20 +00001417 platform_utils.remove(alternates_file)
1418
1419 if self.worktree:
1420 self._InitMRef()
1421 else:
1422 self._InitMirrorHead()
1423 platform_utils.remove(
1424 os.path.join(self.gitdir, "FETCH_HEAD"), missing_ok=True
1425 )
Jason Chang32b59562023-07-14 16:45:35 -07001426 return SyncNetworkHalfResult(remote_fetched)
Gavin Makea2e3302023-03-11 06:46:20 +00001427
1428 def PostRepoUpgrade(self):
1429 self._InitHooks()
1430
1431 def _CopyAndLinkFiles(self):
1432 if self.client.isGitcClient:
1433 return
1434 for copyfile in self.copyfiles:
1435 copyfile._Copy()
1436 for linkfile in self.linkfiles:
1437 linkfile._Link()
1438
1439 def GetCommitRevisionId(self):
1440 """Get revisionId of a commit.
1441
1442 Use this method instead of GetRevisionId to get the id of the commit
1443 rather than the id of the current git object (for example, a tag)
1444
1445 """
1446 if not self.revisionExpr.startswith(R_TAGS):
1447 return self.GetRevisionId(self._allrefs)
1448
1449 try:
1450 return self.bare_git.rev_list(self.revisionExpr, "-1")[0]
1451 except GitError:
1452 raise ManifestInvalidRevisionError(
1453 "revision %s in %s not found" % (self.revisionExpr, self.name)
1454 )
1455
1456 def GetRevisionId(self, all_refs=None):
1457 if self.revisionId:
1458 return self.revisionId
1459
1460 rem = self.GetRemote()
1461 rev = rem.ToLocal(self.revisionExpr)
1462
1463 if all_refs is not None and rev in all_refs:
1464 return all_refs[rev]
1465
1466 try:
1467 return self.bare_git.rev_parse("--verify", "%s^0" % rev)
1468 except GitError:
1469 raise ManifestInvalidRevisionError(
1470 "revision %s in %s not found" % (self.revisionExpr, self.name)
1471 )
1472
1473 def SetRevisionId(self, revisionId):
1474 if self.revisionExpr:
1475 self.upstream = self.revisionExpr
1476
1477 self.revisionId = revisionId
1478
Jason Chang32b59562023-07-14 16:45:35 -07001479 def Sync_LocalHalf(
1480 self, syncbuf, force_sync=False, submodules=False, errors=None
1481 ):
Gavin Makea2e3302023-03-11 06:46:20 +00001482 """Perform only the local IO portion of the sync process.
1483
1484 Network access is not required.
1485 """
Jason Chang32b59562023-07-14 16:45:35 -07001486 if errors is None:
1487 errors = []
1488
1489 def fail(error: Exception):
1490 errors.append(error)
1491 syncbuf.fail(self, error)
1492
Gavin Makea2e3302023-03-11 06:46:20 +00001493 if not os.path.exists(self.gitdir):
Jason Chang32b59562023-07-14 16:45:35 -07001494 fail(
1495 LocalSyncFail(
1496 "Cannot checkout %s due to missing network sync; Run "
1497 "`repo sync -n %s` first." % (self.name, self.name),
1498 project=self.name,
1499 )
Gavin Makea2e3302023-03-11 06:46:20 +00001500 )
1501 return
1502
1503 self._InitWorkTree(force_sync=force_sync, submodules=submodules)
1504 all_refs = self.bare_ref.all
1505 self.CleanPublishedCache(all_refs)
1506 revid = self.GetRevisionId(all_refs)
1507
1508 # Special case the root of the repo client checkout. Make sure it
1509 # doesn't contain files being checked out to dirs we don't allow.
1510 if self.relpath == ".":
1511 PROTECTED_PATHS = {".repo"}
1512 paths = set(
1513 self.work_git.ls_tree("-z", "--name-only", "--", revid).split(
1514 "\0"
1515 )
1516 )
1517 bad_paths = paths & PROTECTED_PATHS
1518 if bad_paths:
Jason Chang32b59562023-07-14 16:45:35 -07001519 fail(
1520 LocalSyncFail(
1521 "Refusing to checkout project that writes to protected "
1522 "paths: %s" % (", ".join(bad_paths),),
1523 project=self.name,
1524 )
Gavin Makea2e3302023-03-11 06:46:20 +00001525 )
1526 return
1527
1528 def _doff():
1529 self._FastForward(revid)
1530 self._CopyAndLinkFiles()
1531
1532 def _dosubmodules():
1533 self._SyncSubmodules(quiet=True)
1534
1535 head = self.work_git.GetHead()
1536 if head.startswith(R_HEADS):
1537 branch = head[len(R_HEADS) :]
1538 try:
1539 head = all_refs[head]
1540 except KeyError:
1541 head = None
1542 else:
1543 branch = None
1544
1545 if branch is None or syncbuf.detach_head:
1546 # Currently on a detached HEAD. The user is assumed to
1547 # not have any local modifications worth worrying about.
1548 if self.IsRebaseInProgress():
Jason Chang32b59562023-07-14 16:45:35 -07001549 fail(_PriorSyncFailedError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001550 return
1551
1552 if head == revid:
1553 # No changes; don't do anything further.
1554 # Except if the head needs to be detached.
1555 if not syncbuf.detach_head:
1556 # The copy/linkfile config may have changed.
1557 self._CopyAndLinkFiles()
1558 return
1559 else:
1560 lost = self._revlist(not_rev(revid), HEAD)
1561 if lost:
1562 syncbuf.info(self, "discarding %d commits", len(lost))
1563
1564 try:
1565 self._Checkout(revid, quiet=True)
1566 if submodules:
1567 self._SyncSubmodules(quiet=True)
1568 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001569 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001570 return
1571 self._CopyAndLinkFiles()
1572 return
1573
1574 if head == revid:
1575 # No changes; don't do anything further.
1576 #
1577 # The copy/linkfile config may have changed.
1578 self._CopyAndLinkFiles()
1579 return
1580
1581 branch = self.GetBranch(branch)
1582
1583 if not branch.LocalMerge:
1584 # The current branch has no tracking configuration.
1585 # Jump off it to a detached HEAD.
1586 syncbuf.info(
1587 self, "leaving %s; does not track upstream", branch.name
1588 )
1589 try:
1590 self._Checkout(revid, quiet=True)
1591 if submodules:
1592 self._SyncSubmodules(quiet=True)
1593 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001594 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001595 return
1596 self._CopyAndLinkFiles()
1597 return
1598
1599 upstream_gain = self._revlist(not_rev(HEAD), revid)
1600
1601 # See if we can perform a fast forward merge. This can happen if our
1602 # branch isn't in the exact same state as we last published.
1603 try:
1604 self.work_git.merge_base("--is-ancestor", HEAD, revid)
1605 # Skip the published logic.
1606 pub = False
1607 except GitError:
1608 pub = self.WasPublished(branch.name, all_refs)
1609
1610 if pub:
1611 not_merged = self._revlist(not_rev(revid), pub)
1612 if not_merged:
1613 if upstream_gain:
1614 # The user has published this branch and some of those
1615 # commits are not yet merged upstream. We do not want
1616 # to rewrite the published commits so we punt.
Jason Chang32b59562023-07-14 16:45:35 -07001617 fail(
1618 LocalSyncFail(
1619 "branch %s is published (but not merged) and is "
1620 "now %d commits behind"
1621 % (branch.name, len(upstream_gain)),
1622 project=self.name,
1623 )
Gavin Makea2e3302023-03-11 06:46:20 +00001624 )
1625 return
1626 elif pub == head:
1627 # All published commits are merged, and thus we are a
1628 # strict subset. We can fast-forward safely.
1629 syncbuf.later1(self, _doff)
1630 if submodules:
1631 syncbuf.later1(self, _dosubmodules)
1632 return
1633
1634 # Examine the local commits not in the remote. Find the
1635 # last one attributed to this user, if any.
1636 local_changes = self._revlist(not_rev(revid), HEAD, format="%H %ce")
1637 last_mine = None
1638 cnt_mine = 0
1639 for commit in local_changes:
1640 commit_id, committer_email = commit.split(" ", 1)
1641 if committer_email == self.UserEmail:
1642 last_mine = commit_id
1643 cnt_mine += 1
1644
1645 if not upstream_gain and cnt_mine == len(local_changes):
1646 # The copy/linkfile config may have changed.
1647 self._CopyAndLinkFiles()
1648 return
1649
1650 if self.IsDirty(consider_untracked=False):
Jason Chang32b59562023-07-14 16:45:35 -07001651 fail(_DirtyError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001652 return
1653
1654 # If the upstream switched on us, warn the user.
1655 if branch.merge != self.revisionExpr:
1656 if branch.merge and self.revisionExpr:
1657 syncbuf.info(
1658 self,
1659 "manifest switched %s...%s",
1660 branch.merge,
1661 self.revisionExpr,
1662 )
1663 elif branch.merge:
1664 syncbuf.info(self, "manifest no longer tracks %s", branch.merge)
1665
1666 if cnt_mine < len(local_changes):
1667 # Upstream rebased. Not everything in HEAD was created by this user.
1668 syncbuf.info(
1669 self,
1670 "discarding %d commits removed from upstream",
1671 len(local_changes) - cnt_mine,
1672 )
1673
1674 branch.remote = self.GetRemote()
1675 if not ID_RE.match(self.revisionExpr):
1676 # In case of manifest sync the revisionExpr might be a SHA1.
1677 branch.merge = self.revisionExpr
1678 if not branch.merge.startswith("refs/"):
1679 branch.merge = R_HEADS + branch.merge
1680 branch.Save()
1681
1682 if cnt_mine > 0 and self.rebase:
1683
1684 def _docopyandlink():
1685 self._CopyAndLinkFiles()
1686
1687 def _dorebase():
1688 self._Rebase(upstream="%s^1" % last_mine, onto=revid)
1689
1690 syncbuf.later2(self, _dorebase)
1691 if submodules:
1692 syncbuf.later2(self, _dosubmodules)
1693 syncbuf.later2(self, _docopyandlink)
1694 elif local_changes:
1695 try:
1696 self._ResetHard(revid)
1697 if submodules:
1698 self._SyncSubmodules(quiet=True)
1699 self._CopyAndLinkFiles()
1700 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001701 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001702 return
1703 else:
1704 syncbuf.later1(self, _doff)
1705 if submodules:
1706 syncbuf.later1(self, _dosubmodules)
1707
1708 def AddCopyFile(self, src, dest, topdir):
1709 """Mark |src| for copying to |dest| (relative to |topdir|).
1710
1711 No filesystem changes occur here. Actual copying happens later on.
1712
1713 Paths should have basic validation run on them before being queued.
1714 Further checking will be handled when the actual copy happens.
1715 """
1716 self.copyfiles.append(_CopyFile(self.worktree, src, topdir, dest))
1717
1718 def AddLinkFile(self, src, dest, topdir):
1719 """Mark |dest| to create a symlink (relative to |topdir|) pointing to
1720 |src|.
1721
1722 No filesystem changes occur here. Actual linking happens later on.
1723
1724 Paths should have basic validation run on them before being queued.
1725 Further checking will be handled when the actual link happens.
1726 """
1727 self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest))
1728
1729 def AddAnnotation(self, name, value, keep):
1730 self.annotations.append(Annotation(name, value, keep))
1731
1732 def DownloadPatchSet(self, change_id, patch_id):
1733 """Download a single patch set of a single change to FETCH_HEAD."""
1734 remote = self.GetRemote()
1735
1736 cmd = ["fetch", remote.name]
1737 cmd.append(
1738 "refs/changes/%2.2d/%d/%d" % (change_id % 100, change_id, patch_id)
1739 )
Jason Chang1a3612f2023-08-08 14:12:53 -07001740 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001741 return DownloadedChange(
1742 self,
1743 self.GetRevisionId(),
1744 change_id,
1745 patch_id,
1746 self.bare_git.rev_parse("FETCH_HEAD"),
1747 )
1748
1749 def DeleteWorktree(self, quiet=False, force=False):
1750 """Delete the source checkout and any other housekeeping tasks.
1751
1752 This currently leaves behind the internal .repo/ cache state. This
1753 helps when switching branches or manifest changes get reverted as we
1754 don't have to redownload all the git objects. But we should do some GC
1755 at some point.
1756
1757 Args:
1758 quiet: Whether to hide normal messages.
1759 force: Always delete tree even if dirty.
Doug Anderson3ba5f952011-04-07 12:51:04 -07001760
1761 Returns:
Gavin Makea2e3302023-03-11 06:46:20 +00001762 True if the worktree was completely cleaned out.
1763 """
1764 if self.IsDirty():
1765 if force:
1766 print(
1767 "warning: %s: Removing dirty project: uncommitted changes "
1768 "lost." % (self.RelPath(local=False),),
1769 file=sys.stderr,
1770 )
1771 else:
Jason Chang32b59562023-07-14 16:45:35 -07001772 msg = (
1773 "error: %s: Cannot remove project: uncommitted"
1774 "changes are present.\n" % self.RelPath(local=False)
Gavin Makea2e3302023-03-11 06:46:20 +00001775 )
Jason Chang32b59562023-07-14 16:45:35 -07001776 print(msg, file=sys.stderr)
1777 raise DeleteDirtyWorktreeError(msg, project=self)
Wink Saville02d79452009-04-10 13:01:24 -07001778
Gavin Makea2e3302023-03-11 06:46:20 +00001779 if not quiet:
1780 print(
1781 "%s: Deleting obsolete checkout." % (self.RelPath(local=False),)
1782 )
Wink Saville02d79452009-04-10 13:01:24 -07001783
Gavin Makea2e3302023-03-11 06:46:20 +00001784 # Unlock and delink from the main worktree. We don't use git's worktree
1785 # remove because it will recursively delete projects -- we handle that
1786 # ourselves below. https://crbug.com/git/48
1787 if self.use_git_worktrees:
1788 needle = platform_utils.realpath(self.gitdir)
1789 # Find the git worktree commondir under .repo/worktrees/.
1790 output = self.bare_git.worktree("list", "--porcelain").splitlines()[
1791 0
1792 ]
1793 assert output.startswith("worktree "), output
1794 commondir = output[9:]
1795 # Walk each of the git worktrees to see where they point.
1796 configs = os.path.join(commondir, "worktrees")
1797 for name in os.listdir(configs):
1798 gitdir = os.path.join(configs, name, "gitdir")
1799 with open(gitdir) as fp:
1800 relpath = fp.read().strip()
1801 # Resolve the checkout path and see if it matches this project.
1802 fullpath = platform_utils.realpath(
1803 os.path.join(configs, name, relpath)
1804 )
1805 if fullpath == needle:
1806 platform_utils.rmtree(os.path.join(configs, name))
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001807
Gavin Makea2e3302023-03-11 06:46:20 +00001808 # Delete the .git directory first, so we're less likely to have a
1809 # partially working git repository around. There shouldn't be any git
1810 # projects here, so rmtree works.
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001811
Gavin Makea2e3302023-03-11 06:46:20 +00001812 # Try to remove plain files first in case of git worktrees. If this
1813 # fails for any reason, we'll fall back to rmtree, and that'll display
1814 # errors if it can't remove things either.
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001815 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001816 platform_utils.remove(self.gitdir)
1817 except OSError:
1818 pass
1819 try:
1820 platform_utils.rmtree(self.gitdir)
1821 except OSError as e:
1822 if e.errno != errno.ENOENT:
1823 print("error: %s: %s" % (self.gitdir, e), file=sys.stderr)
1824 print(
1825 "error: %s: Failed to delete obsolete checkout; remove "
1826 "manually, then run `repo sync -l`."
1827 % (self.RelPath(local=False),),
1828 file=sys.stderr,
1829 )
Jason Chang32b59562023-07-14 16:45:35 -07001830 raise DeleteWorktreeError(aggregate_errors=[e])
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001831
Gavin Makea2e3302023-03-11 06:46:20 +00001832 # Delete everything under the worktree, except for directories that
1833 # contain another git project.
1834 dirs_to_remove = []
1835 failed = False
Jason Chang32b59562023-07-14 16:45:35 -07001836 errors = []
Gavin Makea2e3302023-03-11 06:46:20 +00001837 for root, dirs, files in platform_utils.walk(self.worktree):
1838 for f in files:
1839 path = os.path.join(root, f)
1840 try:
1841 platform_utils.remove(path)
1842 except OSError as e:
1843 if e.errno != errno.ENOENT:
1844 print(
1845 "error: %s: Failed to remove: %s" % (path, e),
1846 file=sys.stderr,
1847 )
1848 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001849 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001850 dirs[:] = [
1851 d
1852 for d in dirs
1853 if not os.path.lexists(os.path.join(root, d, ".git"))
1854 ]
1855 dirs_to_remove += [
1856 os.path.join(root, d)
1857 for d in dirs
1858 if os.path.join(root, d) not in dirs_to_remove
1859 ]
1860 for d in reversed(dirs_to_remove):
1861 if platform_utils.islink(d):
1862 try:
1863 platform_utils.remove(d)
1864 except OSError as e:
1865 if e.errno != errno.ENOENT:
1866 print(
1867 "error: %s: Failed to remove: %s" % (d, e),
1868 file=sys.stderr,
1869 )
1870 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001871 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001872 elif not platform_utils.listdir(d):
1873 try:
1874 platform_utils.rmdir(d)
1875 except OSError as e:
1876 if e.errno != errno.ENOENT:
1877 print(
1878 "error: %s: Failed to remove: %s" % (d, e),
1879 file=sys.stderr,
1880 )
1881 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001882 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001883 if failed:
1884 print(
1885 "error: %s: Failed to delete obsolete checkout."
1886 % (self.RelPath(local=False),),
1887 file=sys.stderr,
1888 )
1889 print(
1890 " Remove manually, then run `repo sync -l`.",
1891 file=sys.stderr,
1892 )
Jason Chang32b59562023-07-14 16:45:35 -07001893 raise DeleteWorktreeError(aggregate_errors=errors)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07001894
Gavin Makea2e3302023-03-11 06:46:20 +00001895 # Try deleting parent dirs if they are empty.
1896 path = self.worktree
1897 while path != self.manifest.topdir:
1898 try:
1899 platform_utils.rmdir(path)
1900 except OSError as e:
1901 if e.errno != errno.ENOENT:
1902 break
1903 path = os.path.dirname(path)
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001904
Gavin Makea2e3302023-03-11 06:46:20 +00001905 return True
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001906
Gavin Makea2e3302023-03-11 06:46:20 +00001907 def StartBranch(self, name, branch_merge="", revision=None):
1908 """Create a new branch off the manifest's revision."""
1909 if not branch_merge:
1910 branch_merge = self.revisionExpr
1911 head = self.work_git.GetHead()
1912 if head == (R_HEADS + name):
1913 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001914
David Pursehouse8a68ff92012-09-24 12:15:13 +09001915 all_refs = self.bare_ref.all
Gavin Makea2e3302023-03-11 06:46:20 +00001916 if R_HEADS + name in all_refs:
Jason Chang1a3612f2023-08-08 14:12:53 -07001917 GitCommand(
1918 self, ["checkout", "-q", name, "--"], verify_command=True
1919 ).Wait()
1920 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001921
Gavin Makea2e3302023-03-11 06:46:20 +00001922 branch = self.GetBranch(name)
1923 branch.remote = self.GetRemote()
1924 branch.merge = branch_merge
1925 if not branch.merge.startswith("refs/") and not ID_RE.match(
1926 branch_merge
1927 ):
1928 branch.merge = R_HEADS + branch_merge
Shawn O. Pearce88443382010-10-08 10:02:09 +02001929
Gavin Makea2e3302023-03-11 06:46:20 +00001930 if revision is None:
1931 revid = self.GetRevisionId(all_refs)
Shawn O. Pearce88443382010-10-08 10:02:09 +02001932 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001933 revid = self.work_git.rev_parse(revision)
Brian Harring14a66742012-09-28 20:21:57 -07001934
Gavin Makea2e3302023-03-11 06:46:20 +00001935 if head.startswith(R_HEADS):
Kevin Degiabaa7f32014-11-12 11:27:45 -07001936 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001937 head = all_refs[head]
1938 except KeyError:
1939 head = None
1940 if revid and head and revid == head:
1941 ref = R_HEADS + name
1942 self.work_git.update_ref(ref, revid)
1943 self.work_git.symbolic_ref(HEAD, ref)
1944 branch.Save()
1945 return True
Kevin Degib1a07b82015-07-27 13:33:43 -06001946
Jason Chang1a3612f2023-08-08 14:12:53 -07001947 GitCommand(
1948 self,
1949 ["checkout", "-q", "-b", branch.name, revid],
1950 verify_command=True,
1951 ).Wait()
1952 branch.Save()
1953 return True
Kevin Degi384b3c52014-10-16 16:02:58 -06001954
Gavin Makea2e3302023-03-11 06:46:20 +00001955 def CheckoutBranch(self, name):
1956 """Checkout a local topic branch.
Shawn O. Pearce2816d4f2009-03-03 17:53:18 -08001957
Gavin Makea2e3302023-03-11 06:46:20 +00001958 Args:
1959 name: The name of the branch to checkout.
Shawn O. Pearce88443382010-10-08 10:02:09 +02001960
Gavin Makea2e3302023-03-11 06:46:20 +00001961 Returns:
Jason Chang1a3612f2023-08-08 14:12:53 -07001962 True if the checkout succeeded; False if the
1963 branch doesn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00001964 """
1965 rev = R_HEADS + name
1966 head = self.work_git.GetHead()
1967 if head == rev:
1968 # Already on the branch.
1969 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001970
Gavin Makea2e3302023-03-11 06:46:20 +00001971 all_refs = self.bare_ref.all
1972 try:
1973 revid = all_refs[rev]
1974 except KeyError:
1975 # Branch does not exist in this project.
Jason Chang1a3612f2023-08-08 14:12:53 -07001976 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001977
Gavin Makea2e3302023-03-11 06:46:20 +00001978 if head.startswith(R_HEADS):
1979 try:
1980 head = all_refs[head]
1981 except KeyError:
1982 head = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001983
Gavin Makea2e3302023-03-11 06:46:20 +00001984 if head == revid:
1985 # Same revision; just update HEAD to point to the new
1986 # target branch, but otherwise take no other action.
1987 _lwrite(
1988 self.work_git.GetDotgitPath(subpath=HEAD),
1989 "ref: %s%s\n" % (R_HEADS, name),
1990 )
1991 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05001992
Jason Chang1a3612f2023-08-08 14:12:53 -07001993 GitCommand(
1994 self,
1995 ["checkout", name, "--"],
1996 capture_stdout=True,
1997 capture_stderr=True,
1998 verify_command=True,
1999 ).Wait()
2000 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05002001
Gavin Makea2e3302023-03-11 06:46:20 +00002002 def AbandonBranch(self, name):
2003 """Destroy a local topic branch.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002004
Gavin Makea2e3302023-03-11 06:46:20 +00002005 Args:
2006 name: The name of the branch to abandon.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002007
Gavin Makea2e3302023-03-11 06:46:20 +00002008 Returns:
Jason Changf9aacd42023-08-03 14:38:00 -07002009 True if the abandon succeeded; Raises GitCommandError if it didn't;
2010 None if the branch didn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00002011 """
2012 rev = R_HEADS + name
2013 all_refs = self.bare_ref.all
2014 if rev not in all_refs:
2015 # Doesn't exist
2016 return None
2017
2018 head = self.work_git.GetHead()
2019 if head == rev:
2020 # We can't destroy the branch while we are sitting
2021 # on it. Switch to a detached HEAD.
2022 head = all_refs[head]
2023
2024 revid = self.GetRevisionId(all_refs)
2025 if head == revid:
2026 _lwrite(
2027 self.work_git.GetDotgitPath(subpath=HEAD), "%s\n" % revid
2028 )
2029 else:
2030 self._Checkout(revid, quiet=True)
Jason Changf9aacd42023-08-03 14:38:00 -07002031 GitCommand(
2032 self,
2033 ["branch", "-D", name],
2034 capture_stdout=True,
2035 capture_stderr=True,
2036 verify_command=True,
2037 ).Wait()
2038 return True
Gavin Makea2e3302023-03-11 06:46:20 +00002039
2040 def PruneHeads(self):
2041 """Prune any topic branches already merged into upstream."""
2042 cb = self.CurrentBranch
2043 kill = []
2044 left = self._allrefs
2045 for name in left.keys():
2046 if name.startswith(R_HEADS):
2047 name = name[len(R_HEADS) :]
2048 if cb is None or name != cb:
2049 kill.append(name)
2050
2051 # Minor optimization: If there's nothing to prune, then don't try to
2052 # read any project state.
2053 if not kill and not cb:
2054 return []
2055
2056 rev = self.GetRevisionId(left)
2057 if (
2058 cb is not None
2059 and not self._revlist(HEAD + "..." + rev)
2060 and not self.IsDirty(consider_untracked=False)
2061 ):
2062 self.work_git.DetachHead(HEAD)
2063 kill.append(cb)
2064
2065 if kill:
2066 old = self.bare_git.GetHead()
2067
2068 try:
2069 self.bare_git.DetachHead(rev)
2070
2071 b = ["branch", "-d"]
2072 b.extend(kill)
2073 b = GitCommand(
2074 self, b, bare=True, capture_stdout=True, capture_stderr=True
2075 )
2076 b.Wait()
2077 finally:
2078 if ID_RE.match(old):
2079 self.bare_git.DetachHead(old)
2080 else:
2081 self.bare_git.SetHead(old)
2082 left = self._allrefs
2083
2084 for branch in kill:
2085 if (R_HEADS + branch) not in left:
2086 self.CleanPublishedCache()
2087 break
2088
2089 if cb and cb not in kill:
2090 kill.append(cb)
2091 kill.sort()
2092
2093 kept = []
2094 for branch in kill:
2095 if R_HEADS + branch in left:
2096 branch = self.GetBranch(branch)
2097 base = branch.LocalMerge
2098 if not base:
2099 base = rev
2100 kept.append(ReviewableBranch(self, branch, base))
2101 return kept
2102
2103 def GetRegisteredSubprojects(self):
2104 result = []
2105
2106 def rec(subprojects):
2107 if not subprojects:
2108 return
2109 result.extend(subprojects)
2110 for p in subprojects:
2111 rec(p.subprojects)
2112
2113 rec(self.subprojects)
2114 return result
2115
2116 def _GetSubmodules(self):
2117 # Unfortunately we cannot call `git submodule status --recursive` here
2118 # because the working tree might not exist yet, and it cannot be used
2119 # without a working tree in its current implementation.
2120
2121 def get_submodules(gitdir, rev):
2122 # Parse .gitmodules for submodule sub_paths and sub_urls.
2123 sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
2124 if not sub_paths:
2125 return []
2126 # Run `git ls-tree` to read SHAs of submodule object, which happen
2127 # to be revision of submodule repository.
2128 sub_revs = git_ls_tree(gitdir, rev, sub_paths)
2129 submodules = []
2130 for sub_path, sub_url in zip(sub_paths, sub_urls):
2131 try:
2132 sub_rev = sub_revs[sub_path]
2133 except KeyError:
2134 # Ignore non-exist submodules.
2135 continue
2136 submodules.append((sub_rev, sub_path, sub_url))
2137 return submodules
2138
2139 re_path = re.compile(r"^submodule\.(.+)\.path=(.*)$")
2140 re_url = re.compile(r"^submodule\.(.+)\.url=(.*)$")
2141
2142 def parse_gitmodules(gitdir, rev):
2143 cmd = ["cat-file", "blob", "%s:.gitmodules" % rev]
2144 try:
2145 p = GitCommand(
2146 None,
2147 cmd,
2148 capture_stdout=True,
2149 capture_stderr=True,
2150 bare=True,
2151 gitdir=gitdir,
2152 )
2153 except GitError:
2154 return [], []
2155 if p.Wait() != 0:
2156 return [], []
2157
2158 gitmodules_lines = []
2159 fd, temp_gitmodules_path = tempfile.mkstemp()
2160 try:
2161 os.write(fd, p.stdout.encode("utf-8"))
2162 os.close(fd)
2163 cmd = ["config", "--file", temp_gitmodules_path, "--list"]
2164 p = GitCommand(
2165 None,
2166 cmd,
2167 capture_stdout=True,
2168 capture_stderr=True,
2169 bare=True,
2170 gitdir=gitdir,
2171 )
2172 if p.Wait() != 0:
2173 return [], []
2174 gitmodules_lines = p.stdout.split("\n")
2175 except GitError:
2176 return [], []
2177 finally:
2178 platform_utils.remove(temp_gitmodules_path)
2179
2180 names = set()
2181 paths = {}
2182 urls = {}
2183 for line in gitmodules_lines:
2184 if not line:
2185 continue
2186 m = re_path.match(line)
2187 if m:
2188 names.add(m.group(1))
2189 paths[m.group(1)] = m.group(2)
2190 continue
2191 m = re_url.match(line)
2192 if m:
2193 names.add(m.group(1))
2194 urls[m.group(1)] = m.group(2)
2195 continue
2196 names = sorted(names)
2197 return (
2198 [paths.get(name, "") for name in names],
2199 [urls.get(name, "") for name in names],
2200 )
2201
2202 def git_ls_tree(gitdir, rev, paths):
2203 cmd = ["ls-tree", rev, "--"]
2204 cmd.extend(paths)
2205 try:
2206 p = GitCommand(
2207 None,
2208 cmd,
2209 capture_stdout=True,
2210 capture_stderr=True,
2211 bare=True,
2212 gitdir=gitdir,
2213 )
2214 except GitError:
2215 return []
2216 if p.Wait() != 0:
2217 return []
2218 objects = {}
2219 for line in p.stdout.split("\n"):
2220 if not line.strip():
2221 continue
2222 object_rev, object_path = line.split()[2:4]
2223 objects[object_path] = object_rev
2224 return objects
2225
2226 try:
2227 rev = self.GetRevisionId()
2228 except GitError:
2229 return []
2230 return get_submodules(self.gitdir, rev)
2231
2232 def GetDerivedSubprojects(self):
2233 result = []
2234 if not self.Exists:
2235 # If git repo does not exist yet, querying its submodules will
2236 # mess up its states; so return here.
2237 return result
2238 for rev, path, url in self._GetSubmodules():
2239 name = self.manifest.GetSubprojectName(self, path)
2240 (
2241 relpath,
2242 worktree,
2243 gitdir,
2244 objdir,
2245 ) = self.manifest.GetSubprojectPaths(self, name, path)
2246 project = self.manifest.paths.get(relpath)
2247 if project:
2248 result.extend(project.GetDerivedSubprojects())
2249 continue
2250
2251 if url.startswith(".."):
2252 url = urllib.parse.urljoin("%s/" % self.remote.url, url)
2253 remote = RemoteSpec(
2254 self.remote.name,
2255 url=url,
2256 pushUrl=self.remote.pushUrl,
2257 review=self.remote.review,
2258 revision=self.remote.revision,
2259 )
2260 subproject = Project(
2261 manifest=self.manifest,
2262 name=name,
2263 remote=remote,
2264 gitdir=gitdir,
2265 objdir=objdir,
2266 worktree=worktree,
2267 relpath=relpath,
2268 revisionExpr=rev,
2269 revisionId=rev,
2270 rebase=self.rebase,
2271 groups=self.groups,
2272 sync_c=self.sync_c,
2273 sync_s=self.sync_s,
2274 sync_tags=self.sync_tags,
2275 parent=self,
2276 is_derived=True,
2277 )
2278 result.append(subproject)
2279 result.extend(subproject.GetDerivedSubprojects())
2280 return result
2281
2282 def EnableRepositoryExtension(self, key, value="true", version=1):
2283 """Enable git repository extension |key| with |value|.
2284
2285 Args:
2286 key: The extension to enabled. Omit the "extensions." prefix.
2287 value: The value to use for the extension.
2288 version: The minimum git repository version needed.
2289 """
2290 # Make sure the git repo version is new enough already.
2291 found_version = self.config.GetInt("core.repositoryFormatVersion")
2292 if found_version is None:
2293 found_version = 0
2294 if found_version < version:
2295 self.config.SetString("core.repositoryFormatVersion", str(version))
2296
2297 # Enable the extension!
2298 self.config.SetString("extensions.%s" % (key,), value)
2299
2300 def ResolveRemoteHead(self, name=None):
2301 """Find out what the default branch (HEAD) points to.
2302
2303 Normally this points to refs/heads/master, but projects are moving to
2304 main. Support whatever the server uses rather than hardcoding "master"
2305 ourselves.
2306 """
2307 if name is None:
2308 name = self.remote.name
2309
2310 # The output will look like (NB: tabs are separators):
2311 # ref: refs/heads/master HEAD
2312 # 5f6803b100bb3cd0f534e96e88c91373e8ed1c44 HEAD
2313 output = self.bare_git.ls_remote(
2314 "-q", "--symref", "--exit-code", name, "HEAD"
2315 )
2316
2317 for line in output.splitlines():
2318 lhs, rhs = line.split("\t", 1)
2319 if rhs == "HEAD" and lhs.startswith("ref:"):
2320 return lhs[4:].strip()
2321
2322 return None
2323
2324 def _CheckForImmutableRevision(self):
2325 try:
2326 # if revision (sha or tag) is not present then following function
2327 # throws an error.
2328 self.bare_git.rev_list(
2329 "-1", "--missing=allow-any", "%s^0" % self.revisionExpr, "--"
2330 )
2331 if self.upstream:
2332 rev = self.GetRemote().ToLocal(self.upstream)
2333 self.bare_git.rev_list(
2334 "-1", "--missing=allow-any", "%s^0" % rev, "--"
2335 )
2336 self.bare_git.merge_base(
2337 "--is-ancestor", self.revisionExpr, rev
2338 )
2339 return True
2340 except GitError:
2341 # There is no such persistent revision. We have to fetch it.
2342 return False
2343
2344 def _FetchArchive(self, tarpath, cwd=None):
2345 cmd = ["archive", "-v", "-o", tarpath]
2346 cmd.append("--remote=%s" % self.remote.url)
2347 cmd.append("--prefix=%s/" % self.RelPath(local=False))
2348 cmd.append(self.revisionExpr)
2349
2350 command = GitCommand(
Jason Chang32b59562023-07-14 16:45:35 -07002351 self,
2352 cmd,
2353 cwd=cwd,
2354 capture_stdout=True,
2355 capture_stderr=True,
2356 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00002357 )
Jason Chang32b59562023-07-14 16:45:35 -07002358 command.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00002359
2360 def _RemoteFetch(
2361 self,
2362 name=None,
2363 current_branch_only=False,
2364 initial=False,
2365 quiet=False,
2366 verbose=False,
2367 output_redir=None,
2368 alt_dir=None,
2369 tags=True,
2370 prune=False,
2371 depth=None,
2372 submodules=False,
2373 ssh_proxy=None,
2374 force_sync=False,
2375 clone_filter=None,
2376 retry_fetches=2,
2377 retry_sleep_initial_sec=4.0,
2378 retry_exp_factor=2.0,
Jason Chang32b59562023-07-14 16:45:35 -07002379 ) -> bool:
Gavin Makea2e3302023-03-11 06:46:20 +00002380 is_sha1 = False
2381 tag_name = None
2382 # The depth should not be used when fetching to a mirror because
2383 # it will result in a shallow repository that cannot be cloned or
2384 # fetched from.
2385 # The repo project should also never be synced with partial depth.
2386 if self.manifest.IsMirror or self.relpath == ".repo/repo":
2387 depth = None
2388
2389 if depth:
2390 current_branch_only = True
2391
2392 if ID_RE.match(self.revisionExpr) is not None:
2393 is_sha1 = True
2394
2395 if current_branch_only:
2396 if self.revisionExpr.startswith(R_TAGS):
2397 # This is a tag and its commit id should never change.
2398 tag_name = self.revisionExpr[len(R_TAGS) :]
2399 elif self.upstream and self.upstream.startswith(R_TAGS):
2400 # This is a tag and its commit id should never change.
2401 tag_name = self.upstream[len(R_TAGS) :]
2402
2403 if is_sha1 or tag_name is not None:
2404 if self._CheckForImmutableRevision():
2405 if verbose:
2406 print(
2407 "Skipped fetching project %s (already have "
2408 "persistent ref)" % self.name
2409 )
2410 return True
2411 if is_sha1 and not depth:
2412 # When syncing a specific commit and --depth is not set:
2413 # * if upstream is explicitly specified and is not a sha1, fetch
2414 # only upstream as users expect only upstream to be fetch.
2415 # Note: The commit might not be in upstream in which case the
2416 # sync will fail.
2417 # * otherwise, fetch all branches to make sure we end up with
2418 # the specific commit.
2419 if self.upstream:
2420 current_branch_only = not ID_RE.match(self.upstream)
2421 else:
2422 current_branch_only = False
2423
2424 if not name:
2425 name = self.remote.name
2426
2427 remote = self.GetRemote(name)
2428 if not remote.PreConnectFetch(ssh_proxy):
2429 ssh_proxy = None
2430
2431 if initial:
2432 if alt_dir and "objects" == os.path.basename(alt_dir):
2433 ref_dir = os.path.dirname(alt_dir)
2434 packed_refs = os.path.join(self.gitdir, "packed-refs")
2435
2436 all_refs = self.bare_ref.all
2437 ids = set(all_refs.values())
2438 tmp = set()
2439
2440 for r, ref_id in GitRefs(ref_dir).all.items():
2441 if r not in all_refs:
2442 if r.startswith(R_TAGS) or remote.WritesTo(r):
2443 all_refs[r] = ref_id
2444 ids.add(ref_id)
2445 continue
2446
2447 if ref_id in ids:
2448 continue
2449
2450 r = "refs/_alt/%s" % ref_id
2451 all_refs[r] = ref_id
2452 ids.add(ref_id)
2453 tmp.add(r)
2454
2455 tmp_packed_lines = []
2456 old_packed_lines = []
2457
2458 for r in sorted(all_refs):
2459 line = "%s %s\n" % (all_refs[r], r)
2460 tmp_packed_lines.append(line)
2461 if r not in tmp:
2462 old_packed_lines.append(line)
2463
2464 tmp_packed = "".join(tmp_packed_lines)
2465 old_packed = "".join(old_packed_lines)
2466 _lwrite(packed_refs, tmp_packed)
2467 else:
2468 alt_dir = None
2469
2470 cmd = ["fetch"]
2471
2472 if clone_filter:
2473 git_require((2, 19, 0), fail=True, msg="partial clones")
2474 cmd.append("--filter=%s" % clone_filter)
2475 self.EnableRepositoryExtension("partialclone", self.remote.name)
2476
2477 if depth:
2478 cmd.append("--depth=%s" % depth)
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002479 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002480 # If this repo has shallow objects, then we don't know which refs
2481 # have shallow objects or not. Tell git to unshallow all fetched
2482 # refs. Don't do this with projects that don't have shallow
2483 # objects, since it is less efficient.
2484 if os.path.exists(os.path.join(self.gitdir, "shallow")):
2485 cmd.append("--depth=2147483647")
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002486
Gavin Makea2e3302023-03-11 06:46:20 +00002487 if not verbose:
2488 cmd.append("--quiet")
2489 if not quiet and sys.stdout.isatty():
2490 cmd.append("--progress")
2491 if not self.worktree:
2492 cmd.append("--update-head-ok")
2493 cmd.append(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002494
Gavin Makea2e3302023-03-11 06:46:20 +00002495 if force_sync:
2496 cmd.append("--force")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002497
Gavin Makea2e3302023-03-11 06:46:20 +00002498 if prune:
2499 cmd.append("--prune")
Mike Frysinger29626b42021-05-01 09:37:13 -04002500
Gavin Makea2e3302023-03-11 06:46:20 +00002501 # Always pass something for --recurse-submodules, git with GIT_DIR
2502 # behaves incorrectly when not given `--recurse-submodules=no`.
2503 # (b/218891912)
2504 cmd.append(
2505 f'--recurse-submodules={"on-demand" if submodules else "no"}'
2506 )
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002507
Gavin Makea2e3302023-03-11 06:46:20 +00002508 spec = []
2509 if not current_branch_only:
2510 # Fetch whole repo.
2511 spec.append(
2512 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2513 )
2514 elif tag_name is not None:
2515 spec.append("tag")
2516 spec.append(tag_name)
Remy Böhmer1469c282020-12-15 18:49:02 +01002517
Gavin Makea2e3302023-03-11 06:46:20 +00002518 if self.manifest.IsMirror and not current_branch_only:
2519 branch = None
Remy Böhmer1469c282020-12-15 18:49:02 +01002520 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002521 branch = self.revisionExpr
2522 if (
2523 not self.manifest.IsMirror
2524 and is_sha1
2525 and depth
2526 and git_require((1, 8, 3))
2527 ):
2528 # Shallow checkout of a specific commit, fetch from that commit and
2529 # not the heads only as the commit might be deeper in the history.
2530 spec.append(branch)
2531 if self.upstream:
2532 spec.append(self.upstream)
David James8d201162013-10-11 17:03:19 -07002533 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002534 if is_sha1:
2535 branch = self.upstream
2536 if branch is not None and branch.strip():
2537 if not branch.startswith("refs/"):
2538 branch = R_HEADS + branch
2539 spec.append(str(("+%s:" % branch) + remote.ToLocal(branch)))
David James8d201162013-10-11 17:03:19 -07002540
Gavin Makea2e3302023-03-11 06:46:20 +00002541 # If mirroring repo and we cannot deduce the tag or branch to fetch,
2542 # fetch whole repo.
2543 if self.manifest.IsMirror and not spec:
2544 spec.append(
2545 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2546 )
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002547
Gavin Makea2e3302023-03-11 06:46:20 +00002548 # If using depth then we should not get all the tags since they may
2549 # be outside of the depth.
2550 if not tags or depth:
2551 cmd.append("--no-tags")
2552 else:
2553 cmd.append("--tags")
2554 spec.append(str(("+refs/tags/*:") + remote.ToLocal("refs/tags/*")))
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002555
Gavin Makea2e3302023-03-11 06:46:20 +00002556 cmd.extend(spec)
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002557
Gavin Makea2e3302023-03-11 06:46:20 +00002558 # At least one retry minimum due to git remote prune.
2559 retry_fetches = max(retry_fetches, 2)
2560 retry_cur_sleep = retry_sleep_initial_sec
2561 ok = prune_tried = False
2562 for try_n in range(retry_fetches):
Jason Chang32b59562023-07-14 16:45:35 -07002563 verify_command = try_n == retry_fetches - 1
Gavin Makea2e3302023-03-11 06:46:20 +00002564 gitcmd = GitCommand(
2565 self,
2566 cmd,
2567 bare=True,
2568 objdir=os.path.join(self.objdir, "objects"),
2569 ssh_proxy=ssh_proxy,
2570 merge_output=True,
2571 capture_stdout=quiet or bool(output_redir),
Jason Chang32b59562023-07-14 16:45:35 -07002572 verify_command=verify_command,
Gavin Makea2e3302023-03-11 06:46:20 +00002573 )
2574 if gitcmd.stdout and not quiet and output_redir:
2575 output_redir.write(gitcmd.stdout)
2576 ret = gitcmd.Wait()
2577 if ret == 0:
2578 ok = True
2579 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002580
Gavin Makea2e3302023-03-11 06:46:20 +00002581 # Retry later due to HTTP 429 Too Many Requests.
2582 elif (
2583 gitcmd.stdout
2584 and "error:" in gitcmd.stdout
2585 and "HTTP 429" in gitcmd.stdout
2586 ):
2587 # Fallthru to sleep+retry logic at the bottom.
2588 pass
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002589
Gavin Makea2e3302023-03-11 06:46:20 +00002590 # Try to prune remote branches once in case there are conflicts.
2591 # For example, if the remote had refs/heads/upstream, but deleted
2592 # that and now has refs/heads/upstream/foo.
2593 elif (
2594 gitcmd.stdout
2595 and "error:" in gitcmd.stdout
2596 and "git remote prune" in gitcmd.stdout
2597 and not prune_tried
2598 ):
2599 prune_tried = True
2600 prunecmd = GitCommand(
2601 self,
2602 ["remote", "prune", name],
2603 bare=True,
2604 ssh_proxy=ssh_proxy,
2605 )
2606 ret = prunecmd.Wait()
2607 if ret:
2608 break
2609 print(
2610 "retrying fetch after pruning remote branches",
2611 file=output_redir,
2612 )
2613 # Continue right away so we don't sleep as we shouldn't need to.
2614 continue
2615 elif current_branch_only and is_sha1 and ret == 128:
2616 # Exit code 128 means "couldn't find the ref you asked for"; if
2617 # we're in sha1 mode, we just tried sync'ing from the upstream
2618 # field; it doesn't exist, thus abort the optimization attempt
2619 # and do a full sync.
2620 break
2621 elif ret < 0:
2622 # Git died with a signal, exit immediately.
2623 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002624
Gavin Makea2e3302023-03-11 06:46:20 +00002625 # Figure out how long to sleep before the next attempt, if there is
2626 # one.
2627 if not verbose and gitcmd.stdout:
2628 print(
2629 "\n%s:\n%s" % (self.name, gitcmd.stdout),
2630 end="",
2631 file=output_redir,
2632 )
2633 if try_n < retry_fetches - 1:
2634 print(
2635 "%s: sleeping %s seconds before retrying"
2636 % (self.name, retry_cur_sleep),
2637 file=output_redir,
2638 )
2639 time.sleep(retry_cur_sleep)
2640 retry_cur_sleep = min(
2641 retry_exp_factor * retry_cur_sleep, MAXIMUM_RETRY_SLEEP_SEC
2642 )
2643 retry_cur_sleep *= 1 - random.uniform(
2644 -RETRY_JITTER_PERCENT, RETRY_JITTER_PERCENT
2645 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002646
Gavin Makea2e3302023-03-11 06:46:20 +00002647 if initial:
2648 if alt_dir:
2649 if old_packed != "":
2650 _lwrite(packed_refs, old_packed)
2651 else:
2652 platform_utils.remove(packed_refs)
2653 self.bare_git.pack_refs("--all", "--prune")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002654
Gavin Makea2e3302023-03-11 06:46:20 +00002655 if is_sha1 and current_branch_only:
2656 # We just synced the upstream given branch; verify we
2657 # got what we wanted, else trigger a second run of all
2658 # refs.
2659 if not self._CheckForImmutableRevision():
2660 # Sync the current branch only with depth set to None.
2661 # We always pass depth=None down to avoid infinite recursion.
2662 return self._RemoteFetch(
2663 name=name,
2664 quiet=quiet,
2665 verbose=verbose,
2666 output_redir=output_redir,
2667 current_branch_only=current_branch_only and depth,
2668 initial=False,
2669 alt_dir=alt_dir,
Nasser Grainawiaafed292023-05-24 12:51:03 -06002670 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00002671 depth=None,
2672 ssh_proxy=ssh_proxy,
2673 clone_filter=clone_filter,
2674 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002675
Gavin Makea2e3302023-03-11 06:46:20 +00002676 return ok
Mike Frysingerf4545122019-11-11 04:34:16 -05002677
Gavin Makea2e3302023-03-11 06:46:20 +00002678 def _ApplyCloneBundle(self, initial=False, quiet=False, verbose=False):
2679 if initial and (
2680 self.manifest.manifestProject.depth or self.clone_depth
2681 ):
2682 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002683
Gavin Makea2e3302023-03-11 06:46:20 +00002684 remote = self.GetRemote()
2685 bundle_url = remote.url + "/clone.bundle"
2686 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
2687 if GetSchemeFromUrl(bundle_url) not in (
2688 "http",
2689 "https",
2690 "persistent-http",
2691 "persistent-https",
2692 ):
2693 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06002694
Gavin Makea2e3302023-03-11 06:46:20 +00002695 bundle_dst = os.path.join(self.gitdir, "clone.bundle")
2696 bundle_tmp = os.path.join(self.gitdir, "clone.bundle.tmp")
2697
2698 exist_dst = os.path.exists(bundle_dst)
2699 exist_tmp = os.path.exists(bundle_tmp)
2700
2701 if not initial and not exist_dst and not exist_tmp:
2702 return False
2703
2704 if not exist_dst:
2705 exist_dst = self._FetchBundle(
2706 bundle_url, bundle_tmp, bundle_dst, quiet, verbose
2707 )
2708 if not exist_dst:
2709 return False
2710
2711 cmd = ["fetch"]
2712 if not verbose:
2713 cmd.append("--quiet")
2714 if not quiet and sys.stdout.isatty():
2715 cmd.append("--progress")
2716 if not self.worktree:
2717 cmd.append("--update-head-ok")
2718 cmd.append(bundle_dst)
2719 for f in remote.fetch:
2720 cmd.append(str(f))
2721 cmd.append("+refs/tags/*:refs/tags/*")
2722
2723 ok = (
2724 GitCommand(
2725 self,
2726 cmd,
2727 bare=True,
2728 objdir=os.path.join(self.objdir, "objects"),
2729 ).Wait()
2730 == 0
2731 )
2732 platform_utils.remove(bundle_dst, missing_ok=True)
2733 platform_utils.remove(bundle_tmp, missing_ok=True)
2734 return ok
2735
2736 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
2737 platform_utils.remove(dstPath, missing_ok=True)
2738
2739 cmd = ["curl", "--fail", "--output", tmpPath, "--netrc", "--location"]
2740 if quiet:
2741 cmd += ["--silent", "--show-error"]
2742 if os.path.exists(tmpPath):
2743 size = os.stat(tmpPath).st_size
2744 if size >= 1024:
2745 cmd += ["--continue-at", "%d" % (size,)]
2746 else:
2747 platform_utils.remove(tmpPath)
2748 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
2749 if cookiefile:
2750 cmd += ["--cookie", cookiefile]
2751 if proxy:
2752 cmd += ["--proxy", proxy]
2753 elif "http_proxy" in os.environ and "darwin" == sys.platform:
2754 cmd += ["--proxy", os.environ["http_proxy"]]
2755 if srcUrl.startswith("persistent-https"):
2756 srcUrl = "http" + srcUrl[len("persistent-https") :]
2757 elif srcUrl.startswith("persistent-http"):
2758 srcUrl = "http" + srcUrl[len("persistent-http") :]
2759 cmd += [srcUrl]
2760
2761 proc = None
2762 with Trace("Fetching bundle: %s", " ".join(cmd)):
2763 if verbose:
2764 print("%s: Downloading bundle: %s" % (self.name, srcUrl))
2765 stdout = None if verbose else subprocess.PIPE
2766 stderr = None if verbose else subprocess.STDOUT
2767 try:
2768 proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
2769 except OSError:
2770 return False
2771
2772 (output, _) = proc.communicate()
2773 curlret = proc.returncode
2774
2775 if curlret == 22:
2776 # From curl man page:
2777 # 22: HTTP page not retrieved. The requested url was not found
2778 # or returned another error with the HTTP error code being 400
2779 # or above. This return code only appears if -f, --fail is used.
2780 if verbose:
2781 print(
2782 "%s: Unable to retrieve clone.bundle; ignoring."
2783 % self.name
2784 )
2785 if output:
2786 print("Curl output:\n%s" % output)
2787 return False
2788 elif curlret and not verbose and output:
2789 print("%s" % output, file=sys.stderr)
2790
2791 if os.path.exists(tmpPath):
2792 if curlret == 0 and self._IsValidBundle(tmpPath, quiet):
2793 platform_utils.rename(tmpPath, dstPath)
2794 return True
2795 else:
2796 platform_utils.remove(tmpPath)
2797 return False
2798 else:
2799 return False
2800
2801 def _IsValidBundle(self, path, quiet):
2802 try:
2803 with open(path, "rb") as f:
2804 if f.read(16) == b"# v2 git bundle\n":
2805 return True
2806 else:
2807 if not quiet:
2808 print(
2809 "Invalid clone.bundle file; ignoring.",
2810 file=sys.stderr,
2811 )
2812 return False
2813 except OSError:
2814 return False
2815
2816 def _Checkout(self, rev, quiet=False):
2817 cmd = ["checkout"]
2818 if quiet:
2819 cmd.append("-q")
2820 cmd.append(rev)
2821 cmd.append("--")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002822 if GitCommand(self, cmd).Wait() != 0:
Gavin Makea2e3302023-03-11 06:46:20 +00002823 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002824 raise GitError(
2825 "%s checkout %s " % (self.name, rev), project=self.name
2826 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002827
Gavin Makea2e3302023-03-11 06:46:20 +00002828 def _CherryPick(self, rev, ffonly=False, record_origin=False):
2829 cmd = ["cherry-pick"]
2830 if ffonly:
2831 cmd.append("--ff")
2832 if record_origin:
2833 cmd.append("-x")
2834 cmd.append(rev)
2835 cmd.append("--")
2836 if GitCommand(self, cmd).Wait() != 0:
2837 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002838 raise GitError(
2839 "%s cherry-pick %s " % (self.name, rev), project=self.name
2840 )
Victor Boivie0960b5b2010-11-26 13:42:13 +01002841
Gavin Makea2e3302023-03-11 06:46:20 +00002842 def _LsRemote(self, refs):
2843 cmd = ["ls-remote", self.remote.name, refs]
2844 p = GitCommand(self, cmd, capture_stdout=True)
2845 if p.Wait() == 0:
2846 return p.stdout
2847 return None
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002848
Gavin Makea2e3302023-03-11 06:46:20 +00002849 def _Revert(self, rev):
2850 cmd = ["revert"]
2851 cmd.append("--no-edit")
2852 cmd.append(rev)
2853 cmd.append("--")
2854 if GitCommand(self, cmd).Wait() != 0:
2855 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002856 raise GitError(
2857 "%s revert %s " % (self.name, rev), project=self.name
2858 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002859
Gavin Makea2e3302023-03-11 06:46:20 +00002860 def _ResetHard(self, rev, quiet=True):
2861 cmd = ["reset", "--hard"]
2862 if quiet:
2863 cmd.append("-q")
2864 cmd.append(rev)
2865 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002866 raise GitError(
2867 "%s reset --hard %s " % (self.name, rev), project=self.name
2868 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002869
Gavin Makea2e3302023-03-11 06:46:20 +00002870 def _SyncSubmodules(self, quiet=True):
2871 cmd = ["submodule", "update", "--init", "--recursive"]
2872 if quiet:
2873 cmd.append("-q")
2874 if GitCommand(self, cmd).Wait() != 0:
2875 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07002876 "%s submodule update --init --recursive " % self.name,
2877 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00002878 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002879
Gavin Makea2e3302023-03-11 06:46:20 +00002880 def _Rebase(self, upstream, onto=None):
2881 cmd = ["rebase"]
2882 if onto is not None:
2883 cmd.extend(["--onto", onto])
2884 cmd.append(upstream)
2885 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002886 raise GitError(
2887 "%s rebase %s " % (self.name, upstream), project=self.name
2888 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002889
Gavin Makea2e3302023-03-11 06:46:20 +00002890 def _FastForward(self, head, ffonly=False):
2891 cmd = ["merge", "--no-stat", head]
2892 if ffonly:
2893 cmd.append("--ff-only")
2894 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002895 raise GitError(
2896 "%s merge %s " % (self.name, head), project=self.name
2897 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002898
Gavin Makea2e3302023-03-11 06:46:20 +00002899 def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False):
2900 init_git_dir = not os.path.exists(self.gitdir)
2901 init_obj_dir = not os.path.exists(self.objdir)
2902 try:
2903 # Initialize the bare repository, which contains all of the objects.
2904 if init_obj_dir:
2905 os.makedirs(self.objdir)
2906 self.bare_objdir.init()
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002907
Gavin Makea2e3302023-03-11 06:46:20 +00002908 self._UpdateHooks(quiet=quiet)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002909
Gavin Makea2e3302023-03-11 06:46:20 +00002910 if self.use_git_worktrees:
2911 # Enable per-worktree config file support if possible. This
2912 # is more a nice-to-have feature for users rather than a
2913 # hard requirement.
2914 if git_require((2, 20, 0)):
2915 self.EnableRepositoryExtension("worktreeConfig")
Renaud Paquay788e9622017-01-27 11:41:12 -08002916
Gavin Makea2e3302023-03-11 06:46:20 +00002917 # If we have a separate directory to hold refs, initialize it as
2918 # well.
2919 if self.objdir != self.gitdir:
2920 if init_git_dir:
2921 os.makedirs(self.gitdir)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002922
Gavin Makea2e3302023-03-11 06:46:20 +00002923 if init_obj_dir or init_git_dir:
2924 self._ReferenceGitDir(
2925 self.objdir, self.gitdir, copy_all=True
2926 )
2927 try:
2928 self._CheckDirReference(self.objdir, self.gitdir)
2929 except GitError as e:
2930 if force_sync:
2931 print(
2932 "Retrying clone after deleting %s" % self.gitdir,
2933 file=sys.stderr,
2934 )
2935 try:
2936 platform_utils.rmtree(
2937 platform_utils.realpath(self.gitdir)
2938 )
2939 if self.worktree and os.path.exists(
2940 platform_utils.realpath(self.worktree)
2941 ):
2942 platform_utils.rmtree(
2943 platform_utils.realpath(self.worktree)
2944 )
2945 return self._InitGitDir(
2946 mirror_git=mirror_git,
2947 force_sync=False,
2948 quiet=quiet,
2949 )
2950 except Exception:
2951 raise e
2952 raise e
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002953
Gavin Makea2e3302023-03-11 06:46:20 +00002954 if init_git_dir:
2955 mp = self.manifest.manifestProject
2956 ref_dir = mp.reference or ""
Julien Camperguedd654222014-01-09 16:21:37 +01002957
Gavin Makea2e3302023-03-11 06:46:20 +00002958 def _expanded_ref_dirs():
2959 """Iterate through possible git reference dir paths."""
2960 name = self.name + ".git"
2961 yield mirror_git or os.path.join(ref_dir, name)
2962 for prefix in "", self.remote.name:
2963 yield os.path.join(
2964 ref_dir, ".repo", "project-objects", prefix, name
2965 )
2966 yield os.path.join(
2967 ref_dir, ".repo", "worktrees", prefix, name
2968 )
2969
2970 if ref_dir or mirror_git:
2971 found_ref_dir = None
2972 for path in _expanded_ref_dirs():
2973 if os.path.exists(path):
2974 found_ref_dir = path
2975 break
2976 ref_dir = found_ref_dir
2977
2978 if ref_dir:
2979 if not os.path.isabs(ref_dir):
2980 # The alternate directory is relative to the object
2981 # database.
2982 ref_dir = os.path.relpath(
2983 ref_dir, os.path.join(self.objdir, "objects")
2984 )
2985 _lwrite(
2986 os.path.join(
2987 self.objdir, "objects/info/alternates"
2988 ),
2989 os.path.join(ref_dir, "objects") + "\n",
2990 )
2991
2992 m = self.manifest.manifestProject.config
2993 for key in ["user.name", "user.email"]:
2994 if m.Has(key, include_defaults=False):
2995 self.config.SetString(key, m.GetString(key))
2996 if not self.manifest.EnableGitLfs:
2997 self.config.SetString(
2998 "filter.lfs.smudge", "git-lfs smudge --skip -- %f"
2999 )
3000 self.config.SetString(
3001 "filter.lfs.process", "git-lfs filter-process --skip"
3002 )
3003 self.config.SetBoolean(
3004 "core.bare", True if self.manifest.IsMirror else None
3005 )
3006 except Exception:
3007 if init_obj_dir and os.path.exists(self.objdir):
3008 platform_utils.rmtree(self.objdir)
3009 if init_git_dir and os.path.exists(self.gitdir):
3010 platform_utils.rmtree(self.gitdir)
3011 raise
3012
3013 def _UpdateHooks(self, quiet=False):
3014 if os.path.exists(self.objdir):
3015 self._InitHooks(quiet=quiet)
3016
3017 def _InitHooks(self, quiet=False):
3018 hooks = platform_utils.realpath(os.path.join(self.objdir, "hooks"))
3019 if not os.path.exists(hooks):
3020 os.makedirs(hooks)
3021
3022 # Delete sample hooks. They're noise.
3023 for hook in glob.glob(os.path.join(hooks, "*.sample")):
3024 try:
3025 platform_utils.remove(hook, missing_ok=True)
3026 except PermissionError:
3027 pass
3028
3029 for stock_hook in _ProjectHooks():
3030 name = os.path.basename(stock_hook)
3031
3032 if (
3033 name in ("commit-msg",)
3034 and not self.remote.review
3035 and self is not self.manifest.manifestProject
3036 ):
3037 # Don't install a Gerrit Code Review hook if this
3038 # project does not appear to use it for reviews.
3039 #
3040 # Since the manifest project is one of those, but also
3041 # managed through gerrit, it's excluded.
3042 continue
3043
3044 dst = os.path.join(hooks, name)
3045 if platform_utils.islink(dst):
3046 continue
3047 if os.path.exists(dst):
3048 # If the files are the same, we'll leave it alone. We create
3049 # symlinks below by default but fallback to hardlinks if the OS
3050 # blocks them. So if we're here, it's probably because we made a
3051 # hardlink below.
3052 if not filecmp.cmp(stock_hook, dst, shallow=False):
3053 if not quiet:
3054 _warn(
3055 "%s: Not replacing locally modified %s hook",
3056 self.RelPath(local=False),
3057 name,
3058 )
3059 continue
3060 try:
3061 platform_utils.symlink(
3062 os.path.relpath(stock_hook, os.path.dirname(dst)), dst
3063 )
3064 except OSError as e:
3065 if e.errno == errno.EPERM:
3066 try:
3067 os.link(stock_hook, dst)
3068 except OSError:
Jason Chang32b59562023-07-14 16:45:35 -07003069 raise GitError(
3070 self._get_symlink_error_message(), project=self.name
3071 )
Gavin Makea2e3302023-03-11 06:46:20 +00003072 else:
3073 raise
3074
3075 def _InitRemote(self):
3076 if self.remote.url:
3077 remote = self.GetRemote()
3078 remote.url = self.remote.url
3079 remote.pushUrl = self.remote.pushUrl
3080 remote.review = self.remote.review
3081 remote.projectname = self.name
3082
3083 if self.worktree:
3084 remote.ResetFetch(mirror=False)
3085 else:
3086 remote.ResetFetch(mirror=True)
3087 remote.Save()
3088
3089 def _InitMRef(self):
3090 """Initialize the pseudo m/<manifest branch> ref."""
3091 if self.manifest.branch:
3092 if self.use_git_worktrees:
3093 # Set up the m/ space to point to the worktree-specific ref
3094 # space. We'll update the worktree-specific ref space on each
3095 # checkout.
3096 ref = R_M + self.manifest.branch
3097 if not self.bare_ref.symref(ref):
3098 self.bare_git.symbolic_ref(
3099 "-m",
3100 "redirecting to worktree scope",
3101 ref,
3102 R_WORKTREE_M + self.manifest.branch,
3103 )
3104
3105 # We can't update this ref with git worktrees until it exists.
3106 # We'll wait until the initial checkout to set it.
3107 if not os.path.exists(self.worktree):
3108 return
3109
3110 base = R_WORKTREE_M
3111 active_git = self.work_git
3112
3113 self._InitAnyMRef(HEAD, self.bare_git, detach=True)
3114 else:
3115 base = R_M
3116 active_git = self.bare_git
3117
3118 self._InitAnyMRef(base + self.manifest.branch, active_git)
3119
3120 def _InitMirrorHead(self):
3121 self._InitAnyMRef(HEAD, self.bare_git)
3122
3123 def _InitAnyMRef(self, ref, active_git, detach=False):
3124 """Initialize |ref| in |active_git| to the value in the manifest.
3125
3126 This points |ref| to the <project> setting in the manifest.
3127
3128 Args:
3129 ref: The branch to update.
3130 active_git: The git repository to make updates in.
3131 detach: Whether to update target of symbolic refs, or overwrite the
3132 ref directly (and thus make it non-symbolic).
3133 """
3134 cur = self.bare_ref.symref(ref)
3135
3136 if self.revisionId:
3137 if cur != "" or self.bare_ref.get(ref) != self.revisionId:
3138 msg = "manifest set to %s" % self.revisionId
3139 dst = self.revisionId + "^0"
3140 active_git.UpdateRef(ref, dst, message=msg, detach=True)
Julien Camperguedd654222014-01-09 16:21:37 +01003141 else:
Gavin Makea2e3302023-03-11 06:46:20 +00003142 remote = self.GetRemote()
3143 dst = remote.ToLocal(self.revisionExpr)
3144 if cur != dst:
3145 msg = "manifest set to %s" % self.revisionExpr
3146 if detach:
3147 active_git.UpdateRef(ref, dst, message=msg, detach=True)
3148 else:
3149 active_git.symbolic_ref("-m", msg, ref, dst)
Julien Camperguedd654222014-01-09 16:21:37 +01003150
Gavin Makea2e3302023-03-11 06:46:20 +00003151 def _CheckDirReference(self, srcdir, destdir):
3152 # Git worktrees don't use symlinks to share at all.
3153 if self.use_git_worktrees:
3154 return
Julien Camperguedd654222014-01-09 16:21:37 +01003155
Gavin Makea2e3302023-03-11 06:46:20 +00003156 for name in self.shareable_dirs:
3157 # Try to self-heal a bit in simple cases.
3158 dst_path = os.path.join(destdir, name)
3159 src_path = os.path.join(srcdir, name)
Julien Camperguedd654222014-01-09 16:21:37 +01003160
Gavin Makea2e3302023-03-11 06:46:20 +00003161 dst = platform_utils.realpath(dst_path)
3162 if os.path.lexists(dst):
3163 src = platform_utils.realpath(src_path)
3164 # Fail if the links are pointing to the wrong place.
3165 if src != dst:
3166 _error("%s is different in %s vs %s", name, destdir, srcdir)
3167 raise GitError(
3168 "--force-sync not enabled; cannot overwrite a local "
3169 "work tree. If you're comfortable with the "
3170 "possibility of losing the work tree's git metadata,"
3171 " use `repo sync --force-sync {0}` to "
Jason Chang32b59562023-07-14 16:45:35 -07003172 "proceed.".format(self.RelPath(local=False)),
3173 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003174 )
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003175
Gavin Makea2e3302023-03-11 06:46:20 +00003176 def _ReferenceGitDir(self, gitdir, dotgit, copy_all):
3177 """Update |dotgit| to reference |gitdir|, using symlinks where possible.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003178
Gavin Makea2e3302023-03-11 06:46:20 +00003179 Args:
3180 gitdir: The bare git repository. Must already be initialized.
3181 dotgit: The repository you would like to initialize.
3182 copy_all: If true, copy all remaining files from |gitdir| ->
3183 |dotgit|. This saves you the effort of initializing |dotgit|
3184 yourself.
3185 """
3186 symlink_dirs = self.shareable_dirs[:]
3187 to_symlink = symlink_dirs
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003188
Gavin Makea2e3302023-03-11 06:46:20 +00003189 to_copy = []
3190 if copy_all:
3191 to_copy = platform_utils.listdir(gitdir)
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003192
Gavin Makea2e3302023-03-11 06:46:20 +00003193 dotgit = platform_utils.realpath(dotgit)
3194 for name in set(to_copy).union(to_symlink):
3195 try:
3196 src = platform_utils.realpath(os.path.join(gitdir, name))
3197 dst = os.path.join(dotgit, name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003198
Gavin Makea2e3302023-03-11 06:46:20 +00003199 if os.path.lexists(dst):
3200 continue
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003201
Gavin Makea2e3302023-03-11 06:46:20 +00003202 # If the source dir doesn't exist, create an empty dir.
3203 if name in symlink_dirs and not os.path.lexists(src):
3204 os.makedirs(src)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003205
Gavin Makea2e3302023-03-11 06:46:20 +00003206 if name in to_symlink:
3207 platform_utils.symlink(
3208 os.path.relpath(src, os.path.dirname(dst)), dst
3209 )
3210 elif copy_all and not platform_utils.islink(dst):
3211 if platform_utils.isdir(src):
3212 shutil.copytree(src, dst)
3213 elif os.path.isfile(src):
3214 shutil.copy(src, dst)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003215
Gavin Makea2e3302023-03-11 06:46:20 +00003216 except OSError as e:
3217 if e.errno == errno.EPERM:
3218 raise DownloadError(self._get_symlink_error_message())
3219 else:
3220 raise
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003221
Gavin Makea2e3302023-03-11 06:46:20 +00003222 def _InitGitWorktree(self):
3223 """Init the project using git worktrees."""
3224 self.bare_git.worktree("prune")
3225 self.bare_git.worktree(
3226 "add",
3227 "-ff",
3228 "--checkout",
3229 "--detach",
3230 "--lock",
3231 self.worktree,
3232 self.GetRevisionId(),
3233 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003234
Gavin Makea2e3302023-03-11 06:46:20 +00003235 # Rewrite the internal state files to use relative paths between the
3236 # checkouts & worktrees.
3237 dotgit = os.path.join(self.worktree, ".git")
3238 with open(dotgit, "r") as fp:
3239 # Figure out the checkout->worktree path.
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003240 setting = fp.read()
Gavin Makea2e3302023-03-11 06:46:20 +00003241 assert setting.startswith("gitdir:")
3242 git_worktree_path = setting.split(":", 1)[1].strip()
3243 # Some platforms (e.g. Windows) won't let us update dotgit in situ
3244 # because of file permissions. Delete it and recreate it from scratch
3245 # to avoid.
3246 platform_utils.remove(dotgit)
3247 # Use relative path from checkout->worktree & maintain Unix line endings
3248 # on all OS's to match git behavior.
3249 with open(dotgit, "w", newline="\n") as fp:
3250 print(
3251 "gitdir:",
3252 os.path.relpath(git_worktree_path, self.worktree),
3253 file=fp,
3254 )
3255 # Use relative path from worktree->checkout & maintain Unix line endings
3256 # on all OS's to match git behavior.
3257 with open(
3258 os.path.join(git_worktree_path, "gitdir"), "w", newline="\n"
3259 ) as fp:
3260 print(os.path.relpath(dotgit, git_worktree_path), file=fp)
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003261
Gavin Makea2e3302023-03-11 06:46:20 +00003262 self._InitMRef()
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003263
Gavin Makea2e3302023-03-11 06:46:20 +00003264 def _InitWorkTree(self, force_sync=False, submodules=False):
3265 """Setup the worktree .git path.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003266
Gavin Makea2e3302023-03-11 06:46:20 +00003267 This is the user-visible path like src/foo/.git/.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003268
Gavin Makea2e3302023-03-11 06:46:20 +00003269 With non-git-worktrees, this will be a symlink to the .repo/projects/
3270 path. With git-worktrees, this will be a .git file using "gitdir: ..."
3271 syntax.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003272
Gavin Makea2e3302023-03-11 06:46:20 +00003273 Older checkouts had .git/ directories. If we see that, migrate it.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003274
Gavin Makea2e3302023-03-11 06:46:20 +00003275 This also handles changes in the manifest. Maybe this project was
3276 backed by "foo/bar" on the server, but now it's "new/foo/bar". We have
3277 to update the path we point to under .repo/projects/ to match.
3278 """
3279 dotgit = os.path.join(self.worktree, ".git")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003280
Gavin Makea2e3302023-03-11 06:46:20 +00003281 # If using an old layout style (a directory), migrate it.
3282 if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
Jason Chang32b59562023-07-14 16:45:35 -07003283 self._MigrateOldWorkTreeGitDir(dotgit, project=self.name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003284
Gavin Makea2e3302023-03-11 06:46:20 +00003285 init_dotgit = not os.path.exists(dotgit)
3286 if self.use_git_worktrees:
3287 if init_dotgit:
3288 self._InitGitWorktree()
3289 self._CopyAndLinkFiles()
3290 else:
3291 if not init_dotgit:
3292 # See if the project has changed.
3293 if platform_utils.realpath(
3294 self.gitdir
3295 ) != platform_utils.realpath(dotgit):
3296 platform_utils.remove(dotgit)
Doug Anderson37282b42011-03-04 11:54:18 -08003297
Gavin Makea2e3302023-03-11 06:46:20 +00003298 if init_dotgit or not os.path.exists(dotgit):
3299 os.makedirs(self.worktree, exist_ok=True)
3300 platform_utils.symlink(
3301 os.path.relpath(self.gitdir, self.worktree), dotgit
3302 )
Doug Anderson37282b42011-03-04 11:54:18 -08003303
Gavin Makea2e3302023-03-11 06:46:20 +00003304 if init_dotgit:
3305 _lwrite(
3306 os.path.join(dotgit, HEAD), "%s\n" % self.GetRevisionId()
3307 )
Doug Anderson37282b42011-03-04 11:54:18 -08003308
Gavin Makea2e3302023-03-11 06:46:20 +00003309 # Finish checking out the worktree.
3310 cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
3311 if GitCommand(self, cmd).Wait() != 0:
3312 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07003313 "Cannot initialize work tree for " + self.name,
3314 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003315 )
Doug Anderson37282b42011-03-04 11:54:18 -08003316
Gavin Makea2e3302023-03-11 06:46:20 +00003317 if submodules:
3318 self._SyncSubmodules(quiet=True)
3319 self._CopyAndLinkFiles()
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003320
Gavin Makea2e3302023-03-11 06:46:20 +00003321 @classmethod
Jason Chang32b59562023-07-14 16:45:35 -07003322 def _MigrateOldWorkTreeGitDir(cls, dotgit, project=None):
Gavin Makea2e3302023-03-11 06:46:20 +00003323 """Migrate the old worktree .git/ dir style to a symlink.
3324
3325 This logic specifically only uses state from |dotgit| to figure out
3326 where to move content and not |self|. This way if the backing project
3327 also changed places, we only do the .git/ dir to .git symlink migration
3328 here. The path updates will happen independently.
3329 """
3330 # Figure out where in .repo/projects/ it's pointing to.
3331 if not os.path.islink(os.path.join(dotgit, "refs")):
Jason Chang32b59562023-07-14 16:45:35 -07003332 raise GitError(
3333 f"{dotgit}: unsupported checkout state", project=project
3334 )
Gavin Makea2e3302023-03-11 06:46:20 +00003335 gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, "refs")))
3336
3337 # Remove known symlink paths that exist in .repo/projects/.
3338 KNOWN_LINKS = {
3339 "config",
3340 "description",
3341 "hooks",
3342 "info",
3343 "logs",
3344 "objects",
3345 "packed-refs",
3346 "refs",
3347 "rr-cache",
3348 "shallow",
3349 "svn",
3350 }
3351 # Paths that we know will be in both, but are safe to clobber in
3352 # .repo/projects/.
3353 SAFE_TO_CLOBBER = {
3354 "COMMIT_EDITMSG",
3355 "FETCH_HEAD",
3356 "HEAD",
3357 "gc.log",
3358 "gitk.cache",
3359 "index",
3360 "ORIG_HEAD",
3361 }
3362
3363 # First see if we'd succeed before starting the migration.
3364 unknown_paths = []
3365 for name in platform_utils.listdir(dotgit):
3366 # Ignore all temporary/backup names. These are common with vim &
3367 # emacs.
3368 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3369 continue
3370
3371 dotgit_path = os.path.join(dotgit, name)
3372 if name in KNOWN_LINKS:
3373 if not platform_utils.islink(dotgit_path):
3374 unknown_paths.append(f"{dotgit_path}: should be a symlink")
3375 else:
3376 gitdir_path = os.path.join(gitdir, name)
3377 if name not in SAFE_TO_CLOBBER and os.path.exists(gitdir_path):
3378 unknown_paths.append(
3379 f"{dotgit_path}: unknown file; please file a bug"
3380 )
3381 if unknown_paths:
Jason Chang32b59562023-07-14 16:45:35 -07003382 raise GitError(
3383 "Aborting migration: " + "\n".join(unknown_paths),
3384 project=project,
3385 )
Gavin Makea2e3302023-03-11 06:46:20 +00003386
3387 # Now walk the paths and sync the .git/ to .repo/projects/.
3388 for name in platform_utils.listdir(dotgit):
3389 dotgit_path = os.path.join(dotgit, name)
3390
3391 # Ignore all temporary/backup names. These are common with vim &
3392 # emacs.
3393 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3394 platform_utils.remove(dotgit_path)
3395 elif name in KNOWN_LINKS:
3396 platform_utils.remove(dotgit_path)
3397 else:
3398 gitdir_path = os.path.join(gitdir, name)
3399 platform_utils.remove(gitdir_path, missing_ok=True)
3400 platform_utils.rename(dotgit_path, gitdir_path)
3401
3402 # Now that the dir should be empty, clear it out, and symlink it over.
3403 platform_utils.rmdir(dotgit)
3404 platform_utils.symlink(
3405 os.path.relpath(gitdir, os.path.dirname(dotgit)), dotgit
3406 )
3407
3408 def _get_symlink_error_message(self):
3409 if platform_utils.isWindows():
3410 return (
3411 "Unable to create symbolic link. Please re-run the command as "
3412 "Administrator, or see "
3413 "https://github.com/git-for-windows/git/wiki/Symbolic-Links "
3414 "for other options."
3415 )
3416 return "filesystem must support symlinks"
3417
3418 def _revlist(self, *args, **kw):
3419 a = []
3420 a.extend(args)
3421 a.append("--")
3422 return self.work_git.rev_list(*a, **kw)
3423
3424 @property
3425 def _allrefs(self):
3426 return self.bare_ref.all
3427
3428 def _getLogs(
3429 self, rev1, rev2, oneline=False, color=True, pretty_format=None
3430 ):
3431 """Get logs between two revisions of this project."""
3432 comp = ".."
3433 if rev1:
3434 revs = [rev1]
3435 if rev2:
3436 revs.extend([comp, rev2])
3437 cmd = ["log", "".join(revs)]
3438 out = DiffColoring(self.config)
3439 if out.is_on and color:
3440 cmd.append("--color")
3441 if pretty_format is not None:
3442 cmd.append("--pretty=format:%s" % pretty_format)
3443 if oneline:
3444 cmd.append("--oneline")
3445
3446 try:
3447 log = GitCommand(
3448 self, cmd, capture_stdout=True, capture_stderr=True
3449 )
3450 if log.Wait() == 0:
3451 return log.stdout
3452 except GitError:
3453 # worktree may not exist if groups changed for example. In that
3454 # case, try in gitdir instead.
3455 if not os.path.exists(self.worktree):
3456 return self.bare_git.log(*cmd[1:])
3457 else:
3458 raise
3459 return None
3460
3461 def getAddedAndRemovedLogs(
3462 self, toProject, oneline=False, color=True, pretty_format=None
3463 ):
3464 """Get the list of logs from this revision to given revisionId"""
3465 logs = {}
3466 selfId = self.GetRevisionId(self._allrefs)
3467 toId = toProject.GetRevisionId(toProject._allrefs)
3468
3469 logs["added"] = self._getLogs(
3470 selfId,
3471 toId,
3472 oneline=oneline,
3473 color=color,
3474 pretty_format=pretty_format,
3475 )
3476 logs["removed"] = self._getLogs(
3477 toId,
3478 selfId,
3479 oneline=oneline,
3480 color=color,
3481 pretty_format=pretty_format,
3482 )
3483 return logs
3484
3485 class _GitGetByExec(object):
3486 def __init__(self, project, bare, gitdir):
3487 self._project = project
3488 self._bare = bare
3489 self._gitdir = gitdir
3490
3491 # __getstate__ and __setstate__ are required for pickling because
3492 # __getattr__ exists.
3493 def __getstate__(self):
3494 return (self._project, self._bare, self._gitdir)
3495
3496 def __setstate__(self, state):
3497 self._project, self._bare, self._gitdir = state
3498
3499 def LsOthers(self):
3500 p = GitCommand(
3501 self._project,
3502 ["ls-files", "-z", "--others", "--exclude-standard"],
3503 bare=False,
3504 gitdir=self._gitdir,
3505 capture_stdout=True,
3506 capture_stderr=True,
3507 )
3508 if p.Wait() == 0:
3509 out = p.stdout
3510 if out:
3511 # Backslash is not anomalous.
3512 return out[:-1].split("\0")
3513 return []
3514
3515 def DiffZ(self, name, *args):
3516 cmd = [name]
3517 cmd.append("-z")
3518 cmd.append("--ignore-submodules")
3519 cmd.extend(args)
3520 p = GitCommand(
3521 self._project,
3522 cmd,
3523 gitdir=self._gitdir,
3524 bare=False,
3525 capture_stdout=True,
3526 capture_stderr=True,
3527 )
3528 p.Wait()
3529 r = {}
3530 out = p.stdout
3531 if out:
3532 out = iter(out[:-1].split("\0"))
3533 while out:
3534 try:
3535 info = next(out)
3536 path = next(out)
3537 except StopIteration:
3538 break
3539
3540 class _Info(object):
3541 def __init__(self, path, omode, nmode, oid, nid, state):
3542 self.path = path
3543 self.src_path = None
3544 self.old_mode = omode
3545 self.new_mode = nmode
3546 self.old_id = oid
3547 self.new_id = nid
3548
3549 if len(state) == 1:
3550 self.status = state
3551 self.level = None
3552 else:
3553 self.status = state[:1]
3554 self.level = state[1:]
3555 while self.level.startswith("0"):
3556 self.level = self.level[1:]
3557
3558 info = info[1:].split(" ")
3559 info = _Info(path, *info)
3560 if info.status in ("R", "C"):
3561 info.src_path = info.path
3562 info.path = next(out)
3563 r[info.path] = info
3564 return r
3565
3566 def GetDotgitPath(self, subpath=None):
3567 """Return the full path to the .git dir.
3568
3569 As a convenience, append |subpath| if provided.
3570 """
3571 if self._bare:
3572 dotgit = self._gitdir
3573 else:
3574 dotgit = os.path.join(self._project.worktree, ".git")
3575 if os.path.isfile(dotgit):
3576 # Git worktrees use a "gitdir:" syntax to point to the
3577 # scratch space.
3578 with open(dotgit) as fp:
3579 setting = fp.read()
3580 assert setting.startswith("gitdir:")
3581 gitdir = setting.split(":", 1)[1].strip()
3582 dotgit = os.path.normpath(
3583 os.path.join(self._project.worktree, gitdir)
3584 )
3585
3586 return dotgit if subpath is None else os.path.join(dotgit, subpath)
3587
3588 def GetHead(self):
3589 """Return the ref that HEAD points to."""
3590 path = self.GetDotgitPath(subpath=HEAD)
3591 try:
3592 with open(path) as fd:
3593 line = fd.readline()
3594 except IOError as e:
3595 raise NoManifestException(path, str(e))
3596 try:
3597 line = line.decode()
3598 except AttributeError:
3599 pass
3600 if line.startswith("ref: "):
3601 return line[5:-1]
3602 return line[:-1]
3603
3604 def SetHead(self, ref, message=None):
3605 cmdv = []
3606 if message is not None:
3607 cmdv.extend(["-m", message])
3608 cmdv.append(HEAD)
3609 cmdv.append(ref)
3610 self.symbolic_ref(*cmdv)
3611
3612 def DetachHead(self, new, message=None):
3613 cmdv = ["--no-deref"]
3614 if message is not None:
3615 cmdv.extend(["-m", message])
3616 cmdv.append(HEAD)
3617 cmdv.append(new)
3618 self.update_ref(*cmdv)
3619
3620 def UpdateRef(self, name, new, old=None, message=None, detach=False):
3621 cmdv = []
3622 if message is not None:
3623 cmdv.extend(["-m", message])
3624 if detach:
3625 cmdv.append("--no-deref")
3626 cmdv.append(name)
3627 cmdv.append(new)
3628 if old is not None:
3629 cmdv.append(old)
3630 self.update_ref(*cmdv)
3631
3632 def DeleteRef(self, name, old=None):
3633 if not old:
3634 old = self.rev_parse(name)
3635 self.update_ref("-d", name, old)
3636 self._project.bare_ref.deleted(name)
3637
3638 def rev_list(self, *args, **kw):
3639 if "format" in kw:
3640 cmdv = ["log", "--pretty=format:%s" % kw["format"]]
3641 else:
3642 cmdv = ["rev-list"]
3643 cmdv.extend(args)
3644 p = GitCommand(
3645 self._project,
3646 cmdv,
3647 bare=self._bare,
3648 gitdir=self._gitdir,
3649 capture_stdout=True,
3650 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003651 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00003652 )
Jason Chang32b59562023-07-14 16:45:35 -07003653 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003654 return p.stdout.splitlines()
3655
3656 def __getattr__(self, name):
3657 """Allow arbitrary git commands using pythonic syntax.
3658
3659 This allows you to do things like:
3660 git_obj.rev_parse('HEAD')
3661
3662 Since we don't have a 'rev_parse' method defined, the __getattr__
3663 will run. We'll replace the '_' with a '-' and try to run a git
3664 command. Any other positional arguments will be passed to the git
3665 command, and the following keyword arguments are supported:
3666 config: An optional dict of git config options to be passed with
3667 '-c'.
3668
3669 Args:
3670 name: The name of the git command to call. Any '_' characters
3671 will be replaced with '-'.
3672
3673 Returns:
3674 A callable object that will try to call git with the named
3675 command.
3676 """
3677 name = name.replace("_", "-")
3678
3679 def runner(*args, **kwargs):
3680 cmdv = []
3681 config = kwargs.pop("config", None)
3682 for k in kwargs:
3683 raise TypeError(
3684 "%s() got an unexpected keyword argument %r" % (name, k)
3685 )
3686 if config is not None:
3687 for k, v in config.items():
3688 cmdv.append("-c")
3689 cmdv.append("%s=%s" % (k, v))
3690 cmdv.append(name)
3691 cmdv.extend(args)
3692 p = GitCommand(
3693 self._project,
3694 cmdv,
3695 bare=self._bare,
3696 gitdir=self._gitdir,
3697 capture_stdout=True,
3698 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003699 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00003700 )
Jason Chang32b59562023-07-14 16:45:35 -07003701 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003702 r = p.stdout
3703 if r.endswith("\n") and r.index("\n") == len(r) - 1:
3704 return r[:-1]
3705 return r
3706
3707 return runner
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003708
3709
Jason Chang32b59562023-07-14 16:45:35 -07003710class LocalSyncFail(RepoError):
3711 """Default error when there is an Sync_LocalHalf error."""
3712
3713
3714class _PriorSyncFailedError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003715 def __str__(self):
3716 return "prior sync failed; rebase still in progress"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003717
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003718
Jason Chang32b59562023-07-14 16:45:35 -07003719class _DirtyError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003720 def __str__(self):
3721 return "contains uncommitted changes"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003722
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003723
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003724class _InfoMessage(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003725 def __init__(self, project, text):
3726 self.project = project
3727 self.text = text
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003728
Gavin Makea2e3302023-03-11 06:46:20 +00003729 def Print(self, syncbuf):
3730 syncbuf.out.info(
3731 "%s/: %s", self.project.RelPath(local=False), self.text
3732 )
3733 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003734
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003735
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003736class _Failure(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003737 def __init__(self, project, why):
3738 self.project = project
3739 self.why = why
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003740
Gavin Makea2e3302023-03-11 06:46:20 +00003741 def Print(self, syncbuf):
3742 syncbuf.out.fail(
3743 "error: %s/: %s", self.project.RelPath(local=False), str(self.why)
3744 )
3745 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003746
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003747
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003748class _Later(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003749 def __init__(self, project, action):
3750 self.project = project
3751 self.action = action
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003752
Gavin Makea2e3302023-03-11 06:46:20 +00003753 def Run(self, syncbuf):
3754 out = syncbuf.out
3755 out.project("project %s/", self.project.RelPath(local=False))
3756 out.nl()
3757 try:
3758 self.action()
3759 out.nl()
3760 return True
3761 except GitError:
3762 out.nl()
3763 return False
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003764
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003765
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003766class _SyncColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +00003767 def __init__(self, config):
3768 super().__init__(config, "reposync")
3769 self.project = self.printer("header", attr="bold")
3770 self.info = self.printer("info")
3771 self.fail = self.printer("fail", fg="red")
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003772
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003773
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003774class SyncBuffer(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003775 def __init__(self, config, detach_head=False):
3776 self._messages = []
3777 self._failures = []
3778 self._later_queue1 = []
3779 self._later_queue2 = []
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003780
Gavin Makea2e3302023-03-11 06:46:20 +00003781 self.out = _SyncColoring(config)
3782 self.out.redirect(sys.stderr)
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003783
Gavin Makea2e3302023-03-11 06:46:20 +00003784 self.detach_head = detach_head
3785 self.clean = True
3786 self.recent_clean = True
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003787
Gavin Makea2e3302023-03-11 06:46:20 +00003788 def info(self, project, fmt, *args):
3789 self._messages.append(_InfoMessage(project, fmt % args))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003790
Gavin Makea2e3302023-03-11 06:46:20 +00003791 def fail(self, project, err=None):
3792 self._failures.append(_Failure(project, err))
David Rileye0684ad2017-04-05 00:02:59 -07003793 self._MarkUnclean()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003794
Gavin Makea2e3302023-03-11 06:46:20 +00003795 def later1(self, project, what):
3796 self._later_queue1.append(_Later(project, what))
Mike Frysinger70d861f2019-08-26 15:22:36 -04003797
Gavin Makea2e3302023-03-11 06:46:20 +00003798 def later2(self, project, what):
3799 self._later_queue2.append(_Later(project, what))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003800
Gavin Makea2e3302023-03-11 06:46:20 +00003801 def Finish(self):
3802 self._PrintMessages()
3803 self._RunLater()
3804 self._PrintMessages()
3805 return self.clean
3806
3807 def Recently(self):
3808 recent_clean = self.recent_clean
3809 self.recent_clean = True
3810 return recent_clean
3811
3812 def _MarkUnclean(self):
3813 self.clean = False
3814 self.recent_clean = False
3815
3816 def _RunLater(self):
3817 for q in ["_later_queue1", "_later_queue2"]:
3818 if not self._RunQueue(q):
3819 return
3820
3821 def _RunQueue(self, queue):
3822 for m in getattr(self, queue):
3823 if not m.Run(self):
3824 self._MarkUnclean()
3825 return False
3826 setattr(self, queue, [])
3827 return True
3828
3829 def _PrintMessages(self):
3830 if self._messages or self._failures:
3831 if os.isatty(2):
3832 self.out.write(progress.CSI_ERASE_LINE)
3833 self.out.write("\r")
3834
3835 for m in self._messages:
3836 m.Print(self)
3837 for m in self._failures:
3838 m.Print(self)
3839
3840 self._messages = []
3841 self._failures = []
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003842
3843
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003844class MetaProject(Project):
Gavin Makea2e3302023-03-11 06:46:20 +00003845 """A special project housed under .repo."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003846
Gavin Makea2e3302023-03-11 06:46:20 +00003847 def __init__(self, manifest, name, gitdir, worktree):
3848 Project.__init__(
3849 self,
3850 manifest=manifest,
3851 name=name,
3852 gitdir=gitdir,
3853 objdir=gitdir,
3854 worktree=worktree,
3855 remote=RemoteSpec("origin"),
3856 relpath=".repo/%s" % name,
3857 revisionExpr="refs/heads/master",
3858 revisionId=None,
3859 groups=None,
3860 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003861
Gavin Makea2e3302023-03-11 06:46:20 +00003862 def PreSync(self):
3863 if self.Exists:
3864 cb = self.CurrentBranch
3865 if cb:
3866 base = self.GetBranch(cb).merge
3867 if base:
3868 self.revisionExpr = base
3869 self.revisionId = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003870
Gavin Makea2e3302023-03-11 06:46:20 +00003871 @property
3872 def HasChanges(self):
3873 """Has the remote received new commits not yet checked out?"""
3874 if not self.remote or not self.revisionExpr:
3875 return False
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003876
Gavin Makea2e3302023-03-11 06:46:20 +00003877 all_refs = self.bare_ref.all
3878 revid = self.GetRevisionId(all_refs)
3879 head = self.work_git.GetHead()
3880 if head.startswith(R_HEADS):
3881 try:
3882 head = all_refs[head]
3883 except KeyError:
3884 head = None
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003885
Gavin Makea2e3302023-03-11 06:46:20 +00003886 if revid == head:
3887 return False
3888 elif self._revlist(not_rev(HEAD), revid):
3889 return True
3890 return False
LaMont Jones9b72cf22022-03-29 21:54:22 +00003891
3892
3893class RepoProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003894 """The MetaProject for repo itself."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003895
Gavin Makea2e3302023-03-11 06:46:20 +00003896 @property
3897 def LastFetch(self):
3898 try:
3899 fh = os.path.join(self.gitdir, "FETCH_HEAD")
3900 return os.path.getmtime(fh)
3901 except OSError:
3902 return 0
LaMont Jones9b72cf22022-03-29 21:54:22 +00003903
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +01003904
LaMont Jones9b72cf22022-03-29 21:54:22 +00003905class ManifestProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003906 """The MetaProject for manifests."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003907
Gavin Makea2e3302023-03-11 06:46:20 +00003908 def MetaBranchSwitch(self, submodules=False):
3909 """Prepare for manifest branch switch."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003910
Gavin Makea2e3302023-03-11 06:46:20 +00003911 # detach and delete manifest branch, allowing a new
3912 # branch to take over
3913 syncbuf = SyncBuffer(self.config, detach_head=True)
3914 self.Sync_LocalHalf(syncbuf, submodules=submodules)
3915 syncbuf.Finish()
LaMont Jones9b72cf22022-03-29 21:54:22 +00003916
Gavin Makea2e3302023-03-11 06:46:20 +00003917 return (
3918 GitCommand(
3919 self,
3920 ["update-ref", "-d", "refs/heads/default"],
3921 capture_stdout=True,
3922 capture_stderr=True,
3923 ).Wait()
3924 == 0
3925 )
LaMont Jones9b03f152022-03-29 23:01:18 +00003926
Gavin Makea2e3302023-03-11 06:46:20 +00003927 @property
3928 def standalone_manifest_url(self):
3929 """The URL of the standalone manifest, or None."""
3930 return self.config.GetString("manifest.standalone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003931
Gavin Makea2e3302023-03-11 06:46:20 +00003932 @property
3933 def manifest_groups(self):
3934 """The manifest groups string."""
3935 return self.config.GetString("manifest.groups")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003936
Gavin Makea2e3302023-03-11 06:46:20 +00003937 @property
3938 def reference(self):
3939 """The --reference for this manifest."""
3940 return self.config.GetString("repo.reference")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003941
Gavin Makea2e3302023-03-11 06:46:20 +00003942 @property
3943 def dissociate(self):
3944 """Whether to dissociate."""
3945 return self.config.GetBoolean("repo.dissociate")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003946
Gavin Makea2e3302023-03-11 06:46:20 +00003947 @property
3948 def archive(self):
3949 """Whether we use archive."""
3950 return self.config.GetBoolean("repo.archive")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003951
Gavin Makea2e3302023-03-11 06:46:20 +00003952 @property
3953 def mirror(self):
3954 """Whether we use mirror."""
3955 return self.config.GetBoolean("repo.mirror")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003956
Gavin Makea2e3302023-03-11 06:46:20 +00003957 @property
3958 def use_worktree(self):
3959 """Whether we use worktree."""
3960 return self.config.GetBoolean("repo.worktree")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003961
Gavin Makea2e3302023-03-11 06:46:20 +00003962 @property
3963 def clone_bundle(self):
3964 """Whether we use clone_bundle."""
3965 return self.config.GetBoolean("repo.clonebundle")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003966
Gavin Makea2e3302023-03-11 06:46:20 +00003967 @property
3968 def submodules(self):
3969 """Whether we use submodules."""
3970 return self.config.GetBoolean("repo.submodules")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003971
Gavin Makea2e3302023-03-11 06:46:20 +00003972 @property
3973 def git_lfs(self):
3974 """Whether we use git_lfs."""
3975 return self.config.GetBoolean("repo.git-lfs")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003976
Gavin Makea2e3302023-03-11 06:46:20 +00003977 @property
3978 def use_superproject(self):
3979 """Whether we use superproject."""
3980 return self.config.GetBoolean("repo.superproject")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003981
Gavin Makea2e3302023-03-11 06:46:20 +00003982 @property
3983 def partial_clone(self):
3984 """Whether this is a partial clone."""
3985 return self.config.GetBoolean("repo.partialclone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003986
Gavin Makea2e3302023-03-11 06:46:20 +00003987 @property
3988 def depth(self):
3989 """Partial clone depth."""
3990 return self.config.GetString("repo.depth")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003991
Gavin Makea2e3302023-03-11 06:46:20 +00003992 @property
3993 def clone_filter(self):
3994 """The clone filter."""
3995 return self.config.GetString("repo.clonefilter")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003996
Gavin Makea2e3302023-03-11 06:46:20 +00003997 @property
3998 def partial_clone_exclude(self):
3999 """Partial clone exclude string"""
4000 return self.config.GetString("repo.partialcloneexclude")
LaMont Jones4ada0432022-04-14 15:10:43 +00004001
Gavin Makea2e3302023-03-11 06:46:20 +00004002 @property
Jason Chang17833322023-05-23 13:06:55 -07004003 def clone_filter_for_depth(self):
4004 """Replace shallow clone with partial clone."""
4005 return self.config.GetString("repo.clonefilterfordepth")
4006
4007 @property
Gavin Makea2e3302023-03-11 06:46:20 +00004008 def manifest_platform(self):
4009 """The --platform argument from `repo init`."""
4010 return self.config.GetString("manifest.platform")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004011
Gavin Makea2e3302023-03-11 06:46:20 +00004012 @property
4013 def _platform_name(self):
4014 """Return the name of the platform."""
4015 return platform.system().lower()
LaMont Jones9b03f152022-03-29 23:01:18 +00004016
Gavin Makea2e3302023-03-11 06:46:20 +00004017 def SyncWithPossibleInit(
4018 self,
4019 submanifest,
4020 verbose=False,
4021 current_branch_only=False,
4022 tags="",
4023 git_event_log=None,
4024 ):
4025 """Sync a manifestProject, possibly for the first time.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004026
Gavin Makea2e3302023-03-11 06:46:20 +00004027 Call Sync() with arguments from the most recent `repo init`. If this is
4028 a new sub manifest, then inherit options from the parent's
4029 manifestProject.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004030
Gavin Makea2e3302023-03-11 06:46:20 +00004031 This is used by subcmds.Sync() to do an initial download of new sub
4032 manifests.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004033
Gavin Makea2e3302023-03-11 06:46:20 +00004034 Args:
4035 submanifest: an XmlSubmanifest, the submanifest to re-sync.
4036 verbose: a boolean, whether to show all output, rather than only
4037 errors.
4038 current_branch_only: a boolean, whether to only fetch the current
4039 manifest branch from the server.
4040 tags: a boolean, whether to fetch tags.
4041 git_event_log: an EventLog, for git tracing.
4042 """
4043 # TODO(lamontjones): when refactoring sync (and init?) consider how to
4044 # better get the init options that we should use for new submanifests
4045 # that are added when syncing an existing workspace.
4046 git_event_log = git_event_log or EventLog()
LaMont Jonesb90a4222022-04-14 15:00:09 +00004047 spec = submanifest.ToSubmanifestSpec()
Gavin Makea2e3302023-03-11 06:46:20 +00004048 # Use the init options from the existing manifestProject, or the parent
4049 # if it doesn't exist.
4050 #
4051 # Today, we only support changing manifest_groups on the sub-manifest,
4052 # with no supported-for-the-user way to change the other arguments from
4053 # those specified by the outermost manifest.
4054 #
4055 # TODO(lamontjones): determine which of these should come from the
4056 # outermost manifest and which should come from the parent manifest.
4057 mp = self if self.Exists else submanifest.parent.manifestProject
4058 return self.Sync(
LaMont Jones55ee3042022-04-06 17:10:21 +00004059 manifest_url=spec.manifestUrl,
4060 manifest_branch=spec.revision,
Gavin Makea2e3302023-03-11 06:46:20 +00004061 standalone_manifest=mp.standalone_manifest_url,
4062 groups=mp.manifest_groups,
4063 platform=mp.manifest_platform,
4064 mirror=mp.mirror,
4065 dissociate=mp.dissociate,
4066 reference=mp.reference,
4067 worktree=mp.use_worktree,
4068 submodules=mp.submodules,
4069 archive=mp.archive,
4070 partial_clone=mp.partial_clone,
4071 clone_filter=mp.clone_filter,
4072 partial_clone_exclude=mp.partial_clone_exclude,
4073 clone_bundle=mp.clone_bundle,
4074 git_lfs=mp.git_lfs,
4075 use_superproject=mp.use_superproject,
LaMont Jones55ee3042022-04-06 17:10:21 +00004076 verbose=verbose,
4077 current_branch_only=current_branch_only,
4078 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00004079 depth=mp.depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004080 git_event_log=git_event_log,
4081 manifest_name=spec.manifestName,
Gavin Makea2e3302023-03-11 06:46:20 +00004082 this_manifest_only=True,
LaMont Jones55ee3042022-04-06 17:10:21 +00004083 outer_manifest=False,
Jason Chang17833322023-05-23 13:06:55 -07004084 clone_filter_for_depth=mp.clone_filter_for_depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004085 )
LaMont Jones409407a2022-04-05 21:21:56 +00004086
Gavin Makea2e3302023-03-11 06:46:20 +00004087 def Sync(
4088 self,
4089 _kwargs_only=(),
4090 manifest_url="",
4091 manifest_branch=None,
4092 standalone_manifest=False,
4093 groups="",
4094 mirror=False,
4095 reference="",
4096 dissociate=False,
4097 worktree=False,
4098 submodules=False,
4099 archive=False,
4100 partial_clone=None,
4101 depth=None,
4102 clone_filter="blob:none",
4103 partial_clone_exclude=None,
4104 clone_bundle=None,
4105 git_lfs=None,
4106 use_superproject=None,
4107 verbose=False,
4108 current_branch_only=False,
4109 git_event_log=None,
4110 platform="",
4111 manifest_name="default.xml",
4112 tags="",
4113 this_manifest_only=False,
4114 outer_manifest=True,
Jason Chang17833322023-05-23 13:06:55 -07004115 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00004116 ):
4117 """Sync the manifest and all submanifests.
LaMont Jones409407a2022-04-05 21:21:56 +00004118
Gavin Makea2e3302023-03-11 06:46:20 +00004119 Args:
4120 manifest_url: a string, the URL of the manifest project.
4121 manifest_branch: a string, the manifest branch to use.
4122 standalone_manifest: a boolean, whether to store the manifest as a
4123 static file.
4124 groups: a string, restricts the checkout to projects with the
4125 specified groups.
4126 mirror: a boolean, whether to create a mirror of the remote
4127 repository.
4128 reference: a string, location of a repo instance to use as a
4129 reference.
4130 dissociate: a boolean, whether to dissociate from reference mirrors
4131 after clone.
4132 worktree: a boolean, whether to use git-worktree to manage projects.
4133 submodules: a boolean, whether sync submodules associated with the
4134 manifest project.
4135 archive: a boolean, whether to checkout each project as an archive.
4136 See git-archive.
4137 partial_clone: a boolean, whether to perform a partial clone.
4138 depth: an int, how deep of a shallow clone to create.
4139 clone_filter: a string, filter to use with partial_clone.
4140 partial_clone_exclude : a string, comma-delimeted list of project
4141 names to exclude from partial clone.
4142 clone_bundle: a boolean, whether to enable /clone.bundle on
4143 HTTP/HTTPS.
4144 git_lfs: a boolean, whether to enable git LFS support.
4145 use_superproject: a boolean, whether to use the manifest
4146 superproject to sync projects.
4147 verbose: a boolean, whether to show all output, rather than only
4148 errors.
4149 current_branch_only: a boolean, whether to only fetch the current
4150 manifest branch from the server.
4151 platform: a string, restrict the checkout to projects with the
4152 specified platform group.
4153 git_event_log: an EventLog, for git tracing.
4154 tags: a boolean, whether to fetch tags.
4155 manifest_name: a string, the name of the manifest file to use.
4156 this_manifest_only: a boolean, whether to only operate on the
4157 current sub manifest.
4158 outer_manifest: a boolean, whether to start at the outermost
4159 manifest.
Jason Chang17833322023-05-23 13:06:55 -07004160 clone_filter_for_depth: a string, when specified replaces shallow
4161 clones with partial.
LaMont Jones9b03f152022-03-29 23:01:18 +00004162
Gavin Makea2e3302023-03-11 06:46:20 +00004163 Returns:
4164 a boolean, whether the sync was successful.
4165 """
4166 assert _kwargs_only == (), "Sync only accepts keyword arguments."
LaMont Jones9b03f152022-03-29 23:01:18 +00004167
Gavin Makea2e3302023-03-11 06:46:20 +00004168 groups = groups or self.manifest.GetDefaultGroupsStr(
4169 with_platform=False
4170 )
4171 platform = platform or "auto"
4172 git_event_log = git_event_log or EventLog()
4173 if outer_manifest and self.manifest.is_submanifest:
4174 # In a multi-manifest checkout, use the outer manifest unless we are
4175 # told not to.
4176 return self.client.outer_manifest.manifestProject.Sync(
4177 manifest_url=manifest_url,
4178 manifest_branch=manifest_branch,
4179 standalone_manifest=standalone_manifest,
4180 groups=groups,
4181 platform=platform,
4182 mirror=mirror,
4183 dissociate=dissociate,
4184 reference=reference,
4185 worktree=worktree,
4186 submodules=submodules,
4187 archive=archive,
4188 partial_clone=partial_clone,
4189 clone_filter=clone_filter,
4190 partial_clone_exclude=partial_clone_exclude,
4191 clone_bundle=clone_bundle,
4192 git_lfs=git_lfs,
4193 use_superproject=use_superproject,
4194 verbose=verbose,
4195 current_branch_only=current_branch_only,
4196 tags=tags,
4197 depth=depth,
4198 git_event_log=git_event_log,
4199 manifest_name=manifest_name,
4200 this_manifest_only=this_manifest_only,
4201 outer_manifest=False,
4202 )
LaMont Jones9b03f152022-03-29 23:01:18 +00004203
Gavin Makea2e3302023-03-11 06:46:20 +00004204 # If repo has already been initialized, we take -u with the absence of
4205 # --standalone-manifest to mean "transition to a standard repo set up",
4206 # which necessitates starting fresh.
4207 # If --standalone-manifest is set, we always tear everything down and
4208 # start anew.
4209 if self.Exists:
4210 was_standalone_manifest = self.config.GetString(
4211 "manifest.standalone"
4212 )
4213 if was_standalone_manifest and not manifest_url:
4214 print(
4215 "fatal: repo was initialized with a standlone manifest, "
4216 "cannot be re-initialized without --manifest-url/-u"
4217 )
4218 return False
4219
4220 if standalone_manifest or (
4221 was_standalone_manifest and manifest_url
4222 ):
4223 self.config.ClearCache()
4224 if self.gitdir and os.path.exists(self.gitdir):
4225 platform_utils.rmtree(self.gitdir)
4226 if self.worktree and os.path.exists(self.worktree):
4227 platform_utils.rmtree(self.worktree)
4228
4229 is_new = not self.Exists
4230 if is_new:
4231 if not manifest_url:
4232 print("fatal: manifest url is required.", file=sys.stderr)
4233 return False
4234
4235 if verbose:
4236 print(
4237 "Downloading manifest from %s"
4238 % (GitConfig.ForUser().UrlInsteadOf(manifest_url),),
4239 file=sys.stderr,
4240 )
4241
4242 # The manifest project object doesn't keep track of the path on the
4243 # server where this git is located, so let's save that here.
4244 mirrored_manifest_git = None
4245 if reference:
4246 manifest_git_path = urllib.parse.urlparse(manifest_url).path[1:]
4247 mirrored_manifest_git = os.path.join(
4248 reference, manifest_git_path
4249 )
4250 if not mirrored_manifest_git.endswith(".git"):
4251 mirrored_manifest_git += ".git"
4252 if not os.path.exists(mirrored_manifest_git):
4253 mirrored_manifest_git = os.path.join(
4254 reference, ".repo/manifests.git"
4255 )
4256
4257 self._InitGitDir(mirror_git=mirrored_manifest_git)
4258
4259 # If standalone_manifest is set, mark the project as "standalone" --
4260 # we'll still do much of the manifests.git set up, but will avoid actual
4261 # syncs to a remote.
4262 if standalone_manifest:
4263 self.config.SetString("manifest.standalone", manifest_url)
4264 elif not manifest_url and not manifest_branch:
4265 # If -u is set and --standalone-manifest is not, then we're not in
4266 # standalone mode. Otherwise, use config to infer what we were in
4267 # the last init.
4268 standalone_manifest = bool(
4269 self.config.GetString("manifest.standalone")
4270 )
4271 if not standalone_manifest:
4272 self.config.SetString("manifest.standalone", None)
4273
4274 self._ConfigureDepth(depth)
4275
4276 # Set the remote URL before the remote branch as we might need it below.
4277 if manifest_url:
4278 r = self.GetRemote()
4279 r.url = manifest_url
4280 r.ResetFetch()
4281 r.Save()
4282
4283 if not standalone_manifest:
4284 if manifest_branch:
4285 if manifest_branch == "HEAD":
4286 manifest_branch = self.ResolveRemoteHead()
4287 if manifest_branch is None:
4288 print("fatal: unable to resolve HEAD", file=sys.stderr)
4289 return False
4290 self.revisionExpr = manifest_branch
4291 else:
4292 if is_new:
4293 default_branch = self.ResolveRemoteHead()
4294 if default_branch is None:
4295 # If the remote doesn't have HEAD configured, default to
4296 # master.
4297 default_branch = "refs/heads/master"
4298 self.revisionExpr = default_branch
4299 else:
4300 self.PreSync()
4301
4302 groups = re.split(r"[,\s]+", groups or "")
4303 all_platforms = ["linux", "darwin", "windows"]
4304 platformize = lambda x: "platform-" + x
4305 if platform == "auto":
4306 if not mirror and not self.mirror:
4307 groups.append(platformize(self._platform_name))
4308 elif platform == "all":
4309 groups.extend(map(platformize, all_platforms))
4310 elif platform in all_platforms:
4311 groups.append(platformize(platform))
4312 elif platform != "none":
4313 print("fatal: invalid platform flag", file=sys.stderr)
4314 return False
4315 self.config.SetString("manifest.platform", platform)
4316
4317 groups = [x for x in groups if x]
4318 groupstr = ",".join(groups)
4319 if (
4320 platform == "auto"
4321 and groupstr == self.manifest.GetDefaultGroupsStr()
4322 ):
4323 groupstr = None
4324 self.config.SetString("manifest.groups", groupstr)
4325
4326 if reference:
4327 self.config.SetString("repo.reference", reference)
4328
4329 if dissociate:
4330 self.config.SetBoolean("repo.dissociate", dissociate)
4331
4332 if worktree:
4333 if mirror:
4334 print(
4335 "fatal: --mirror and --worktree are incompatible",
4336 file=sys.stderr,
4337 )
4338 return False
4339 if submodules:
4340 print(
4341 "fatal: --submodules and --worktree are incompatible",
4342 file=sys.stderr,
4343 )
4344 return False
4345 self.config.SetBoolean("repo.worktree", worktree)
4346 if is_new:
4347 self.use_git_worktrees = True
4348 print("warning: --worktree is experimental!", file=sys.stderr)
4349
4350 if archive:
4351 if is_new:
4352 self.config.SetBoolean("repo.archive", archive)
4353 else:
4354 print(
4355 "fatal: --archive is only supported when initializing a "
4356 "new workspace.",
4357 file=sys.stderr,
4358 )
4359 print(
4360 "Either delete the .repo folder in this workspace, or "
4361 "initialize in another location.",
4362 file=sys.stderr,
4363 )
4364 return False
4365
4366 if mirror:
4367 if is_new:
4368 self.config.SetBoolean("repo.mirror", mirror)
4369 else:
4370 print(
4371 "fatal: --mirror is only supported when initializing a new "
4372 "workspace.",
4373 file=sys.stderr,
4374 )
4375 print(
4376 "Either delete the .repo folder in this workspace, or "
4377 "initialize in another location.",
4378 file=sys.stderr,
4379 )
4380 return False
4381
4382 if partial_clone is not None:
4383 if mirror:
4384 print(
4385 "fatal: --mirror and --partial-clone are mutually "
4386 "exclusive",
4387 file=sys.stderr,
4388 )
4389 return False
4390 self.config.SetBoolean("repo.partialclone", partial_clone)
4391 if clone_filter:
4392 self.config.SetString("repo.clonefilter", clone_filter)
4393 elif self.partial_clone:
4394 clone_filter = self.clone_filter
4395 else:
4396 clone_filter = None
4397
4398 if partial_clone_exclude is not None:
4399 self.config.SetString(
4400 "repo.partialcloneexclude", partial_clone_exclude
4401 )
4402
4403 if clone_bundle is None:
4404 clone_bundle = False if partial_clone else True
4405 else:
4406 self.config.SetBoolean("repo.clonebundle", clone_bundle)
4407
4408 if submodules:
4409 self.config.SetBoolean("repo.submodules", submodules)
4410
4411 if git_lfs is not None:
4412 if git_lfs:
4413 git_require((2, 17, 0), fail=True, msg="Git LFS support")
4414
4415 self.config.SetBoolean("repo.git-lfs", git_lfs)
4416 if not is_new:
4417 print(
4418 "warning: Changing --git-lfs settings will only affect new "
4419 "project checkouts.\n"
4420 " Existing projects will require manual updates.\n",
4421 file=sys.stderr,
4422 )
4423
Jason Chang17833322023-05-23 13:06:55 -07004424 if clone_filter_for_depth is not None:
4425 self.ConfigureCloneFilterForDepth(clone_filter_for_depth)
4426
Gavin Makea2e3302023-03-11 06:46:20 +00004427 if use_superproject is not None:
4428 self.config.SetBoolean("repo.superproject", use_superproject)
4429
4430 if not standalone_manifest:
4431 success = self.Sync_NetworkHalf(
4432 is_new=is_new,
4433 quiet=not verbose,
4434 verbose=verbose,
4435 clone_bundle=clone_bundle,
4436 current_branch_only=current_branch_only,
4437 tags=tags,
4438 submodules=submodules,
4439 clone_filter=clone_filter,
4440 partial_clone_exclude=self.manifest.PartialCloneExclude,
Jason Chang17833322023-05-23 13:06:55 -07004441 clone_filter_for_depth=self.manifest.CloneFilterForDepth,
Gavin Makea2e3302023-03-11 06:46:20 +00004442 ).success
4443 if not success:
4444 r = self.GetRemote()
4445 print(
4446 "fatal: cannot obtain manifest %s" % r.url, file=sys.stderr
4447 )
4448
4449 # Better delete the manifest git dir if we created it; otherwise
4450 # next time (when user fixes problems) we won't go through the
4451 # "is_new" logic.
4452 if is_new:
4453 platform_utils.rmtree(self.gitdir)
4454 return False
4455
4456 if manifest_branch:
4457 self.MetaBranchSwitch(submodules=submodules)
4458
4459 syncbuf = SyncBuffer(self.config)
4460 self.Sync_LocalHalf(syncbuf, submodules=submodules)
4461 syncbuf.Finish()
4462
4463 if is_new or self.CurrentBranch is None:
Jason Chang1a3612f2023-08-08 14:12:53 -07004464 try:
4465 self.StartBranch("default")
4466 except GitError as e:
4467 msg = str(e)
Gavin Makea2e3302023-03-11 06:46:20 +00004468 print(
Jason Chang1a3612f2023-08-08 14:12:53 -07004469 f"fatal: cannot create default in manifest {msg}",
Gavin Makea2e3302023-03-11 06:46:20 +00004470 file=sys.stderr,
4471 )
4472 return False
4473
4474 if not manifest_name:
4475 print("fatal: manifest name (-m) is required.", file=sys.stderr)
4476 return False
4477
4478 elif is_new:
4479 # This is a new standalone manifest.
4480 manifest_name = "default.xml"
4481 manifest_data = fetch.fetch_file(manifest_url, verbose=verbose)
4482 dest = os.path.join(self.worktree, manifest_name)
4483 os.makedirs(os.path.dirname(dest), exist_ok=True)
4484 with open(dest, "wb") as f:
4485 f.write(manifest_data)
4486
4487 try:
4488 self.manifest.Link(manifest_name)
4489 except ManifestParseError as e:
4490 print(
4491 "fatal: manifest '%s' not available" % manifest_name,
4492 file=sys.stderr,
4493 )
4494 print("fatal: %s" % str(e), file=sys.stderr)
4495 return False
4496
4497 if not this_manifest_only:
4498 for submanifest in self.manifest.submanifests.values():
4499 spec = submanifest.ToSubmanifestSpec()
4500 submanifest.repo_client.manifestProject.Sync(
4501 manifest_url=spec.manifestUrl,
4502 manifest_branch=spec.revision,
4503 standalone_manifest=standalone_manifest,
4504 groups=self.manifest_groups,
4505 platform=platform,
4506 mirror=mirror,
4507 dissociate=dissociate,
4508 reference=reference,
4509 worktree=worktree,
4510 submodules=submodules,
4511 archive=archive,
4512 partial_clone=partial_clone,
4513 clone_filter=clone_filter,
4514 partial_clone_exclude=partial_clone_exclude,
4515 clone_bundle=clone_bundle,
4516 git_lfs=git_lfs,
4517 use_superproject=use_superproject,
4518 verbose=verbose,
4519 current_branch_only=current_branch_only,
4520 tags=tags,
4521 depth=depth,
4522 git_event_log=git_event_log,
4523 manifest_name=spec.manifestName,
4524 this_manifest_only=False,
4525 outer_manifest=False,
4526 )
4527
4528 # Lastly, if the manifest has a <superproject> then have the
4529 # superproject sync it (if it will be used).
4530 if git_superproject.UseSuperproject(use_superproject, self.manifest):
4531 sync_result = self.manifest.superproject.Sync(git_event_log)
4532 if not sync_result.success:
4533 submanifest = ""
4534 if self.manifest.path_prefix:
4535 submanifest = f"for {self.manifest.path_prefix} "
4536 print(
4537 f"warning: git update of superproject {submanifest}failed, "
4538 "repo sync will not use superproject to fetch source; "
4539 "while this error is not fatal, and you can continue to "
4540 "run repo sync, please run repo init with the "
4541 "--no-use-superproject option to stop seeing this warning",
4542 file=sys.stderr,
4543 )
4544 if sync_result.fatal and use_superproject is not None:
4545 return False
4546
4547 return True
4548
Jason Chang17833322023-05-23 13:06:55 -07004549 def ConfigureCloneFilterForDepth(self, clone_filter_for_depth):
4550 """Configure clone filter to replace shallow clones.
4551
4552 Args:
4553 clone_filter_for_depth: a string or None, e.g. 'blob:none' will
4554 disable shallow clones and replace with partial clone. None will
4555 enable shallow clones.
4556 """
4557 self.config.SetString(
4558 "repo.clonefilterfordepth", clone_filter_for_depth
4559 )
4560
Gavin Makea2e3302023-03-11 06:46:20 +00004561 def _ConfigureDepth(self, depth):
4562 """Configure the depth we'll sync down.
4563
4564 Args:
4565 depth: an int, how deep of a partial clone to create.
4566 """
4567 # Opt.depth will be non-None if user actually passed --depth to repo
4568 # init.
4569 if depth is not None:
4570 if depth > 0:
4571 # Positive values will set the depth.
4572 depth = str(depth)
4573 else:
4574 # Negative numbers will clear the depth; passing None to
4575 # SetString will do that.
4576 depth = None
4577
4578 # We store the depth in the main manifest project.
4579 self.config.SetString("repo.depth", depth)