blob: bf4f9ae15e59ba680fe5e4750b3f4c4df48aa735 [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 Chang5a3a5f72023-08-17 11:36:41 -07001185 GitCommand(
1186 self, cmd, bare=True, capture_stderr=True, verify_command=True
1187 ).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001188
1189 if not dryrun:
1190 msg = "posted to %s for %s" % (branch.remote.review, dest_branch)
1191 self.bare_git.UpdateRef(
1192 R_PUB + branch.name, R_HEADS + branch.name, message=msg
1193 )
1194
1195 def _ExtractArchive(self, tarpath, path=None):
1196 """Extract the given tar on its current location
1197
1198 Args:
1199 tarpath: The path to the actual tar file
1200
1201 """
1202 try:
1203 with tarfile.open(tarpath, "r") as tar:
1204 tar.extractall(path=path)
1205 return True
1206 except (IOError, tarfile.TarError) as e:
1207 _error("Cannot extract archive %s: %s", tarpath, str(e))
1208 return False
1209
1210 def Sync_NetworkHalf(
1211 self,
1212 quiet=False,
1213 verbose=False,
1214 output_redir=None,
1215 is_new=None,
1216 current_branch_only=None,
1217 force_sync=False,
1218 clone_bundle=True,
1219 tags=None,
1220 archive=False,
1221 optimized_fetch=False,
1222 retry_fetches=0,
1223 prune=False,
1224 submodules=False,
1225 ssh_proxy=None,
1226 clone_filter=None,
1227 partial_clone_exclude=set(),
Jason Chang17833322023-05-23 13:06:55 -07001228 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001229 ):
1230 """Perform only the network IO portion of the sync process.
1231 Local working directory/branch state is not affected.
1232 """
1233 if archive and not isinstance(self, MetaProject):
1234 if self.remote.url.startswith(("http://", "https://")):
Jason Chang32b59562023-07-14 16:45:35 -07001235 msg_template = (
1236 "%s: Cannot fetch archives from http/https remotes."
Gavin Makea2e3302023-03-11 06:46:20 +00001237 )
Jason Chang32b59562023-07-14 16:45:35 -07001238 msg_args = self.name
1239 msg = msg_template % msg_args
1240 _error(
1241 msg_template,
1242 msg_args,
1243 )
1244 return SyncNetworkHalfResult(
1245 False, SyncNetworkHalfError(msg, project=self.name)
1246 )
Gavin Makea2e3302023-03-11 06:46:20 +00001247
1248 name = self.relpath.replace("\\", "/")
1249 name = name.replace("/", "_")
1250 tarpath = "%s.tar" % name
1251 topdir = self.manifest.topdir
1252
1253 try:
1254 self._FetchArchive(tarpath, cwd=topdir)
1255 except GitError as e:
1256 _error("%s", e)
Jason Chang32b59562023-07-14 16:45:35 -07001257 return SyncNetworkHalfResult(False, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001258
1259 # From now on, we only need absolute tarpath.
1260 tarpath = os.path.join(topdir, tarpath)
1261
1262 if not self._ExtractArchive(tarpath, path=topdir):
Jason Chang32b59562023-07-14 16:45:35 -07001263 return SyncNetworkHalfResult(
1264 True,
1265 SyncNetworkHalfError(
1266 f"Unable to Extract Archive {tarpath}",
1267 project=self.name,
1268 ),
1269 )
Gavin Makea2e3302023-03-11 06:46:20 +00001270 try:
1271 platform_utils.remove(tarpath)
1272 except OSError as e:
1273 _warn("Cannot remove archive %s: %s", tarpath, str(e))
1274 self._CopyAndLinkFiles()
Jason Chang32b59562023-07-14 16:45:35 -07001275 return SyncNetworkHalfResult(True)
Gavin Makea2e3302023-03-11 06:46:20 +00001276
1277 # If the shared object dir already exists, don't try to rebootstrap with
1278 # a clone bundle download. We should have the majority of objects
1279 # already.
1280 if clone_bundle and os.path.exists(self.objdir):
1281 clone_bundle = False
1282
1283 if self.name in partial_clone_exclude:
1284 clone_bundle = True
1285 clone_filter = None
1286
1287 if is_new is None:
1288 is_new = not self.Exists
1289 if is_new:
1290 self._InitGitDir(force_sync=force_sync, quiet=quiet)
1291 else:
1292 self._UpdateHooks(quiet=quiet)
1293 self._InitRemote()
1294
1295 if self.UseAlternates:
1296 # If gitdir/objects is a symlink, migrate it from the old layout.
1297 gitdir_objects = os.path.join(self.gitdir, "objects")
1298 if platform_utils.islink(gitdir_objects):
1299 platform_utils.remove(gitdir_objects, missing_ok=True)
1300 gitdir_alt = os.path.join(self.gitdir, "objects/info/alternates")
1301 if not os.path.exists(gitdir_alt):
1302 os.makedirs(os.path.dirname(gitdir_alt), exist_ok=True)
1303 _lwrite(
1304 gitdir_alt,
1305 os.path.join(
1306 os.path.relpath(self.objdir, gitdir_objects), "objects"
1307 )
1308 + "\n",
1309 )
1310
1311 if is_new:
1312 alt = os.path.join(self.objdir, "objects/info/alternates")
1313 try:
1314 with open(alt) as fd:
1315 # This works for both absolute and relative alternate
1316 # directories.
1317 alt_dir = os.path.join(
1318 self.objdir, "objects", fd.readline().rstrip()
1319 )
1320 except IOError:
1321 alt_dir = None
1322 else:
1323 alt_dir = None
1324
1325 if (
1326 clone_bundle
1327 and alt_dir is None
1328 and self._ApplyCloneBundle(
1329 initial=is_new, quiet=quiet, verbose=verbose
1330 )
1331 ):
1332 is_new = False
1333
1334 if current_branch_only is None:
1335 if self.sync_c:
1336 current_branch_only = True
1337 elif not self.manifest._loaded:
1338 # Manifest cannot check defaults until it syncs.
1339 current_branch_only = False
1340 elif self.manifest.default.sync_c:
1341 current_branch_only = True
1342
1343 if tags is None:
1344 tags = self.sync_tags
1345
1346 if self.clone_depth:
1347 depth = self.clone_depth
1348 else:
1349 depth = self.manifest.manifestProject.depth
1350
Jason Chang17833322023-05-23 13:06:55 -07001351 if depth and clone_filter_for_depth:
1352 depth = None
1353 clone_filter = clone_filter_for_depth
1354
Gavin Makea2e3302023-03-11 06:46:20 +00001355 # See if we can skip the network fetch entirely.
1356 remote_fetched = False
1357 if not (
1358 optimized_fetch
1359 and (
1360 ID_RE.match(self.revisionExpr)
1361 and self._CheckForImmutableRevision()
1362 )
1363 ):
1364 remote_fetched = True
Jason Chang32b59562023-07-14 16:45:35 -07001365 try:
1366 if not self._RemoteFetch(
1367 initial=is_new,
1368 quiet=quiet,
1369 verbose=verbose,
1370 output_redir=output_redir,
1371 alt_dir=alt_dir,
1372 current_branch_only=current_branch_only,
1373 tags=tags,
1374 prune=prune,
1375 depth=depth,
1376 submodules=submodules,
1377 force_sync=force_sync,
1378 ssh_proxy=ssh_proxy,
1379 clone_filter=clone_filter,
1380 retry_fetches=retry_fetches,
1381 ):
1382 return SyncNetworkHalfResult(
1383 remote_fetched,
1384 SyncNetworkHalfError(
1385 f"Unable to remote fetch project {self.name}",
1386 project=self.name,
1387 ),
1388 )
1389 except RepoError as e:
1390 return SyncNetworkHalfResult(
1391 remote_fetched,
1392 e,
1393 )
Gavin Makea2e3302023-03-11 06:46:20 +00001394
1395 mp = self.manifest.manifestProject
1396 dissociate = mp.dissociate
1397 if dissociate:
1398 alternates_file = os.path.join(
1399 self.objdir, "objects/info/alternates"
1400 )
1401 if os.path.exists(alternates_file):
1402 cmd = ["repack", "-a", "-d"]
1403 p = GitCommand(
1404 self,
1405 cmd,
1406 bare=True,
1407 capture_stdout=bool(output_redir),
1408 merge_output=bool(output_redir),
1409 )
1410 if p.stdout and output_redir:
1411 output_redir.write(p.stdout)
1412 if p.Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07001413 return SyncNetworkHalfResult(
1414 remote_fetched,
1415 GitError(
1416 "Unable to repack alternates", project=self.name
1417 ),
1418 )
Gavin Makea2e3302023-03-11 06:46:20 +00001419 platform_utils.remove(alternates_file)
1420
1421 if self.worktree:
1422 self._InitMRef()
1423 else:
1424 self._InitMirrorHead()
1425 platform_utils.remove(
1426 os.path.join(self.gitdir, "FETCH_HEAD"), missing_ok=True
1427 )
Jason Chang32b59562023-07-14 16:45:35 -07001428 return SyncNetworkHalfResult(remote_fetched)
Gavin Makea2e3302023-03-11 06:46:20 +00001429
1430 def PostRepoUpgrade(self):
1431 self._InitHooks()
1432
1433 def _CopyAndLinkFiles(self):
1434 if self.client.isGitcClient:
1435 return
1436 for copyfile in self.copyfiles:
1437 copyfile._Copy()
1438 for linkfile in self.linkfiles:
1439 linkfile._Link()
1440
1441 def GetCommitRevisionId(self):
1442 """Get revisionId of a commit.
1443
1444 Use this method instead of GetRevisionId to get the id of the commit
1445 rather than the id of the current git object (for example, a tag)
1446
1447 """
1448 if not self.revisionExpr.startswith(R_TAGS):
1449 return self.GetRevisionId(self._allrefs)
1450
1451 try:
1452 return self.bare_git.rev_list(self.revisionExpr, "-1")[0]
1453 except GitError:
1454 raise ManifestInvalidRevisionError(
1455 "revision %s in %s not found" % (self.revisionExpr, self.name)
1456 )
1457
1458 def GetRevisionId(self, all_refs=None):
1459 if self.revisionId:
1460 return self.revisionId
1461
1462 rem = self.GetRemote()
1463 rev = rem.ToLocal(self.revisionExpr)
1464
1465 if all_refs is not None and rev in all_refs:
1466 return all_refs[rev]
1467
1468 try:
1469 return self.bare_git.rev_parse("--verify", "%s^0" % rev)
1470 except GitError:
1471 raise ManifestInvalidRevisionError(
1472 "revision %s in %s not found" % (self.revisionExpr, self.name)
1473 )
1474
1475 def SetRevisionId(self, revisionId):
1476 if self.revisionExpr:
1477 self.upstream = self.revisionExpr
1478
1479 self.revisionId = revisionId
1480
Jason Chang32b59562023-07-14 16:45:35 -07001481 def Sync_LocalHalf(
1482 self, syncbuf, force_sync=False, submodules=False, errors=None
1483 ):
Gavin Makea2e3302023-03-11 06:46:20 +00001484 """Perform only the local IO portion of the sync process.
1485
1486 Network access is not required.
1487 """
Jason Chang32b59562023-07-14 16:45:35 -07001488 if errors is None:
1489 errors = []
1490
1491 def fail(error: Exception):
1492 errors.append(error)
1493 syncbuf.fail(self, error)
1494
Gavin Makea2e3302023-03-11 06:46:20 +00001495 if not os.path.exists(self.gitdir):
Jason Chang32b59562023-07-14 16:45:35 -07001496 fail(
1497 LocalSyncFail(
1498 "Cannot checkout %s due to missing network sync; Run "
1499 "`repo sync -n %s` first." % (self.name, self.name),
1500 project=self.name,
1501 )
Gavin Makea2e3302023-03-11 06:46:20 +00001502 )
1503 return
1504
1505 self._InitWorkTree(force_sync=force_sync, submodules=submodules)
1506 all_refs = self.bare_ref.all
1507 self.CleanPublishedCache(all_refs)
1508 revid = self.GetRevisionId(all_refs)
1509
1510 # Special case the root of the repo client checkout. Make sure it
1511 # doesn't contain files being checked out to dirs we don't allow.
1512 if self.relpath == ".":
1513 PROTECTED_PATHS = {".repo"}
1514 paths = set(
1515 self.work_git.ls_tree("-z", "--name-only", "--", revid).split(
1516 "\0"
1517 )
1518 )
1519 bad_paths = paths & PROTECTED_PATHS
1520 if bad_paths:
Jason Chang32b59562023-07-14 16:45:35 -07001521 fail(
1522 LocalSyncFail(
1523 "Refusing to checkout project that writes to protected "
1524 "paths: %s" % (", ".join(bad_paths),),
1525 project=self.name,
1526 )
Gavin Makea2e3302023-03-11 06:46:20 +00001527 )
1528 return
1529
1530 def _doff():
1531 self._FastForward(revid)
1532 self._CopyAndLinkFiles()
1533
1534 def _dosubmodules():
1535 self._SyncSubmodules(quiet=True)
1536
1537 head = self.work_git.GetHead()
1538 if head.startswith(R_HEADS):
1539 branch = head[len(R_HEADS) :]
1540 try:
1541 head = all_refs[head]
1542 except KeyError:
1543 head = None
1544 else:
1545 branch = None
1546
1547 if branch is None or syncbuf.detach_head:
1548 # Currently on a detached HEAD. The user is assumed to
1549 # not have any local modifications worth worrying about.
1550 if self.IsRebaseInProgress():
Jason Chang32b59562023-07-14 16:45:35 -07001551 fail(_PriorSyncFailedError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001552 return
1553
1554 if head == revid:
1555 # No changes; don't do anything further.
1556 # Except if the head needs to be detached.
1557 if not syncbuf.detach_head:
1558 # The copy/linkfile config may have changed.
1559 self._CopyAndLinkFiles()
1560 return
1561 else:
1562 lost = self._revlist(not_rev(revid), HEAD)
1563 if lost:
1564 syncbuf.info(self, "discarding %d commits", len(lost))
1565
1566 try:
1567 self._Checkout(revid, quiet=True)
1568 if submodules:
1569 self._SyncSubmodules(quiet=True)
1570 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001571 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001572 return
1573 self._CopyAndLinkFiles()
1574 return
1575
1576 if head == revid:
1577 # No changes; don't do anything further.
1578 #
1579 # The copy/linkfile config may have changed.
1580 self._CopyAndLinkFiles()
1581 return
1582
1583 branch = self.GetBranch(branch)
1584
1585 if not branch.LocalMerge:
1586 # The current branch has no tracking configuration.
1587 # Jump off it to a detached HEAD.
1588 syncbuf.info(
1589 self, "leaving %s; does not track upstream", branch.name
1590 )
1591 try:
1592 self._Checkout(revid, quiet=True)
1593 if submodules:
1594 self._SyncSubmodules(quiet=True)
1595 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001596 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001597 return
1598 self._CopyAndLinkFiles()
1599 return
1600
1601 upstream_gain = self._revlist(not_rev(HEAD), revid)
1602
1603 # See if we can perform a fast forward merge. This can happen if our
1604 # branch isn't in the exact same state as we last published.
1605 try:
1606 self.work_git.merge_base("--is-ancestor", HEAD, revid)
1607 # Skip the published logic.
1608 pub = False
1609 except GitError:
1610 pub = self.WasPublished(branch.name, all_refs)
1611
1612 if pub:
1613 not_merged = self._revlist(not_rev(revid), pub)
1614 if not_merged:
1615 if upstream_gain:
1616 # The user has published this branch and some of those
1617 # commits are not yet merged upstream. We do not want
1618 # to rewrite the published commits so we punt.
Jason Chang32b59562023-07-14 16:45:35 -07001619 fail(
1620 LocalSyncFail(
1621 "branch %s is published (but not merged) and is "
1622 "now %d commits behind"
1623 % (branch.name, len(upstream_gain)),
1624 project=self.name,
1625 )
Gavin Makea2e3302023-03-11 06:46:20 +00001626 )
1627 return
1628 elif pub == head:
1629 # All published commits are merged, and thus we are a
1630 # strict subset. We can fast-forward safely.
1631 syncbuf.later1(self, _doff)
1632 if submodules:
1633 syncbuf.later1(self, _dosubmodules)
1634 return
1635
1636 # Examine the local commits not in the remote. Find the
1637 # last one attributed to this user, if any.
1638 local_changes = self._revlist(not_rev(revid), HEAD, format="%H %ce")
1639 last_mine = None
1640 cnt_mine = 0
1641 for commit in local_changes:
1642 commit_id, committer_email = commit.split(" ", 1)
1643 if committer_email == self.UserEmail:
1644 last_mine = commit_id
1645 cnt_mine += 1
1646
1647 if not upstream_gain and cnt_mine == len(local_changes):
1648 # The copy/linkfile config may have changed.
1649 self._CopyAndLinkFiles()
1650 return
1651
1652 if self.IsDirty(consider_untracked=False):
Jason Chang32b59562023-07-14 16:45:35 -07001653 fail(_DirtyError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001654 return
1655
1656 # If the upstream switched on us, warn the user.
1657 if branch.merge != self.revisionExpr:
1658 if branch.merge and self.revisionExpr:
1659 syncbuf.info(
1660 self,
1661 "manifest switched %s...%s",
1662 branch.merge,
1663 self.revisionExpr,
1664 )
1665 elif branch.merge:
1666 syncbuf.info(self, "manifest no longer tracks %s", branch.merge)
1667
1668 if cnt_mine < len(local_changes):
1669 # Upstream rebased. Not everything in HEAD was created by this user.
1670 syncbuf.info(
1671 self,
1672 "discarding %d commits removed from upstream",
1673 len(local_changes) - cnt_mine,
1674 )
1675
1676 branch.remote = self.GetRemote()
1677 if not ID_RE.match(self.revisionExpr):
1678 # In case of manifest sync the revisionExpr might be a SHA1.
1679 branch.merge = self.revisionExpr
1680 if not branch.merge.startswith("refs/"):
1681 branch.merge = R_HEADS + branch.merge
1682 branch.Save()
1683
1684 if cnt_mine > 0 and self.rebase:
1685
1686 def _docopyandlink():
1687 self._CopyAndLinkFiles()
1688
1689 def _dorebase():
1690 self._Rebase(upstream="%s^1" % last_mine, onto=revid)
1691
1692 syncbuf.later2(self, _dorebase)
1693 if submodules:
1694 syncbuf.later2(self, _dosubmodules)
1695 syncbuf.later2(self, _docopyandlink)
1696 elif local_changes:
1697 try:
1698 self._ResetHard(revid)
1699 if submodules:
1700 self._SyncSubmodules(quiet=True)
1701 self._CopyAndLinkFiles()
1702 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001703 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001704 return
1705 else:
1706 syncbuf.later1(self, _doff)
1707 if submodules:
1708 syncbuf.later1(self, _dosubmodules)
1709
1710 def AddCopyFile(self, src, dest, topdir):
1711 """Mark |src| for copying to |dest| (relative to |topdir|).
1712
1713 No filesystem changes occur here. Actual copying happens later on.
1714
1715 Paths should have basic validation run on them before being queued.
1716 Further checking will be handled when the actual copy happens.
1717 """
1718 self.copyfiles.append(_CopyFile(self.worktree, src, topdir, dest))
1719
1720 def AddLinkFile(self, src, dest, topdir):
1721 """Mark |dest| to create a symlink (relative to |topdir|) pointing to
1722 |src|.
1723
1724 No filesystem changes occur here. Actual linking happens later on.
1725
1726 Paths should have basic validation run on them before being queued.
1727 Further checking will be handled when the actual link happens.
1728 """
1729 self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest))
1730
1731 def AddAnnotation(self, name, value, keep):
1732 self.annotations.append(Annotation(name, value, keep))
1733
1734 def DownloadPatchSet(self, change_id, patch_id):
1735 """Download a single patch set of a single change to FETCH_HEAD."""
1736 remote = self.GetRemote()
1737
1738 cmd = ["fetch", remote.name]
1739 cmd.append(
1740 "refs/changes/%2.2d/%d/%d" % (change_id % 100, change_id, patch_id)
1741 )
Jason Chang1a3612f2023-08-08 14:12:53 -07001742 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001743 return DownloadedChange(
1744 self,
1745 self.GetRevisionId(),
1746 change_id,
1747 patch_id,
1748 self.bare_git.rev_parse("FETCH_HEAD"),
1749 )
1750
1751 def DeleteWorktree(self, quiet=False, force=False):
1752 """Delete the source checkout and any other housekeeping tasks.
1753
1754 This currently leaves behind the internal .repo/ cache state. This
1755 helps when switching branches or manifest changes get reverted as we
1756 don't have to redownload all the git objects. But we should do some GC
1757 at some point.
1758
1759 Args:
1760 quiet: Whether to hide normal messages.
1761 force: Always delete tree even if dirty.
Doug Anderson3ba5f952011-04-07 12:51:04 -07001762
1763 Returns:
Gavin Makea2e3302023-03-11 06:46:20 +00001764 True if the worktree was completely cleaned out.
1765 """
1766 if self.IsDirty():
1767 if force:
1768 print(
1769 "warning: %s: Removing dirty project: uncommitted changes "
1770 "lost." % (self.RelPath(local=False),),
1771 file=sys.stderr,
1772 )
1773 else:
Jason Chang32b59562023-07-14 16:45:35 -07001774 msg = (
1775 "error: %s: Cannot remove project: uncommitted"
1776 "changes are present.\n" % self.RelPath(local=False)
Gavin Makea2e3302023-03-11 06:46:20 +00001777 )
Jason Chang32b59562023-07-14 16:45:35 -07001778 print(msg, file=sys.stderr)
1779 raise DeleteDirtyWorktreeError(msg, project=self)
Wink Saville02d79452009-04-10 13:01:24 -07001780
Gavin Makea2e3302023-03-11 06:46:20 +00001781 if not quiet:
1782 print(
1783 "%s: Deleting obsolete checkout." % (self.RelPath(local=False),)
1784 )
Wink Saville02d79452009-04-10 13:01:24 -07001785
Gavin Makea2e3302023-03-11 06:46:20 +00001786 # Unlock and delink from the main worktree. We don't use git's worktree
1787 # remove because it will recursively delete projects -- we handle that
1788 # ourselves below. https://crbug.com/git/48
1789 if self.use_git_worktrees:
1790 needle = platform_utils.realpath(self.gitdir)
1791 # Find the git worktree commondir under .repo/worktrees/.
1792 output = self.bare_git.worktree("list", "--porcelain").splitlines()[
1793 0
1794 ]
1795 assert output.startswith("worktree "), output
1796 commondir = output[9:]
1797 # Walk each of the git worktrees to see where they point.
1798 configs = os.path.join(commondir, "worktrees")
1799 for name in os.listdir(configs):
1800 gitdir = os.path.join(configs, name, "gitdir")
1801 with open(gitdir) as fp:
1802 relpath = fp.read().strip()
1803 # Resolve the checkout path and see if it matches this project.
1804 fullpath = platform_utils.realpath(
1805 os.path.join(configs, name, relpath)
1806 )
1807 if fullpath == needle:
1808 platform_utils.rmtree(os.path.join(configs, name))
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001809
Gavin Makea2e3302023-03-11 06:46:20 +00001810 # Delete the .git directory first, so we're less likely to have a
1811 # partially working git repository around. There shouldn't be any git
1812 # projects here, so rmtree works.
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001813
Gavin Makea2e3302023-03-11 06:46:20 +00001814 # Try to remove plain files first in case of git worktrees. If this
1815 # fails for any reason, we'll fall back to rmtree, and that'll display
1816 # errors if it can't remove things either.
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001817 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001818 platform_utils.remove(self.gitdir)
1819 except OSError:
1820 pass
1821 try:
1822 platform_utils.rmtree(self.gitdir)
1823 except OSError as e:
1824 if e.errno != errno.ENOENT:
1825 print("error: %s: %s" % (self.gitdir, e), file=sys.stderr)
1826 print(
1827 "error: %s: Failed to delete obsolete checkout; remove "
1828 "manually, then run `repo sync -l`."
1829 % (self.RelPath(local=False),),
1830 file=sys.stderr,
1831 )
Jason Chang32b59562023-07-14 16:45:35 -07001832 raise DeleteWorktreeError(aggregate_errors=[e])
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001833
Gavin Makea2e3302023-03-11 06:46:20 +00001834 # Delete everything under the worktree, except for directories that
1835 # contain another git project.
1836 dirs_to_remove = []
1837 failed = False
Jason Chang32b59562023-07-14 16:45:35 -07001838 errors = []
Gavin Makea2e3302023-03-11 06:46:20 +00001839 for root, dirs, files in platform_utils.walk(self.worktree):
1840 for f in files:
1841 path = os.path.join(root, f)
1842 try:
1843 platform_utils.remove(path)
1844 except OSError as e:
1845 if e.errno != errno.ENOENT:
1846 print(
1847 "error: %s: Failed to remove: %s" % (path, e),
1848 file=sys.stderr,
1849 )
1850 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001851 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001852 dirs[:] = [
1853 d
1854 for d in dirs
1855 if not os.path.lexists(os.path.join(root, d, ".git"))
1856 ]
1857 dirs_to_remove += [
1858 os.path.join(root, d)
1859 for d in dirs
1860 if os.path.join(root, d) not in dirs_to_remove
1861 ]
1862 for d in reversed(dirs_to_remove):
1863 if platform_utils.islink(d):
1864 try:
1865 platform_utils.remove(d)
1866 except OSError as e:
1867 if e.errno != errno.ENOENT:
1868 print(
1869 "error: %s: Failed to remove: %s" % (d, e),
1870 file=sys.stderr,
1871 )
1872 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001873 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001874 elif not platform_utils.listdir(d):
1875 try:
1876 platform_utils.rmdir(d)
1877 except OSError as e:
1878 if e.errno != errno.ENOENT:
1879 print(
1880 "error: %s: Failed to remove: %s" % (d, e),
1881 file=sys.stderr,
1882 )
1883 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001884 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001885 if failed:
1886 print(
1887 "error: %s: Failed to delete obsolete checkout."
1888 % (self.RelPath(local=False),),
1889 file=sys.stderr,
1890 )
1891 print(
1892 " Remove manually, then run `repo sync -l`.",
1893 file=sys.stderr,
1894 )
Jason Chang32b59562023-07-14 16:45:35 -07001895 raise DeleteWorktreeError(aggregate_errors=errors)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07001896
Gavin Makea2e3302023-03-11 06:46:20 +00001897 # Try deleting parent dirs if they are empty.
1898 path = self.worktree
1899 while path != self.manifest.topdir:
1900 try:
1901 platform_utils.rmdir(path)
1902 except OSError as e:
1903 if e.errno != errno.ENOENT:
1904 break
1905 path = os.path.dirname(path)
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001906
Gavin Makea2e3302023-03-11 06:46:20 +00001907 return True
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001908
Gavin Makea2e3302023-03-11 06:46:20 +00001909 def StartBranch(self, name, branch_merge="", revision=None):
1910 """Create a new branch off the manifest's revision."""
1911 if not branch_merge:
1912 branch_merge = self.revisionExpr
1913 head = self.work_git.GetHead()
1914 if head == (R_HEADS + name):
1915 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001916
David Pursehouse8a68ff92012-09-24 12:15:13 +09001917 all_refs = self.bare_ref.all
Gavin Makea2e3302023-03-11 06:46:20 +00001918 if R_HEADS + name in all_refs:
Jason Chang1a3612f2023-08-08 14:12:53 -07001919 GitCommand(
1920 self, ["checkout", "-q", name, "--"], verify_command=True
1921 ).Wait()
1922 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001923
Gavin Makea2e3302023-03-11 06:46:20 +00001924 branch = self.GetBranch(name)
1925 branch.remote = self.GetRemote()
1926 branch.merge = branch_merge
1927 if not branch.merge.startswith("refs/") and not ID_RE.match(
1928 branch_merge
1929 ):
1930 branch.merge = R_HEADS + branch_merge
Shawn O. Pearce88443382010-10-08 10:02:09 +02001931
Gavin Makea2e3302023-03-11 06:46:20 +00001932 if revision is None:
1933 revid = self.GetRevisionId(all_refs)
Shawn O. Pearce88443382010-10-08 10:02:09 +02001934 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001935 revid = self.work_git.rev_parse(revision)
Brian Harring14a66742012-09-28 20:21:57 -07001936
Gavin Makea2e3302023-03-11 06:46:20 +00001937 if head.startswith(R_HEADS):
Kevin Degiabaa7f32014-11-12 11:27:45 -07001938 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001939 head = all_refs[head]
1940 except KeyError:
1941 head = None
1942 if revid and head and revid == head:
1943 ref = R_HEADS + name
1944 self.work_git.update_ref(ref, revid)
1945 self.work_git.symbolic_ref(HEAD, ref)
1946 branch.Save()
1947 return True
Kevin Degib1a07b82015-07-27 13:33:43 -06001948
Jason Chang1a3612f2023-08-08 14:12:53 -07001949 GitCommand(
1950 self,
1951 ["checkout", "-q", "-b", branch.name, revid],
1952 verify_command=True,
1953 ).Wait()
1954 branch.Save()
1955 return True
Kevin Degi384b3c52014-10-16 16:02:58 -06001956
Gavin Makea2e3302023-03-11 06:46:20 +00001957 def CheckoutBranch(self, name):
1958 """Checkout a local topic branch.
Shawn O. Pearce2816d4f2009-03-03 17:53:18 -08001959
Gavin Makea2e3302023-03-11 06:46:20 +00001960 Args:
1961 name: The name of the branch to checkout.
Shawn O. Pearce88443382010-10-08 10:02:09 +02001962
Gavin Makea2e3302023-03-11 06:46:20 +00001963 Returns:
Jason Chang1a3612f2023-08-08 14:12:53 -07001964 True if the checkout succeeded; False if the
1965 branch doesn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00001966 """
1967 rev = R_HEADS + name
1968 head = self.work_git.GetHead()
1969 if head == rev:
1970 # Already on the branch.
1971 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001972
Gavin Makea2e3302023-03-11 06:46:20 +00001973 all_refs = self.bare_ref.all
1974 try:
1975 revid = all_refs[rev]
1976 except KeyError:
1977 # Branch does not exist in this project.
Jason Chang1a3612f2023-08-08 14:12:53 -07001978 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001979
Gavin Makea2e3302023-03-11 06:46:20 +00001980 if head.startswith(R_HEADS):
1981 try:
1982 head = all_refs[head]
1983 except KeyError:
1984 head = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001985
Gavin Makea2e3302023-03-11 06:46:20 +00001986 if head == revid:
1987 # Same revision; just update HEAD to point to the new
1988 # target branch, but otherwise take no other action.
1989 _lwrite(
1990 self.work_git.GetDotgitPath(subpath=HEAD),
1991 "ref: %s%s\n" % (R_HEADS, name),
1992 )
1993 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05001994
Jason Chang1a3612f2023-08-08 14:12:53 -07001995 GitCommand(
1996 self,
1997 ["checkout", name, "--"],
1998 capture_stdout=True,
1999 capture_stderr=True,
2000 verify_command=True,
2001 ).Wait()
2002 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05002003
Gavin Makea2e3302023-03-11 06:46:20 +00002004 def AbandonBranch(self, name):
2005 """Destroy a local topic branch.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002006
Gavin Makea2e3302023-03-11 06:46:20 +00002007 Args:
2008 name: The name of the branch to abandon.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002009
Gavin Makea2e3302023-03-11 06:46:20 +00002010 Returns:
Jason Changf9aacd42023-08-03 14:38:00 -07002011 True if the abandon succeeded; Raises GitCommandError if it didn't;
2012 None if the branch didn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00002013 """
2014 rev = R_HEADS + name
2015 all_refs = self.bare_ref.all
2016 if rev not in all_refs:
2017 # Doesn't exist
2018 return None
2019
2020 head = self.work_git.GetHead()
2021 if head == rev:
2022 # We can't destroy the branch while we are sitting
2023 # on it. Switch to a detached HEAD.
2024 head = all_refs[head]
2025
2026 revid = self.GetRevisionId(all_refs)
2027 if head == revid:
2028 _lwrite(
2029 self.work_git.GetDotgitPath(subpath=HEAD), "%s\n" % revid
2030 )
2031 else:
2032 self._Checkout(revid, quiet=True)
Jason Changf9aacd42023-08-03 14:38:00 -07002033 GitCommand(
2034 self,
2035 ["branch", "-D", name],
2036 capture_stdout=True,
2037 capture_stderr=True,
2038 verify_command=True,
2039 ).Wait()
2040 return True
Gavin Makea2e3302023-03-11 06:46:20 +00002041
2042 def PruneHeads(self):
2043 """Prune any topic branches already merged into upstream."""
2044 cb = self.CurrentBranch
2045 kill = []
2046 left = self._allrefs
2047 for name in left.keys():
2048 if name.startswith(R_HEADS):
2049 name = name[len(R_HEADS) :]
2050 if cb is None or name != cb:
2051 kill.append(name)
2052
2053 # Minor optimization: If there's nothing to prune, then don't try to
2054 # read any project state.
2055 if not kill and not cb:
2056 return []
2057
2058 rev = self.GetRevisionId(left)
2059 if (
2060 cb is not None
2061 and not self._revlist(HEAD + "..." + rev)
2062 and not self.IsDirty(consider_untracked=False)
2063 ):
2064 self.work_git.DetachHead(HEAD)
2065 kill.append(cb)
2066
2067 if kill:
2068 old = self.bare_git.GetHead()
2069
2070 try:
2071 self.bare_git.DetachHead(rev)
2072
2073 b = ["branch", "-d"]
2074 b.extend(kill)
2075 b = GitCommand(
2076 self, b, bare=True, capture_stdout=True, capture_stderr=True
2077 )
2078 b.Wait()
2079 finally:
2080 if ID_RE.match(old):
2081 self.bare_git.DetachHead(old)
2082 else:
2083 self.bare_git.SetHead(old)
2084 left = self._allrefs
2085
2086 for branch in kill:
2087 if (R_HEADS + branch) not in left:
2088 self.CleanPublishedCache()
2089 break
2090
2091 if cb and cb not in kill:
2092 kill.append(cb)
2093 kill.sort()
2094
2095 kept = []
2096 for branch in kill:
2097 if R_HEADS + branch in left:
2098 branch = self.GetBranch(branch)
2099 base = branch.LocalMerge
2100 if not base:
2101 base = rev
2102 kept.append(ReviewableBranch(self, branch, base))
2103 return kept
2104
2105 def GetRegisteredSubprojects(self):
2106 result = []
2107
2108 def rec(subprojects):
2109 if not subprojects:
2110 return
2111 result.extend(subprojects)
2112 for p in subprojects:
2113 rec(p.subprojects)
2114
2115 rec(self.subprojects)
2116 return result
2117
2118 def _GetSubmodules(self):
2119 # Unfortunately we cannot call `git submodule status --recursive` here
2120 # because the working tree might not exist yet, and it cannot be used
2121 # without a working tree in its current implementation.
2122
2123 def get_submodules(gitdir, rev):
2124 # Parse .gitmodules for submodule sub_paths and sub_urls.
2125 sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
2126 if not sub_paths:
2127 return []
2128 # Run `git ls-tree` to read SHAs of submodule object, which happen
2129 # to be revision of submodule repository.
2130 sub_revs = git_ls_tree(gitdir, rev, sub_paths)
2131 submodules = []
2132 for sub_path, sub_url in zip(sub_paths, sub_urls):
2133 try:
2134 sub_rev = sub_revs[sub_path]
2135 except KeyError:
2136 # Ignore non-exist submodules.
2137 continue
2138 submodules.append((sub_rev, sub_path, sub_url))
2139 return submodules
2140
2141 re_path = re.compile(r"^submodule\.(.+)\.path=(.*)$")
2142 re_url = re.compile(r"^submodule\.(.+)\.url=(.*)$")
2143
2144 def parse_gitmodules(gitdir, rev):
2145 cmd = ["cat-file", "blob", "%s:.gitmodules" % rev]
2146 try:
2147 p = GitCommand(
2148 None,
2149 cmd,
2150 capture_stdout=True,
2151 capture_stderr=True,
2152 bare=True,
2153 gitdir=gitdir,
2154 )
2155 except GitError:
2156 return [], []
2157 if p.Wait() != 0:
2158 return [], []
2159
2160 gitmodules_lines = []
2161 fd, temp_gitmodules_path = tempfile.mkstemp()
2162 try:
2163 os.write(fd, p.stdout.encode("utf-8"))
2164 os.close(fd)
2165 cmd = ["config", "--file", temp_gitmodules_path, "--list"]
2166 p = GitCommand(
2167 None,
2168 cmd,
2169 capture_stdout=True,
2170 capture_stderr=True,
2171 bare=True,
2172 gitdir=gitdir,
2173 )
2174 if p.Wait() != 0:
2175 return [], []
2176 gitmodules_lines = p.stdout.split("\n")
2177 except GitError:
2178 return [], []
2179 finally:
2180 platform_utils.remove(temp_gitmodules_path)
2181
2182 names = set()
2183 paths = {}
2184 urls = {}
2185 for line in gitmodules_lines:
2186 if not line:
2187 continue
2188 m = re_path.match(line)
2189 if m:
2190 names.add(m.group(1))
2191 paths[m.group(1)] = m.group(2)
2192 continue
2193 m = re_url.match(line)
2194 if m:
2195 names.add(m.group(1))
2196 urls[m.group(1)] = m.group(2)
2197 continue
2198 names = sorted(names)
2199 return (
2200 [paths.get(name, "") for name in names],
2201 [urls.get(name, "") for name in names],
2202 )
2203
2204 def git_ls_tree(gitdir, rev, paths):
2205 cmd = ["ls-tree", rev, "--"]
2206 cmd.extend(paths)
2207 try:
2208 p = GitCommand(
2209 None,
2210 cmd,
2211 capture_stdout=True,
2212 capture_stderr=True,
2213 bare=True,
2214 gitdir=gitdir,
2215 )
2216 except GitError:
2217 return []
2218 if p.Wait() != 0:
2219 return []
2220 objects = {}
2221 for line in p.stdout.split("\n"):
2222 if not line.strip():
2223 continue
2224 object_rev, object_path = line.split()[2:4]
2225 objects[object_path] = object_rev
2226 return objects
2227
2228 try:
2229 rev = self.GetRevisionId()
2230 except GitError:
2231 return []
2232 return get_submodules(self.gitdir, rev)
2233
2234 def GetDerivedSubprojects(self):
2235 result = []
2236 if not self.Exists:
2237 # If git repo does not exist yet, querying its submodules will
2238 # mess up its states; so return here.
2239 return result
2240 for rev, path, url in self._GetSubmodules():
2241 name = self.manifest.GetSubprojectName(self, path)
2242 (
2243 relpath,
2244 worktree,
2245 gitdir,
2246 objdir,
2247 ) = self.manifest.GetSubprojectPaths(self, name, path)
2248 project = self.manifest.paths.get(relpath)
2249 if project:
2250 result.extend(project.GetDerivedSubprojects())
2251 continue
2252
2253 if url.startswith(".."):
2254 url = urllib.parse.urljoin("%s/" % self.remote.url, url)
2255 remote = RemoteSpec(
2256 self.remote.name,
2257 url=url,
2258 pushUrl=self.remote.pushUrl,
2259 review=self.remote.review,
2260 revision=self.remote.revision,
2261 )
2262 subproject = Project(
2263 manifest=self.manifest,
2264 name=name,
2265 remote=remote,
2266 gitdir=gitdir,
2267 objdir=objdir,
2268 worktree=worktree,
2269 relpath=relpath,
2270 revisionExpr=rev,
2271 revisionId=rev,
2272 rebase=self.rebase,
2273 groups=self.groups,
2274 sync_c=self.sync_c,
2275 sync_s=self.sync_s,
2276 sync_tags=self.sync_tags,
2277 parent=self,
2278 is_derived=True,
2279 )
2280 result.append(subproject)
2281 result.extend(subproject.GetDerivedSubprojects())
2282 return result
2283
2284 def EnableRepositoryExtension(self, key, value="true", version=1):
2285 """Enable git repository extension |key| with |value|.
2286
2287 Args:
2288 key: The extension to enabled. Omit the "extensions." prefix.
2289 value: The value to use for the extension.
2290 version: The minimum git repository version needed.
2291 """
2292 # Make sure the git repo version is new enough already.
2293 found_version = self.config.GetInt("core.repositoryFormatVersion")
2294 if found_version is None:
2295 found_version = 0
2296 if found_version < version:
2297 self.config.SetString("core.repositoryFormatVersion", str(version))
2298
2299 # Enable the extension!
2300 self.config.SetString("extensions.%s" % (key,), value)
2301
2302 def ResolveRemoteHead(self, name=None):
2303 """Find out what the default branch (HEAD) points to.
2304
2305 Normally this points to refs/heads/master, but projects are moving to
2306 main. Support whatever the server uses rather than hardcoding "master"
2307 ourselves.
2308 """
2309 if name is None:
2310 name = self.remote.name
2311
2312 # The output will look like (NB: tabs are separators):
2313 # ref: refs/heads/master HEAD
2314 # 5f6803b100bb3cd0f534e96e88c91373e8ed1c44 HEAD
2315 output = self.bare_git.ls_remote(
2316 "-q", "--symref", "--exit-code", name, "HEAD"
2317 )
2318
2319 for line in output.splitlines():
2320 lhs, rhs = line.split("\t", 1)
2321 if rhs == "HEAD" and lhs.startswith("ref:"):
2322 return lhs[4:].strip()
2323
2324 return None
2325
2326 def _CheckForImmutableRevision(self):
2327 try:
2328 # if revision (sha or tag) is not present then following function
2329 # throws an error.
2330 self.bare_git.rev_list(
2331 "-1", "--missing=allow-any", "%s^0" % self.revisionExpr, "--"
2332 )
2333 if self.upstream:
2334 rev = self.GetRemote().ToLocal(self.upstream)
2335 self.bare_git.rev_list(
2336 "-1", "--missing=allow-any", "%s^0" % rev, "--"
2337 )
2338 self.bare_git.merge_base(
2339 "--is-ancestor", self.revisionExpr, rev
2340 )
2341 return True
2342 except GitError:
2343 # There is no such persistent revision. We have to fetch it.
2344 return False
2345
2346 def _FetchArchive(self, tarpath, cwd=None):
2347 cmd = ["archive", "-v", "-o", tarpath]
2348 cmd.append("--remote=%s" % self.remote.url)
2349 cmd.append("--prefix=%s/" % self.RelPath(local=False))
2350 cmd.append(self.revisionExpr)
2351
2352 command = GitCommand(
Jason Chang32b59562023-07-14 16:45:35 -07002353 self,
2354 cmd,
2355 cwd=cwd,
2356 capture_stdout=True,
2357 capture_stderr=True,
2358 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00002359 )
Jason Chang32b59562023-07-14 16:45:35 -07002360 command.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00002361
2362 def _RemoteFetch(
2363 self,
2364 name=None,
2365 current_branch_only=False,
2366 initial=False,
2367 quiet=False,
2368 verbose=False,
2369 output_redir=None,
2370 alt_dir=None,
2371 tags=True,
2372 prune=False,
2373 depth=None,
2374 submodules=False,
2375 ssh_proxy=None,
2376 force_sync=False,
2377 clone_filter=None,
2378 retry_fetches=2,
2379 retry_sleep_initial_sec=4.0,
2380 retry_exp_factor=2.0,
Jason Chang32b59562023-07-14 16:45:35 -07002381 ) -> bool:
Gavin Makea2e3302023-03-11 06:46:20 +00002382 is_sha1 = False
2383 tag_name = None
2384 # The depth should not be used when fetching to a mirror because
2385 # it will result in a shallow repository that cannot be cloned or
2386 # fetched from.
2387 # The repo project should also never be synced with partial depth.
2388 if self.manifest.IsMirror or self.relpath == ".repo/repo":
2389 depth = None
2390
2391 if depth:
2392 current_branch_only = True
2393
2394 if ID_RE.match(self.revisionExpr) is not None:
2395 is_sha1 = True
2396
2397 if current_branch_only:
2398 if self.revisionExpr.startswith(R_TAGS):
2399 # This is a tag and its commit id should never change.
2400 tag_name = self.revisionExpr[len(R_TAGS) :]
2401 elif self.upstream and self.upstream.startswith(R_TAGS):
2402 # This is a tag and its commit id should never change.
2403 tag_name = self.upstream[len(R_TAGS) :]
2404
2405 if is_sha1 or tag_name is not None:
2406 if self._CheckForImmutableRevision():
2407 if verbose:
2408 print(
2409 "Skipped fetching project %s (already have "
2410 "persistent ref)" % self.name
2411 )
2412 return True
2413 if is_sha1 and not depth:
2414 # When syncing a specific commit and --depth is not set:
2415 # * if upstream is explicitly specified and is not a sha1, fetch
2416 # only upstream as users expect only upstream to be fetch.
2417 # Note: The commit might not be in upstream in which case the
2418 # sync will fail.
2419 # * otherwise, fetch all branches to make sure we end up with
2420 # the specific commit.
2421 if self.upstream:
2422 current_branch_only = not ID_RE.match(self.upstream)
2423 else:
2424 current_branch_only = False
2425
2426 if not name:
2427 name = self.remote.name
2428
2429 remote = self.GetRemote(name)
2430 if not remote.PreConnectFetch(ssh_proxy):
2431 ssh_proxy = None
2432
2433 if initial:
2434 if alt_dir and "objects" == os.path.basename(alt_dir):
2435 ref_dir = os.path.dirname(alt_dir)
2436 packed_refs = os.path.join(self.gitdir, "packed-refs")
2437
2438 all_refs = self.bare_ref.all
2439 ids = set(all_refs.values())
2440 tmp = set()
2441
2442 for r, ref_id in GitRefs(ref_dir).all.items():
2443 if r not in all_refs:
2444 if r.startswith(R_TAGS) or remote.WritesTo(r):
2445 all_refs[r] = ref_id
2446 ids.add(ref_id)
2447 continue
2448
2449 if ref_id in ids:
2450 continue
2451
2452 r = "refs/_alt/%s" % ref_id
2453 all_refs[r] = ref_id
2454 ids.add(ref_id)
2455 tmp.add(r)
2456
2457 tmp_packed_lines = []
2458 old_packed_lines = []
2459
2460 for r in sorted(all_refs):
2461 line = "%s %s\n" % (all_refs[r], r)
2462 tmp_packed_lines.append(line)
2463 if r not in tmp:
2464 old_packed_lines.append(line)
2465
2466 tmp_packed = "".join(tmp_packed_lines)
2467 old_packed = "".join(old_packed_lines)
2468 _lwrite(packed_refs, tmp_packed)
2469 else:
2470 alt_dir = None
2471
2472 cmd = ["fetch"]
2473
2474 if clone_filter:
2475 git_require((2, 19, 0), fail=True, msg="partial clones")
2476 cmd.append("--filter=%s" % clone_filter)
2477 self.EnableRepositoryExtension("partialclone", self.remote.name)
2478
2479 if depth:
2480 cmd.append("--depth=%s" % depth)
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002481 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002482 # If this repo has shallow objects, then we don't know which refs
2483 # have shallow objects or not. Tell git to unshallow all fetched
2484 # refs. Don't do this with projects that don't have shallow
2485 # objects, since it is less efficient.
2486 if os.path.exists(os.path.join(self.gitdir, "shallow")):
2487 cmd.append("--depth=2147483647")
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002488
Gavin Makea2e3302023-03-11 06:46:20 +00002489 if not verbose:
2490 cmd.append("--quiet")
2491 if not quiet and sys.stdout.isatty():
2492 cmd.append("--progress")
2493 if not self.worktree:
2494 cmd.append("--update-head-ok")
2495 cmd.append(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002496
Gavin Makea2e3302023-03-11 06:46:20 +00002497 if force_sync:
2498 cmd.append("--force")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002499
Gavin Makea2e3302023-03-11 06:46:20 +00002500 if prune:
2501 cmd.append("--prune")
Mike Frysinger29626b42021-05-01 09:37:13 -04002502
Gavin Makea2e3302023-03-11 06:46:20 +00002503 # Always pass something for --recurse-submodules, git with GIT_DIR
2504 # behaves incorrectly when not given `--recurse-submodules=no`.
2505 # (b/218891912)
2506 cmd.append(
2507 f'--recurse-submodules={"on-demand" if submodules else "no"}'
2508 )
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002509
Gavin Makea2e3302023-03-11 06:46:20 +00002510 spec = []
2511 if not current_branch_only:
2512 # Fetch whole repo.
2513 spec.append(
2514 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2515 )
2516 elif tag_name is not None:
2517 spec.append("tag")
2518 spec.append(tag_name)
Remy Böhmer1469c282020-12-15 18:49:02 +01002519
Gavin Makea2e3302023-03-11 06:46:20 +00002520 if self.manifest.IsMirror and not current_branch_only:
2521 branch = None
Remy Böhmer1469c282020-12-15 18:49:02 +01002522 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002523 branch = self.revisionExpr
2524 if (
2525 not self.manifest.IsMirror
2526 and is_sha1
2527 and depth
2528 and git_require((1, 8, 3))
2529 ):
2530 # Shallow checkout of a specific commit, fetch from that commit and
2531 # not the heads only as the commit might be deeper in the history.
2532 spec.append(branch)
2533 if self.upstream:
2534 spec.append(self.upstream)
David James8d201162013-10-11 17:03:19 -07002535 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002536 if is_sha1:
2537 branch = self.upstream
2538 if branch is not None and branch.strip():
2539 if not branch.startswith("refs/"):
2540 branch = R_HEADS + branch
2541 spec.append(str(("+%s:" % branch) + remote.ToLocal(branch)))
David James8d201162013-10-11 17:03:19 -07002542
Gavin Makea2e3302023-03-11 06:46:20 +00002543 # If mirroring repo and we cannot deduce the tag or branch to fetch,
2544 # fetch whole repo.
2545 if self.manifest.IsMirror and not spec:
2546 spec.append(
2547 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2548 )
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002549
Gavin Makea2e3302023-03-11 06:46:20 +00002550 # If using depth then we should not get all the tags since they may
2551 # be outside of the depth.
2552 if not tags or depth:
2553 cmd.append("--no-tags")
2554 else:
2555 cmd.append("--tags")
2556 spec.append(str(("+refs/tags/*:") + remote.ToLocal("refs/tags/*")))
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002557
Gavin Makea2e3302023-03-11 06:46:20 +00002558 cmd.extend(spec)
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002559
Gavin Makea2e3302023-03-11 06:46:20 +00002560 # At least one retry minimum due to git remote prune.
2561 retry_fetches = max(retry_fetches, 2)
2562 retry_cur_sleep = retry_sleep_initial_sec
2563 ok = prune_tried = False
2564 for try_n in range(retry_fetches):
Jason Chang32b59562023-07-14 16:45:35 -07002565 verify_command = try_n == retry_fetches - 1
Gavin Makea2e3302023-03-11 06:46:20 +00002566 gitcmd = GitCommand(
2567 self,
2568 cmd,
2569 bare=True,
2570 objdir=os.path.join(self.objdir, "objects"),
2571 ssh_proxy=ssh_proxy,
2572 merge_output=True,
2573 capture_stdout=quiet or bool(output_redir),
Jason Chang32b59562023-07-14 16:45:35 -07002574 verify_command=verify_command,
Gavin Makea2e3302023-03-11 06:46:20 +00002575 )
2576 if gitcmd.stdout and not quiet and output_redir:
2577 output_redir.write(gitcmd.stdout)
2578 ret = gitcmd.Wait()
2579 if ret == 0:
2580 ok = True
2581 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002582
Gavin Makea2e3302023-03-11 06:46:20 +00002583 # Retry later due to HTTP 429 Too Many Requests.
2584 elif (
2585 gitcmd.stdout
2586 and "error:" in gitcmd.stdout
2587 and "HTTP 429" in gitcmd.stdout
2588 ):
2589 # Fallthru to sleep+retry logic at the bottom.
2590 pass
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002591
Gavin Makea2e3302023-03-11 06:46:20 +00002592 # Try to prune remote branches once in case there are conflicts.
2593 # For example, if the remote had refs/heads/upstream, but deleted
2594 # that and now has refs/heads/upstream/foo.
2595 elif (
2596 gitcmd.stdout
2597 and "error:" in gitcmd.stdout
2598 and "git remote prune" in gitcmd.stdout
2599 and not prune_tried
2600 ):
2601 prune_tried = True
2602 prunecmd = GitCommand(
2603 self,
2604 ["remote", "prune", name],
2605 bare=True,
2606 ssh_proxy=ssh_proxy,
2607 )
2608 ret = prunecmd.Wait()
2609 if ret:
2610 break
2611 print(
2612 "retrying fetch after pruning remote branches",
2613 file=output_redir,
2614 )
2615 # Continue right away so we don't sleep as we shouldn't need to.
2616 continue
2617 elif current_branch_only and is_sha1 and ret == 128:
2618 # Exit code 128 means "couldn't find the ref you asked for"; if
2619 # we're in sha1 mode, we just tried sync'ing from the upstream
2620 # field; it doesn't exist, thus abort the optimization attempt
2621 # and do a full sync.
2622 break
2623 elif ret < 0:
2624 # Git died with a signal, exit immediately.
2625 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002626
Gavin Makea2e3302023-03-11 06:46:20 +00002627 # Figure out how long to sleep before the next attempt, if there is
2628 # one.
2629 if not verbose and gitcmd.stdout:
2630 print(
2631 "\n%s:\n%s" % (self.name, gitcmd.stdout),
2632 end="",
2633 file=output_redir,
2634 )
2635 if try_n < retry_fetches - 1:
2636 print(
2637 "%s: sleeping %s seconds before retrying"
2638 % (self.name, retry_cur_sleep),
2639 file=output_redir,
2640 )
2641 time.sleep(retry_cur_sleep)
2642 retry_cur_sleep = min(
2643 retry_exp_factor * retry_cur_sleep, MAXIMUM_RETRY_SLEEP_SEC
2644 )
2645 retry_cur_sleep *= 1 - random.uniform(
2646 -RETRY_JITTER_PERCENT, RETRY_JITTER_PERCENT
2647 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002648
Gavin Makea2e3302023-03-11 06:46:20 +00002649 if initial:
2650 if alt_dir:
2651 if old_packed != "":
2652 _lwrite(packed_refs, old_packed)
2653 else:
2654 platform_utils.remove(packed_refs)
2655 self.bare_git.pack_refs("--all", "--prune")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002656
Gavin Makea2e3302023-03-11 06:46:20 +00002657 if is_sha1 and current_branch_only:
2658 # We just synced the upstream given branch; verify we
2659 # got what we wanted, else trigger a second run of all
2660 # refs.
2661 if not self._CheckForImmutableRevision():
2662 # Sync the current branch only with depth set to None.
2663 # We always pass depth=None down to avoid infinite recursion.
2664 return self._RemoteFetch(
2665 name=name,
2666 quiet=quiet,
2667 verbose=verbose,
2668 output_redir=output_redir,
2669 current_branch_only=current_branch_only and depth,
2670 initial=False,
2671 alt_dir=alt_dir,
Nasser Grainawiaafed292023-05-24 12:51:03 -06002672 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00002673 depth=None,
2674 ssh_proxy=ssh_proxy,
2675 clone_filter=clone_filter,
2676 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002677
Gavin Makea2e3302023-03-11 06:46:20 +00002678 return ok
Mike Frysingerf4545122019-11-11 04:34:16 -05002679
Gavin Makea2e3302023-03-11 06:46:20 +00002680 def _ApplyCloneBundle(self, initial=False, quiet=False, verbose=False):
2681 if initial and (
2682 self.manifest.manifestProject.depth or self.clone_depth
2683 ):
2684 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002685
Gavin Makea2e3302023-03-11 06:46:20 +00002686 remote = self.GetRemote()
2687 bundle_url = remote.url + "/clone.bundle"
2688 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
2689 if GetSchemeFromUrl(bundle_url) not in (
2690 "http",
2691 "https",
2692 "persistent-http",
2693 "persistent-https",
2694 ):
2695 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06002696
Gavin Makea2e3302023-03-11 06:46:20 +00002697 bundle_dst = os.path.join(self.gitdir, "clone.bundle")
2698 bundle_tmp = os.path.join(self.gitdir, "clone.bundle.tmp")
2699
2700 exist_dst = os.path.exists(bundle_dst)
2701 exist_tmp = os.path.exists(bundle_tmp)
2702
2703 if not initial and not exist_dst and not exist_tmp:
2704 return False
2705
2706 if not exist_dst:
2707 exist_dst = self._FetchBundle(
2708 bundle_url, bundle_tmp, bundle_dst, quiet, verbose
2709 )
2710 if not exist_dst:
2711 return False
2712
2713 cmd = ["fetch"]
2714 if not verbose:
2715 cmd.append("--quiet")
2716 if not quiet and sys.stdout.isatty():
2717 cmd.append("--progress")
2718 if not self.worktree:
2719 cmd.append("--update-head-ok")
2720 cmd.append(bundle_dst)
2721 for f in remote.fetch:
2722 cmd.append(str(f))
2723 cmd.append("+refs/tags/*:refs/tags/*")
2724
2725 ok = (
2726 GitCommand(
2727 self,
2728 cmd,
2729 bare=True,
2730 objdir=os.path.join(self.objdir, "objects"),
2731 ).Wait()
2732 == 0
2733 )
2734 platform_utils.remove(bundle_dst, missing_ok=True)
2735 platform_utils.remove(bundle_tmp, missing_ok=True)
2736 return ok
2737
2738 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
2739 platform_utils.remove(dstPath, missing_ok=True)
2740
2741 cmd = ["curl", "--fail", "--output", tmpPath, "--netrc", "--location"]
2742 if quiet:
2743 cmd += ["--silent", "--show-error"]
2744 if os.path.exists(tmpPath):
2745 size = os.stat(tmpPath).st_size
2746 if size >= 1024:
2747 cmd += ["--continue-at", "%d" % (size,)]
2748 else:
2749 platform_utils.remove(tmpPath)
2750 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
2751 if cookiefile:
2752 cmd += ["--cookie", cookiefile]
2753 if proxy:
2754 cmd += ["--proxy", proxy]
2755 elif "http_proxy" in os.environ and "darwin" == sys.platform:
2756 cmd += ["--proxy", os.environ["http_proxy"]]
2757 if srcUrl.startswith("persistent-https"):
2758 srcUrl = "http" + srcUrl[len("persistent-https") :]
2759 elif srcUrl.startswith("persistent-http"):
2760 srcUrl = "http" + srcUrl[len("persistent-http") :]
2761 cmd += [srcUrl]
2762
2763 proc = None
2764 with Trace("Fetching bundle: %s", " ".join(cmd)):
2765 if verbose:
2766 print("%s: Downloading bundle: %s" % (self.name, srcUrl))
2767 stdout = None if verbose else subprocess.PIPE
2768 stderr = None if verbose else subprocess.STDOUT
2769 try:
2770 proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
2771 except OSError:
2772 return False
2773
2774 (output, _) = proc.communicate()
2775 curlret = proc.returncode
2776
2777 if curlret == 22:
2778 # From curl man page:
2779 # 22: HTTP page not retrieved. The requested url was not found
2780 # or returned another error with the HTTP error code being 400
2781 # or above. This return code only appears if -f, --fail is used.
2782 if verbose:
2783 print(
2784 "%s: Unable to retrieve clone.bundle; ignoring."
2785 % self.name
2786 )
2787 if output:
2788 print("Curl output:\n%s" % output)
2789 return False
2790 elif curlret and not verbose and output:
2791 print("%s" % output, file=sys.stderr)
2792
2793 if os.path.exists(tmpPath):
2794 if curlret == 0 and self._IsValidBundle(tmpPath, quiet):
2795 platform_utils.rename(tmpPath, dstPath)
2796 return True
2797 else:
2798 platform_utils.remove(tmpPath)
2799 return False
2800 else:
2801 return False
2802
2803 def _IsValidBundle(self, path, quiet):
2804 try:
2805 with open(path, "rb") as f:
2806 if f.read(16) == b"# v2 git bundle\n":
2807 return True
2808 else:
2809 if not quiet:
2810 print(
2811 "Invalid clone.bundle file; ignoring.",
2812 file=sys.stderr,
2813 )
2814 return False
2815 except OSError:
2816 return False
2817
2818 def _Checkout(self, rev, quiet=False):
2819 cmd = ["checkout"]
2820 if quiet:
2821 cmd.append("-q")
2822 cmd.append(rev)
2823 cmd.append("--")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002824 if GitCommand(self, cmd).Wait() != 0:
Gavin Makea2e3302023-03-11 06:46:20 +00002825 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002826 raise GitError(
2827 "%s checkout %s " % (self.name, rev), project=self.name
2828 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002829
Gavin Makea2e3302023-03-11 06:46:20 +00002830 def _CherryPick(self, rev, ffonly=False, record_origin=False):
2831 cmd = ["cherry-pick"]
2832 if ffonly:
2833 cmd.append("--ff")
2834 if record_origin:
2835 cmd.append("-x")
2836 cmd.append(rev)
2837 cmd.append("--")
2838 if GitCommand(self, cmd).Wait() != 0:
2839 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002840 raise GitError(
2841 "%s cherry-pick %s " % (self.name, rev), project=self.name
2842 )
Victor Boivie0960b5b2010-11-26 13:42:13 +01002843
Gavin Makea2e3302023-03-11 06:46:20 +00002844 def _LsRemote(self, refs):
2845 cmd = ["ls-remote", self.remote.name, refs]
2846 p = GitCommand(self, cmd, capture_stdout=True)
2847 if p.Wait() == 0:
2848 return p.stdout
2849 return None
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002850
Gavin Makea2e3302023-03-11 06:46:20 +00002851 def _Revert(self, rev):
2852 cmd = ["revert"]
2853 cmd.append("--no-edit")
2854 cmd.append(rev)
2855 cmd.append("--")
2856 if GitCommand(self, cmd).Wait() != 0:
2857 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002858 raise GitError(
2859 "%s revert %s " % (self.name, rev), project=self.name
2860 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002861
Gavin Makea2e3302023-03-11 06:46:20 +00002862 def _ResetHard(self, rev, quiet=True):
2863 cmd = ["reset", "--hard"]
2864 if quiet:
2865 cmd.append("-q")
2866 cmd.append(rev)
2867 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002868 raise GitError(
2869 "%s reset --hard %s " % (self.name, rev), project=self.name
2870 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002871
Gavin Makea2e3302023-03-11 06:46:20 +00002872 def _SyncSubmodules(self, quiet=True):
2873 cmd = ["submodule", "update", "--init", "--recursive"]
2874 if quiet:
2875 cmd.append("-q")
2876 if GitCommand(self, cmd).Wait() != 0:
2877 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07002878 "%s submodule update --init --recursive " % self.name,
2879 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00002880 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002881
Gavin Makea2e3302023-03-11 06:46:20 +00002882 def _Rebase(self, upstream, onto=None):
2883 cmd = ["rebase"]
2884 if onto is not None:
2885 cmd.extend(["--onto", onto])
2886 cmd.append(upstream)
2887 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002888 raise GitError(
2889 "%s rebase %s " % (self.name, upstream), project=self.name
2890 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002891
Gavin Makea2e3302023-03-11 06:46:20 +00002892 def _FastForward(self, head, ffonly=False):
2893 cmd = ["merge", "--no-stat", head]
2894 if ffonly:
2895 cmd.append("--ff-only")
2896 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002897 raise GitError(
2898 "%s merge %s " % (self.name, head), project=self.name
2899 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002900
Gavin Makea2e3302023-03-11 06:46:20 +00002901 def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False):
2902 init_git_dir = not os.path.exists(self.gitdir)
2903 init_obj_dir = not os.path.exists(self.objdir)
2904 try:
2905 # Initialize the bare repository, which contains all of the objects.
2906 if init_obj_dir:
2907 os.makedirs(self.objdir)
2908 self.bare_objdir.init()
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002909
Gavin Makea2e3302023-03-11 06:46:20 +00002910 self._UpdateHooks(quiet=quiet)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002911
Gavin Makea2e3302023-03-11 06:46:20 +00002912 if self.use_git_worktrees:
2913 # Enable per-worktree config file support if possible. This
2914 # is more a nice-to-have feature for users rather than a
2915 # hard requirement.
2916 if git_require((2, 20, 0)):
2917 self.EnableRepositoryExtension("worktreeConfig")
Renaud Paquay788e9622017-01-27 11:41:12 -08002918
Gavin Makea2e3302023-03-11 06:46:20 +00002919 # If we have a separate directory to hold refs, initialize it as
2920 # well.
2921 if self.objdir != self.gitdir:
2922 if init_git_dir:
2923 os.makedirs(self.gitdir)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002924
Gavin Makea2e3302023-03-11 06:46:20 +00002925 if init_obj_dir or init_git_dir:
2926 self._ReferenceGitDir(
2927 self.objdir, self.gitdir, copy_all=True
2928 )
2929 try:
2930 self._CheckDirReference(self.objdir, self.gitdir)
2931 except GitError as e:
2932 if force_sync:
2933 print(
2934 "Retrying clone after deleting %s" % self.gitdir,
2935 file=sys.stderr,
2936 )
2937 try:
2938 platform_utils.rmtree(
2939 platform_utils.realpath(self.gitdir)
2940 )
2941 if self.worktree and os.path.exists(
2942 platform_utils.realpath(self.worktree)
2943 ):
2944 platform_utils.rmtree(
2945 platform_utils.realpath(self.worktree)
2946 )
2947 return self._InitGitDir(
2948 mirror_git=mirror_git,
2949 force_sync=False,
2950 quiet=quiet,
2951 )
2952 except Exception:
2953 raise e
2954 raise e
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002955
Gavin Makea2e3302023-03-11 06:46:20 +00002956 if init_git_dir:
2957 mp = self.manifest.manifestProject
2958 ref_dir = mp.reference or ""
Julien Camperguedd654222014-01-09 16:21:37 +01002959
Gavin Makea2e3302023-03-11 06:46:20 +00002960 def _expanded_ref_dirs():
2961 """Iterate through possible git reference dir paths."""
2962 name = self.name + ".git"
2963 yield mirror_git or os.path.join(ref_dir, name)
2964 for prefix in "", self.remote.name:
2965 yield os.path.join(
2966 ref_dir, ".repo", "project-objects", prefix, name
2967 )
2968 yield os.path.join(
2969 ref_dir, ".repo", "worktrees", prefix, name
2970 )
2971
2972 if ref_dir or mirror_git:
2973 found_ref_dir = None
2974 for path in _expanded_ref_dirs():
2975 if os.path.exists(path):
2976 found_ref_dir = path
2977 break
2978 ref_dir = found_ref_dir
2979
2980 if ref_dir:
2981 if not os.path.isabs(ref_dir):
2982 # The alternate directory is relative to the object
2983 # database.
2984 ref_dir = os.path.relpath(
2985 ref_dir, os.path.join(self.objdir, "objects")
2986 )
2987 _lwrite(
2988 os.path.join(
2989 self.objdir, "objects/info/alternates"
2990 ),
2991 os.path.join(ref_dir, "objects") + "\n",
2992 )
2993
2994 m = self.manifest.manifestProject.config
2995 for key in ["user.name", "user.email"]:
2996 if m.Has(key, include_defaults=False):
2997 self.config.SetString(key, m.GetString(key))
2998 if not self.manifest.EnableGitLfs:
2999 self.config.SetString(
3000 "filter.lfs.smudge", "git-lfs smudge --skip -- %f"
3001 )
3002 self.config.SetString(
3003 "filter.lfs.process", "git-lfs filter-process --skip"
3004 )
3005 self.config.SetBoolean(
3006 "core.bare", True if self.manifest.IsMirror else None
3007 )
3008 except Exception:
3009 if init_obj_dir and os.path.exists(self.objdir):
3010 platform_utils.rmtree(self.objdir)
3011 if init_git_dir and os.path.exists(self.gitdir):
3012 platform_utils.rmtree(self.gitdir)
3013 raise
3014
3015 def _UpdateHooks(self, quiet=False):
3016 if os.path.exists(self.objdir):
3017 self._InitHooks(quiet=quiet)
3018
3019 def _InitHooks(self, quiet=False):
3020 hooks = platform_utils.realpath(os.path.join(self.objdir, "hooks"))
3021 if not os.path.exists(hooks):
3022 os.makedirs(hooks)
3023
3024 # Delete sample hooks. They're noise.
3025 for hook in glob.glob(os.path.join(hooks, "*.sample")):
3026 try:
3027 platform_utils.remove(hook, missing_ok=True)
3028 except PermissionError:
3029 pass
3030
3031 for stock_hook in _ProjectHooks():
3032 name = os.path.basename(stock_hook)
3033
3034 if (
3035 name in ("commit-msg",)
3036 and not self.remote.review
3037 and self is not self.manifest.manifestProject
3038 ):
3039 # Don't install a Gerrit Code Review hook if this
3040 # project does not appear to use it for reviews.
3041 #
3042 # Since the manifest project is one of those, but also
3043 # managed through gerrit, it's excluded.
3044 continue
3045
3046 dst = os.path.join(hooks, name)
3047 if platform_utils.islink(dst):
3048 continue
3049 if os.path.exists(dst):
3050 # If the files are the same, we'll leave it alone. We create
3051 # symlinks below by default but fallback to hardlinks if the OS
3052 # blocks them. So if we're here, it's probably because we made a
3053 # hardlink below.
3054 if not filecmp.cmp(stock_hook, dst, shallow=False):
3055 if not quiet:
3056 _warn(
3057 "%s: Not replacing locally modified %s hook",
3058 self.RelPath(local=False),
3059 name,
3060 )
3061 continue
3062 try:
3063 platform_utils.symlink(
3064 os.path.relpath(stock_hook, os.path.dirname(dst)), dst
3065 )
3066 except OSError as e:
3067 if e.errno == errno.EPERM:
3068 try:
3069 os.link(stock_hook, dst)
3070 except OSError:
Jason Chang32b59562023-07-14 16:45:35 -07003071 raise GitError(
3072 self._get_symlink_error_message(), project=self.name
3073 )
Gavin Makea2e3302023-03-11 06:46:20 +00003074 else:
3075 raise
3076
3077 def _InitRemote(self):
3078 if self.remote.url:
3079 remote = self.GetRemote()
3080 remote.url = self.remote.url
3081 remote.pushUrl = self.remote.pushUrl
3082 remote.review = self.remote.review
3083 remote.projectname = self.name
3084
3085 if self.worktree:
3086 remote.ResetFetch(mirror=False)
3087 else:
3088 remote.ResetFetch(mirror=True)
3089 remote.Save()
3090
3091 def _InitMRef(self):
3092 """Initialize the pseudo m/<manifest branch> ref."""
3093 if self.manifest.branch:
3094 if self.use_git_worktrees:
3095 # Set up the m/ space to point to the worktree-specific ref
3096 # space. We'll update the worktree-specific ref space on each
3097 # checkout.
3098 ref = R_M + self.manifest.branch
3099 if not self.bare_ref.symref(ref):
3100 self.bare_git.symbolic_ref(
3101 "-m",
3102 "redirecting to worktree scope",
3103 ref,
3104 R_WORKTREE_M + self.manifest.branch,
3105 )
3106
3107 # We can't update this ref with git worktrees until it exists.
3108 # We'll wait until the initial checkout to set it.
3109 if not os.path.exists(self.worktree):
3110 return
3111
3112 base = R_WORKTREE_M
3113 active_git = self.work_git
3114
3115 self._InitAnyMRef(HEAD, self.bare_git, detach=True)
3116 else:
3117 base = R_M
3118 active_git = self.bare_git
3119
3120 self._InitAnyMRef(base + self.manifest.branch, active_git)
3121
3122 def _InitMirrorHead(self):
3123 self._InitAnyMRef(HEAD, self.bare_git)
3124
3125 def _InitAnyMRef(self, ref, active_git, detach=False):
3126 """Initialize |ref| in |active_git| to the value in the manifest.
3127
3128 This points |ref| to the <project> setting in the manifest.
3129
3130 Args:
3131 ref: The branch to update.
3132 active_git: The git repository to make updates in.
3133 detach: Whether to update target of symbolic refs, or overwrite the
3134 ref directly (and thus make it non-symbolic).
3135 """
3136 cur = self.bare_ref.symref(ref)
3137
3138 if self.revisionId:
3139 if cur != "" or self.bare_ref.get(ref) != self.revisionId:
3140 msg = "manifest set to %s" % self.revisionId
3141 dst = self.revisionId + "^0"
3142 active_git.UpdateRef(ref, dst, message=msg, detach=True)
Julien Camperguedd654222014-01-09 16:21:37 +01003143 else:
Gavin Makea2e3302023-03-11 06:46:20 +00003144 remote = self.GetRemote()
3145 dst = remote.ToLocal(self.revisionExpr)
3146 if cur != dst:
3147 msg = "manifest set to %s" % self.revisionExpr
3148 if detach:
3149 active_git.UpdateRef(ref, dst, message=msg, detach=True)
3150 else:
3151 active_git.symbolic_ref("-m", msg, ref, dst)
Julien Camperguedd654222014-01-09 16:21:37 +01003152
Gavin Makea2e3302023-03-11 06:46:20 +00003153 def _CheckDirReference(self, srcdir, destdir):
3154 # Git worktrees don't use symlinks to share at all.
3155 if self.use_git_worktrees:
3156 return
Julien Camperguedd654222014-01-09 16:21:37 +01003157
Gavin Makea2e3302023-03-11 06:46:20 +00003158 for name in self.shareable_dirs:
3159 # Try to self-heal a bit in simple cases.
3160 dst_path = os.path.join(destdir, name)
3161 src_path = os.path.join(srcdir, name)
Julien Camperguedd654222014-01-09 16:21:37 +01003162
Gavin Makea2e3302023-03-11 06:46:20 +00003163 dst = platform_utils.realpath(dst_path)
3164 if os.path.lexists(dst):
3165 src = platform_utils.realpath(src_path)
3166 # Fail if the links are pointing to the wrong place.
3167 if src != dst:
3168 _error("%s is different in %s vs %s", name, destdir, srcdir)
3169 raise GitError(
3170 "--force-sync not enabled; cannot overwrite a local "
3171 "work tree. If you're comfortable with the "
3172 "possibility of losing the work tree's git metadata,"
3173 " use `repo sync --force-sync {0}` to "
Jason Chang32b59562023-07-14 16:45:35 -07003174 "proceed.".format(self.RelPath(local=False)),
3175 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003176 )
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003177
Gavin Makea2e3302023-03-11 06:46:20 +00003178 def _ReferenceGitDir(self, gitdir, dotgit, copy_all):
3179 """Update |dotgit| to reference |gitdir|, using symlinks where possible.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003180
Gavin Makea2e3302023-03-11 06:46:20 +00003181 Args:
3182 gitdir: The bare git repository. Must already be initialized.
3183 dotgit: The repository you would like to initialize.
3184 copy_all: If true, copy all remaining files from |gitdir| ->
3185 |dotgit|. This saves you the effort of initializing |dotgit|
3186 yourself.
3187 """
3188 symlink_dirs = self.shareable_dirs[:]
3189 to_symlink = symlink_dirs
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003190
Gavin Makea2e3302023-03-11 06:46:20 +00003191 to_copy = []
3192 if copy_all:
3193 to_copy = platform_utils.listdir(gitdir)
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003194
Gavin Makea2e3302023-03-11 06:46:20 +00003195 dotgit = platform_utils.realpath(dotgit)
3196 for name in set(to_copy).union(to_symlink):
3197 try:
3198 src = platform_utils.realpath(os.path.join(gitdir, name))
3199 dst = os.path.join(dotgit, name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003200
Gavin Makea2e3302023-03-11 06:46:20 +00003201 if os.path.lexists(dst):
3202 continue
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003203
Gavin Makea2e3302023-03-11 06:46:20 +00003204 # If the source dir doesn't exist, create an empty dir.
3205 if name in symlink_dirs and not os.path.lexists(src):
3206 os.makedirs(src)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003207
Gavin Makea2e3302023-03-11 06:46:20 +00003208 if name in to_symlink:
3209 platform_utils.symlink(
3210 os.path.relpath(src, os.path.dirname(dst)), dst
3211 )
3212 elif copy_all and not platform_utils.islink(dst):
3213 if platform_utils.isdir(src):
3214 shutil.copytree(src, dst)
3215 elif os.path.isfile(src):
3216 shutil.copy(src, dst)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003217
Gavin Makea2e3302023-03-11 06:46:20 +00003218 except OSError as e:
3219 if e.errno == errno.EPERM:
3220 raise DownloadError(self._get_symlink_error_message())
3221 else:
3222 raise
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003223
Gavin Makea2e3302023-03-11 06:46:20 +00003224 def _InitGitWorktree(self):
3225 """Init the project using git worktrees."""
3226 self.bare_git.worktree("prune")
3227 self.bare_git.worktree(
3228 "add",
3229 "-ff",
3230 "--checkout",
3231 "--detach",
3232 "--lock",
3233 self.worktree,
3234 self.GetRevisionId(),
3235 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003236
Gavin Makea2e3302023-03-11 06:46:20 +00003237 # Rewrite the internal state files to use relative paths between the
3238 # checkouts & worktrees.
3239 dotgit = os.path.join(self.worktree, ".git")
3240 with open(dotgit, "r") as fp:
3241 # Figure out the checkout->worktree path.
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003242 setting = fp.read()
Gavin Makea2e3302023-03-11 06:46:20 +00003243 assert setting.startswith("gitdir:")
3244 git_worktree_path = setting.split(":", 1)[1].strip()
3245 # Some platforms (e.g. Windows) won't let us update dotgit in situ
3246 # because of file permissions. Delete it and recreate it from scratch
3247 # to avoid.
3248 platform_utils.remove(dotgit)
3249 # Use relative path from checkout->worktree & maintain Unix line endings
3250 # on all OS's to match git behavior.
3251 with open(dotgit, "w", newline="\n") as fp:
3252 print(
3253 "gitdir:",
3254 os.path.relpath(git_worktree_path, self.worktree),
3255 file=fp,
3256 )
3257 # Use relative path from worktree->checkout & maintain Unix line endings
3258 # on all OS's to match git behavior.
3259 with open(
3260 os.path.join(git_worktree_path, "gitdir"), "w", newline="\n"
3261 ) as fp:
3262 print(os.path.relpath(dotgit, git_worktree_path), file=fp)
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003263
Gavin Makea2e3302023-03-11 06:46:20 +00003264 self._InitMRef()
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003265
Gavin Makea2e3302023-03-11 06:46:20 +00003266 def _InitWorkTree(self, force_sync=False, submodules=False):
3267 """Setup the worktree .git path.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003268
Gavin Makea2e3302023-03-11 06:46:20 +00003269 This is the user-visible path like src/foo/.git/.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003270
Gavin Makea2e3302023-03-11 06:46:20 +00003271 With non-git-worktrees, this will be a symlink to the .repo/projects/
3272 path. With git-worktrees, this will be a .git file using "gitdir: ..."
3273 syntax.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003274
Gavin Makea2e3302023-03-11 06:46:20 +00003275 Older checkouts had .git/ directories. If we see that, migrate it.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003276
Gavin Makea2e3302023-03-11 06:46:20 +00003277 This also handles changes in the manifest. Maybe this project was
3278 backed by "foo/bar" on the server, but now it's "new/foo/bar". We have
3279 to update the path we point to under .repo/projects/ to match.
3280 """
3281 dotgit = os.path.join(self.worktree, ".git")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003282
Gavin Makea2e3302023-03-11 06:46:20 +00003283 # If using an old layout style (a directory), migrate it.
3284 if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
Jason Chang32b59562023-07-14 16:45:35 -07003285 self._MigrateOldWorkTreeGitDir(dotgit, project=self.name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003286
Gavin Makea2e3302023-03-11 06:46:20 +00003287 init_dotgit = not os.path.exists(dotgit)
3288 if self.use_git_worktrees:
3289 if init_dotgit:
3290 self._InitGitWorktree()
3291 self._CopyAndLinkFiles()
3292 else:
3293 if not init_dotgit:
3294 # See if the project has changed.
3295 if platform_utils.realpath(
3296 self.gitdir
3297 ) != platform_utils.realpath(dotgit):
3298 platform_utils.remove(dotgit)
Doug Anderson37282b42011-03-04 11:54:18 -08003299
Gavin Makea2e3302023-03-11 06:46:20 +00003300 if init_dotgit or not os.path.exists(dotgit):
3301 os.makedirs(self.worktree, exist_ok=True)
3302 platform_utils.symlink(
3303 os.path.relpath(self.gitdir, self.worktree), dotgit
3304 )
Doug Anderson37282b42011-03-04 11:54:18 -08003305
Gavin Makea2e3302023-03-11 06:46:20 +00003306 if init_dotgit:
3307 _lwrite(
3308 os.path.join(dotgit, HEAD), "%s\n" % self.GetRevisionId()
3309 )
Doug Anderson37282b42011-03-04 11:54:18 -08003310
Gavin Makea2e3302023-03-11 06:46:20 +00003311 # Finish checking out the worktree.
3312 cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
3313 if GitCommand(self, cmd).Wait() != 0:
3314 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07003315 "Cannot initialize work tree for " + self.name,
3316 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003317 )
Doug Anderson37282b42011-03-04 11:54:18 -08003318
Gavin Makea2e3302023-03-11 06:46:20 +00003319 if submodules:
3320 self._SyncSubmodules(quiet=True)
3321 self._CopyAndLinkFiles()
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003322
Gavin Makea2e3302023-03-11 06:46:20 +00003323 @classmethod
Jason Chang32b59562023-07-14 16:45:35 -07003324 def _MigrateOldWorkTreeGitDir(cls, dotgit, project=None):
Gavin Makea2e3302023-03-11 06:46:20 +00003325 """Migrate the old worktree .git/ dir style to a symlink.
3326
3327 This logic specifically only uses state from |dotgit| to figure out
3328 where to move content and not |self|. This way if the backing project
3329 also changed places, we only do the .git/ dir to .git symlink migration
3330 here. The path updates will happen independently.
3331 """
3332 # Figure out where in .repo/projects/ it's pointing to.
3333 if not os.path.islink(os.path.join(dotgit, "refs")):
Jason Chang32b59562023-07-14 16:45:35 -07003334 raise GitError(
3335 f"{dotgit}: unsupported checkout state", project=project
3336 )
Gavin Makea2e3302023-03-11 06:46:20 +00003337 gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, "refs")))
3338
3339 # Remove known symlink paths that exist in .repo/projects/.
3340 KNOWN_LINKS = {
3341 "config",
3342 "description",
3343 "hooks",
3344 "info",
3345 "logs",
3346 "objects",
3347 "packed-refs",
3348 "refs",
3349 "rr-cache",
3350 "shallow",
3351 "svn",
3352 }
3353 # Paths that we know will be in both, but are safe to clobber in
3354 # .repo/projects/.
3355 SAFE_TO_CLOBBER = {
3356 "COMMIT_EDITMSG",
3357 "FETCH_HEAD",
3358 "HEAD",
3359 "gc.log",
3360 "gitk.cache",
3361 "index",
3362 "ORIG_HEAD",
3363 }
3364
3365 # First see if we'd succeed before starting the migration.
3366 unknown_paths = []
3367 for name in platform_utils.listdir(dotgit):
3368 # Ignore all temporary/backup names. These are common with vim &
3369 # emacs.
3370 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3371 continue
3372
3373 dotgit_path = os.path.join(dotgit, name)
3374 if name in KNOWN_LINKS:
3375 if not platform_utils.islink(dotgit_path):
3376 unknown_paths.append(f"{dotgit_path}: should be a symlink")
3377 else:
3378 gitdir_path = os.path.join(gitdir, name)
3379 if name not in SAFE_TO_CLOBBER and os.path.exists(gitdir_path):
3380 unknown_paths.append(
3381 f"{dotgit_path}: unknown file; please file a bug"
3382 )
3383 if unknown_paths:
Jason Chang32b59562023-07-14 16:45:35 -07003384 raise GitError(
3385 "Aborting migration: " + "\n".join(unknown_paths),
3386 project=project,
3387 )
Gavin Makea2e3302023-03-11 06:46:20 +00003388
3389 # Now walk the paths and sync the .git/ to .repo/projects/.
3390 for name in platform_utils.listdir(dotgit):
3391 dotgit_path = os.path.join(dotgit, name)
3392
3393 # Ignore all temporary/backup names. These are common with vim &
3394 # emacs.
3395 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3396 platform_utils.remove(dotgit_path)
3397 elif name in KNOWN_LINKS:
3398 platform_utils.remove(dotgit_path)
3399 else:
3400 gitdir_path = os.path.join(gitdir, name)
3401 platform_utils.remove(gitdir_path, missing_ok=True)
3402 platform_utils.rename(dotgit_path, gitdir_path)
3403
3404 # Now that the dir should be empty, clear it out, and symlink it over.
3405 platform_utils.rmdir(dotgit)
3406 platform_utils.symlink(
3407 os.path.relpath(gitdir, os.path.dirname(dotgit)), dotgit
3408 )
3409
3410 def _get_symlink_error_message(self):
3411 if platform_utils.isWindows():
3412 return (
3413 "Unable to create symbolic link. Please re-run the command as "
3414 "Administrator, or see "
3415 "https://github.com/git-for-windows/git/wiki/Symbolic-Links "
3416 "for other options."
3417 )
3418 return "filesystem must support symlinks"
3419
3420 def _revlist(self, *args, **kw):
3421 a = []
3422 a.extend(args)
3423 a.append("--")
3424 return self.work_git.rev_list(*a, **kw)
3425
3426 @property
3427 def _allrefs(self):
3428 return self.bare_ref.all
3429
3430 def _getLogs(
3431 self, rev1, rev2, oneline=False, color=True, pretty_format=None
3432 ):
3433 """Get logs between two revisions of this project."""
3434 comp = ".."
3435 if rev1:
3436 revs = [rev1]
3437 if rev2:
3438 revs.extend([comp, rev2])
3439 cmd = ["log", "".join(revs)]
3440 out = DiffColoring(self.config)
3441 if out.is_on and color:
3442 cmd.append("--color")
3443 if pretty_format is not None:
3444 cmd.append("--pretty=format:%s" % pretty_format)
3445 if oneline:
3446 cmd.append("--oneline")
3447
3448 try:
3449 log = GitCommand(
3450 self, cmd, capture_stdout=True, capture_stderr=True
3451 )
3452 if log.Wait() == 0:
3453 return log.stdout
3454 except GitError:
3455 # worktree may not exist if groups changed for example. In that
3456 # case, try in gitdir instead.
3457 if not os.path.exists(self.worktree):
3458 return self.bare_git.log(*cmd[1:])
3459 else:
3460 raise
3461 return None
3462
3463 def getAddedAndRemovedLogs(
3464 self, toProject, oneline=False, color=True, pretty_format=None
3465 ):
3466 """Get the list of logs from this revision to given revisionId"""
3467 logs = {}
3468 selfId = self.GetRevisionId(self._allrefs)
3469 toId = toProject.GetRevisionId(toProject._allrefs)
3470
3471 logs["added"] = self._getLogs(
3472 selfId,
3473 toId,
3474 oneline=oneline,
3475 color=color,
3476 pretty_format=pretty_format,
3477 )
3478 logs["removed"] = self._getLogs(
3479 toId,
3480 selfId,
3481 oneline=oneline,
3482 color=color,
3483 pretty_format=pretty_format,
3484 )
3485 return logs
3486
3487 class _GitGetByExec(object):
3488 def __init__(self, project, bare, gitdir):
3489 self._project = project
3490 self._bare = bare
3491 self._gitdir = gitdir
3492
3493 # __getstate__ and __setstate__ are required for pickling because
3494 # __getattr__ exists.
3495 def __getstate__(self):
3496 return (self._project, self._bare, self._gitdir)
3497
3498 def __setstate__(self, state):
3499 self._project, self._bare, self._gitdir = state
3500
3501 def LsOthers(self):
3502 p = GitCommand(
3503 self._project,
3504 ["ls-files", "-z", "--others", "--exclude-standard"],
3505 bare=False,
3506 gitdir=self._gitdir,
3507 capture_stdout=True,
3508 capture_stderr=True,
3509 )
3510 if p.Wait() == 0:
3511 out = p.stdout
3512 if out:
3513 # Backslash is not anomalous.
3514 return out[:-1].split("\0")
3515 return []
3516
3517 def DiffZ(self, name, *args):
3518 cmd = [name]
3519 cmd.append("-z")
3520 cmd.append("--ignore-submodules")
3521 cmd.extend(args)
3522 p = GitCommand(
3523 self._project,
3524 cmd,
3525 gitdir=self._gitdir,
3526 bare=False,
3527 capture_stdout=True,
3528 capture_stderr=True,
3529 )
3530 p.Wait()
3531 r = {}
3532 out = p.stdout
3533 if out:
3534 out = iter(out[:-1].split("\0"))
3535 while out:
3536 try:
3537 info = next(out)
3538 path = next(out)
3539 except StopIteration:
3540 break
3541
3542 class _Info(object):
3543 def __init__(self, path, omode, nmode, oid, nid, state):
3544 self.path = path
3545 self.src_path = None
3546 self.old_mode = omode
3547 self.new_mode = nmode
3548 self.old_id = oid
3549 self.new_id = nid
3550
3551 if len(state) == 1:
3552 self.status = state
3553 self.level = None
3554 else:
3555 self.status = state[:1]
3556 self.level = state[1:]
3557 while self.level.startswith("0"):
3558 self.level = self.level[1:]
3559
3560 info = info[1:].split(" ")
3561 info = _Info(path, *info)
3562 if info.status in ("R", "C"):
3563 info.src_path = info.path
3564 info.path = next(out)
3565 r[info.path] = info
3566 return r
3567
3568 def GetDotgitPath(self, subpath=None):
3569 """Return the full path to the .git dir.
3570
3571 As a convenience, append |subpath| if provided.
3572 """
3573 if self._bare:
3574 dotgit = self._gitdir
3575 else:
3576 dotgit = os.path.join(self._project.worktree, ".git")
3577 if os.path.isfile(dotgit):
3578 # Git worktrees use a "gitdir:" syntax to point to the
3579 # scratch space.
3580 with open(dotgit) as fp:
3581 setting = fp.read()
3582 assert setting.startswith("gitdir:")
3583 gitdir = setting.split(":", 1)[1].strip()
3584 dotgit = os.path.normpath(
3585 os.path.join(self._project.worktree, gitdir)
3586 )
3587
3588 return dotgit if subpath is None else os.path.join(dotgit, subpath)
3589
3590 def GetHead(self):
3591 """Return the ref that HEAD points to."""
3592 path = self.GetDotgitPath(subpath=HEAD)
3593 try:
3594 with open(path) as fd:
3595 line = fd.readline()
3596 except IOError as e:
3597 raise NoManifestException(path, str(e))
3598 try:
3599 line = line.decode()
3600 except AttributeError:
3601 pass
3602 if line.startswith("ref: "):
3603 return line[5:-1]
3604 return line[:-1]
3605
3606 def SetHead(self, ref, message=None):
3607 cmdv = []
3608 if message is not None:
3609 cmdv.extend(["-m", message])
3610 cmdv.append(HEAD)
3611 cmdv.append(ref)
3612 self.symbolic_ref(*cmdv)
3613
3614 def DetachHead(self, new, message=None):
3615 cmdv = ["--no-deref"]
3616 if message is not None:
3617 cmdv.extend(["-m", message])
3618 cmdv.append(HEAD)
3619 cmdv.append(new)
3620 self.update_ref(*cmdv)
3621
3622 def UpdateRef(self, name, new, old=None, message=None, detach=False):
3623 cmdv = []
3624 if message is not None:
3625 cmdv.extend(["-m", message])
3626 if detach:
3627 cmdv.append("--no-deref")
3628 cmdv.append(name)
3629 cmdv.append(new)
3630 if old is not None:
3631 cmdv.append(old)
3632 self.update_ref(*cmdv)
3633
3634 def DeleteRef(self, name, old=None):
3635 if not old:
3636 old = self.rev_parse(name)
3637 self.update_ref("-d", name, old)
3638 self._project.bare_ref.deleted(name)
3639
3640 def rev_list(self, *args, **kw):
3641 if "format" in kw:
3642 cmdv = ["log", "--pretty=format:%s" % kw["format"]]
3643 else:
3644 cmdv = ["rev-list"]
3645 cmdv.extend(args)
3646 p = GitCommand(
3647 self._project,
3648 cmdv,
3649 bare=self._bare,
3650 gitdir=self._gitdir,
3651 capture_stdout=True,
3652 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003653 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00003654 )
Jason Chang32b59562023-07-14 16:45:35 -07003655 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003656 return p.stdout.splitlines()
3657
3658 def __getattr__(self, name):
3659 """Allow arbitrary git commands using pythonic syntax.
3660
3661 This allows you to do things like:
3662 git_obj.rev_parse('HEAD')
3663
3664 Since we don't have a 'rev_parse' method defined, the __getattr__
3665 will run. We'll replace the '_' with a '-' and try to run a git
3666 command. Any other positional arguments will be passed to the git
3667 command, and the following keyword arguments are supported:
3668 config: An optional dict of git config options to be passed with
3669 '-c'.
3670
3671 Args:
3672 name: The name of the git command to call. Any '_' characters
3673 will be replaced with '-'.
3674
3675 Returns:
3676 A callable object that will try to call git with the named
3677 command.
3678 """
3679 name = name.replace("_", "-")
3680
3681 def runner(*args, **kwargs):
3682 cmdv = []
3683 config = kwargs.pop("config", None)
3684 for k in kwargs:
3685 raise TypeError(
3686 "%s() got an unexpected keyword argument %r" % (name, k)
3687 )
3688 if config is not None:
3689 for k, v in config.items():
3690 cmdv.append("-c")
3691 cmdv.append("%s=%s" % (k, v))
3692 cmdv.append(name)
3693 cmdv.extend(args)
3694 p = GitCommand(
3695 self._project,
3696 cmdv,
3697 bare=self._bare,
3698 gitdir=self._gitdir,
3699 capture_stdout=True,
3700 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003701 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00003702 )
Jason Chang32b59562023-07-14 16:45:35 -07003703 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003704 r = p.stdout
3705 if r.endswith("\n") and r.index("\n") == len(r) - 1:
3706 return r[:-1]
3707 return r
3708
3709 return runner
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003710
3711
Jason Chang32b59562023-07-14 16:45:35 -07003712class LocalSyncFail(RepoError):
3713 """Default error when there is an Sync_LocalHalf error."""
3714
3715
3716class _PriorSyncFailedError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003717 def __str__(self):
3718 return "prior sync failed; rebase still in progress"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003719
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003720
Jason Chang32b59562023-07-14 16:45:35 -07003721class _DirtyError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003722 def __str__(self):
3723 return "contains uncommitted changes"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003724
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003725
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003726class _InfoMessage(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003727 def __init__(self, project, text):
3728 self.project = project
3729 self.text = text
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003730
Gavin Makea2e3302023-03-11 06:46:20 +00003731 def Print(self, syncbuf):
3732 syncbuf.out.info(
3733 "%s/: %s", self.project.RelPath(local=False), self.text
3734 )
3735 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003736
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003737
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003738class _Failure(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003739 def __init__(self, project, why):
3740 self.project = project
3741 self.why = why
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003742
Gavin Makea2e3302023-03-11 06:46:20 +00003743 def Print(self, syncbuf):
3744 syncbuf.out.fail(
3745 "error: %s/: %s", self.project.RelPath(local=False), str(self.why)
3746 )
3747 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003748
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003749
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003750class _Later(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003751 def __init__(self, project, action):
3752 self.project = project
3753 self.action = action
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003754
Gavin Makea2e3302023-03-11 06:46:20 +00003755 def Run(self, syncbuf):
3756 out = syncbuf.out
3757 out.project("project %s/", self.project.RelPath(local=False))
3758 out.nl()
3759 try:
3760 self.action()
3761 out.nl()
3762 return True
3763 except GitError:
3764 out.nl()
3765 return False
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003766
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003767
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003768class _SyncColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +00003769 def __init__(self, config):
3770 super().__init__(config, "reposync")
3771 self.project = self.printer("header", attr="bold")
3772 self.info = self.printer("info")
3773 self.fail = self.printer("fail", fg="red")
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003774
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003775
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003776class SyncBuffer(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003777 def __init__(self, config, detach_head=False):
3778 self._messages = []
3779 self._failures = []
3780 self._later_queue1 = []
3781 self._later_queue2 = []
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003782
Gavin Makea2e3302023-03-11 06:46:20 +00003783 self.out = _SyncColoring(config)
3784 self.out.redirect(sys.stderr)
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003785
Gavin Makea2e3302023-03-11 06:46:20 +00003786 self.detach_head = detach_head
3787 self.clean = True
3788 self.recent_clean = True
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003789
Gavin Makea2e3302023-03-11 06:46:20 +00003790 def info(self, project, fmt, *args):
3791 self._messages.append(_InfoMessage(project, fmt % args))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003792
Gavin Makea2e3302023-03-11 06:46:20 +00003793 def fail(self, project, err=None):
3794 self._failures.append(_Failure(project, err))
David Rileye0684ad2017-04-05 00:02:59 -07003795 self._MarkUnclean()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003796
Gavin Makea2e3302023-03-11 06:46:20 +00003797 def later1(self, project, what):
3798 self._later_queue1.append(_Later(project, what))
Mike Frysinger70d861f2019-08-26 15:22:36 -04003799
Gavin Makea2e3302023-03-11 06:46:20 +00003800 def later2(self, project, what):
3801 self._later_queue2.append(_Later(project, what))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003802
Gavin Makea2e3302023-03-11 06:46:20 +00003803 def Finish(self):
3804 self._PrintMessages()
3805 self._RunLater()
3806 self._PrintMessages()
3807 return self.clean
3808
3809 def Recently(self):
3810 recent_clean = self.recent_clean
3811 self.recent_clean = True
3812 return recent_clean
3813
3814 def _MarkUnclean(self):
3815 self.clean = False
3816 self.recent_clean = False
3817
3818 def _RunLater(self):
3819 for q in ["_later_queue1", "_later_queue2"]:
3820 if not self._RunQueue(q):
3821 return
3822
3823 def _RunQueue(self, queue):
3824 for m in getattr(self, queue):
3825 if not m.Run(self):
3826 self._MarkUnclean()
3827 return False
3828 setattr(self, queue, [])
3829 return True
3830
3831 def _PrintMessages(self):
3832 if self._messages or self._failures:
3833 if os.isatty(2):
3834 self.out.write(progress.CSI_ERASE_LINE)
3835 self.out.write("\r")
3836
3837 for m in self._messages:
3838 m.Print(self)
3839 for m in self._failures:
3840 m.Print(self)
3841
3842 self._messages = []
3843 self._failures = []
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003844
3845
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003846class MetaProject(Project):
Gavin Makea2e3302023-03-11 06:46:20 +00003847 """A special project housed under .repo."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003848
Gavin Makea2e3302023-03-11 06:46:20 +00003849 def __init__(self, manifest, name, gitdir, worktree):
3850 Project.__init__(
3851 self,
3852 manifest=manifest,
3853 name=name,
3854 gitdir=gitdir,
3855 objdir=gitdir,
3856 worktree=worktree,
3857 remote=RemoteSpec("origin"),
3858 relpath=".repo/%s" % name,
3859 revisionExpr="refs/heads/master",
3860 revisionId=None,
3861 groups=None,
3862 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003863
Gavin Makea2e3302023-03-11 06:46:20 +00003864 def PreSync(self):
3865 if self.Exists:
3866 cb = self.CurrentBranch
3867 if cb:
3868 base = self.GetBranch(cb).merge
3869 if base:
3870 self.revisionExpr = base
3871 self.revisionId = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003872
Gavin Makea2e3302023-03-11 06:46:20 +00003873 @property
3874 def HasChanges(self):
3875 """Has the remote received new commits not yet checked out?"""
3876 if not self.remote or not self.revisionExpr:
3877 return False
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003878
Gavin Makea2e3302023-03-11 06:46:20 +00003879 all_refs = self.bare_ref.all
3880 revid = self.GetRevisionId(all_refs)
3881 head = self.work_git.GetHead()
3882 if head.startswith(R_HEADS):
3883 try:
3884 head = all_refs[head]
3885 except KeyError:
3886 head = None
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003887
Gavin Makea2e3302023-03-11 06:46:20 +00003888 if revid == head:
3889 return False
3890 elif self._revlist(not_rev(HEAD), revid):
3891 return True
3892 return False
LaMont Jones9b72cf22022-03-29 21:54:22 +00003893
3894
3895class RepoProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003896 """The MetaProject for repo itself."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003897
Gavin Makea2e3302023-03-11 06:46:20 +00003898 @property
3899 def LastFetch(self):
3900 try:
3901 fh = os.path.join(self.gitdir, "FETCH_HEAD")
3902 return os.path.getmtime(fh)
3903 except OSError:
3904 return 0
LaMont Jones9b72cf22022-03-29 21:54:22 +00003905
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +01003906
LaMont Jones9b72cf22022-03-29 21:54:22 +00003907class ManifestProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003908 """The MetaProject for manifests."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003909
Gavin Makea2e3302023-03-11 06:46:20 +00003910 def MetaBranchSwitch(self, submodules=False):
3911 """Prepare for manifest branch switch."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003912
Gavin Makea2e3302023-03-11 06:46:20 +00003913 # detach and delete manifest branch, allowing a new
3914 # branch to take over
3915 syncbuf = SyncBuffer(self.config, detach_head=True)
3916 self.Sync_LocalHalf(syncbuf, submodules=submodules)
3917 syncbuf.Finish()
LaMont Jones9b72cf22022-03-29 21:54:22 +00003918
Gavin Makea2e3302023-03-11 06:46:20 +00003919 return (
3920 GitCommand(
3921 self,
3922 ["update-ref", "-d", "refs/heads/default"],
3923 capture_stdout=True,
3924 capture_stderr=True,
3925 ).Wait()
3926 == 0
3927 )
LaMont Jones9b03f152022-03-29 23:01:18 +00003928
Gavin Makea2e3302023-03-11 06:46:20 +00003929 @property
3930 def standalone_manifest_url(self):
3931 """The URL of the standalone manifest, or None."""
3932 return self.config.GetString("manifest.standalone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003933
Gavin Makea2e3302023-03-11 06:46:20 +00003934 @property
3935 def manifest_groups(self):
3936 """The manifest groups string."""
3937 return self.config.GetString("manifest.groups")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003938
Gavin Makea2e3302023-03-11 06:46:20 +00003939 @property
3940 def reference(self):
3941 """The --reference for this manifest."""
3942 return self.config.GetString("repo.reference")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003943
Gavin Makea2e3302023-03-11 06:46:20 +00003944 @property
3945 def dissociate(self):
3946 """Whether to dissociate."""
3947 return self.config.GetBoolean("repo.dissociate")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003948
Gavin Makea2e3302023-03-11 06:46:20 +00003949 @property
3950 def archive(self):
3951 """Whether we use archive."""
3952 return self.config.GetBoolean("repo.archive")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003953
Gavin Makea2e3302023-03-11 06:46:20 +00003954 @property
3955 def mirror(self):
3956 """Whether we use mirror."""
3957 return self.config.GetBoolean("repo.mirror")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003958
Gavin Makea2e3302023-03-11 06:46:20 +00003959 @property
3960 def use_worktree(self):
3961 """Whether we use worktree."""
3962 return self.config.GetBoolean("repo.worktree")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003963
Gavin Makea2e3302023-03-11 06:46:20 +00003964 @property
3965 def clone_bundle(self):
3966 """Whether we use clone_bundle."""
3967 return self.config.GetBoolean("repo.clonebundle")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003968
Gavin Makea2e3302023-03-11 06:46:20 +00003969 @property
3970 def submodules(self):
3971 """Whether we use submodules."""
3972 return self.config.GetBoolean("repo.submodules")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003973
Gavin Makea2e3302023-03-11 06:46:20 +00003974 @property
3975 def git_lfs(self):
3976 """Whether we use git_lfs."""
3977 return self.config.GetBoolean("repo.git-lfs")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003978
Gavin Makea2e3302023-03-11 06:46:20 +00003979 @property
3980 def use_superproject(self):
3981 """Whether we use superproject."""
3982 return self.config.GetBoolean("repo.superproject")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003983
Gavin Makea2e3302023-03-11 06:46:20 +00003984 @property
3985 def partial_clone(self):
3986 """Whether this is a partial clone."""
3987 return self.config.GetBoolean("repo.partialclone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003988
Gavin Makea2e3302023-03-11 06:46:20 +00003989 @property
3990 def depth(self):
3991 """Partial clone depth."""
3992 return self.config.GetString("repo.depth")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003993
Gavin Makea2e3302023-03-11 06:46:20 +00003994 @property
3995 def clone_filter(self):
3996 """The clone filter."""
3997 return self.config.GetString("repo.clonefilter")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003998
Gavin Makea2e3302023-03-11 06:46:20 +00003999 @property
4000 def partial_clone_exclude(self):
4001 """Partial clone exclude string"""
4002 return self.config.GetString("repo.partialcloneexclude")
LaMont Jones4ada0432022-04-14 15:10:43 +00004003
Gavin Makea2e3302023-03-11 06:46:20 +00004004 @property
Jason Chang17833322023-05-23 13:06:55 -07004005 def clone_filter_for_depth(self):
4006 """Replace shallow clone with partial clone."""
4007 return self.config.GetString("repo.clonefilterfordepth")
4008
4009 @property
Gavin Makea2e3302023-03-11 06:46:20 +00004010 def manifest_platform(self):
4011 """The --platform argument from `repo init`."""
4012 return self.config.GetString("manifest.platform")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004013
Gavin Makea2e3302023-03-11 06:46:20 +00004014 @property
4015 def _platform_name(self):
4016 """Return the name of the platform."""
4017 return platform.system().lower()
LaMont Jones9b03f152022-03-29 23:01:18 +00004018
Gavin Makea2e3302023-03-11 06:46:20 +00004019 def SyncWithPossibleInit(
4020 self,
4021 submanifest,
4022 verbose=False,
4023 current_branch_only=False,
4024 tags="",
4025 git_event_log=None,
4026 ):
4027 """Sync a manifestProject, possibly for the first time.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004028
Gavin Makea2e3302023-03-11 06:46:20 +00004029 Call Sync() with arguments from the most recent `repo init`. If this is
4030 a new sub manifest, then inherit options from the parent's
4031 manifestProject.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004032
Gavin Makea2e3302023-03-11 06:46:20 +00004033 This is used by subcmds.Sync() to do an initial download of new sub
4034 manifests.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004035
Gavin Makea2e3302023-03-11 06:46:20 +00004036 Args:
4037 submanifest: an XmlSubmanifest, the submanifest to re-sync.
4038 verbose: a boolean, whether to show all output, rather than only
4039 errors.
4040 current_branch_only: a boolean, whether to only fetch the current
4041 manifest branch from the server.
4042 tags: a boolean, whether to fetch tags.
4043 git_event_log: an EventLog, for git tracing.
4044 """
4045 # TODO(lamontjones): when refactoring sync (and init?) consider how to
4046 # better get the init options that we should use for new submanifests
4047 # that are added when syncing an existing workspace.
4048 git_event_log = git_event_log or EventLog()
LaMont Jonesb90a4222022-04-14 15:00:09 +00004049 spec = submanifest.ToSubmanifestSpec()
Gavin Makea2e3302023-03-11 06:46:20 +00004050 # Use the init options from the existing manifestProject, or the parent
4051 # if it doesn't exist.
4052 #
4053 # Today, we only support changing manifest_groups on the sub-manifest,
4054 # with no supported-for-the-user way to change the other arguments from
4055 # those specified by the outermost manifest.
4056 #
4057 # TODO(lamontjones): determine which of these should come from the
4058 # outermost manifest and which should come from the parent manifest.
4059 mp = self if self.Exists else submanifest.parent.manifestProject
4060 return self.Sync(
LaMont Jones55ee3042022-04-06 17:10:21 +00004061 manifest_url=spec.manifestUrl,
4062 manifest_branch=spec.revision,
Gavin Makea2e3302023-03-11 06:46:20 +00004063 standalone_manifest=mp.standalone_manifest_url,
4064 groups=mp.manifest_groups,
4065 platform=mp.manifest_platform,
4066 mirror=mp.mirror,
4067 dissociate=mp.dissociate,
4068 reference=mp.reference,
4069 worktree=mp.use_worktree,
4070 submodules=mp.submodules,
4071 archive=mp.archive,
4072 partial_clone=mp.partial_clone,
4073 clone_filter=mp.clone_filter,
4074 partial_clone_exclude=mp.partial_clone_exclude,
4075 clone_bundle=mp.clone_bundle,
4076 git_lfs=mp.git_lfs,
4077 use_superproject=mp.use_superproject,
LaMont Jones55ee3042022-04-06 17:10:21 +00004078 verbose=verbose,
4079 current_branch_only=current_branch_only,
4080 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00004081 depth=mp.depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004082 git_event_log=git_event_log,
4083 manifest_name=spec.manifestName,
Gavin Makea2e3302023-03-11 06:46:20 +00004084 this_manifest_only=True,
LaMont Jones55ee3042022-04-06 17:10:21 +00004085 outer_manifest=False,
Jason Chang17833322023-05-23 13:06:55 -07004086 clone_filter_for_depth=mp.clone_filter_for_depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004087 )
LaMont Jones409407a2022-04-05 21:21:56 +00004088
Gavin Makea2e3302023-03-11 06:46:20 +00004089 def Sync(
4090 self,
4091 _kwargs_only=(),
4092 manifest_url="",
4093 manifest_branch=None,
4094 standalone_manifest=False,
4095 groups="",
4096 mirror=False,
4097 reference="",
4098 dissociate=False,
4099 worktree=False,
4100 submodules=False,
4101 archive=False,
4102 partial_clone=None,
4103 depth=None,
4104 clone_filter="blob:none",
4105 partial_clone_exclude=None,
4106 clone_bundle=None,
4107 git_lfs=None,
4108 use_superproject=None,
4109 verbose=False,
4110 current_branch_only=False,
4111 git_event_log=None,
4112 platform="",
4113 manifest_name="default.xml",
4114 tags="",
4115 this_manifest_only=False,
4116 outer_manifest=True,
Jason Chang17833322023-05-23 13:06:55 -07004117 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00004118 ):
4119 """Sync the manifest and all submanifests.
LaMont Jones409407a2022-04-05 21:21:56 +00004120
Gavin Makea2e3302023-03-11 06:46:20 +00004121 Args:
4122 manifest_url: a string, the URL of the manifest project.
4123 manifest_branch: a string, the manifest branch to use.
4124 standalone_manifest: a boolean, whether to store the manifest as a
4125 static file.
4126 groups: a string, restricts the checkout to projects with the
4127 specified groups.
4128 mirror: a boolean, whether to create a mirror of the remote
4129 repository.
4130 reference: a string, location of a repo instance to use as a
4131 reference.
4132 dissociate: a boolean, whether to dissociate from reference mirrors
4133 after clone.
4134 worktree: a boolean, whether to use git-worktree to manage projects.
4135 submodules: a boolean, whether sync submodules associated with the
4136 manifest project.
4137 archive: a boolean, whether to checkout each project as an archive.
4138 See git-archive.
4139 partial_clone: a boolean, whether to perform a partial clone.
4140 depth: an int, how deep of a shallow clone to create.
4141 clone_filter: a string, filter to use with partial_clone.
4142 partial_clone_exclude : a string, comma-delimeted list of project
4143 names to exclude from partial clone.
4144 clone_bundle: a boolean, whether to enable /clone.bundle on
4145 HTTP/HTTPS.
4146 git_lfs: a boolean, whether to enable git LFS support.
4147 use_superproject: a boolean, whether to use the manifest
4148 superproject to sync projects.
4149 verbose: a boolean, whether to show all output, rather than only
4150 errors.
4151 current_branch_only: a boolean, whether to only fetch the current
4152 manifest branch from the server.
4153 platform: a string, restrict the checkout to projects with the
4154 specified platform group.
4155 git_event_log: an EventLog, for git tracing.
4156 tags: a boolean, whether to fetch tags.
4157 manifest_name: a string, the name of the manifest file to use.
4158 this_manifest_only: a boolean, whether to only operate on the
4159 current sub manifest.
4160 outer_manifest: a boolean, whether to start at the outermost
4161 manifest.
Jason Chang17833322023-05-23 13:06:55 -07004162 clone_filter_for_depth: a string, when specified replaces shallow
4163 clones with partial.
LaMont Jones9b03f152022-03-29 23:01:18 +00004164
Gavin Makea2e3302023-03-11 06:46:20 +00004165 Returns:
4166 a boolean, whether the sync was successful.
4167 """
4168 assert _kwargs_only == (), "Sync only accepts keyword arguments."
LaMont Jones9b03f152022-03-29 23:01:18 +00004169
Gavin Makea2e3302023-03-11 06:46:20 +00004170 groups = groups or self.manifest.GetDefaultGroupsStr(
4171 with_platform=False
4172 )
4173 platform = platform or "auto"
4174 git_event_log = git_event_log or EventLog()
4175 if outer_manifest and self.manifest.is_submanifest:
4176 # In a multi-manifest checkout, use the outer manifest unless we are
4177 # told not to.
4178 return self.client.outer_manifest.manifestProject.Sync(
4179 manifest_url=manifest_url,
4180 manifest_branch=manifest_branch,
4181 standalone_manifest=standalone_manifest,
4182 groups=groups,
4183 platform=platform,
4184 mirror=mirror,
4185 dissociate=dissociate,
4186 reference=reference,
4187 worktree=worktree,
4188 submodules=submodules,
4189 archive=archive,
4190 partial_clone=partial_clone,
4191 clone_filter=clone_filter,
4192 partial_clone_exclude=partial_clone_exclude,
4193 clone_bundle=clone_bundle,
4194 git_lfs=git_lfs,
4195 use_superproject=use_superproject,
4196 verbose=verbose,
4197 current_branch_only=current_branch_only,
4198 tags=tags,
4199 depth=depth,
4200 git_event_log=git_event_log,
4201 manifest_name=manifest_name,
4202 this_manifest_only=this_manifest_only,
4203 outer_manifest=False,
4204 )
LaMont Jones9b03f152022-03-29 23:01:18 +00004205
Gavin Makea2e3302023-03-11 06:46:20 +00004206 # If repo has already been initialized, we take -u with the absence of
4207 # --standalone-manifest to mean "transition to a standard repo set up",
4208 # which necessitates starting fresh.
4209 # If --standalone-manifest is set, we always tear everything down and
4210 # start anew.
4211 if self.Exists:
4212 was_standalone_manifest = self.config.GetString(
4213 "manifest.standalone"
4214 )
4215 if was_standalone_manifest and not manifest_url:
4216 print(
4217 "fatal: repo was initialized with a standlone manifest, "
4218 "cannot be re-initialized without --manifest-url/-u"
4219 )
4220 return False
4221
4222 if standalone_manifest or (
4223 was_standalone_manifest and manifest_url
4224 ):
4225 self.config.ClearCache()
4226 if self.gitdir and os.path.exists(self.gitdir):
4227 platform_utils.rmtree(self.gitdir)
4228 if self.worktree and os.path.exists(self.worktree):
4229 platform_utils.rmtree(self.worktree)
4230
4231 is_new = not self.Exists
4232 if is_new:
4233 if not manifest_url:
4234 print("fatal: manifest url is required.", file=sys.stderr)
4235 return False
4236
4237 if verbose:
4238 print(
4239 "Downloading manifest from %s"
4240 % (GitConfig.ForUser().UrlInsteadOf(manifest_url),),
4241 file=sys.stderr,
4242 )
4243
4244 # The manifest project object doesn't keep track of the path on the
4245 # server where this git is located, so let's save that here.
4246 mirrored_manifest_git = None
4247 if reference:
4248 manifest_git_path = urllib.parse.urlparse(manifest_url).path[1:]
4249 mirrored_manifest_git = os.path.join(
4250 reference, manifest_git_path
4251 )
4252 if not mirrored_manifest_git.endswith(".git"):
4253 mirrored_manifest_git += ".git"
4254 if not os.path.exists(mirrored_manifest_git):
4255 mirrored_manifest_git = os.path.join(
4256 reference, ".repo/manifests.git"
4257 )
4258
4259 self._InitGitDir(mirror_git=mirrored_manifest_git)
4260
4261 # If standalone_manifest is set, mark the project as "standalone" --
4262 # we'll still do much of the manifests.git set up, but will avoid actual
4263 # syncs to a remote.
4264 if standalone_manifest:
4265 self.config.SetString("manifest.standalone", manifest_url)
4266 elif not manifest_url and not manifest_branch:
4267 # If -u is set and --standalone-manifest is not, then we're not in
4268 # standalone mode. Otherwise, use config to infer what we were in
4269 # the last init.
4270 standalone_manifest = bool(
4271 self.config.GetString("manifest.standalone")
4272 )
4273 if not standalone_manifest:
4274 self.config.SetString("manifest.standalone", None)
4275
4276 self._ConfigureDepth(depth)
4277
4278 # Set the remote URL before the remote branch as we might need it below.
4279 if manifest_url:
4280 r = self.GetRemote()
4281 r.url = manifest_url
4282 r.ResetFetch()
4283 r.Save()
4284
4285 if not standalone_manifest:
4286 if manifest_branch:
4287 if manifest_branch == "HEAD":
4288 manifest_branch = self.ResolveRemoteHead()
4289 if manifest_branch is None:
4290 print("fatal: unable to resolve HEAD", file=sys.stderr)
4291 return False
4292 self.revisionExpr = manifest_branch
4293 else:
4294 if is_new:
4295 default_branch = self.ResolveRemoteHead()
4296 if default_branch is None:
4297 # If the remote doesn't have HEAD configured, default to
4298 # master.
4299 default_branch = "refs/heads/master"
4300 self.revisionExpr = default_branch
4301 else:
4302 self.PreSync()
4303
4304 groups = re.split(r"[,\s]+", groups or "")
4305 all_platforms = ["linux", "darwin", "windows"]
4306 platformize = lambda x: "platform-" + x
4307 if platform == "auto":
4308 if not mirror and not self.mirror:
4309 groups.append(platformize(self._platform_name))
4310 elif platform == "all":
4311 groups.extend(map(platformize, all_platforms))
4312 elif platform in all_platforms:
4313 groups.append(platformize(platform))
4314 elif platform != "none":
4315 print("fatal: invalid platform flag", file=sys.stderr)
4316 return False
4317 self.config.SetString("manifest.platform", platform)
4318
4319 groups = [x for x in groups if x]
4320 groupstr = ",".join(groups)
4321 if (
4322 platform == "auto"
4323 and groupstr == self.manifest.GetDefaultGroupsStr()
4324 ):
4325 groupstr = None
4326 self.config.SetString("manifest.groups", groupstr)
4327
4328 if reference:
4329 self.config.SetString("repo.reference", reference)
4330
4331 if dissociate:
4332 self.config.SetBoolean("repo.dissociate", dissociate)
4333
4334 if worktree:
4335 if mirror:
4336 print(
4337 "fatal: --mirror and --worktree are incompatible",
4338 file=sys.stderr,
4339 )
4340 return False
4341 if submodules:
4342 print(
4343 "fatal: --submodules and --worktree are incompatible",
4344 file=sys.stderr,
4345 )
4346 return False
4347 self.config.SetBoolean("repo.worktree", worktree)
4348 if is_new:
4349 self.use_git_worktrees = True
4350 print("warning: --worktree is experimental!", file=sys.stderr)
4351
4352 if archive:
4353 if is_new:
4354 self.config.SetBoolean("repo.archive", archive)
4355 else:
4356 print(
4357 "fatal: --archive is only supported when initializing a "
4358 "new workspace.",
4359 file=sys.stderr,
4360 )
4361 print(
4362 "Either delete the .repo folder in this workspace, or "
4363 "initialize in another location.",
4364 file=sys.stderr,
4365 )
4366 return False
4367
4368 if mirror:
4369 if is_new:
4370 self.config.SetBoolean("repo.mirror", mirror)
4371 else:
4372 print(
4373 "fatal: --mirror is only supported when initializing a new "
4374 "workspace.",
4375 file=sys.stderr,
4376 )
4377 print(
4378 "Either delete the .repo folder in this workspace, or "
4379 "initialize in another location.",
4380 file=sys.stderr,
4381 )
4382 return False
4383
4384 if partial_clone is not None:
4385 if mirror:
4386 print(
4387 "fatal: --mirror and --partial-clone are mutually "
4388 "exclusive",
4389 file=sys.stderr,
4390 )
4391 return False
4392 self.config.SetBoolean("repo.partialclone", partial_clone)
4393 if clone_filter:
4394 self.config.SetString("repo.clonefilter", clone_filter)
4395 elif self.partial_clone:
4396 clone_filter = self.clone_filter
4397 else:
4398 clone_filter = None
4399
4400 if partial_clone_exclude is not None:
4401 self.config.SetString(
4402 "repo.partialcloneexclude", partial_clone_exclude
4403 )
4404
4405 if clone_bundle is None:
4406 clone_bundle = False if partial_clone else True
4407 else:
4408 self.config.SetBoolean("repo.clonebundle", clone_bundle)
4409
4410 if submodules:
4411 self.config.SetBoolean("repo.submodules", submodules)
4412
4413 if git_lfs is not None:
4414 if git_lfs:
4415 git_require((2, 17, 0), fail=True, msg="Git LFS support")
4416
4417 self.config.SetBoolean("repo.git-lfs", git_lfs)
4418 if not is_new:
4419 print(
4420 "warning: Changing --git-lfs settings will only affect new "
4421 "project checkouts.\n"
4422 " Existing projects will require manual updates.\n",
4423 file=sys.stderr,
4424 )
4425
Jason Chang17833322023-05-23 13:06:55 -07004426 if clone_filter_for_depth is not None:
4427 self.ConfigureCloneFilterForDepth(clone_filter_for_depth)
4428
Gavin Makea2e3302023-03-11 06:46:20 +00004429 if use_superproject is not None:
4430 self.config.SetBoolean("repo.superproject", use_superproject)
4431
4432 if not standalone_manifest:
4433 success = self.Sync_NetworkHalf(
4434 is_new=is_new,
4435 quiet=not verbose,
4436 verbose=verbose,
4437 clone_bundle=clone_bundle,
4438 current_branch_only=current_branch_only,
4439 tags=tags,
4440 submodules=submodules,
4441 clone_filter=clone_filter,
4442 partial_clone_exclude=self.manifest.PartialCloneExclude,
Jason Chang17833322023-05-23 13:06:55 -07004443 clone_filter_for_depth=self.manifest.CloneFilterForDepth,
Gavin Makea2e3302023-03-11 06:46:20 +00004444 ).success
4445 if not success:
4446 r = self.GetRemote()
4447 print(
4448 "fatal: cannot obtain manifest %s" % r.url, file=sys.stderr
4449 )
4450
4451 # Better delete the manifest git dir if we created it; otherwise
4452 # next time (when user fixes problems) we won't go through the
4453 # "is_new" logic.
4454 if is_new:
4455 platform_utils.rmtree(self.gitdir)
4456 return False
4457
4458 if manifest_branch:
4459 self.MetaBranchSwitch(submodules=submodules)
4460
4461 syncbuf = SyncBuffer(self.config)
4462 self.Sync_LocalHalf(syncbuf, submodules=submodules)
4463 syncbuf.Finish()
4464
4465 if is_new or self.CurrentBranch is None:
Jason Chang1a3612f2023-08-08 14:12:53 -07004466 try:
4467 self.StartBranch("default")
4468 except GitError as e:
4469 msg = str(e)
Gavin Makea2e3302023-03-11 06:46:20 +00004470 print(
Jason Chang1a3612f2023-08-08 14:12:53 -07004471 f"fatal: cannot create default in manifest {msg}",
Gavin Makea2e3302023-03-11 06:46:20 +00004472 file=sys.stderr,
4473 )
4474 return False
4475
4476 if not manifest_name:
4477 print("fatal: manifest name (-m) is required.", file=sys.stderr)
4478 return False
4479
4480 elif is_new:
4481 # This is a new standalone manifest.
4482 manifest_name = "default.xml"
4483 manifest_data = fetch.fetch_file(manifest_url, verbose=verbose)
4484 dest = os.path.join(self.worktree, manifest_name)
4485 os.makedirs(os.path.dirname(dest), exist_ok=True)
4486 with open(dest, "wb") as f:
4487 f.write(manifest_data)
4488
4489 try:
4490 self.manifest.Link(manifest_name)
4491 except ManifestParseError as e:
4492 print(
4493 "fatal: manifest '%s' not available" % manifest_name,
4494 file=sys.stderr,
4495 )
4496 print("fatal: %s" % str(e), file=sys.stderr)
4497 return False
4498
4499 if not this_manifest_only:
4500 for submanifest in self.manifest.submanifests.values():
4501 spec = submanifest.ToSubmanifestSpec()
4502 submanifest.repo_client.manifestProject.Sync(
4503 manifest_url=spec.manifestUrl,
4504 manifest_branch=spec.revision,
4505 standalone_manifest=standalone_manifest,
4506 groups=self.manifest_groups,
4507 platform=platform,
4508 mirror=mirror,
4509 dissociate=dissociate,
4510 reference=reference,
4511 worktree=worktree,
4512 submodules=submodules,
4513 archive=archive,
4514 partial_clone=partial_clone,
4515 clone_filter=clone_filter,
4516 partial_clone_exclude=partial_clone_exclude,
4517 clone_bundle=clone_bundle,
4518 git_lfs=git_lfs,
4519 use_superproject=use_superproject,
4520 verbose=verbose,
4521 current_branch_only=current_branch_only,
4522 tags=tags,
4523 depth=depth,
4524 git_event_log=git_event_log,
4525 manifest_name=spec.manifestName,
4526 this_manifest_only=False,
4527 outer_manifest=False,
4528 )
4529
4530 # Lastly, if the manifest has a <superproject> then have the
4531 # superproject sync it (if it will be used).
4532 if git_superproject.UseSuperproject(use_superproject, self.manifest):
4533 sync_result = self.manifest.superproject.Sync(git_event_log)
4534 if not sync_result.success:
4535 submanifest = ""
4536 if self.manifest.path_prefix:
4537 submanifest = f"for {self.manifest.path_prefix} "
4538 print(
4539 f"warning: git update of superproject {submanifest}failed, "
4540 "repo sync will not use superproject to fetch source; "
4541 "while this error is not fatal, and you can continue to "
4542 "run repo sync, please run repo init with the "
4543 "--no-use-superproject option to stop seeing this warning",
4544 file=sys.stderr,
4545 )
4546 if sync_result.fatal and use_superproject is not None:
4547 return False
4548
4549 return True
4550
Jason Chang17833322023-05-23 13:06:55 -07004551 def ConfigureCloneFilterForDepth(self, clone_filter_for_depth):
4552 """Configure clone filter to replace shallow clones.
4553
4554 Args:
4555 clone_filter_for_depth: a string or None, e.g. 'blob:none' will
4556 disable shallow clones and replace with partial clone. None will
4557 enable shallow clones.
4558 """
4559 self.config.SetString(
4560 "repo.clonefilterfordepth", clone_filter_for_depth
4561 )
4562
Gavin Makea2e3302023-03-11 06:46:20 +00004563 def _ConfigureDepth(self, depth):
4564 """Configure the depth we'll sync down.
4565
4566 Args:
4567 depth: an int, how deep of a partial clone to create.
4568 """
4569 # Opt.depth will be non-None if user actually passed --depth to repo
4570 # init.
4571 if depth is not None:
4572 if depth > 0:
4573 # Positive values will set the depth.
4574 depth = str(depth)
4575 else:
4576 # Negative numbers will clear the depth; passing None to
4577 # SetString will do that.
4578 depth = None
4579
4580 # We store the depth in the main manifest project.
4581 self.config.SetString("repo.depth", depth)