blob: a3b6312eb5be988b99366d1d20c200dd8921d5de [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
Jason Chang32b59562023-07-14 16:45:35 -070029from typing import NamedTuple, List
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
LaMont Jones0de4fc32022-04-21 17:18:35 +000033import fetch
Dave Borowitzb42b4742012-10-31 12:27:27 -070034from git_command import GitCommand, git_require
Gavin Makea2e3302023-03-11 06:46:20 +000035from git_config import (
36 GitConfig,
37 IsId,
38 GetSchemeFromUrl,
39 GetUrlCookieFile,
40 ID_RE,
41)
LaMont Jonesff6b1da2022-06-01 21:03:34 +000042import git_superproject
LaMont Jones55ee3042022-04-06 17:10:21 +000043from git_trace2_event_log import EventLog
Jason Chang32b59562023-07-14 16:45:35 -070044from error import (
45 GitError,
46 UploadError,
47 DownloadError,
48 RepoError,
49)
Mike Frysingere6a202f2019-08-02 15:57:57 -040050from error import ManifestInvalidRevisionError, ManifestInvalidPathError
LaMont Jones409407a2022-04-05 21:21:56 +000051from error import NoManifestException, ManifestParseError
Renaud Paquayd5cec5e2016-11-01 11:24:03 -070052import platform_utils
Mike Frysinger70d861f2019-08-26 15:22:36 -040053import progress
Joanna Wanga6c52f52022-11-03 16:51:19 -040054from repo_trace import Trace
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070055
Mike Frysinger21b7fbe2020-02-26 23:53:36 -050056from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M, R_WORKTREE_M
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070057
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -070058
LaMont Jones1eddca82022-09-01 15:15:04 +000059class SyncNetworkHalfResult(NamedTuple):
Gavin Makea2e3302023-03-11 06:46:20 +000060 """Sync_NetworkHalf return value."""
61
Gavin Makea2e3302023-03-11 06:46:20 +000062 # Did we query the remote? False when optimized_fetch is True and we have
63 # the commit already present.
64 remote_fetched: bool
Jason Chang32b59562023-07-14 16:45:35 -070065 # Error from SyncNetworkHalf
66 error: Exception = None
67
68 @property
69 def success(self) -> bool:
70 return not self.error
71
72
73class SyncNetworkHalfError(RepoError):
74 """Failure trying to sync."""
75
76
77class DeleteWorktreeError(RepoError):
78 """Failure to delete worktree."""
79
80 def __init__(
81 self, *args, aggregate_errors: List[Exception] = None, **kwargs
82 ) -> None:
83 super().__init__(*args, **kwargs)
84 self.aggregate_errors = aggregate_errors or []
85
86
87class DeleteDirtyWorktreeError(DeleteWorktreeError):
88 """Failure to delete worktree due to uncommitted changes."""
LaMont Jones1eddca82022-09-01 15:15:04 +000089
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +010090
George Engelbrecht9bc283e2020-04-02 12:36:09 -060091# Maximum sleep time allowed during retries.
92MAXIMUM_RETRY_SLEEP_SEC = 3600.0
93# +-10% random jitter is added to each Fetches retry sleep duration.
94RETRY_JITTER_PERCENT = 0.1
95
LaMont Jonesfa8d9392022-11-02 22:01:29 +000096# Whether to use alternates. Switching back and forth is *NOT* supported.
Mike Frysinger1d00a7e2021-12-21 00:40:31 -050097# TODO(vapier): Remove knob once behavior is verified.
Gavin Makea2e3302023-03-11 06:46:20 +000098_ALTERNATES = os.environ.get("REPO_USE_ALTERNATES") == "1"
George Engelbrecht9bc283e2020-04-02 12:36:09 -060099
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +0100100
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700101def _lwrite(path, content):
Gavin Makea2e3302023-03-11 06:46:20 +0000102 lock = "%s.lock" % path
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700103
Gavin Makea2e3302023-03-11 06:46:20 +0000104 # Maintain Unix line endings on all OS's to match git behavior.
105 with open(lock, "w", newline="\n") as fd:
106 fd.write(content)
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700107
Gavin Makea2e3302023-03-11 06:46:20 +0000108 try:
109 platform_utils.rename(lock, path)
110 except OSError:
111 platform_utils.remove(lock)
112 raise
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700113
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700114
Shawn O. Pearce48244782009-04-16 08:25:57 -0700115def _error(fmt, *args):
Gavin Makea2e3302023-03-11 06:46:20 +0000116 msg = fmt % args
117 print("error: %s" % msg, file=sys.stderr)
Shawn O. Pearce48244782009-04-16 08:25:57 -0700118
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700119
David Pursehousef33929d2015-08-24 14:39:14 +0900120def _warn(fmt, *args):
Gavin Makea2e3302023-03-11 06:46:20 +0000121 msg = fmt % args
122 print("warn: %s" % msg, file=sys.stderr)
David Pursehousef33929d2015-08-24 14:39:14 +0900123
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700124
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700125def not_rev(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000126 return "^" + r
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700127
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700128
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800129def sq(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000130 return "'" + r.replace("'", "'''") + "'"
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -0800131
David Pursehouse819827a2020-02-12 15:20:19 +0900132
Jonathan Nieder93719792015-03-17 11:29:58 -0700133_project_hook_list = None
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700134
135
Jonathan Nieder93719792015-03-17 11:29:58 -0700136def _ProjectHooks():
Gavin Makea2e3302023-03-11 06:46:20 +0000137 """List the hooks present in the 'hooks' directory.
Jonathan Nieder93719792015-03-17 11:29:58 -0700138
Gavin Makea2e3302023-03-11 06:46:20 +0000139 These hooks are project hooks and are copied to the '.git/hooks' directory
140 of all subprojects.
Jonathan Nieder93719792015-03-17 11:29:58 -0700141
Gavin Makea2e3302023-03-11 06:46:20 +0000142 This function caches the list of hooks (based on the contents of the
143 'repo/hooks' directory) on the first call.
Jonathan Nieder93719792015-03-17 11:29:58 -0700144
Gavin Makea2e3302023-03-11 06:46:20 +0000145 Returns:
146 A list of absolute paths to all of the files in the hooks directory.
147 """
148 global _project_hook_list
149 if _project_hook_list is None:
150 d = platform_utils.realpath(os.path.abspath(os.path.dirname(__file__)))
151 d = os.path.join(d, "hooks")
152 _project_hook_list = [
153 os.path.join(d, x) for x in platform_utils.listdir(d)
154 ]
155 return _project_hook_list
Jonathan Nieder93719792015-03-17 11:29:58 -0700156
157
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700158class DownloadedChange(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000159 _commit_cache = None
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700160
Gavin Makea2e3302023-03-11 06:46:20 +0000161 def __init__(self, project, base, change_id, ps_id, commit):
162 self.project = project
163 self.base = base
164 self.change_id = change_id
165 self.ps_id = ps_id
166 self.commit = commit
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700167
Gavin Makea2e3302023-03-11 06:46:20 +0000168 @property
169 def commits(self):
170 if self._commit_cache is None:
171 self._commit_cache = self.project.bare_git.rev_list(
172 "--abbrev=8",
173 "--abbrev-commit",
174 "--pretty=oneline",
175 "--reverse",
176 "--date-order",
177 not_rev(self.base),
178 self.commit,
179 "--",
180 )
181 return self._commit_cache
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700182
183
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700184class ReviewableBranch(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000185 _commit_cache = None
186 _base_exists = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700187
Gavin Makea2e3302023-03-11 06:46:20 +0000188 def __init__(self, project, branch, base):
189 self.project = project
190 self.branch = branch
191 self.base = base
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700192
Gavin Makea2e3302023-03-11 06:46:20 +0000193 @property
194 def name(self):
195 return self.branch.name
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700196
Gavin Makea2e3302023-03-11 06:46:20 +0000197 @property
198 def commits(self):
199 if self._commit_cache is None:
200 args = (
201 "--abbrev=8",
202 "--abbrev-commit",
203 "--pretty=oneline",
204 "--reverse",
205 "--date-order",
206 not_rev(self.base),
207 R_HEADS + self.name,
208 "--",
209 )
210 try:
211 self._commit_cache = self.project.bare_git.rev_list(*args)
212 except GitError:
213 # We weren't able to probe the commits for this branch. Was it
214 # tracking a branch that no longer exists? If so, return no
215 # commits. Otherwise, rethrow the error as we don't know what's
216 # going on.
217 if self.base_exists:
218 raise
Mike Frysinger6da17752019-09-11 18:43:17 -0400219
Gavin Makea2e3302023-03-11 06:46:20 +0000220 self._commit_cache = []
Mike Frysinger6da17752019-09-11 18:43:17 -0400221
Gavin Makea2e3302023-03-11 06:46:20 +0000222 return self._commit_cache
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700223
Gavin Makea2e3302023-03-11 06:46:20 +0000224 @property
225 def unabbrev_commits(self):
226 r = dict()
227 for commit in self.project.bare_git.rev_list(
228 not_rev(self.base), R_HEADS + self.name, "--"
229 ):
230 r[commit[0:8]] = commit
231 return r
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800232
Gavin Makea2e3302023-03-11 06:46:20 +0000233 @property
234 def date(self):
235 return self.project.bare_git.log(
236 "--pretty=format:%cd", "-n", "1", R_HEADS + self.name, "--"
237 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700238
Gavin Makea2e3302023-03-11 06:46:20 +0000239 @property
240 def base_exists(self):
241 """Whether the branch we're tracking exists.
Mike Frysinger6da17752019-09-11 18:43:17 -0400242
Gavin Makea2e3302023-03-11 06:46:20 +0000243 Normally it should, but sometimes branches we track can get deleted.
244 """
245 if self._base_exists is None:
246 try:
247 self.project.bare_git.rev_parse("--verify", not_rev(self.base))
248 # If we're still here, the base branch exists.
249 self._base_exists = True
250 except GitError:
251 # If we failed to verify, the base branch doesn't exist.
252 self._base_exists = False
Mike Frysinger6da17752019-09-11 18:43:17 -0400253
Gavin Makea2e3302023-03-11 06:46:20 +0000254 return self._base_exists
Mike Frysinger6da17752019-09-11 18:43:17 -0400255
Gavin Makea2e3302023-03-11 06:46:20 +0000256 def UploadForReview(
257 self,
258 people,
259 dryrun=False,
260 auto_topic=False,
261 hashtags=(),
262 labels=(),
263 private=False,
264 notify=None,
265 wip=False,
266 ready=False,
267 dest_branch=None,
268 validate_certs=True,
269 push_options=None,
270 ):
271 self.project.UploadForReview(
272 branch=self.name,
273 people=people,
274 dryrun=dryrun,
275 auto_topic=auto_topic,
276 hashtags=hashtags,
277 labels=labels,
278 private=private,
279 notify=notify,
280 wip=wip,
281 ready=ready,
282 dest_branch=dest_branch,
283 validate_certs=validate_certs,
284 push_options=push_options,
285 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700286
Gavin Makea2e3302023-03-11 06:46:20 +0000287 def GetPublishedRefs(self):
288 refs = {}
289 output = self.project.bare_git.ls_remote(
290 self.branch.remote.SshReviewUrl(self.project.UserEmail),
291 "refs/changes/*",
292 )
293 for line in output.split("\n"):
294 try:
295 (sha, ref) = line.split()
296 refs[sha] = ref
297 except ValueError:
298 pass
Ficus Kirkpatrickbc7ef672009-05-04 12:45:11 -0700299
Gavin Makea2e3302023-03-11 06:46:20 +0000300 return refs
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700301
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700302
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700303class StatusColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000304 def __init__(self, config):
305 super().__init__(config, "status")
306 self.project = self.printer("header", attr="bold")
307 self.branch = self.printer("header", attr="bold")
308 self.nobranch = self.printer("nobranch", fg="red")
309 self.important = self.printer("important", fg="red")
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700310
Gavin Makea2e3302023-03-11 06:46:20 +0000311 self.added = self.printer("added", fg="green")
312 self.changed = self.printer("changed", fg="red")
313 self.untracked = self.printer("untracked", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700314
315
316class DiffColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000317 def __init__(self, config):
318 super().__init__(config, "diff")
319 self.project = self.printer("header", attr="bold")
320 self.fail = self.printer("fail", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700321
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700322
Jack Neus6ea0cae2021-07-20 20:52:33 +0000323class Annotation(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000324 def __init__(self, name, value, keep):
325 self.name = name
326 self.value = value
327 self.keep = keep
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700328
Gavin Makea2e3302023-03-11 06:46:20 +0000329 def __eq__(self, other):
330 if not isinstance(other, Annotation):
331 return False
332 return self.__dict__ == other.__dict__
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700333
Gavin Makea2e3302023-03-11 06:46:20 +0000334 def __lt__(self, other):
335 # This exists just so that lists of Annotation objects can be sorted,
336 # for use in comparisons.
337 if not isinstance(other, Annotation):
338 raise ValueError("comparison is not between two Annotation objects")
339 if self.name == other.name:
340 if self.value == other.value:
341 return self.keep < other.keep
342 return self.value < other.value
343 return self.name < other.name
Jack Neus6ea0cae2021-07-20 20:52:33 +0000344
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700345
Mike Frysingere6a202f2019-08-02 15:57:57 -0400346def _SafeExpandPath(base, subpath, skipfinal=False):
Gavin Makea2e3302023-03-11 06:46:20 +0000347 """Make sure |subpath| is completely safe under |base|.
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700348
Gavin Makea2e3302023-03-11 06:46:20 +0000349 We make sure no intermediate symlinks are traversed, and that the final path
350 is not a special file (e.g. not a socket or fifo).
Mike Frysingere6a202f2019-08-02 15:57:57 -0400351
Gavin Makea2e3302023-03-11 06:46:20 +0000352 NB: We rely on a number of paths already being filtered out while parsing
353 the manifest. See the validation logic in manifest_xml.py for more details.
354 """
355 # Split up the path by its components. We can't use os.path.sep exclusively
356 # as some platforms (like Windows) will convert / to \ and that bypasses all
357 # our constructed logic here. Especially since manifest authors only use
358 # / in their paths.
359 resep = re.compile(r"[/%s]" % re.escape(os.path.sep))
360 components = resep.split(subpath)
361 if skipfinal:
362 # Whether the caller handles the final component itself.
363 finalpart = components.pop()
Mike Frysingere6a202f2019-08-02 15:57:57 -0400364
Gavin Makea2e3302023-03-11 06:46:20 +0000365 path = base
366 for part in components:
367 if part in {".", ".."}:
368 raise ManifestInvalidPathError(
369 '%s: "%s" not allowed in paths' % (subpath, part)
370 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400371
Gavin Makea2e3302023-03-11 06:46:20 +0000372 path = os.path.join(path, part)
373 if platform_utils.islink(path):
374 raise ManifestInvalidPathError(
375 "%s: traversing symlinks not allow" % (path,)
376 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400377
Gavin Makea2e3302023-03-11 06:46:20 +0000378 if os.path.exists(path):
379 if not os.path.isfile(path) and not platform_utils.isdir(path):
380 raise ManifestInvalidPathError(
381 "%s: only regular files & directories allowed" % (path,)
382 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400383
Gavin Makea2e3302023-03-11 06:46:20 +0000384 if skipfinal:
385 path = os.path.join(path, finalpart)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400386
Gavin Makea2e3302023-03-11 06:46:20 +0000387 return path
Mike Frysingere6a202f2019-08-02 15:57:57 -0400388
389
390class _CopyFile(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000391 """Container for <copyfile> manifest element."""
Mike Frysingere6a202f2019-08-02 15:57:57 -0400392
Gavin Makea2e3302023-03-11 06:46:20 +0000393 def __init__(self, git_worktree, src, topdir, dest):
394 """Register a <copyfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400395
Gavin Makea2e3302023-03-11 06:46:20 +0000396 Args:
397 git_worktree: Absolute path to the git project checkout.
398 src: Relative path under |git_worktree| of file to read.
399 topdir: Absolute path to the top of the repo client checkout.
400 dest: Relative path under |topdir| of file to write.
401 """
402 self.git_worktree = git_worktree
403 self.topdir = topdir
404 self.src = src
405 self.dest = dest
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700406
Gavin Makea2e3302023-03-11 06:46:20 +0000407 def _Copy(self):
408 src = _SafeExpandPath(self.git_worktree, self.src)
409 dest = _SafeExpandPath(self.topdir, self.dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400410
Gavin Makea2e3302023-03-11 06:46:20 +0000411 if platform_utils.isdir(src):
412 raise ManifestInvalidPathError(
413 "%s: copying from directory not supported" % (self.src,)
414 )
415 if platform_utils.isdir(dest):
416 raise ManifestInvalidPathError(
417 "%s: copying to directory not allowed" % (self.dest,)
418 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400419
Gavin Makea2e3302023-03-11 06:46:20 +0000420 # Copy file if it does not exist or is out of date.
421 if not os.path.exists(dest) or not filecmp.cmp(src, dest):
422 try:
423 # Remove existing file first, since it might be read-only.
424 if os.path.exists(dest):
425 platform_utils.remove(dest)
426 else:
427 dest_dir = os.path.dirname(dest)
428 if not platform_utils.isdir(dest_dir):
429 os.makedirs(dest_dir)
430 shutil.copy(src, dest)
431 # Make the file read-only.
432 mode = os.stat(dest)[stat.ST_MODE]
433 mode = mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
434 os.chmod(dest, mode)
435 except IOError:
436 _error("Cannot copy file %s to %s", src, dest)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700437
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700438
Anthony King7bdac712014-07-16 12:56:40 +0100439class _LinkFile(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000440 """Container for <linkfile> manifest element."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700441
Gavin Makea2e3302023-03-11 06:46:20 +0000442 def __init__(self, git_worktree, src, topdir, dest):
443 """Register a <linkfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400444
Gavin Makea2e3302023-03-11 06:46:20 +0000445 Args:
446 git_worktree: Absolute path to the git project checkout.
447 src: Target of symlink relative to path under |git_worktree|.
448 topdir: Absolute path to the top of the repo client checkout.
449 dest: Relative path under |topdir| of symlink to create.
450 """
451 self.git_worktree = git_worktree
452 self.topdir = topdir
453 self.src = src
454 self.dest = dest
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500455
Gavin Makea2e3302023-03-11 06:46:20 +0000456 def __linkIt(self, relSrc, absDest):
457 # Link file if it does not exist or is out of date.
458 if not platform_utils.islink(absDest) or (
459 platform_utils.readlink(absDest) != relSrc
460 ):
461 try:
462 # Remove existing file first, since it might be read-only.
463 if os.path.lexists(absDest):
464 platform_utils.remove(absDest)
465 else:
466 dest_dir = os.path.dirname(absDest)
467 if not platform_utils.isdir(dest_dir):
468 os.makedirs(dest_dir)
469 platform_utils.symlink(relSrc, absDest)
470 except IOError:
471 _error("Cannot link file %s to %s", relSrc, absDest)
472
473 def _Link(self):
474 """Link the self.src & self.dest paths.
475
476 Handles wild cards on the src linking all of the files in the source in
477 to the destination directory.
478 """
479 # Some people use src="." to create stable links to projects. Let's
480 # allow that but reject all other uses of "." to keep things simple.
481 if self.src == ".":
482 src = self.git_worktree
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500483 else:
Gavin Makea2e3302023-03-11 06:46:20 +0000484 src = _SafeExpandPath(self.git_worktree, self.src)
Wink Saville4c426ef2015-06-03 08:05:17 -0700485
Gavin Makea2e3302023-03-11 06:46:20 +0000486 if not glob.has_magic(src):
487 # Entity does not contain a wild card so just a simple one to one
488 # link operation.
489 dest = _SafeExpandPath(self.topdir, self.dest, skipfinal=True)
490 # dest & src are absolute paths at this point. Make sure the target
491 # of the symlink is relative in the context of the repo client
492 # checkout.
493 relpath = os.path.relpath(src, os.path.dirname(dest))
494 self.__linkIt(relpath, dest)
495 else:
496 dest = _SafeExpandPath(self.topdir, self.dest)
497 # Entity contains a wild card.
498 if os.path.exists(dest) and not platform_utils.isdir(dest):
499 _error(
500 "Link error: src with wildcard, %s must be a directory",
501 dest,
502 )
503 else:
504 for absSrcFile in glob.glob(src):
505 # Create a releative path from source dir to destination
506 # dir.
507 absSrcDir = os.path.dirname(absSrcFile)
508 relSrcDir = os.path.relpath(absSrcDir, dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400509
Gavin Makea2e3302023-03-11 06:46:20 +0000510 # Get the source file name.
511 srcFile = os.path.basename(absSrcFile)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400512
Gavin Makea2e3302023-03-11 06:46:20 +0000513 # Now form the final full paths to srcFile. They will be
514 # absolute for the desintaiton and relative for the source.
515 absDest = os.path.join(dest, srcFile)
516 relSrc = os.path.join(relSrcDir, srcFile)
517 self.__linkIt(relSrc, absDest)
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500518
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700519
Shawn O. Pearced1f70d92009-05-19 14:58:02 -0700520class RemoteSpec(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000521 def __init__(
522 self,
523 name,
524 url=None,
525 pushUrl=None,
526 review=None,
527 revision=None,
528 orig_name=None,
529 fetchUrl=None,
530 ):
531 self.name = name
532 self.url = url
533 self.pushUrl = pushUrl
534 self.review = review
535 self.revision = revision
536 self.orig_name = orig_name
537 self.fetchUrl = fetchUrl
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700538
Ian Kasprzak0286e312021-02-05 10:06:18 -0800539
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700540class Project(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000541 # These objects can be shared between several working trees.
542 @property
543 def shareable_dirs(self):
544 """Return the shareable directories"""
545 if self.UseAlternates:
546 return ["hooks", "rr-cache"]
547 else:
548 return ["hooks", "objects", "rr-cache"]
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700549
Gavin Makea2e3302023-03-11 06:46:20 +0000550 def __init__(
551 self,
552 manifest,
553 name,
554 remote,
555 gitdir,
556 objdir,
557 worktree,
558 relpath,
559 revisionExpr,
560 revisionId,
561 rebase=True,
562 groups=None,
563 sync_c=False,
564 sync_s=False,
565 sync_tags=True,
566 clone_depth=None,
567 upstream=None,
568 parent=None,
569 use_git_worktrees=False,
570 is_derived=False,
571 dest_branch=None,
572 optimized_fetch=False,
573 retry_fetches=0,
574 old_revision=None,
575 ):
576 """Init a Project object.
Doug Anderson3ba5f952011-04-07 12:51:04 -0700577
578 Args:
Gavin Makea2e3302023-03-11 06:46:20 +0000579 manifest: The XmlManifest object.
580 name: The `name` attribute of manifest.xml's project element.
581 remote: RemoteSpec object specifying its remote's properties.
582 gitdir: Absolute path of git directory.
583 objdir: Absolute path of directory to store git objects.
584 worktree: Absolute path of git working tree.
585 relpath: Relative path of git working tree to repo's top directory.
586 revisionExpr: The `revision` attribute of manifest.xml's project
587 element.
588 revisionId: git commit id for checking out.
589 rebase: The `rebase` attribute of manifest.xml's project element.
590 groups: The `groups` attribute of manifest.xml's project element.
591 sync_c: The `sync-c` attribute of manifest.xml's project element.
592 sync_s: The `sync-s` attribute of manifest.xml's project element.
593 sync_tags: The `sync-tags` attribute of manifest.xml's project
594 element.
595 upstream: The `upstream` attribute of manifest.xml's project
596 element.
597 parent: The parent Project object.
598 use_git_worktrees: Whether to use `git worktree` for this project.
599 is_derived: False if the project was explicitly defined in the
600 manifest; True if the project is a discovered submodule.
601 dest_branch: The branch to which to push changes for review by
602 default.
603 optimized_fetch: If True, when a project is set to a sha1 revision,
604 only fetch from the remote if the sha1 is not present locally.
605 retry_fetches: Retry remote fetches n times upon receiving transient
606 error with exponential backoff and jitter.
607 old_revision: saved git commit id for open GITC projects.
608 """
609 self.client = self.manifest = manifest
610 self.name = name
611 self.remote = remote
612 self.UpdatePaths(relpath, worktree, gitdir, objdir)
613 self.SetRevision(revisionExpr, revisionId=revisionId)
614
615 self.rebase = rebase
616 self.groups = groups
617 self.sync_c = sync_c
618 self.sync_s = sync_s
619 self.sync_tags = sync_tags
620 self.clone_depth = clone_depth
621 self.upstream = upstream
622 self.parent = parent
623 # NB: Do not use this setting in __init__ to change behavior so that the
624 # manifest.git checkout can inspect & change it after instantiating.
625 # See the XmlManifest init code for more info.
626 self.use_git_worktrees = use_git_worktrees
627 self.is_derived = is_derived
628 self.optimized_fetch = optimized_fetch
629 self.retry_fetches = max(0, retry_fetches)
630 self.subprojects = []
631
632 self.snapshots = {}
633 self.copyfiles = []
634 self.linkfiles = []
635 self.annotations = []
636 self.dest_branch = dest_branch
637 self.old_revision = old_revision
638
639 # This will be filled in if a project is later identified to be the
640 # project containing repo hooks.
641 self.enabled_repo_hooks = []
642
643 def RelPath(self, local=True):
644 """Return the path for the project relative to a manifest.
645
646 Args:
647 local: a boolean, if True, the path is relative to the local
648 (sub)manifest. If false, the path is relative to the outermost
649 manifest.
650 """
651 if local:
652 return self.relpath
653 return os.path.join(self.manifest.path_prefix, self.relpath)
654
655 def SetRevision(self, revisionExpr, revisionId=None):
656 """Set revisionId based on revision expression and id"""
657 self.revisionExpr = revisionExpr
658 if revisionId is None and revisionExpr and IsId(revisionExpr):
659 self.revisionId = self.revisionExpr
660 else:
661 self.revisionId = revisionId
662
663 def UpdatePaths(self, relpath, worktree, gitdir, objdir):
664 """Update paths used by this project"""
665 self.gitdir = gitdir.replace("\\", "/")
666 self.objdir = objdir.replace("\\", "/")
667 if worktree:
668 self.worktree = os.path.normpath(worktree).replace("\\", "/")
669 else:
670 self.worktree = None
671 self.relpath = relpath
672
673 self.config = GitConfig.ForRepository(
674 gitdir=self.gitdir, defaults=self.manifest.globalConfig
675 )
676
677 if self.worktree:
678 self.work_git = self._GitGetByExec(
679 self, bare=False, gitdir=self.gitdir
680 )
681 else:
682 self.work_git = None
683 self.bare_git = self._GitGetByExec(self, bare=True, gitdir=self.gitdir)
684 self.bare_ref = GitRefs(self.gitdir)
685 self.bare_objdir = self._GitGetByExec(
686 self, bare=True, gitdir=self.objdir
687 )
688
689 @property
690 def UseAlternates(self):
691 """Whether git alternates are in use.
692
693 This will be removed once migration to alternates is complete.
694 """
695 return _ALTERNATES or self.manifest.is_multimanifest
696
697 @property
698 def Derived(self):
699 return self.is_derived
700
701 @property
702 def Exists(self):
703 return platform_utils.isdir(self.gitdir) and platform_utils.isdir(
704 self.objdir
705 )
706
707 @property
708 def CurrentBranch(self):
709 """Obtain the name of the currently checked out branch.
710
711 The branch name omits the 'refs/heads/' prefix.
712 None is returned if the project is on a detached HEAD, or if the
713 work_git is otheriwse inaccessible (e.g. an incomplete sync).
714 """
715 try:
716 b = self.work_git.GetHead()
717 except NoManifestException:
718 # If the local checkout is in a bad state, don't barf. Let the
719 # callers process this like the head is unreadable.
720 return None
721 if b.startswith(R_HEADS):
722 return b[len(R_HEADS) :]
723 return None
724
725 def IsRebaseInProgress(self):
726 return (
727 os.path.exists(self.work_git.GetDotgitPath("rebase-apply"))
728 or os.path.exists(self.work_git.GetDotgitPath("rebase-merge"))
729 or os.path.exists(os.path.join(self.worktree, ".dotest"))
730 )
731
732 def IsDirty(self, consider_untracked=True):
733 """Is the working directory modified in some way?"""
734 self.work_git.update_index(
735 "-q", "--unmerged", "--ignore-missing", "--refresh"
736 )
737 if self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD):
738 return True
739 if self.work_git.DiffZ("diff-files"):
740 return True
741 if consider_untracked and self.UntrackedFiles():
742 return True
743 return False
744
745 _userident_name = None
746 _userident_email = None
747
748 @property
749 def UserName(self):
750 """Obtain the user's personal name."""
751 if self._userident_name is None:
752 self._LoadUserIdentity()
753 return self._userident_name
754
755 @property
756 def UserEmail(self):
757 """Obtain the user's email address. This is very likely
758 to be their Gerrit login.
759 """
760 if self._userident_email is None:
761 self._LoadUserIdentity()
762 return self._userident_email
763
764 def _LoadUserIdentity(self):
765 u = self.bare_git.var("GIT_COMMITTER_IDENT")
766 m = re.compile("^(.*) <([^>]*)> ").match(u)
767 if m:
768 self._userident_name = m.group(1)
769 self._userident_email = m.group(2)
770 else:
771 self._userident_name = ""
772 self._userident_email = ""
773
774 def GetRemote(self, name=None):
775 """Get the configuration for a single remote.
776
777 Defaults to the current project's remote.
778 """
779 if name is None:
780 name = self.remote.name
781 return self.config.GetRemote(name)
782
783 def GetBranch(self, name):
784 """Get the configuration for a single branch."""
785 return self.config.GetBranch(name)
786
787 def GetBranches(self):
788 """Get all existing local branches."""
789 current = self.CurrentBranch
790 all_refs = self._allrefs
791 heads = {}
792
793 for name, ref_id in all_refs.items():
794 if name.startswith(R_HEADS):
795 name = name[len(R_HEADS) :]
796 b = self.GetBranch(name)
797 b.current = name == current
798 b.published = None
799 b.revision = ref_id
800 heads[name] = b
801
802 for name, ref_id in all_refs.items():
803 if name.startswith(R_PUB):
804 name = name[len(R_PUB) :]
805 b = heads.get(name)
806 if b:
807 b.published = ref_id
808
809 return heads
810
811 def MatchesGroups(self, manifest_groups):
812 """Returns true if the manifest groups specified at init should cause
813 this project to be synced.
814 Prefixing a manifest group with "-" inverts the meaning of a group.
815 All projects are implicitly labelled with "all".
816
817 labels are resolved in order. In the example case of
818 project_groups: "all,group1,group2"
819 manifest_groups: "-group1,group2"
820 the project will be matched.
821
822 The special manifest group "default" will match any project that
823 does not have the special project group "notdefault"
824 """
825 default_groups = self.manifest.default_groups or ["default"]
826 expanded_manifest_groups = manifest_groups or default_groups
827 expanded_project_groups = ["all"] + (self.groups or [])
828 if "notdefault" not in expanded_project_groups:
829 expanded_project_groups += ["default"]
830
831 matched = False
832 for group in expanded_manifest_groups:
833 if group.startswith("-") and group[1:] in expanded_project_groups:
834 matched = False
835 elif group in expanded_project_groups:
836 matched = True
837
838 return matched
839
840 def UncommitedFiles(self, get_all=True):
841 """Returns a list of strings, uncommitted files in the git tree.
842
843 Args:
844 get_all: a boolean, if True - get information about all different
845 uncommitted files. If False - return as soon as any kind of
846 uncommitted files is detected.
847 """
848 details = []
849 self.work_git.update_index(
850 "-q", "--unmerged", "--ignore-missing", "--refresh"
851 )
852 if self.IsRebaseInProgress():
853 details.append("rebase in progress")
854 if not get_all:
855 return details
856
857 changes = self.work_git.DiffZ("diff-index", "--cached", HEAD).keys()
858 if changes:
859 details.extend(changes)
860 if not get_all:
861 return details
862
863 changes = self.work_git.DiffZ("diff-files").keys()
864 if changes:
865 details.extend(changes)
866 if not get_all:
867 return details
868
869 changes = self.UntrackedFiles()
870 if changes:
871 details.extend(changes)
872
873 return details
874
875 def UntrackedFiles(self):
876 """Returns a list of strings, untracked files in the git tree."""
877 return self.work_git.LsOthers()
878
879 def HasChanges(self):
880 """Returns true if there are uncommitted changes."""
881 return bool(self.UncommitedFiles(get_all=False))
882
883 def PrintWorkTreeStatus(self, output_redir=None, quiet=False, local=False):
884 """Prints the status of the repository to stdout.
885
886 Args:
887 output_redir: If specified, redirect the output to this object.
888 quiet: If True then only print the project name. Do not print
889 the modified files, branch name, etc.
890 local: a boolean, if True, the path is relative to the local
891 (sub)manifest. If false, the path is relative to the outermost
892 manifest.
893 """
894 if not platform_utils.isdir(self.worktree):
895 if output_redir is None:
896 output_redir = sys.stdout
897 print(file=output_redir)
898 print("project %s/" % self.RelPath(local), file=output_redir)
899 print(' missing (run "repo sync")', file=output_redir)
900 return
901
902 self.work_git.update_index(
903 "-q", "--unmerged", "--ignore-missing", "--refresh"
904 )
905 rb = self.IsRebaseInProgress()
906 di = self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD)
907 df = self.work_git.DiffZ("diff-files")
908 do = self.work_git.LsOthers()
909 if not rb and not di and not df and not do and not self.CurrentBranch:
910 return "CLEAN"
911
912 out = StatusColoring(self.config)
913 if output_redir is not None:
914 out.redirect(output_redir)
915 out.project("project %-40s", self.RelPath(local) + "/ ")
916
917 if quiet:
918 out.nl()
919 return "DIRTY"
920
921 branch = self.CurrentBranch
922 if branch is None:
923 out.nobranch("(*** NO BRANCH ***)")
924 else:
925 out.branch("branch %s", branch)
926 out.nl()
927
928 if rb:
929 out.important("prior sync failed; rebase still in progress")
930 out.nl()
931
932 paths = list()
933 paths.extend(di.keys())
934 paths.extend(df.keys())
935 paths.extend(do)
936
937 for p in sorted(set(paths)):
938 try:
939 i = di[p]
940 except KeyError:
941 i = None
942
943 try:
944 f = df[p]
945 except KeyError:
946 f = None
947
948 if i:
949 i_status = i.status.upper()
950 else:
951 i_status = "-"
952
953 if f:
954 f_status = f.status.lower()
955 else:
956 f_status = "-"
957
958 if i and i.src_path:
959 line = " %s%s\t%s => %s (%s%%)" % (
960 i_status,
961 f_status,
962 i.src_path,
963 p,
964 i.level,
965 )
966 else:
967 line = " %s%s\t%s" % (i_status, f_status, p)
968
969 if i and not f:
970 out.added("%s", line)
971 elif (i and f) or (not i and f):
972 out.changed("%s", line)
973 elif not i and not f:
974 out.untracked("%s", line)
975 else:
976 out.write("%s", line)
977 out.nl()
978
979 return "DIRTY"
980
981 def PrintWorkTreeDiff(
982 self, absolute_paths=False, output_redir=None, local=False
983 ):
984 """Prints the status of the repository to stdout."""
985 out = DiffColoring(self.config)
986 if output_redir:
987 out.redirect(output_redir)
988 cmd = ["diff"]
989 if out.is_on:
990 cmd.append("--color")
991 cmd.append(HEAD)
992 if absolute_paths:
993 cmd.append("--src-prefix=a/%s/" % self.RelPath(local))
994 cmd.append("--dst-prefix=b/%s/" % self.RelPath(local))
995 cmd.append("--")
996 try:
997 p = GitCommand(self, cmd, capture_stdout=True, capture_stderr=True)
998 p.Wait()
999 except GitError as e:
1000 out.nl()
1001 out.project("project %s/" % self.RelPath(local))
1002 out.nl()
1003 out.fail("%s", str(e))
1004 out.nl()
1005 return False
1006 if p.stdout:
1007 out.nl()
1008 out.project("project %s/" % self.RelPath(local))
1009 out.nl()
1010 out.write("%s", p.stdout)
1011 return p.Wait() == 0
1012
1013 def WasPublished(self, branch, all_refs=None):
1014 """Was the branch published (uploaded) for code review?
1015 If so, returns the SHA-1 hash of the last published
1016 state for the branch.
1017 """
1018 key = R_PUB + branch
1019 if all_refs is None:
1020 try:
1021 return self.bare_git.rev_parse(key)
1022 except GitError:
1023 return None
1024 else:
1025 try:
1026 return all_refs[key]
1027 except KeyError:
1028 return None
1029
1030 def CleanPublishedCache(self, all_refs=None):
1031 """Prunes any stale published refs."""
1032 if all_refs is None:
1033 all_refs = self._allrefs
1034 heads = set()
1035 canrm = {}
1036 for name, ref_id in all_refs.items():
1037 if name.startswith(R_HEADS):
1038 heads.add(name)
1039 elif name.startswith(R_PUB):
1040 canrm[name] = ref_id
1041
1042 for name, ref_id in canrm.items():
1043 n = name[len(R_PUB) :]
1044 if R_HEADS + n not in heads:
1045 self.bare_git.DeleteRef(name, ref_id)
1046
1047 def GetUploadableBranches(self, selected_branch=None):
1048 """List any branches which can be uploaded for review."""
1049 heads = {}
1050 pubed = {}
1051
1052 for name, ref_id in self._allrefs.items():
1053 if name.startswith(R_HEADS):
1054 heads[name[len(R_HEADS) :]] = ref_id
1055 elif name.startswith(R_PUB):
1056 pubed[name[len(R_PUB) :]] = ref_id
1057
1058 ready = []
1059 for branch, ref_id in heads.items():
1060 if branch in pubed and pubed[branch] == ref_id:
1061 continue
1062 if selected_branch and branch != selected_branch:
1063 continue
1064
1065 rb = self.GetUploadableBranch(branch)
1066 if rb:
1067 ready.append(rb)
1068 return ready
1069
1070 def GetUploadableBranch(self, branch_name):
1071 """Get a single uploadable branch, or None."""
1072 branch = self.GetBranch(branch_name)
1073 base = branch.LocalMerge
1074 if branch.LocalMerge:
1075 rb = ReviewableBranch(self, branch, base)
1076 if rb.commits:
1077 return rb
1078 return None
1079
1080 def UploadForReview(
1081 self,
1082 branch=None,
1083 people=([], []),
1084 dryrun=False,
1085 auto_topic=False,
1086 hashtags=(),
1087 labels=(),
1088 private=False,
1089 notify=None,
1090 wip=False,
1091 ready=False,
1092 dest_branch=None,
1093 validate_certs=True,
1094 push_options=None,
1095 ):
1096 """Uploads the named branch for code review."""
1097 if branch is None:
1098 branch = self.CurrentBranch
1099 if branch is None:
Jason Chang32b59562023-07-14 16:45:35 -07001100 raise GitError("not currently on a branch", project=self.name)
Gavin Makea2e3302023-03-11 06:46:20 +00001101
1102 branch = self.GetBranch(branch)
1103 if not branch.LocalMerge:
Jason Chang32b59562023-07-14 16:45:35 -07001104 raise GitError(
1105 "branch %s does not track a remote" % branch.name,
1106 project=self.name,
1107 )
Gavin Makea2e3302023-03-11 06:46:20 +00001108 if not branch.remote.review:
Jason Chang32b59562023-07-14 16:45:35 -07001109 raise GitError(
1110 "remote %s has no review url" % branch.remote.name,
1111 project=self.name,
1112 )
Gavin Makea2e3302023-03-11 06:46:20 +00001113
1114 # Basic validity check on label syntax.
1115 for label in labels:
1116 if not re.match(r"^.+[+-][0-9]+$", label):
1117 raise UploadError(
1118 f'invalid label syntax "{label}": labels use forms like '
Jason Chang5a3a5f72023-08-17 11:36:41 -07001119 "CodeReview+1 or Verified-1",
1120 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00001121 )
1122
1123 if dest_branch is None:
1124 dest_branch = self.dest_branch
1125 if dest_branch is None:
1126 dest_branch = branch.merge
1127 if not dest_branch.startswith(R_HEADS):
1128 dest_branch = R_HEADS + dest_branch
1129
1130 if not branch.remote.projectname:
1131 branch.remote.projectname = self.name
1132 branch.remote.Save()
1133
1134 url = branch.remote.ReviewUrl(self.UserEmail, validate_certs)
1135 if url is None:
Jason Chang5a3a5f72023-08-17 11:36:41 -07001136 raise UploadError("review not configured", project=self.name)
Gavin Makea2e3302023-03-11 06:46:20 +00001137 cmd = ["push"]
1138 if dryrun:
1139 cmd.append("-n")
1140
1141 if url.startswith("ssh://"):
1142 cmd.append("--receive-pack=gerrit receive-pack")
1143
Aravind Vasudevan99ebf622023-04-04 23:44:37 +00001144 # This stops git from pushing all reachable annotated tags when
1145 # push.followTags is configured. Gerrit does not accept any tags
1146 # pushed to a CL.
1147 if git_require((1, 8, 3)):
1148 cmd.append("--no-follow-tags")
1149
Gavin Makea2e3302023-03-11 06:46:20 +00001150 for push_option in push_options or []:
1151 cmd.append("-o")
1152 cmd.append(push_option)
1153
1154 cmd.append(url)
1155
1156 if dest_branch.startswith(R_HEADS):
1157 dest_branch = dest_branch[len(R_HEADS) :]
1158
1159 ref_spec = "%s:refs/for/%s" % (R_HEADS + branch.name, dest_branch)
1160 opts = []
1161 if auto_topic:
1162 opts += ["topic=" + branch.name]
1163 opts += ["t=%s" % p for p in hashtags]
1164 # NB: No need to encode labels as they've been validated above.
1165 opts += ["l=%s" % p for p in labels]
1166
1167 opts += ["r=%s" % p for p in people[0]]
1168 opts += ["cc=%s" % p for p in people[1]]
1169 if notify:
1170 opts += ["notify=" + notify]
1171 if private:
1172 opts += ["private"]
1173 if wip:
1174 opts += ["wip"]
1175 if ready:
1176 opts += ["ready"]
1177 if opts:
1178 ref_spec = ref_spec + "%" + ",".join(opts)
1179 cmd.append(ref_spec)
1180
Jason Chang5a3a5f72023-08-17 11:36:41 -07001181 GitCommand(
1182 self, cmd, bare=True, capture_stderr=True, verify_command=True
1183 ).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001184
1185 if not dryrun:
1186 msg = "posted to %s for %s" % (branch.remote.review, dest_branch)
1187 self.bare_git.UpdateRef(
1188 R_PUB + branch.name, R_HEADS + branch.name, message=msg
1189 )
1190
1191 def _ExtractArchive(self, tarpath, path=None):
1192 """Extract the given tar on its current location
1193
1194 Args:
1195 tarpath: The path to the actual tar file
1196
1197 """
1198 try:
1199 with tarfile.open(tarpath, "r") as tar:
1200 tar.extractall(path=path)
1201 return True
1202 except (IOError, tarfile.TarError) as e:
1203 _error("Cannot extract archive %s: %s", tarpath, str(e))
1204 return False
1205
1206 def Sync_NetworkHalf(
1207 self,
1208 quiet=False,
1209 verbose=False,
1210 output_redir=None,
1211 is_new=None,
1212 current_branch_only=None,
1213 force_sync=False,
1214 clone_bundle=True,
1215 tags=None,
1216 archive=False,
1217 optimized_fetch=False,
1218 retry_fetches=0,
1219 prune=False,
1220 submodules=False,
1221 ssh_proxy=None,
1222 clone_filter=None,
1223 partial_clone_exclude=set(),
Jason Chang17833322023-05-23 13:06:55 -07001224 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001225 ):
1226 """Perform only the network IO portion of the sync process.
1227 Local working directory/branch state is not affected.
1228 """
1229 if archive and not isinstance(self, MetaProject):
1230 if self.remote.url.startswith(("http://", "https://")):
Jason Chang32b59562023-07-14 16:45:35 -07001231 msg_template = (
1232 "%s: Cannot fetch archives from http/https remotes."
Gavin Makea2e3302023-03-11 06:46:20 +00001233 )
Jason Chang32b59562023-07-14 16:45:35 -07001234 msg_args = self.name
1235 msg = msg_template % msg_args
1236 _error(
1237 msg_template,
1238 msg_args,
1239 )
1240 return SyncNetworkHalfResult(
1241 False, SyncNetworkHalfError(msg, project=self.name)
1242 )
Gavin Makea2e3302023-03-11 06:46:20 +00001243
1244 name = self.relpath.replace("\\", "/")
1245 name = name.replace("/", "_")
1246 tarpath = "%s.tar" % name
1247 topdir = self.manifest.topdir
1248
1249 try:
1250 self._FetchArchive(tarpath, cwd=topdir)
1251 except GitError as e:
1252 _error("%s", e)
Jason Chang32b59562023-07-14 16:45:35 -07001253 return SyncNetworkHalfResult(False, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001254
1255 # From now on, we only need absolute tarpath.
1256 tarpath = os.path.join(topdir, tarpath)
1257
1258 if not self._ExtractArchive(tarpath, path=topdir):
Jason Chang32b59562023-07-14 16:45:35 -07001259 return SyncNetworkHalfResult(
1260 True,
1261 SyncNetworkHalfError(
1262 f"Unable to Extract Archive {tarpath}",
1263 project=self.name,
1264 ),
1265 )
Gavin Makea2e3302023-03-11 06:46:20 +00001266 try:
1267 platform_utils.remove(tarpath)
1268 except OSError as e:
1269 _warn("Cannot remove archive %s: %s", tarpath, str(e))
1270 self._CopyAndLinkFiles()
Jason Chang32b59562023-07-14 16:45:35 -07001271 return SyncNetworkHalfResult(True)
Gavin Makea2e3302023-03-11 06:46:20 +00001272
1273 # If the shared object dir already exists, don't try to rebootstrap with
1274 # a clone bundle download. We should have the majority of objects
1275 # already.
1276 if clone_bundle and os.path.exists(self.objdir):
1277 clone_bundle = False
1278
1279 if self.name in partial_clone_exclude:
1280 clone_bundle = True
1281 clone_filter = None
1282
1283 if is_new is None:
1284 is_new = not self.Exists
1285 if is_new:
1286 self._InitGitDir(force_sync=force_sync, quiet=quiet)
1287 else:
1288 self._UpdateHooks(quiet=quiet)
1289 self._InitRemote()
1290
1291 if self.UseAlternates:
1292 # If gitdir/objects is a symlink, migrate it from the old layout.
1293 gitdir_objects = os.path.join(self.gitdir, "objects")
1294 if platform_utils.islink(gitdir_objects):
1295 platform_utils.remove(gitdir_objects, missing_ok=True)
1296 gitdir_alt = os.path.join(self.gitdir, "objects/info/alternates")
1297 if not os.path.exists(gitdir_alt):
1298 os.makedirs(os.path.dirname(gitdir_alt), exist_ok=True)
1299 _lwrite(
1300 gitdir_alt,
1301 os.path.join(
1302 os.path.relpath(self.objdir, gitdir_objects), "objects"
1303 )
1304 + "\n",
1305 )
1306
1307 if is_new:
1308 alt = os.path.join(self.objdir, "objects/info/alternates")
1309 try:
1310 with open(alt) as fd:
1311 # This works for both absolute and relative alternate
1312 # directories.
1313 alt_dir = os.path.join(
1314 self.objdir, "objects", fd.readline().rstrip()
1315 )
1316 except IOError:
1317 alt_dir = None
1318 else:
1319 alt_dir = None
1320
1321 if (
1322 clone_bundle
1323 and alt_dir is None
1324 and self._ApplyCloneBundle(
1325 initial=is_new, quiet=quiet, verbose=verbose
1326 )
1327 ):
1328 is_new = False
1329
1330 if current_branch_only is None:
1331 if self.sync_c:
1332 current_branch_only = True
1333 elif not self.manifest._loaded:
1334 # Manifest cannot check defaults until it syncs.
1335 current_branch_only = False
1336 elif self.manifest.default.sync_c:
1337 current_branch_only = True
1338
1339 if tags is None:
1340 tags = self.sync_tags
1341
1342 if self.clone_depth:
1343 depth = self.clone_depth
1344 else:
1345 depth = self.manifest.manifestProject.depth
1346
Jason Chang17833322023-05-23 13:06:55 -07001347 if depth and clone_filter_for_depth:
1348 depth = None
1349 clone_filter = clone_filter_for_depth
1350
Gavin Makea2e3302023-03-11 06:46:20 +00001351 # See if we can skip the network fetch entirely.
1352 remote_fetched = False
1353 if not (
1354 optimized_fetch
1355 and (
1356 ID_RE.match(self.revisionExpr)
1357 and self._CheckForImmutableRevision()
1358 )
1359 ):
1360 remote_fetched = True
Jason Chang32b59562023-07-14 16:45:35 -07001361 try:
1362 if not self._RemoteFetch(
1363 initial=is_new,
1364 quiet=quiet,
1365 verbose=verbose,
1366 output_redir=output_redir,
1367 alt_dir=alt_dir,
1368 current_branch_only=current_branch_only,
1369 tags=tags,
1370 prune=prune,
1371 depth=depth,
1372 submodules=submodules,
1373 force_sync=force_sync,
1374 ssh_proxy=ssh_proxy,
1375 clone_filter=clone_filter,
1376 retry_fetches=retry_fetches,
1377 ):
1378 return SyncNetworkHalfResult(
1379 remote_fetched,
1380 SyncNetworkHalfError(
1381 f"Unable to remote fetch project {self.name}",
1382 project=self.name,
1383 ),
1384 )
1385 except RepoError as e:
1386 return SyncNetworkHalfResult(
1387 remote_fetched,
1388 e,
1389 )
Gavin Makea2e3302023-03-11 06:46:20 +00001390
1391 mp = self.manifest.manifestProject
1392 dissociate = mp.dissociate
1393 if dissociate:
1394 alternates_file = os.path.join(
1395 self.objdir, "objects/info/alternates"
1396 )
1397 if os.path.exists(alternates_file):
1398 cmd = ["repack", "-a", "-d"]
1399 p = GitCommand(
1400 self,
1401 cmd,
1402 bare=True,
1403 capture_stdout=bool(output_redir),
1404 merge_output=bool(output_redir),
1405 )
1406 if p.stdout and output_redir:
1407 output_redir.write(p.stdout)
1408 if p.Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07001409 return SyncNetworkHalfResult(
1410 remote_fetched,
1411 GitError(
1412 "Unable to repack alternates", project=self.name
1413 ),
1414 )
Gavin Makea2e3302023-03-11 06:46:20 +00001415 platform_utils.remove(alternates_file)
1416
1417 if self.worktree:
1418 self._InitMRef()
1419 else:
1420 self._InitMirrorHead()
1421 platform_utils.remove(
1422 os.path.join(self.gitdir, "FETCH_HEAD"), missing_ok=True
1423 )
Jason Chang32b59562023-07-14 16:45:35 -07001424 return SyncNetworkHalfResult(remote_fetched)
Gavin Makea2e3302023-03-11 06:46:20 +00001425
1426 def PostRepoUpgrade(self):
1427 self._InitHooks()
1428
1429 def _CopyAndLinkFiles(self):
1430 if self.client.isGitcClient:
1431 return
1432 for copyfile in self.copyfiles:
1433 copyfile._Copy()
1434 for linkfile in self.linkfiles:
1435 linkfile._Link()
1436
1437 def GetCommitRevisionId(self):
1438 """Get revisionId of a commit.
1439
1440 Use this method instead of GetRevisionId to get the id of the commit
1441 rather than the id of the current git object (for example, a tag)
1442
1443 """
1444 if not self.revisionExpr.startswith(R_TAGS):
1445 return self.GetRevisionId(self._allrefs)
1446
1447 try:
1448 return self.bare_git.rev_list(self.revisionExpr, "-1")[0]
1449 except GitError:
1450 raise ManifestInvalidRevisionError(
1451 "revision %s in %s not found" % (self.revisionExpr, self.name)
1452 )
1453
1454 def GetRevisionId(self, all_refs=None):
1455 if self.revisionId:
1456 return self.revisionId
1457
1458 rem = self.GetRemote()
1459 rev = rem.ToLocal(self.revisionExpr)
1460
1461 if all_refs is not None and rev in all_refs:
1462 return all_refs[rev]
1463
1464 try:
1465 return self.bare_git.rev_parse("--verify", "%s^0" % rev)
1466 except GitError:
1467 raise ManifestInvalidRevisionError(
1468 "revision %s in %s not found" % (self.revisionExpr, self.name)
1469 )
1470
1471 def SetRevisionId(self, revisionId):
1472 if self.revisionExpr:
1473 self.upstream = self.revisionExpr
1474
1475 self.revisionId = revisionId
1476
Jason Chang32b59562023-07-14 16:45:35 -07001477 def Sync_LocalHalf(
1478 self, syncbuf, force_sync=False, submodules=False, errors=None
1479 ):
Gavin Makea2e3302023-03-11 06:46:20 +00001480 """Perform only the local IO portion of the sync process.
1481
1482 Network access is not required.
1483 """
Jason Chang32b59562023-07-14 16:45:35 -07001484 if errors is None:
1485 errors = []
1486
1487 def fail(error: Exception):
1488 errors.append(error)
1489 syncbuf.fail(self, error)
1490
Gavin Makea2e3302023-03-11 06:46:20 +00001491 if not os.path.exists(self.gitdir):
Jason Chang32b59562023-07-14 16:45:35 -07001492 fail(
1493 LocalSyncFail(
1494 "Cannot checkout %s due to missing network sync; Run "
1495 "`repo sync -n %s` first." % (self.name, self.name),
1496 project=self.name,
1497 )
Gavin Makea2e3302023-03-11 06:46:20 +00001498 )
1499 return
1500
1501 self._InitWorkTree(force_sync=force_sync, submodules=submodules)
1502 all_refs = self.bare_ref.all
1503 self.CleanPublishedCache(all_refs)
1504 revid = self.GetRevisionId(all_refs)
1505
1506 # Special case the root of the repo client checkout. Make sure it
1507 # doesn't contain files being checked out to dirs we don't allow.
1508 if self.relpath == ".":
1509 PROTECTED_PATHS = {".repo"}
1510 paths = set(
1511 self.work_git.ls_tree("-z", "--name-only", "--", revid).split(
1512 "\0"
1513 )
1514 )
1515 bad_paths = paths & PROTECTED_PATHS
1516 if bad_paths:
Jason Chang32b59562023-07-14 16:45:35 -07001517 fail(
1518 LocalSyncFail(
1519 "Refusing to checkout project that writes to protected "
1520 "paths: %s" % (", ".join(bad_paths),),
1521 project=self.name,
1522 )
Gavin Makea2e3302023-03-11 06:46:20 +00001523 )
1524 return
1525
1526 def _doff():
1527 self._FastForward(revid)
1528 self._CopyAndLinkFiles()
1529
1530 def _dosubmodules():
1531 self._SyncSubmodules(quiet=True)
1532
1533 head = self.work_git.GetHead()
1534 if head.startswith(R_HEADS):
1535 branch = head[len(R_HEADS) :]
1536 try:
1537 head = all_refs[head]
1538 except KeyError:
1539 head = None
1540 else:
1541 branch = None
1542
1543 if branch is None or syncbuf.detach_head:
1544 # Currently on a detached HEAD. The user is assumed to
1545 # not have any local modifications worth worrying about.
1546 if self.IsRebaseInProgress():
Jason Chang32b59562023-07-14 16:45:35 -07001547 fail(_PriorSyncFailedError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001548 return
1549
1550 if head == revid:
1551 # No changes; don't do anything further.
1552 # Except if the head needs to be detached.
1553 if not syncbuf.detach_head:
1554 # The copy/linkfile config may have changed.
1555 self._CopyAndLinkFiles()
1556 return
1557 else:
1558 lost = self._revlist(not_rev(revid), HEAD)
1559 if lost:
1560 syncbuf.info(self, "discarding %d commits", len(lost))
1561
1562 try:
1563 self._Checkout(revid, quiet=True)
1564 if submodules:
1565 self._SyncSubmodules(quiet=True)
1566 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001567 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001568 return
1569 self._CopyAndLinkFiles()
1570 return
1571
1572 if head == revid:
1573 # No changes; don't do anything further.
1574 #
1575 # The copy/linkfile config may have changed.
1576 self._CopyAndLinkFiles()
1577 return
1578
1579 branch = self.GetBranch(branch)
1580
1581 if not branch.LocalMerge:
1582 # The current branch has no tracking configuration.
1583 # Jump off it to a detached HEAD.
1584 syncbuf.info(
1585 self, "leaving %s; does not track upstream", branch.name
1586 )
1587 try:
1588 self._Checkout(revid, quiet=True)
1589 if submodules:
1590 self._SyncSubmodules(quiet=True)
1591 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001592 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001593 return
1594 self._CopyAndLinkFiles()
1595 return
1596
1597 upstream_gain = self._revlist(not_rev(HEAD), revid)
1598
1599 # See if we can perform a fast forward merge. This can happen if our
1600 # branch isn't in the exact same state as we last published.
1601 try:
1602 self.work_git.merge_base("--is-ancestor", HEAD, revid)
1603 # Skip the published logic.
1604 pub = False
1605 except GitError:
1606 pub = self.WasPublished(branch.name, all_refs)
1607
1608 if pub:
1609 not_merged = self._revlist(not_rev(revid), pub)
1610 if not_merged:
1611 if upstream_gain:
1612 # The user has published this branch and some of those
1613 # commits are not yet merged upstream. We do not want
1614 # to rewrite the published commits so we punt.
Jason Chang32b59562023-07-14 16:45:35 -07001615 fail(
1616 LocalSyncFail(
1617 "branch %s is published (but not merged) and is "
1618 "now %d commits behind"
1619 % (branch.name, len(upstream_gain)),
1620 project=self.name,
1621 )
Gavin Makea2e3302023-03-11 06:46:20 +00001622 )
1623 return
1624 elif pub == head:
1625 # All published commits are merged, and thus we are a
1626 # strict subset. We can fast-forward safely.
1627 syncbuf.later1(self, _doff)
1628 if submodules:
1629 syncbuf.later1(self, _dosubmodules)
1630 return
1631
1632 # Examine the local commits not in the remote. Find the
1633 # last one attributed to this user, if any.
1634 local_changes = self._revlist(not_rev(revid), HEAD, format="%H %ce")
1635 last_mine = None
1636 cnt_mine = 0
1637 for commit in local_changes:
1638 commit_id, committer_email = commit.split(" ", 1)
1639 if committer_email == self.UserEmail:
1640 last_mine = commit_id
1641 cnt_mine += 1
1642
1643 if not upstream_gain and cnt_mine == len(local_changes):
1644 # The copy/linkfile config may have changed.
1645 self._CopyAndLinkFiles()
1646 return
1647
1648 if self.IsDirty(consider_untracked=False):
Jason Chang32b59562023-07-14 16:45:35 -07001649 fail(_DirtyError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001650 return
1651
1652 # If the upstream switched on us, warn the user.
1653 if branch.merge != self.revisionExpr:
1654 if branch.merge and self.revisionExpr:
1655 syncbuf.info(
1656 self,
1657 "manifest switched %s...%s",
1658 branch.merge,
1659 self.revisionExpr,
1660 )
1661 elif branch.merge:
1662 syncbuf.info(self, "manifest no longer tracks %s", branch.merge)
1663
1664 if cnt_mine < len(local_changes):
1665 # Upstream rebased. Not everything in HEAD was created by this user.
1666 syncbuf.info(
1667 self,
1668 "discarding %d commits removed from upstream",
1669 len(local_changes) - cnt_mine,
1670 )
1671
1672 branch.remote = self.GetRemote()
1673 if not ID_RE.match(self.revisionExpr):
1674 # In case of manifest sync the revisionExpr might be a SHA1.
1675 branch.merge = self.revisionExpr
1676 if not branch.merge.startswith("refs/"):
1677 branch.merge = R_HEADS + branch.merge
1678 branch.Save()
1679
1680 if cnt_mine > 0 and self.rebase:
1681
1682 def _docopyandlink():
1683 self._CopyAndLinkFiles()
1684
1685 def _dorebase():
1686 self._Rebase(upstream="%s^1" % last_mine, onto=revid)
1687
1688 syncbuf.later2(self, _dorebase)
1689 if submodules:
1690 syncbuf.later2(self, _dosubmodules)
1691 syncbuf.later2(self, _docopyandlink)
1692 elif local_changes:
1693 try:
1694 self._ResetHard(revid)
1695 if submodules:
1696 self._SyncSubmodules(quiet=True)
1697 self._CopyAndLinkFiles()
1698 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001699 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001700 return
1701 else:
1702 syncbuf.later1(self, _doff)
1703 if submodules:
1704 syncbuf.later1(self, _dosubmodules)
1705
1706 def AddCopyFile(self, src, dest, topdir):
1707 """Mark |src| for copying to |dest| (relative to |topdir|).
1708
1709 No filesystem changes occur here. Actual copying happens later on.
1710
1711 Paths should have basic validation run on them before being queued.
1712 Further checking will be handled when the actual copy happens.
1713 """
1714 self.copyfiles.append(_CopyFile(self.worktree, src, topdir, dest))
1715
1716 def AddLinkFile(self, src, dest, topdir):
1717 """Mark |dest| to create a symlink (relative to |topdir|) pointing to
1718 |src|.
1719
1720 No filesystem changes occur here. Actual linking happens later on.
1721
1722 Paths should have basic validation run on them before being queued.
1723 Further checking will be handled when the actual link happens.
1724 """
1725 self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest))
1726
1727 def AddAnnotation(self, name, value, keep):
1728 self.annotations.append(Annotation(name, value, keep))
1729
1730 def DownloadPatchSet(self, change_id, patch_id):
1731 """Download a single patch set of a single change to FETCH_HEAD."""
1732 remote = self.GetRemote()
1733
1734 cmd = ["fetch", remote.name]
1735 cmd.append(
1736 "refs/changes/%2.2d/%d/%d" % (change_id % 100, change_id, patch_id)
1737 )
Jason Chang1a3612f2023-08-08 14:12:53 -07001738 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001739 return DownloadedChange(
1740 self,
1741 self.GetRevisionId(),
1742 change_id,
1743 patch_id,
1744 self.bare_git.rev_parse("FETCH_HEAD"),
1745 )
1746
1747 def DeleteWorktree(self, quiet=False, force=False):
1748 """Delete the source checkout and any other housekeeping tasks.
1749
1750 This currently leaves behind the internal .repo/ cache state. This
1751 helps when switching branches or manifest changes get reverted as we
1752 don't have to redownload all the git objects. But we should do some GC
1753 at some point.
1754
1755 Args:
1756 quiet: Whether to hide normal messages.
1757 force: Always delete tree even if dirty.
Doug Anderson3ba5f952011-04-07 12:51:04 -07001758
1759 Returns:
Gavin Makea2e3302023-03-11 06:46:20 +00001760 True if the worktree was completely cleaned out.
1761 """
1762 if self.IsDirty():
1763 if force:
1764 print(
1765 "warning: %s: Removing dirty project: uncommitted changes "
1766 "lost." % (self.RelPath(local=False),),
1767 file=sys.stderr,
1768 )
1769 else:
Jason Chang32b59562023-07-14 16:45:35 -07001770 msg = (
1771 "error: %s: Cannot remove project: uncommitted"
1772 "changes are present.\n" % self.RelPath(local=False)
Gavin Makea2e3302023-03-11 06:46:20 +00001773 )
Jason Chang32b59562023-07-14 16:45:35 -07001774 print(msg, file=sys.stderr)
1775 raise DeleteDirtyWorktreeError(msg, project=self)
Wink Saville02d79452009-04-10 13:01:24 -07001776
Gavin Makea2e3302023-03-11 06:46:20 +00001777 if not quiet:
1778 print(
1779 "%s: Deleting obsolete checkout." % (self.RelPath(local=False),)
1780 )
Wink Saville02d79452009-04-10 13:01:24 -07001781
Gavin Makea2e3302023-03-11 06:46:20 +00001782 # Unlock and delink from the main worktree. We don't use git's worktree
1783 # remove because it will recursively delete projects -- we handle that
1784 # ourselves below. https://crbug.com/git/48
1785 if self.use_git_worktrees:
1786 needle = platform_utils.realpath(self.gitdir)
1787 # Find the git worktree commondir under .repo/worktrees/.
1788 output = self.bare_git.worktree("list", "--porcelain").splitlines()[
1789 0
1790 ]
1791 assert output.startswith("worktree "), output
1792 commondir = output[9:]
1793 # Walk each of the git worktrees to see where they point.
1794 configs = os.path.join(commondir, "worktrees")
1795 for name in os.listdir(configs):
1796 gitdir = os.path.join(configs, name, "gitdir")
1797 with open(gitdir) as fp:
1798 relpath = fp.read().strip()
1799 # Resolve the checkout path and see if it matches this project.
1800 fullpath = platform_utils.realpath(
1801 os.path.join(configs, name, relpath)
1802 )
1803 if fullpath == needle:
1804 platform_utils.rmtree(os.path.join(configs, name))
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001805
Gavin Makea2e3302023-03-11 06:46:20 +00001806 # Delete the .git directory first, so we're less likely to have a
1807 # partially working git repository around. There shouldn't be any git
1808 # projects here, so rmtree works.
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001809
Gavin Makea2e3302023-03-11 06:46:20 +00001810 # Try to remove plain files first in case of git worktrees. If this
1811 # fails for any reason, we'll fall back to rmtree, and that'll display
1812 # errors if it can't remove things either.
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001813 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001814 platform_utils.remove(self.gitdir)
1815 except OSError:
1816 pass
1817 try:
1818 platform_utils.rmtree(self.gitdir)
1819 except OSError as e:
1820 if e.errno != errno.ENOENT:
1821 print("error: %s: %s" % (self.gitdir, e), file=sys.stderr)
1822 print(
1823 "error: %s: Failed to delete obsolete checkout; remove "
1824 "manually, then run `repo sync -l`."
1825 % (self.RelPath(local=False),),
1826 file=sys.stderr,
1827 )
Jason Chang32b59562023-07-14 16:45:35 -07001828 raise DeleteWorktreeError(aggregate_errors=[e])
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001829
Gavin Makea2e3302023-03-11 06:46:20 +00001830 # Delete everything under the worktree, except for directories that
1831 # contain another git project.
1832 dirs_to_remove = []
1833 failed = False
Jason Chang32b59562023-07-14 16:45:35 -07001834 errors = []
Gavin Makea2e3302023-03-11 06:46:20 +00001835 for root, dirs, files in platform_utils.walk(self.worktree):
1836 for f in files:
1837 path = os.path.join(root, f)
1838 try:
1839 platform_utils.remove(path)
1840 except OSError as e:
1841 if e.errno != errno.ENOENT:
1842 print(
1843 "error: %s: Failed to remove: %s" % (path, e),
1844 file=sys.stderr,
1845 )
1846 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001847 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001848 dirs[:] = [
1849 d
1850 for d in dirs
1851 if not os.path.lexists(os.path.join(root, d, ".git"))
1852 ]
1853 dirs_to_remove += [
1854 os.path.join(root, d)
1855 for d in dirs
1856 if os.path.join(root, d) not in dirs_to_remove
1857 ]
1858 for d in reversed(dirs_to_remove):
1859 if platform_utils.islink(d):
1860 try:
1861 platform_utils.remove(d)
1862 except OSError as e:
1863 if e.errno != errno.ENOENT:
1864 print(
1865 "error: %s: Failed to remove: %s" % (d, e),
1866 file=sys.stderr,
1867 )
1868 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001869 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001870 elif not platform_utils.listdir(d):
1871 try:
1872 platform_utils.rmdir(d)
1873 except OSError as e:
1874 if e.errno != errno.ENOENT:
1875 print(
1876 "error: %s: Failed to remove: %s" % (d, e),
1877 file=sys.stderr,
1878 )
1879 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001880 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001881 if failed:
1882 print(
1883 "error: %s: Failed to delete obsolete checkout."
1884 % (self.RelPath(local=False),),
1885 file=sys.stderr,
1886 )
1887 print(
1888 " Remove manually, then run `repo sync -l`.",
1889 file=sys.stderr,
1890 )
Jason Chang32b59562023-07-14 16:45:35 -07001891 raise DeleteWorktreeError(aggregate_errors=errors)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07001892
Gavin Makea2e3302023-03-11 06:46:20 +00001893 # Try deleting parent dirs if they are empty.
1894 path = self.worktree
1895 while path != self.manifest.topdir:
1896 try:
1897 platform_utils.rmdir(path)
1898 except OSError as e:
1899 if e.errno != errno.ENOENT:
1900 break
1901 path = os.path.dirname(path)
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001902
Gavin Makea2e3302023-03-11 06:46:20 +00001903 return True
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001904
Gavin Makea2e3302023-03-11 06:46:20 +00001905 def StartBranch(self, name, branch_merge="", revision=None):
1906 """Create a new branch off the manifest's revision."""
1907 if not branch_merge:
1908 branch_merge = self.revisionExpr
1909 head = self.work_git.GetHead()
1910 if head == (R_HEADS + name):
1911 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001912
David Pursehouse8a68ff92012-09-24 12:15:13 +09001913 all_refs = self.bare_ref.all
Gavin Makea2e3302023-03-11 06:46:20 +00001914 if R_HEADS + name in all_refs:
Jason Chang1a3612f2023-08-08 14:12:53 -07001915 GitCommand(
1916 self, ["checkout", "-q", name, "--"], verify_command=True
1917 ).Wait()
1918 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001919
Gavin Makea2e3302023-03-11 06:46:20 +00001920 branch = self.GetBranch(name)
1921 branch.remote = self.GetRemote()
1922 branch.merge = branch_merge
1923 if not branch.merge.startswith("refs/") and not ID_RE.match(
1924 branch_merge
1925 ):
1926 branch.merge = R_HEADS + branch_merge
Shawn O. Pearce88443382010-10-08 10:02:09 +02001927
Gavin Makea2e3302023-03-11 06:46:20 +00001928 if revision is None:
1929 revid = self.GetRevisionId(all_refs)
Shawn O. Pearce88443382010-10-08 10:02:09 +02001930 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001931 revid = self.work_git.rev_parse(revision)
Brian Harring14a66742012-09-28 20:21:57 -07001932
Gavin Makea2e3302023-03-11 06:46:20 +00001933 if head.startswith(R_HEADS):
Kevin Degiabaa7f32014-11-12 11:27:45 -07001934 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001935 head = all_refs[head]
1936 except KeyError:
1937 head = None
1938 if revid and head and revid == head:
1939 ref = R_HEADS + name
1940 self.work_git.update_ref(ref, revid)
1941 self.work_git.symbolic_ref(HEAD, ref)
1942 branch.Save()
1943 return True
Kevin Degib1a07b82015-07-27 13:33:43 -06001944
Jason Chang1a3612f2023-08-08 14:12:53 -07001945 GitCommand(
1946 self,
1947 ["checkout", "-q", "-b", branch.name, revid],
1948 verify_command=True,
1949 ).Wait()
1950 branch.Save()
1951 return True
Kevin Degi384b3c52014-10-16 16:02:58 -06001952
Gavin Makea2e3302023-03-11 06:46:20 +00001953 def CheckoutBranch(self, name):
1954 """Checkout a local topic branch.
Shawn O. Pearce2816d4f2009-03-03 17:53:18 -08001955
Gavin Makea2e3302023-03-11 06:46:20 +00001956 Args:
1957 name: The name of the branch to checkout.
Shawn O. Pearce88443382010-10-08 10:02:09 +02001958
Gavin Makea2e3302023-03-11 06:46:20 +00001959 Returns:
Jason Chang1a3612f2023-08-08 14:12:53 -07001960 True if the checkout succeeded; False if the
1961 branch doesn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00001962 """
1963 rev = R_HEADS + name
1964 head = self.work_git.GetHead()
1965 if head == rev:
1966 # Already on the branch.
1967 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001968
Gavin Makea2e3302023-03-11 06:46:20 +00001969 all_refs = self.bare_ref.all
1970 try:
1971 revid = all_refs[rev]
1972 except KeyError:
1973 # Branch does not exist in this project.
Jason Chang1a3612f2023-08-08 14:12:53 -07001974 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001975
Gavin Makea2e3302023-03-11 06:46:20 +00001976 if head.startswith(R_HEADS):
1977 try:
1978 head = all_refs[head]
1979 except KeyError:
1980 head = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001981
Gavin Makea2e3302023-03-11 06:46:20 +00001982 if head == revid:
1983 # Same revision; just update HEAD to point to the new
1984 # target branch, but otherwise take no other action.
1985 _lwrite(
1986 self.work_git.GetDotgitPath(subpath=HEAD),
1987 "ref: %s%s\n" % (R_HEADS, name),
1988 )
1989 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05001990
Jason Chang1a3612f2023-08-08 14:12:53 -07001991 GitCommand(
1992 self,
1993 ["checkout", name, "--"],
1994 capture_stdout=True,
1995 capture_stderr=True,
1996 verify_command=True,
1997 ).Wait()
1998 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05001999
Gavin Makea2e3302023-03-11 06:46:20 +00002000 def AbandonBranch(self, name):
2001 """Destroy a local topic branch.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002002
Gavin Makea2e3302023-03-11 06:46:20 +00002003 Args:
2004 name: The name of the branch to abandon.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002005
Gavin Makea2e3302023-03-11 06:46:20 +00002006 Returns:
Jason Changf9aacd42023-08-03 14:38:00 -07002007 True if the abandon succeeded; Raises GitCommandError if it didn't;
2008 None if the branch didn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00002009 """
2010 rev = R_HEADS + name
2011 all_refs = self.bare_ref.all
2012 if rev not in all_refs:
2013 # Doesn't exist
2014 return None
2015
2016 head = self.work_git.GetHead()
2017 if head == rev:
2018 # We can't destroy the branch while we are sitting
2019 # on it. Switch to a detached HEAD.
2020 head = all_refs[head]
2021
2022 revid = self.GetRevisionId(all_refs)
2023 if head == revid:
2024 _lwrite(
2025 self.work_git.GetDotgitPath(subpath=HEAD), "%s\n" % revid
2026 )
2027 else:
2028 self._Checkout(revid, quiet=True)
Jason Changf9aacd42023-08-03 14:38:00 -07002029 GitCommand(
2030 self,
2031 ["branch", "-D", name],
2032 capture_stdout=True,
2033 capture_stderr=True,
2034 verify_command=True,
2035 ).Wait()
2036 return True
Gavin Makea2e3302023-03-11 06:46:20 +00002037
2038 def PruneHeads(self):
2039 """Prune any topic branches already merged into upstream."""
2040 cb = self.CurrentBranch
2041 kill = []
2042 left = self._allrefs
2043 for name in left.keys():
2044 if name.startswith(R_HEADS):
2045 name = name[len(R_HEADS) :]
2046 if cb is None or name != cb:
2047 kill.append(name)
2048
2049 # Minor optimization: If there's nothing to prune, then don't try to
2050 # read any project state.
2051 if not kill and not cb:
2052 return []
2053
2054 rev = self.GetRevisionId(left)
2055 if (
2056 cb is not None
2057 and not self._revlist(HEAD + "..." + rev)
2058 and not self.IsDirty(consider_untracked=False)
2059 ):
2060 self.work_git.DetachHead(HEAD)
2061 kill.append(cb)
2062
2063 if kill:
2064 old = self.bare_git.GetHead()
2065
2066 try:
2067 self.bare_git.DetachHead(rev)
2068
2069 b = ["branch", "-d"]
2070 b.extend(kill)
2071 b = GitCommand(
2072 self, b, bare=True, capture_stdout=True, capture_stderr=True
2073 )
2074 b.Wait()
2075 finally:
2076 if ID_RE.match(old):
2077 self.bare_git.DetachHead(old)
2078 else:
2079 self.bare_git.SetHead(old)
2080 left = self._allrefs
2081
2082 for branch in kill:
2083 if (R_HEADS + branch) not in left:
2084 self.CleanPublishedCache()
2085 break
2086
2087 if cb and cb not in kill:
2088 kill.append(cb)
2089 kill.sort()
2090
2091 kept = []
2092 for branch in kill:
2093 if R_HEADS + branch in left:
2094 branch = self.GetBranch(branch)
2095 base = branch.LocalMerge
2096 if not base:
2097 base = rev
2098 kept.append(ReviewableBranch(self, branch, base))
2099 return kept
2100
2101 def GetRegisteredSubprojects(self):
2102 result = []
2103
2104 def rec(subprojects):
2105 if not subprojects:
2106 return
2107 result.extend(subprojects)
2108 for p in subprojects:
2109 rec(p.subprojects)
2110
2111 rec(self.subprojects)
2112 return result
2113
2114 def _GetSubmodules(self):
2115 # Unfortunately we cannot call `git submodule status --recursive` here
2116 # because the working tree might not exist yet, and it cannot be used
2117 # without a working tree in its current implementation.
2118
2119 def get_submodules(gitdir, rev):
2120 # Parse .gitmodules for submodule sub_paths and sub_urls.
2121 sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
2122 if not sub_paths:
2123 return []
2124 # Run `git ls-tree` to read SHAs of submodule object, which happen
2125 # to be revision of submodule repository.
2126 sub_revs = git_ls_tree(gitdir, rev, sub_paths)
2127 submodules = []
2128 for sub_path, sub_url in zip(sub_paths, sub_urls):
2129 try:
2130 sub_rev = sub_revs[sub_path]
2131 except KeyError:
2132 # Ignore non-exist submodules.
2133 continue
2134 submodules.append((sub_rev, sub_path, sub_url))
2135 return submodules
2136
2137 re_path = re.compile(r"^submodule\.(.+)\.path=(.*)$")
2138 re_url = re.compile(r"^submodule\.(.+)\.url=(.*)$")
2139
2140 def parse_gitmodules(gitdir, rev):
2141 cmd = ["cat-file", "blob", "%s:.gitmodules" % rev]
2142 try:
2143 p = GitCommand(
2144 None,
2145 cmd,
2146 capture_stdout=True,
2147 capture_stderr=True,
2148 bare=True,
2149 gitdir=gitdir,
2150 )
2151 except GitError:
2152 return [], []
2153 if p.Wait() != 0:
2154 return [], []
2155
2156 gitmodules_lines = []
2157 fd, temp_gitmodules_path = tempfile.mkstemp()
2158 try:
2159 os.write(fd, p.stdout.encode("utf-8"))
2160 os.close(fd)
2161 cmd = ["config", "--file", temp_gitmodules_path, "--list"]
2162 p = GitCommand(
2163 None,
2164 cmd,
2165 capture_stdout=True,
2166 capture_stderr=True,
2167 bare=True,
2168 gitdir=gitdir,
2169 )
2170 if p.Wait() != 0:
2171 return [], []
2172 gitmodules_lines = p.stdout.split("\n")
2173 except GitError:
2174 return [], []
2175 finally:
2176 platform_utils.remove(temp_gitmodules_path)
2177
2178 names = set()
2179 paths = {}
2180 urls = {}
2181 for line in gitmodules_lines:
2182 if not line:
2183 continue
2184 m = re_path.match(line)
2185 if m:
2186 names.add(m.group(1))
2187 paths[m.group(1)] = m.group(2)
2188 continue
2189 m = re_url.match(line)
2190 if m:
2191 names.add(m.group(1))
2192 urls[m.group(1)] = m.group(2)
2193 continue
2194 names = sorted(names)
2195 return (
2196 [paths.get(name, "") for name in names],
2197 [urls.get(name, "") for name in names],
2198 )
2199
2200 def git_ls_tree(gitdir, rev, paths):
2201 cmd = ["ls-tree", rev, "--"]
2202 cmd.extend(paths)
2203 try:
2204 p = GitCommand(
2205 None,
2206 cmd,
2207 capture_stdout=True,
2208 capture_stderr=True,
2209 bare=True,
2210 gitdir=gitdir,
2211 )
2212 except GitError:
2213 return []
2214 if p.Wait() != 0:
2215 return []
2216 objects = {}
2217 for line in p.stdout.split("\n"):
2218 if not line.strip():
2219 continue
2220 object_rev, object_path = line.split()[2:4]
2221 objects[object_path] = object_rev
2222 return objects
2223
2224 try:
2225 rev = self.GetRevisionId()
2226 except GitError:
2227 return []
2228 return get_submodules(self.gitdir, rev)
2229
2230 def GetDerivedSubprojects(self):
2231 result = []
2232 if not self.Exists:
2233 # If git repo does not exist yet, querying its submodules will
2234 # mess up its states; so return here.
2235 return result
2236 for rev, path, url in self._GetSubmodules():
2237 name = self.manifest.GetSubprojectName(self, path)
2238 (
2239 relpath,
2240 worktree,
2241 gitdir,
2242 objdir,
2243 ) = self.manifest.GetSubprojectPaths(self, name, path)
2244 project = self.manifest.paths.get(relpath)
2245 if project:
2246 result.extend(project.GetDerivedSubprojects())
2247 continue
2248
2249 if url.startswith(".."):
2250 url = urllib.parse.urljoin("%s/" % self.remote.url, url)
2251 remote = RemoteSpec(
2252 self.remote.name,
2253 url=url,
2254 pushUrl=self.remote.pushUrl,
2255 review=self.remote.review,
2256 revision=self.remote.revision,
2257 )
2258 subproject = Project(
2259 manifest=self.manifest,
2260 name=name,
2261 remote=remote,
2262 gitdir=gitdir,
2263 objdir=objdir,
2264 worktree=worktree,
2265 relpath=relpath,
2266 revisionExpr=rev,
2267 revisionId=rev,
2268 rebase=self.rebase,
2269 groups=self.groups,
2270 sync_c=self.sync_c,
2271 sync_s=self.sync_s,
2272 sync_tags=self.sync_tags,
2273 parent=self,
2274 is_derived=True,
2275 )
2276 result.append(subproject)
2277 result.extend(subproject.GetDerivedSubprojects())
2278 return result
2279
2280 def EnableRepositoryExtension(self, key, value="true", version=1):
2281 """Enable git repository extension |key| with |value|.
2282
2283 Args:
2284 key: The extension to enabled. Omit the "extensions." prefix.
2285 value: The value to use for the extension.
2286 version: The minimum git repository version needed.
2287 """
2288 # Make sure the git repo version is new enough already.
2289 found_version = self.config.GetInt("core.repositoryFormatVersion")
2290 if found_version is None:
2291 found_version = 0
2292 if found_version < version:
2293 self.config.SetString("core.repositoryFormatVersion", str(version))
2294
2295 # Enable the extension!
2296 self.config.SetString("extensions.%s" % (key,), value)
2297
2298 def ResolveRemoteHead(self, name=None):
2299 """Find out what the default branch (HEAD) points to.
2300
2301 Normally this points to refs/heads/master, but projects are moving to
2302 main. Support whatever the server uses rather than hardcoding "master"
2303 ourselves.
2304 """
2305 if name is None:
2306 name = self.remote.name
2307
2308 # The output will look like (NB: tabs are separators):
2309 # ref: refs/heads/master HEAD
2310 # 5f6803b100bb3cd0f534e96e88c91373e8ed1c44 HEAD
2311 output = self.bare_git.ls_remote(
2312 "-q", "--symref", "--exit-code", name, "HEAD"
2313 )
2314
2315 for line in output.splitlines():
2316 lhs, rhs = line.split("\t", 1)
2317 if rhs == "HEAD" and lhs.startswith("ref:"):
2318 return lhs[4:].strip()
2319
2320 return None
2321
2322 def _CheckForImmutableRevision(self):
2323 try:
2324 # if revision (sha or tag) is not present then following function
2325 # throws an error.
2326 self.bare_git.rev_list(
2327 "-1", "--missing=allow-any", "%s^0" % self.revisionExpr, "--"
2328 )
2329 if self.upstream:
2330 rev = self.GetRemote().ToLocal(self.upstream)
2331 self.bare_git.rev_list(
2332 "-1", "--missing=allow-any", "%s^0" % rev, "--"
2333 )
2334 self.bare_git.merge_base(
2335 "--is-ancestor", self.revisionExpr, rev
2336 )
2337 return True
2338 except GitError:
2339 # There is no such persistent revision. We have to fetch it.
2340 return False
2341
2342 def _FetchArchive(self, tarpath, cwd=None):
2343 cmd = ["archive", "-v", "-o", tarpath]
2344 cmd.append("--remote=%s" % self.remote.url)
2345 cmd.append("--prefix=%s/" % self.RelPath(local=False))
2346 cmd.append(self.revisionExpr)
2347
2348 command = GitCommand(
Jason Chang32b59562023-07-14 16:45:35 -07002349 self,
2350 cmd,
2351 cwd=cwd,
2352 capture_stdout=True,
2353 capture_stderr=True,
2354 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00002355 )
Jason Chang32b59562023-07-14 16:45:35 -07002356 command.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00002357
2358 def _RemoteFetch(
2359 self,
2360 name=None,
2361 current_branch_only=False,
2362 initial=False,
2363 quiet=False,
2364 verbose=False,
2365 output_redir=None,
2366 alt_dir=None,
2367 tags=True,
2368 prune=False,
2369 depth=None,
2370 submodules=False,
2371 ssh_proxy=None,
2372 force_sync=False,
2373 clone_filter=None,
2374 retry_fetches=2,
2375 retry_sleep_initial_sec=4.0,
2376 retry_exp_factor=2.0,
Jason Chang32b59562023-07-14 16:45:35 -07002377 ) -> bool:
Gavin Makea2e3302023-03-11 06:46:20 +00002378 is_sha1 = False
2379 tag_name = None
2380 # The depth should not be used when fetching to a mirror because
2381 # it will result in a shallow repository that cannot be cloned or
2382 # fetched from.
2383 # The repo project should also never be synced with partial depth.
2384 if self.manifest.IsMirror or self.relpath == ".repo/repo":
2385 depth = None
2386
2387 if depth:
2388 current_branch_only = True
2389
2390 if ID_RE.match(self.revisionExpr) is not None:
2391 is_sha1 = True
2392
2393 if current_branch_only:
2394 if self.revisionExpr.startswith(R_TAGS):
2395 # This is a tag and its commit id should never change.
2396 tag_name = self.revisionExpr[len(R_TAGS) :]
2397 elif self.upstream and self.upstream.startswith(R_TAGS):
2398 # This is a tag and its commit id should never change.
2399 tag_name = self.upstream[len(R_TAGS) :]
2400
2401 if is_sha1 or tag_name is not None:
2402 if self._CheckForImmutableRevision():
2403 if verbose:
2404 print(
2405 "Skipped fetching project %s (already have "
2406 "persistent ref)" % self.name
2407 )
2408 return True
2409 if is_sha1 and not depth:
2410 # When syncing a specific commit and --depth is not set:
2411 # * if upstream is explicitly specified and is not a sha1, fetch
2412 # only upstream as users expect only upstream to be fetch.
2413 # Note: The commit might not be in upstream in which case the
2414 # sync will fail.
2415 # * otherwise, fetch all branches to make sure we end up with
2416 # the specific commit.
2417 if self.upstream:
2418 current_branch_only = not ID_RE.match(self.upstream)
2419 else:
2420 current_branch_only = False
2421
2422 if not name:
2423 name = self.remote.name
2424
2425 remote = self.GetRemote(name)
2426 if not remote.PreConnectFetch(ssh_proxy):
2427 ssh_proxy = None
2428
2429 if initial:
2430 if alt_dir and "objects" == os.path.basename(alt_dir):
2431 ref_dir = os.path.dirname(alt_dir)
2432 packed_refs = os.path.join(self.gitdir, "packed-refs")
2433
2434 all_refs = self.bare_ref.all
2435 ids = set(all_refs.values())
2436 tmp = set()
2437
2438 for r, ref_id in GitRefs(ref_dir).all.items():
2439 if r not in all_refs:
2440 if r.startswith(R_TAGS) or remote.WritesTo(r):
2441 all_refs[r] = ref_id
2442 ids.add(ref_id)
2443 continue
2444
2445 if ref_id in ids:
2446 continue
2447
2448 r = "refs/_alt/%s" % ref_id
2449 all_refs[r] = ref_id
2450 ids.add(ref_id)
2451 tmp.add(r)
2452
2453 tmp_packed_lines = []
2454 old_packed_lines = []
2455
2456 for r in sorted(all_refs):
2457 line = "%s %s\n" % (all_refs[r], r)
2458 tmp_packed_lines.append(line)
2459 if r not in tmp:
2460 old_packed_lines.append(line)
2461
2462 tmp_packed = "".join(tmp_packed_lines)
2463 old_packed = "".join(old_packed_lines)
2464 _lwrite(packed_refs, tmp_packed)
2465 else:
2466 alt_dir = None
2467
2468 cmd = ["fetch"]
2469
2470 if clone_filter:
2471 git_require((2, 19, 0), fail=True, msg="partial clones")
2472 cmd.append("--filter=%s" % clone_filter)
2473 self.EnableRepositoryExtension("partialclone", self.remote.name)
2474
2475 if depth:
2476 cmd.append("--depth=%s" % depth)
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002477 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002478 # If this repo has shallow objects, then we don't know which refs
2479 # have shallow objects or not. Tell git to unshallow all fetched
2480 # refs. Don't do this with projects that don't have shallow
2481 # objects, since it is less efficient.
2482 if os.path.exists(os.path.join(self.gitdir, "shallow")):
2483 cmd.append("--depth=2147483647")
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002484
Gavin Makea2e3302023-03-11 06:46:20 +00002485 if not verbose:
2486 cmd.append("--quiet")
2487 if not quiet and sys.stdout.isatty():
2488 cmd.append("--progress")
2489 if not self.worktree:
2490 cmd.append("--update-head-ok")
2491 cmd.append(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002492
Gavin Makea2e3302023-03-11 06:46:20 +00002493 if force_sync:
2494 cmd.append("--force")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002495
Gavin Makea2e3302023-03-11 06:46:20 +00002496 if prune:
2497 cmd.append("--prune")
Mike Frysinger29626b42021-05-01 09:37:13 -04002498
Gavin Makea2e3302023-03-11 06:46:20 +00002499 # Always pass something for --recurse-submodules, git with GIT_DIR
2500 # behaves incorrectly when not given `--recurse-submodules=no`.
2501 # (b/218891912)
2502 cmd.append(
2503 f'--recurse-submodules={"on-demand" if submodules else "no"}'
2504 )
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002505
Gavin Makea2e3302023-03-11 06:46:20 +00002506 spec = []
2507 if not current_branch_only:
2508 # Fetch whole repo.
2509 spec.append(
2510 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2511 )
2512 elif tag_name is not None:
2513 spec.append("tag")
2514 spec.append(tag_name)
Remy Böhmer1469c282020-12-15 18:49:02 +01002515
Gavin Makea2e3302023-03-11 06:46:20 +00002516 if self.manifest.IsMirror and not current_branch_only:
2517 branch = None
Remy Böhmer1469c282020-12-15 18:49:02 +01002518 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002519 branch = self.revisionExpr
2520 if (
2521 not self.manifest.IsMirror
2522 and is_sha1
2523 and depth
2524 and git_require((1, 8, 3))
2525 ):
2526 # Shallow checkout of a specific commit, fetch from that commit and
2527 # not the heads only as the commit might be deeper in the history.
2528 spec.append(branch)
2529 if self.upstream:
2530 spec.append(self.upstream)
David James8d201162013-10-11 17:03:19 -07002531 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002532 if is_sha1:
2533 branch = self.upstream
2534 if branch is not None and branch.strip():
2535 if not branch.startswith("refs/"):
2536 branch = R_HEADS + branch
2537 spec.append(str(("+%s:" % branch) + remote.ToLocal(branch)))
David James8d201162013-10-11 17:03:19 -07002538
Gavin Makea2e3302023-03-11 06:46:20 +00002539 # If mirroring repo and we cannot deduce the tag or branch to fetch,
2540 # fetch whole repo.
2541 if self.manifest.IsMirror and not spec:
2542 spec.append(
2543 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2544 )
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002545
Gavin Makea2e3302023-03-11 06:46:20 +00002546 # If using depth then we should not get all the tags since they may
2547 # be outside of the depth.
2548 if not tags or depth:
2549 cmd.append("--no-tags")
2550 else:
2551 cmd.append("--tags")
2552 spec.append(str(("+refs/tags/*:") + remote.ToLocal("refs/tags/*")))
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002553
Gavin Makea2e3302023-03-11 06:46:20 +00002554 cmd.extend(spec)
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002555
Gavin Makea2e3302023-03-11 06:46:20 +00002556 # At least one retry minimum due to git remote prune.
2557 retry_fetches = max(retry_fetches, 2)
2558 retry_cur_sleep = retry_sleep_initial_sec
2559 ok = prune_tried = False
2560 for try_n in range(retry_fetches):
Jason Chang32b59562023-07-14 16:45:35 -07002561 verify_command = try_n == retry_fetches - 1
Gavin Makea2e3302023-03-11 06:46:20 +00002562 gitcmd = GitCommand(
2563 self,
2564 cmd,
2565 bare=True,
2566 objdir=os.path.join(self.objdir, "objects"),
2567 ssh_proxy=ssh_proxy,
2568 merge_output=True,
2569 capture_stdout=quiet or bool(output_redir),
Jason Chang32b59562023-07-14 16:45:35 -07002570 verify_command=verify_command,
Gavin Makea2e3302023-03-11 06:46:20 +00002571 )
2572 if gitcmd.stdout and not quiet and output_redir:
2573 output_redir.write(gitcmd.stdout)
2574 ret = gitcmd.Wait()
2575 if ret == 0:
2576 ok = True
2577 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002578
Gavin Makea2e3302023-03-11 06:46:20 +00002579 # Retry later due to HTTP 429 Too Many Requests.
2580 elif (
2581 gitcmd.stdout
2582 and "error:" in gitcmd.stdout
2583 and "HTTP 429" in gitcmd.stdout
2584 ):
2585 # Fallthru to sleep+retry logic at the bottom.
2586 pass
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002587
Gavin Makea2e3302023-03-11 06:46:20 +00002588 # Try to prune remote branches once in case there are conflicts.
2589 # For example, if the remote had refs/heads/upstream, but deleted
2590 # that and now has refs/heads/upstream/foo.
2591 elif (
2592 gitcmd.stdout
2593 and "error:" in gitcmd.stdout
2594 and "git remote prune" in gitcmd.stdout
2595 and not prune_tried
2596 ):
2597 prune_tried = True
2598 prunecmd = GitCommand(
2599 self,
2600 ["remote", "prune", name],
2601 bare=True,
2602 ssh_proxy=ssh_proxy,
2603 )
2604 ret = prunecmd.Wait()
2605 if ret:
2606 break
2607 print(
2608 "retrying fetch after pruning remote branches",
2609 file=output_redir,
2610 )
2611 # Continue right away so we don't sleep as we shouldn't need to.
2612 continue
2613 elif current_branch_only and is_sha1 and ret == 128:
2614 # Exit code 128 means "couldn't find the ref you asked for"; if
2615 # we're in sha1 mode, we just tried sync'ing from the upstream
2616 # field; it doesn't exist, thus abort the optimization attempt
2617 # and do a full sync.
2618 break
2619 elif ret < 0:
2620 # Git died with a signal, exit immediately.
2621 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002622
Gavin Makea2e3302023-03-11 06:46:20 +00002623 # Figure out how long to sleep before the next attempt, if there is
2624 # one.
2625 if not verbose and gitcmd.stdout:
2626 print(
2627 "\n%s:\n%s" % (self.name, gitcmd.stdout),
2628 end="",
2629 file=output_redir,
2630 )
2631 if try_n < retry_fetches - 1:
2632 print(
2633 "%s: sleeping %s seconds before retrying"
2634 % (self.name, retry_cur_sleep),
2635 file=output_redir,
2636 )
2637 time.sleep(retry_cur_sleep)
2638 retry_cur_sleep = min(
2639 retry_exp_factor * retry_cur_sleep, MAXIMUM_RETRY_SLEEP_SEC
2640 )
2641 retry_cur_sleep *= 1 - random.uniform(
2642 -RETRY_JITTER_PERCENT, RETRY_JITTER_PERCENT
2643 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002644
Gavin Makea2e3302023-03-11 06:46:20 +00002645 if initial:
2646 if alt_dir:
2647 if old_packed != "":
2648 _lwrite(packed_refs, old_packed)
2649 else:
2650 platform_utils.remove(packed_refs)
2651 self.bare_git.pack_refs("--all", "--prune")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002652
Gavin Makea2e3302023-03-11 06:46:20 +00002653 if is_sha1 and current_branch_only:
2654 # We just synced the upstream given branch; verify we
2655 # got what we wanted, else trigger a second run of all
2656 # refs.
2657 if not self._CheckForImmutableRevision():
2658 # Sync the current branch only with depth set to None.
2659 # We always pass depth=None down to avoid infinite recursion.
2660 return self._RemoteFetch(
2661 name=name,
2662 quiet=quiet,
2663 verbose=verbose,
2664 output_redir=output_redir,
2665 current_branch_only=current_branch_only and depth,
2666 initial=False,
2667 alt_dir=alt_dir,
Nasser Grainawiaafed292023-05-24 12:51:03 -06002668 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00002669 depth=None,
2670 ssh_proxy=ssh_proxy,
2671 clone_filter=clone_filter,
2672 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002673
Gavin Makea2e3302023-03-11 06:46:20 +00002674 return ok
Mike Frysingerf4545122019-11-11 04:34:16 -05002675
Gavin Makea2e3302023-03-11 06:46:20 +00002676 def _ApplyCloneBundle(self, initial=False, quiet=False, verbose=False):
2677 if initial and (
2678 self.manifest.manifestProject.depth or self.clone_depth
2679 ):
2680 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002681
Gavin Makea2e3302023-03-11 06:46:20 +00002682 remote = self.GetRemote()
2683 bundle_url = remote.url + "/clone.bundle"
2684 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
2685 if GetSchemeFromUrl(bundle_url) not in (
2686 "http",
2687 "https",
2688 "persistent-http",
2689 "persistent-https",
2690 ):
2691 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06002692
Gavin Makea2e3302023-03-11 06:46:20 +00002693 bundle_dst = os.path.join(self.gitdir, "clone.bundle")
2694 bundle_tmp = os.path.join(self.gitdir, "clone.bundle.tmp")
2695
2696 exist_dst = os.path.exists(bundle_dst)
2697 exist_tmp = os.path.exists(bundle_tmp)
2698
2699 if not initial and not exist_dst and not exist_tmp:
2700 return False
2701
2702 if not exist_dst:
2703 exist_dst = self._FetchBundle(
2704 bundle_url, bundle_tmp, bundle_dst, quiet, verbose
2705 )
2706 if not exist_dst:
2707 return False
2708
2709 cmd = ["fetch"]
2710 if not verbose:
2711 cmd.append("--quiet")
2712 if not quiet and sys.stdout.isatty():
2713 cmd.append("--progress")
2714 if not self.worktree:
2715 cmd.append("--update-head-ok")
2716 cmd.append(bundle_dst)
2717 for f in remote.fetch:
2718 cmd.append(str(f))
2719 cmd.append("+refs/tags/*:refs/tags/*")
2720
2721 ok = (
2722 GitCommand(
2723 self,
2724 cmd,
2725 bare=True,
2726 objdir=os.path.join(self.objdir, "objects"),
2727 ).Wait()
2728 == 0
2729 )
2730 platform_utils.remove(bundle_dst, missing_ok=True)
2731 platform_utils.remove(bundle_tmp, missing_ok=True)
2732 return ok
2733
2734 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
2735 platform_utils.remove(dstPath, missing_ok=True)
2736
2737 cmd = ["curl", "--fail", "--output", tmpPath, "--netrc", "--location"]
2738 if quiet:
2739 cmd += ["--silent", "--show-error"]
2740 if os.path.exists(tmpPath):
2741 size = os.stat(tmpPath).st_size
2742 if size >= 1024:
2743 cmd += ["--continue-at", "%d" % (size,)]
2744 else:
2745 platform_utils.remove(tmpPath)
2746 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
2747 if cookiefile:
2748 cmd += ["--cookie", cookiefile]
2749 if proxy:
2750 cmd += ["--proxy", proxy]
2751 elif "http_proxy" in os.environ and "darwin" == sys.platform:
2752 cmd += ["--proxy", os.environ["http_proxy"]]
2753 if srcUrl.startswith("persistent-https"):
2754 srcUrl = "http" + srcUrl[len("persistent-https") :]
2755 elif srcUrl.startswith("persistent-http"):
2756 srcUrl = "http" + srcUrl[len("persistent-http") :]
2757 cmd += [srcUrl]
2758
2759 proc = None
2760 with Trace("Fetching bundle: %s", " ".join(cmd)):
2761 if verbose:
2762 print("%s: Downloading bundle: %s" % (self.name, srcUrl))
2763 stdout = None if verbose else subprocess.PIPE
2764 stderr = None if verbose else subprocess.STDOUT
2765 try:
2766 proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
2767 except OSError:
2768 return False
2769
2770 (output, _) = proc.communicate()
2771 curlret = proc.returncode
2772
2773 if curlret == 22:
2774 # From curl man page:
2775 # 22: HTTP page not retrieved. The requested url was not found
2776 # or returned another error with the HTTP error code being 400
2777 # or above. This return code only appears if -f, --fail is used.
2778 if verbose:
2779 print(
2780 "%s: Unable to retrieve clone.bundle; ignoring."
2781 % self.name
2782 )
2783 if output:
2784 print("Curl output:\n%s" % output)
2785 return False
2786 elif curlret and not verbose and output:
2787 print("%s" % output, file=sys.stderr)
2788
2789 if os.path.exists(tmpPath):
2790 if curlret == 0 and self._IsValidBundle(tmpPath, quiet):
2791 platform_utils.rename(tmpPath, dstPath)
2792 return True
2793 else:
2794 platform_utils.remove(tmpPath)
2795 return False
2796 else:
2797 return False
2798
2799 def _IsValidBundle(self, path, quiet):
2800 try:
2801 with open(path, "rb") as f:
2802 if f.read(16) == b"# v2 git bundle\n":
2803 return True
2804 else:
2805 if not quiet:
2806 print(
2807 "Invalid clone.bundle file; ignoring.",
2808 file=sys.stderr,
2809 )
2810 return False
2811 except OSError:
2812 return False
2813
2814 def _Checkout(self, rev, quiet=False):
2815 cmd = ["checkout"]
2816 if quiet:
2817 cmd.append("-q")
2818 cmd.append(rev)
2819 cmd.append("--")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002820 if GitCommand(self, cmd).Wait() != 0:
Gavin Makea2e3302023-03-11 06:46:20 +00002821 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002822 raise GitError(
2823 "%s checkout %s " % (self.name, rev), project=self.name
2824 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002825
Gavin Makea2e3302023-03-11 06:46:20 +00002826 def _CherryPick(self, rev, ffonly=False, record_origin=False):
2827 cmd = ["cherry-pick"]
2828 if ffonly:
2829 cmd.append("--ff")
2830 if record_origin:
2831 cmd.append("-x")
2832 cmd.append(rev)
2833 cmd.append("--")
2834 if GitCommand(self, cmd).Wait() != 0:
2835 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002836 raise GitError(
2837 "%s cherry-pick %s " % (self.name, rev), project=self.name
2838 )
Victor Boivie0960b5b2010-11-26 13:42:13 +01002839
Gavin Makea2e3302023-03-11 06:46:20 +00002840 def _LsRemote(self, refs):
2841 cmd = ["ls-remote", self.remote.name, refs]
2842 p = GitCommand(self, cmd, capture_stdout=True)
2843 if p.Wait() == 0:
2844 return p.stdout
2845 return None
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002846
Gavin Makea2e3302023-03-11 06:46:20 +00002847 def _Revert(self, rev):
2848 cmd = ["revert"]
2849 cmd.append("--no-edit")
2850 cmd.append(rev)
2851 cmd.append("--")
2852 if GitCommand(self, cmd).Wait() != 0:
2853 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002854 raise GitError(
2855 "%s revert %s " % (self.name, rev), project=self.name
2856 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002857
Gavin Makea2e3302023-03-11 06:46:20 +00002858 def _ResetHard(self, rev, quiet=True):
2859 cmd = ["reset", "--hard"]
2860 if quiet:
2861 cmd.append("-q")
2862 cmd.append(rev)
2863 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002864 raise GitError(
2865 "%s reset --hard %s " % (self.name, rev), project=self.name
2866 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002867
Gavin Makea2e3302023-03-11 06:46:20 +00002868 def _SyncSubmodules(self, quiet=True):
2869 cmd = ["submodule", "update", "--init", "--recursive"]
2870 if quiet:
2871 cmd.append("-q")
2872 if GitCommand(self, cmd).Wait() != 0:
2873 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07002874 "%s submodule update --init --recursive " % self.name,
2875 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00002876 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002877
Gavin Makea2e3302023-03-11 06:46:20 +00002878 def _Rebase(self, upstream, onto=None):
2879 cmd = ["rebase"]
2880 if onto is not None:
2881 cmd.extend(["--onto", onto])
2882 cmd.append(upstream)
2883 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002884 raise GitError(
2885 "%s rebase %s " % (self.name, upstream), project=self.name
2886 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002887
Gavin Makea2e3302023-03-11 06:46:20 +00002888 def _FastForward(self, head, ffonly=False):
2889 cmd = ["merge", "--no-stat", head]
2890 if ffonly:
2891 cmd.append("--ff-only")
2892 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002893 raise GitError(
2894 "%s merge %s " % (self.name, head), project=self.name
2895 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002896
Gavin Makea2e3302023-03-11 06:46:20 +00002897 def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False):
2898 init_git_dir = not os.path.exists(self.gitdir)
2899 init_obj_dir = not os.path.exists(self.objdir)
2900 try:
2901 # Initialize the bare repository, which contains all of the objects.
2902 if init_obj_dir:
2903 os.makedirs(self.objdir)
2904 self.bare_objdir.init()
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002905
Gavin Makea2e3302023-03-11 06:46:20 +00002906 self._UpdateHooks(quiet=quiet)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002907
Gavin Makea2e3302023-03-11 06:46:20 +00002908 if self.use_git_worktrees:
2909 # Enable per-worktree config file support if possible. This
2910 # is more a nice-to-have feature for users rather than a
2911 # hard requirement.
2912 if git_require((2, 20, 0)):
2913 self.EnableRepositoryExtension("worktreeConfig")
Renaud Paquay788e9622017-01-27 11:41:12 -08002914
Gavin Makea2e3302023-03-11 06:46:20 +00002915 # If we have a separate directory to hold refs, initialize it as
2916 # well.
2917 if self.objdir != self.gitdir:
2918 if init_git_dir:
2919 os.makedirs(self.gitdir)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002920
Gavin Makea2e3302023-03-11 06:46:20 +00002921 if init_obj_dir or init_git_dir:
2922 self._ReferenceGitDir(
2923 self.objdir, self.gitdir, copy_all=True
2924 )
2925 try:
2926 self._CheckDirReference(self.objdir, self.gitdir)
2927 except GitError as e:
2928 if force_sync:
2929 print(
2930 "Retrying clone after deleting %s" % self.gitdir,
2931 file=sys.stderr,
2932 )
2933 try:
2934 platform_utils.rmtree(
2935 platform_utils.realpath(self.gitdir)
2936 )
2937 if self.worktree and os.path.exists(
2938 platform_utils.realpath(self.worktree)
2939 ):
2940 platform_utils.rmtree(
2941 platform_utils.realpath(self.worktree)
2942 )
2943 return self._InitGitDir(
2944 mirror_git=mirror_git,
2945 force_sync=False,
2946 quiet=quiet,
2947 )
2948 except Exception:
2949 raise e
2950 raise e
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002951
Gavin Makea2e3302023-03-11 06:46:20 +00002952 if init_git_dir:
2953 mp = self.manifest.manifestProject
2954 ref_dir = mp.reference or ""
Julien Camperguedd654222014-01-09 16:21:37 +01002955
Gavin Makea2e3302023-03-11 06:46:20 +00002956 def _expanded_ref_dirs():
2957 """Iterate through possible git reference dir paths."""
2958 name = self.name + ".git"
2959 yield mirror_git or os.path.join(ref_dir, name)
2960 for prefix in "", self.remote.name:
2961 yield os.path.join(
2962 ref_dir, ".repo", "project-objects", prefix, name
2963 )
2964 yield os.path.join(
2965 ref_dir, ".repo", "worktrees", prefix, name
2966 )
2967
2968 if ref_dir or mirror_git:
2969 found_ref_dir = None
2970 for path in _expanded_ref_dirs():
2971 if os.path.exists(path):
2972 found_ref_dir = path
2973 break
2974 ref_dir = found_ref_dir
2975
2976 if ref_dir:
2977 if not os.path.isabs(ref_dir):
2978 # The alternate directory is relative to the object
2979 # database.
2980 ref_dir = os.path.relpath(
2981 ref_dir, os.path.join(self.objdir, "objects")
2982 )
2983 _lwrite(
2984 os.path.join(
2985 self.objdir, "objects/info/alternates"
2986 ),
2987 os.path.join(ref_dir, "objects") + "\n",
2988 )
2989
2990 m = self.manifest.manifestProject.config
2991 for key in ["user.name", "user.email"]:
2992 if m.Has(key, include_defaults=False):
2993 self.config.SetString(key, m.GetString(key))
2994 if not self.manifest.EnableGitLfs:
2995 self.config.SetString(
2996 "filter.lfs.smudge", "git-lfs smudge --skip -- %f"
2997 )
2998 self.config.SetString(
2999 "filter.lfs.process", "git-lfs filter-process --skip"
3000 )
3001 self.config.SetBoolean(
3002 "core.bare", True if self.manifest.IsMirror else None
3003 )
3004 except Exception:
3005 if init_obj_dir and os.path.exists(self.objdir):
3006 platform_utils.rmtree(self.objdir)
3007 if init_git_dir and os.path.exists(self.gitdir):
3008 platform_utils.rmtree(self.gitdir)
3009 raise
3010
3011 def _UpdateHooks(self, quiet=False):
3012 if os.path.exists(self.objdir):
3013 self._InitHooks(quiet=quiet)
3014
3015 def _InitHooks(self, quiet=False):
3016 hooks = platform_utils.realpath(os.path.join(self.objdir, "hooks"))
3017 if not os.path.exists(hooks):
3018 os.makedirs(hooks)
3019
3020 # Delete sample hooks. They're noise.
3021 for hook in glob.glob(os.path.join(hooks, "*.sample")):
3022 try:
3023 platform_utils.remove(hook, missing_ok=True)
3024 except PermissionError:
3025 pass
3026
3027 for stock_hook in _ProjectHooks():
3028 name = os.path.basename(stock_hook)
3029
3030 if (
3031 name in ("commit-msg",)
3032 and not self.remote.review
3033 and self is not self.manifest.manifestProject
3034 ):
3035 # Don't install a Gerrit Code Review hook if this
3036 # project does not appear to use it for reviews.
3037 #
3038 # Since the manifest project is one of those, but also
3039 # managed through gerrit, it's excluded.
3040 continue
3041
3042 dst = os.path.join(hooks, name)
3043 if platform_utils.islink(dst):
3044 continue
3045 if os.path.exists(dst):
3046 # If the files are the same, we'll leave it alone. We create
3047 # symlinks below by default but fallback to hardlinks if the OS
3048 # blocks them. So if we're here, it's probably because we made a
3049 # hardlink below.
3050 if not filecmp.cmp(stock_hook, dst, shallow=False):
3051 if not quiet:
3052 _warn(
3053 "%s: Not replacing locally modified %s hook",
3054 self.RelPath(local=False),
3055 name,
3056 )
3057 continue
3058 try:
3059 platform_utils.symlink(
3060 os.path.relpath(stock_hook, os.path.dirname(dst)), dst
3061 )
3062 except OSError as e:
3063 if e.errno == errno.EPERM:
3064 try:
3065 os.link(stock_hook, dst)
3066 except OSError:
Jason Chang32b59562023-07-14 16:45:35 -07003067 raise GitError(
3068 self._get_symlink_error_message(), project=self.name
3069 )
Gavin Makea2e3302023-03-11 06:46:20 +00003070 else:
3071 raise
3072
3073 def _InitRemote(self):
3074 if self.remote.url:
3075 remote = self.GetRemote()
3076 remote.url = self.remote.url
3077 remote.pushUrl = self.remote.pushUrl
3078 remote.review = self.remote.review
3079 remote.projectname = self.name
3080
3081 if self.worktree:
3082 remote.ResetFetch(mirror=False)
3083 else:
3084 remote.ResetFetch(mirror=True)
3085 remote.Save()
3086
3087 def _InitMRef(self):
3088 """Initialize the pseudo m/<manifest branch> ref."""
3089 if self.manifest.branch:
3090 if self.use_git_worktrees:
3091 # Set up the m/ space to point to the worktree-specific ref
3092 # space. We'll update the worktree-specific ref space on each
3093 # checkout.
3094 ref = R_M + self.manifest.branch
3095 if not self.bare_ref.symref(ref):
3096 self.bare_git.symbolic_ref(
3097 "-m",
3098 "redirecting to worktree scope",
3099 ref,
3100 R_WORKTREE_M + self.manifest.branch,
3101 )
3102
3103 # We can't update this ref with git worktrees until it exists.
3104 # We'll wait until the initial checkout to set it.
3105 if not os.path.exists(self.worktree):
3106 return
3107
3108 base = R_WORKTREE_M
3109 active_git = self.work_git
3110
3111 self._InitAnyMRef(HEAD, self.bare_git, detach=True)
3112 else:
3113 base = R_M
3114 active_git = self.bare_git
3115
3116 self._InitAnyMRef(base + self.manifest.branch, active_git)
3117
3118 def _InitMirrorHead(self):
3119 self._InitAnyMRef(HEAD, self.bare_git)
3120
3121 def _InitAnyMRef(self, ref, active_git, detach=False):
3122 """Initialize |ref| in |active_git| to the value in the manifest.
3123
3124 This points |ref| to the <project> setting in the manifest.
3125
3126 Args:
3127 ref: The branch to update.
3128 active_git: The git repository to make updates in.
3129 detach: Whether to update target of symbolic refs, or overwrite the
3130 ref directly (and thus make it non-symbolic).
3131 """
3132 cur = self.bare_ref.symref(ref)
3133
3134 if self.revisionId:
3135 if cur != "" or self.bare_ref.get(ref) != self.revisionId:
3136 msg = "manifest set to %s" % self.revisionId
3137 dst = self.revisionId + "^0"
3138 active_git.UpdateRef(ref, dst, message=msg, detach=True)
Julien Camperguedd654222014-01-09 16:21:37 +01003139 else:
Gavin Makea2e3302023-03-11 06:46:20 +00003140 remote = self.GetRemote()
3141 dst = remote.ToLocal(self.revisionExpr)
3142 if cur != dst:
3143 msg = "manifest set to %s" % self.revisionExpr
3144 if detach:
3145 active_git.UpdateRef(ref, dst, message=msg, detach=True)
3146 else:
3147 active_git.symbolic_ref("-m", msg, ref, dst)
Julien Camperguedd654222014-01-09 16:21:37 +01003148
Gavin Makea2e3302023-03-11 06:46:20 +00003149 def _CheckDirReference(self, srcdir, destdir):
3150 # Git worktrees don't use symlinks to share at all.
3151 if self.use_git_worktrees:
3152 return
Julien Camperguedd654222014-01-09 16:21:37 +01003153
Gavin Makea2e3302023-03-11 06:46:20 +00003154 for name in self.shareable_dirs:
3155 # Try to self-heal a bit in simple cases.
3156 dst_path = os.path.join(destdir, name)
3157 src_path = os.path.join(srcdir, name)
Julien Camperguedd654222014-01-09 16:21:37 +01003158
Gavin Makea2e3302023-03-11 06:46:20 +00003159 dst = platform_utils.realpath(dst_path)
3160 if os.path.lexists(dst):
3161 src = platform_utils.realpath(src_path)
3162 # Fail if the links are pointing to the wrong place.
3163 if src != dst:
3164 _error("%s is different in %s vs %s", name, destdir, srcdir)
3165 raise GitError(
3166 "--force-sync not enabled; cannot overwrite a local "
3167 "work tree. If you're comfortable with the "
3168 "possibility of losing the work tree's git metadata,"
3169 " use `repo sync --force-sync {0}` to "
Jason Chang32b59562023-07-14 16:45:35 -07003170 "proceed.".format(self.RelPath(local=False)),
3171 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003172 )
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003173
Gavin Makea2e3302023-03-11 06:46:20 +00003174 def _ReferenceGitDir(self, gitdir, dotgit, copy_all):
3175 """Update |dotgit| to reference |gitdir|, using symlinks where possible.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003176
Gavin Makea2e3302023-03-11 06:46:20 +00003177 Args:
3178 gitdir: The bare git repository. Must already be initialized.
3179 dotgit: The repository you would like to initialize.
3180 copy_all: If true, copy all remaining files from |gitdir| ->
3181 |dotgit|. This saves you the effort of initializing |dotgit|
3182 yourself.
3183 """
3184 symlink_dirs = self.shareable_dirs[:]
3185 to_symlink = symlink_dirs
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003186
Gavin Makea2e3302023-03-11 06:46:20 +00003187 to_copy = []
3188 if copy_all:
3189 to_copy = platform_utils.listdir(gitdir)
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003190
Gavin Makea2e3302023-03-11 06:46:20 +00003191 dotgit = platform_utils.realpath(dotgit)
3192 for name in set(to_copy).union(to_symlink):
3193 try:
3194 src = platform_utils.realpath(os.path.join(gitdir, name))
3195 dst = os.path.join(dotgit, name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003196
Gavin Makea2e3302023-03-11 06:46:20 +00003197 if os.path.lexists(dst):
3198 continue
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003199
Gavin Makea2e3302023-03-11 06:46:20 +00003200 # If the source dir doesn't exist, create an empty dir.
3201 if name in symlink_dirs and not os.path.lexists(src):
3202 os.makedirs(src)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003203
Gavin Makea2e3302023-03-11 06:46:20 +00003204 if name in to_symlink:
3205 platform_utils.symlink(
3206 os.path.relpath(src, os.path.dirname(dst)), dst
3207 )
3208 elif copy_all and not platform_utils.islink(dst):
3209 if platform_utils.isdir(src):
3210 shutil.copytree(src, dst)
3211 elif os.path.isfile(src):
3212 shutil.copy(src, dst)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003213
Gavin Makea2e3302023-03-11 06:46:20 +00003214 except OSError as e:
3215 if e.errno == errno.EPERM:
3216 raise DownloadError(self._get_symlink_error_message())
3217 else:
3218 raise
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003219
Gavin Makea2e3302023-03-11 06:46:20 +00003220 def _InitGitWorktree(self):
3221 """Init the project using git worktrees."""
3222 self.bare_git.worktree("prune")
3223 self.bare_git.worktree(
3224 "add",
3225 "-ff",
3226 "--checkout",
3227 "--detach",
3228 "--lock",
3229 self.worktree,
3230 self.GetRevisionId(),
3231 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003232
Gavin Makea2e3302023-03-11 06:46:20 +00003233 # Rewrite the internal state files to use relative paths between the
3234 # checkouts & worktrees.
3235 dotgit = os.path.join(self.worktree, ".git")
3236 with open(dotgit, "r") as fp:
3237 # Figure out the checkout->worktree path.
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003238 setting = fp.read()
Gavin Makea2e3302023-03-11 06:46:20 +00003239 assert setting.startswith("gitdir:")
3240 git_worktree_path = setting.split(":", 1)[1].strip()
3241 # Some platforms (e.g. Windows) won't let us update dotgit in situ
3242 # because of file permissions. Delete it and recreate it from scratch
3243 # to avoid.
3244 platform_utils.remove(dotgit)
3245 # Use relative path from checkout->worktree & maintain Unix line endings
3246 # on all OS's to match git behavior.
3247 with open(dotgit, "w", newline="\n") as fp:
3248 print(
3249 "gitdir:",
3250 os.path.relpath(git_worktree_path, self.worktree),
3251 file=fp,
3252 )
3253 # Use relative path from worktree->checkout & maintain Unix line endings
3254 # on all OS's to match git behavior.
3255 with open(
3256 os.path.join(git_worktree_path, "gitdir"), "w", newline="\n"
3257 ) as fp:
3258 print(os.path.relpath(dotgit, git_worktree_path), file=fp)
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003259
Gavin Makea2e3302023-03-11 06:46:20 +00003260 self._InitMRef()
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003261
Gavin Makea2e3302023-03-11 06:46:20 +00003262 def _InitWorkTree(self, force_sync=False, submodules=False):
3263 """Setup the worktree .git path.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003264
Gavin Makea2e3302023-03-11 06:46:20 +00003265 This is the user-visible path like src/foo/.git/.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003266
Gavin Makea2e3302023-03-11 06:46:20 +00003267 With non-git-worktrees, this will be a symlink to the .repo/projects/
3268 path. With git-worktrees, this will be a .git file using "gitdir: ..."
3269 syntax.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003270
Gavin Makea2e3302023-03-11 06:46:20 +00003271 Older checkouts had .git/ directories. If we see that, migrate it.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003272
Gavin Makea2e3302023-03-11 06:46:20 +00003273 This also handles changes in the manifest. Maybe this project was
3274 backed by "foo/bar" on the server, but now it's "new/foo/bar". We have
3275 to update the path we point to under .repo/projects/ to match.
3276 """
3277 dotgit = os.path.join(self.worktree, ".git")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003278
Gavin Makea2e3302023-03-11 06:46:20 +00003279 # If using an old layout style (a directory), migrate it.
3280 if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
Jason Chang32b59562023-07-14 16:45:35 -07003281 self._MigrateOldWorkTreeGitDir(dotgit, project=self.name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003282
Gavin Makea2e3302023-03-11 06:46:20 +00003283 init_dotgit = not os.path.exists(dotgit)
3284 if self.use_git_worktrees:
3285 if init_dotgit:
3286 self._InitGitWorktree()
3287 self._CopyAndLinkFiles()
3288 else:
3289 if not init_dotgit:
3290 # See if the project has changed.
3291 if platform_utils.realpath(
3292 self.gitdir
3293 ) != platform_utils.realpath(dotgit):
3294 platform_utils.remove(dotgit)
Doug Anderson37282b42011-03-04 11:54:18 -08003295
Gavin Makea2e3302023-03-11 06:46:20 +00003296 if init_dotgit or not os.path.exists(dotgit):
3297 os.makedirs(self.worktree, exist_ok=True)
3298 platform_utils.symlink(
3299 os.path.relpath(self.gitdir, self.worktree), dotgit
3300 )
Doug Anderson37282b42011-03-04 11:54:18 -08003301
Gavin Makea2e3302023-03-11 06:46:20 +00003302 if init_dotgit:
3303 _lwrite(
3304 os.path.join(dotgit, HEAD), "%s\n" % self.GetRevisionId()
3305 )
Doug Anderson37282b42011-03-04 11:54:18 -08003306
Gavin Makea2e3302023-03-11 06:46:20 +00003307 # Finish checking out the worktree.
3308 cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
3309 if GitCommand(self, cmd).Wait() != 0:
3310 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07003311 "Cannot initialize work tree for " + self.name,
3312 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003313 )
Doug Anderson37282b42011-03-04 11:54:18 -08003314
Gavin Makea2e3302023-03-11 06:46:20 +00003315 if submodules:
3316 self._SyncSubmodules(quiet=True)
3317 self._CopyAndLinkFiles()
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003318
Gavin Makea2e3302023-03-11 06:46:20 +00003319 @classmethod
Jason Chang32b59562023-07-14 16:45:35 -07003320 def _MigrateOldWorkTreeGitDir(cls, dotgit, project=None):
Gavin Makea2e3302023-03-11 06:46:20 +00003321 """Migrate the old worktree .git/ dir style to a symlink.
3322
3323 This logic specifically only uses state from |dotgit| to figure out
3324 where to move content and not |self|. This way if the backing project
3325 also changed places, we only do the .git/ dir to .git symlink migration
3326 here. The path updates will happen independently.
3327 """
3328 # Figure out where in .repo/projects/ it's pointing to.
3329 if not os.path.islink(os.path.join(dotgit, "refs")):
Jason Chang32b59562023-07-14 16:45:35 -07003330 raise GitError(
3331 f"{dotgit}: unsupported checkout state", project=project
3332 )
Gavin Makea2e3302023-03-11 06:46:20 +00003333 gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, "refs")))
3334
3335 # Remove known symlink paths that exist in .repo/projects/.
3336 KNOWN_LINKS = {
3337 "config",
3338 "description",
3339 "hooks",
3340 "info",
3341 "logs",
3342 "objects",
3343 "packed-refs",
3344 "refs",
3345 "rr-cache",
3346 "shallow",
3347 "svn",
3348 }
3349 # Paths that we know will be in both, but are safe to clobber in
3350 # .repo/projects/.
3351 SAFE_TO_CLOBBER = {
3352 "COMMIT_EDITMSG",
3353 "FETCH_HEAD",
3354 "HEAD",
3355 "gc.log",
3356 "gitk.cache",
3357 "index",
3358 "ORIG_HEAD",
3359 }
3360
3361 # First see if we'd succeed before starting the migration.
3362 unknown_paths = []
3363 for name in platform_utils.listdir(dotgit):
3364 # Ignore all temporary/backup names. These are common with vim &
3365 # emacs.
3366 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3367 continue
3368
3369 dotgit_path = os.path.join(dotgit, name)
3370 if name in KNOWN_LINKS:
3371 if not platform_utils.islink(dotgit_path):
3372 unknown_paths.append(f"{dotgit_path}: should be a symlink")
3373 else:
3374 gitdir_path = os.path.join(gitdir, name)
3375 if name not in SAFE_TO_CLOBBER and os.path.exists(gitdir_path):
3376 unknown_paths.append(
3377 f"{dotgit_path}: unknown file; please file a bug"
3378 )
3379 if unknown_paths:
Jason Chang32b59562023-07-14 16:45:35 -07003380 raise GitError(
3381 "Aborting migration: " + "\n".join(unknown_paths),
3382 project=project,
3383 )
Gavin Makea2e3302023-03-11 06:46:20 +00003384
3385 # Now walk the paths and sync the .git/ to .repo/projects/.
3386 for name in platform_utils.listdir(dotgit):
3387 dotgit_path = os.path.join(dotgit, name)
3388
3389 # Ignore all temporary/backup names. These are common with vim &
3390 # emacs.
3391 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3392 platform_utils.remove(dotgit_path)
3393 elif name in KNOWN_LINKS:
3394 platform_utils.remove(dotgit_path)
3395 else:
3396 gitdir_path = os.path.join(gitdir, name)
3397 platform_utils.remove(gitdir_path, missing_ok=True)
3398 platform_utils.rename(dotgit_path, gitdir_path)
3399
3400 # Now that the dir should be empty, clear it out, and symlink it over.
3401 platform_utils.rmdir(dotgit)
3402 platform_utils.symlink(
3403 os.path.relpath(gitdir, os.path.dirname(dotgit)), dotgit
3404 )
3405
3406 def _get_symlink_error_message(self):
3407 if platform_utils.isWindows():
3408 return (
3409 "Unable to create symbolic link. Please re-run the command as "
3410 "Administrator, or see "
3411 "https://github.com/git-for-windows/git/wiki/Symbolic-Links "
3412 "for other options."
3413 )
3414 return "filesystem must support symlinks"
3415
3416 def _revlist(self, *args, **kw):
3417 a = []
3418 a.extend(args)
3419 a.append("--")
3420 return self.work_git.rev_list(*a, **kw)
3421
3422 @property
3423 def _allrefs(self):
3424 return self.bare_ref.all
3425
3426 def _getLogs(
3427 self, rev1, rev2, oneline=False, color=True, pretty_format=None
3428 ):
3429 """Get logs between two revisions of this project."""
3430 comp = ".."
3431 if rev1:
3432 revs = [rev1]
3433 if rev2:
3434 revs.extend([comp, rev2])
3435 cmd = ["log", "".join(revs)]
3436 out = DiffColoring(self.config)
3437 if out.is_on and color:
3438 cmd.append("--color")
3439 if pretty_format is not None:
3440 cmd.append("--pretty=format:%s" % pretty_format)
3441 if oneline:
3442 cmd.append("--oneline")
3443
3444 try:
3445 log = GitCommand(
3446 self, cmd, capture_stdout=True, capture_stderr=True
3447 )
3448 if log.Wait() == 0:
3449 return log.stdout
3450 except GitError:
3451 # worktree may not exist if groups changed for example. In that
3452 # case, try in gitdir instead.
3453 if not os.path.exists(self.worktree):
3454 return self.bare_git.log(*cmd[1:])
3455 else:
3456 raise
3457 return None
3458
3459 def getAddedAndRemovedLogs(
3460 self, toProject, oneline=False, color=True, pretty_format=None
3461 ):
3462 """Get the list of logs from this revision to given revisionId"""
3463 logs = {}
3464 selfId = self.GetRevisionId(self._allrefs)
3465 toId = toProject.GetRevisionId(toProject._allrefs)
3466
3467 logs["added"] = self._getLogs(
3468 selfId,
3469 toId,
3470 oneline=oneline,
3471 color=color,
3472 pretty_format=pretty_format,
3473 )
3474 logs["removed"] = self._getLogs(
3475 toId,
3476 selfId,
3477 oneline=oneline,
3478 color=color,
3479 pretty_format=pretty_format,
3480 )
3481 return logs
3482
3483 class _GitGetByExec(object):
3484 def __init__(self, project, bare, gitdir):
3485 self._project = project
3486 self._bare = bare
3487 self._gitdir = gitdir
3488
3489 # __getstate__ and __setstate__ are required for pickling because
3490 # __getattr__ exists.
3491 def __getstate__(self):
3492 return (self._project, self._bare, self._gitdir)
3493
3494 def __setstate__(self, state):
3495 self._project, self._bare, self._gitdir = state
3496
3497 def LsOthers(self):
3498 p = GitCommand(
3499 self._project,
3500 ["ls-files", "-z", "--others", "--exclude-standard"],
3501 bare=False,
3502 gitdir=self._gitdir,
3503 capture_stdout=True,
3504 capture_stderr=True,
3505 )
3506 if p.Wait() == 0:
3507 out = p.stdout
3508 if out:
3509 # Backslash is not anomalous.
3510 return out[:-1].split("\0")
3511 return []
3512
3513 def DiffZ(self, name, *args):
3514 cmd = [name]
3515 cmd.append("-z")
3516 cmd.append("--ignore-submodules")
3517 cmd.extend(args)
3518 p = GitCommand(
3519 self._project,
3520 cmd,
3521 gitdir=self._gitdir,
3522 bare=False,
3523 capture_stdout=True,
3524 capture_stderr=True,
3525 )
3526 p.Wait()
3527 r = {}
3528 out = p.stdout
3529 if out:
3530 out = iter(out[:-1].split("\0"))
3531 while out:
3532 try:
3533 info = next(out)
3534 path = next(out)
3535 except StopIteration:
3536 break
3537
3538 class _Info(object):
3539 def __init__(self, path, omode, nmode, oid, nid, state):
3540 self.path = path
3541 self.src_path = None
3542 self.old_mode = omode
3543 self.new_mode = nmode
3544 self.old_id = oid
3545 self.new_id = nid
3546
3547 if len(state) == 1:
3548 self.status = state
3549 self.level = None
3550 else:
3551 self.status = state[:1]
3552 self.level = state[1:]
3553 while self.level.startswith("0"):
3554 self.level = self.level[1:]
3555
3556 info = info[1:].split(" ")
3557 info = _Info(path, *info)
3558 if info.status in ("R", "C"):
3559 info.src_path = info.path
3560 info.path = next(out)
3561 r[info.path] = info
3562 return r
3563
3564 def GetDotgitPath(self, subpath=None):
3565 """Return the full path to the .git dir.
3566
3567 As a convenience, append |subpath| if provided.
3568 """
3569 if self._bare:
3570 dotgit = self._gitdir
3571 else:
3572 dotgit = os.path.join(self._project.worktree, ".git")
3573 if os.path.isfile(dotgit):
3574 # Git worktrees use a "gitdir:" syntax to point to the
3575 # scratch space.
3576 with open(dotgit) as fp:
3577 setting = fp.read()
3578 assert setting.startswith("gitdir:")
3579 gitdir = setting.split(":", 1)[1].strip()
3580 dotgit = os.path.normpath(
3581 os.path.join(self._project.worktree, gitdir)
3582 )
3583
3584 return dotgit if subpath is None else os.path.join(dotgit, subpath)
3585
3586 def GetHead(self):
3587 """Return the ref that HEAD points to."""
3588 path = self.GetDotgitPath(subpath=HEAD)
3589 try:
3590 with open(path) as fd:
3591 line = fd.readline()
3592 except IOError as e:
3593 raise NoManifestException(path, str(e))
3594 try:
3595 line = line.decode()
3596 except AttributeError:
3597 pass
3598 if line.startswith("ref: "):
3599 return line[5:-1]
3600 return line[:-1]
3601
3602 def SetHead(self, ref, message=None):
3603 cmdv = []
3604 if message is not None:
3605 cmdv.extend(["-m", message])
3606 cmdv.append(HEAD)
3607 cmdv.append(ref)
3608 self.symbolic_ref(*cmdv)
3609
3610 def DetachHead(self, new, message=None):
3611 cmdv = ["--no-deref"]
3612 if message is not None:
3613 cmdv.extend(["-m", message])
3614 cmdv.append(HEAD)
3615 cmdv.append(new)
3616 self.update_ref(*cmdv)
3617
3618 def UpdateRef(self, name, new, old=None, message=None, detach=False):
3619 cmdv = []
3620 if message is not None:
3621 cmdv.extend(["-m", message])
3622 if detach:
3623 cmdv.append("--no-deref")
3624 cmdv.append(name)
3625 cmdv.append(new)
3626 if old is not None:
3627 cmdv.append(old)
3628 self.update_ref(*cmdv)
3629
3630 def DeleteRef(self, name, old=None):
3631 if not old:
3632 old = self.rev_parse(name)
3633 self.update_ref("-d", name, old)
3634 self._project.bare_ref.deleted(name)
3635
3636 def rev_list(self, *args, **kw):
3637 if "format" in kw:
3638 cmdv = ["log", "--pretty=format:%s" % kw["format"]]
3639 else:
3640 cmdv = ["rev-list"]
3641 cmdv.extend(args)
3642 p = GitCommand(
3643 self._project,
3644 cmdv,
3645 bare=self._bare,
3646 gitdir=self._gitdir,
3647 capture_stdout=True,
3648 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003649 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00003650 )
Jason Chang32b59562023-07-14 16:45:35 -07003651 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003652 return p.stdout.splitlines()
3653
3654 def __getattr__(self, name):
3655 """Allow arbitrary git commands using pythonic syntax.
3656
3657 This allows you to do things like:
3658 git_obj.rev_parse('HEAD')
3659
3660 Since we don't have a 'rev_parse' method defined, the __getattr__
3661 will run. We'll replace the '_' with a '-' and try to run a git
3662 command. Any other positional arguments will be passed to the git
3663 command, and the following keyword arguments are supported:
3664 config: An optional dict of git config options to be passed with
3665 '-c'.
3666
3667 Args:
3668 name: The name of the git command to call. Any '_' characters
3669 will be replaced with '-'.
3670
3671 Returns:
3672 A callable object that will try to call git with the named
3673 command.
3674 """
3675 name = name.replace("_", "-")
3676
3677 def runner(*args, **kwargs):
3678 cmdv = []
3679 config = kwargs.pop("config", None)
3680 for k in kwargs:
3681 raise TypeError(
3682 "%s() got an unexpected keyword argument %r" % (name, k)
3683 )
3684 if config is not None:
3685 for k, v in config.items():
3686 cmdv.append("-c")
3687 cmdv.append("%s=%s" % (k, v))
3688 cmdv.append(name)
3689 cmdv.extend(args)
3690 p = GitCommand(
3691 self._project,
3692 cmdv,
3693 bare=self._bare,
3694 gitdir=self._gitdir,
3695 capture_stdout=True,
3696 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003697 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00003698 )
Jason Chang32b59562023-07-14 16:45:35 -07003699 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003700 r = p.stdout
3701 if r.endswith("\n") and r.index("\n") == len(r) - 1:
3702 return r[:-1]
3703 return r
3704
3705 return runner
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003706
3707
Jason Chang32b59562023-07-14 16:45:35 -07003708class LocalSyncFail(RepoError):
3709 """Default error when there is an Sync_LocalHalf error."""
3710
3711
3712class _PriorSyncFailedError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003713 def __str__(self):
3714 return "prior sync failed; rebase still in progress"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003715
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003716
Jason Chang32b59562023-07-14 16:45:35 -07003717class _DirtyError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003718 def __str__(self):
3719 return "contains uncommitted changes"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003720
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003721
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003722class _InfoMessage(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003723 def __init__(self, project, text):
3724 self.project = project
3725 self.text = text
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003726
Gavin Makea2e3302023-03-11 06:46:20 +00003727 def Print(self, syncbuf):
3728 syncbuf.out.info(
3729 "%s/: %s", self.project.RelPath(local=False), self.text
3730 )
3731 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003732
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003733
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003734class _Failure(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003735 def __init__(self, project, why):
3736 self.project = project
3737 self.why = why
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003738
Gavin Makea2e3302023-03-11 06:46:20 +00003739 def Print(self, syncbuf):
3740 syncbuf.out.fail(
3741 "error: %s/: %s", self.project.RelPath(local=False), str(self.why)
3742 )
3743 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003744
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003745
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003746class _Later(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003747 def __init__(self, project, action):
3748 self.project = project
3749 self.action = action
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003750
Gavin Makea2e3302023-03-11 06:46:20 +00003751 def Run(self, syncbuf):
3752 out = syncbuf.out
3753 out.project("project %s/", self.project.RelPath(local=False))
3754 out.nl()
3755 try:
3756 self.action()
3757 out.nl()
3758 return True
3759 except GitError:
3760 out.nl()
3761 return False
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003762
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003763
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003764class _SyncColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +00003765 def __init__(self, config):
3766 super().__init__(config, "reposync")
3767 self.project = self.printer("header", attr="bold")
3768 self.info = self.printer("info")
3769 self.fail = self.printer("fail", fg="red")
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003770
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003771
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003772class SyncBuffer(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003773 def __init__(self, config, detach_head=False):
3774 self._messages = []
3775 self._failures = []
3776 self._later_queue1 = []
3777 self._later_queue2 = []
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003778
Gavin Makea2e3302023-03-11 06:46:20 +00003779 self.out = _SyncColoring(config)
3780 self.out.redirect(sys.stderr)
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003781
Gavin Makea2e3302023-03-11 06:46:20 +00003782 self.detach_head = detach_head
3783 self.clean = True
3784 self.recent_clean = True
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003785
Gavin Makea2e3302023-03-11 06:46:20 +00003786 def info(self, project, fmt, *args):
3787 self._messages.append(_InfoMessage(project, fmt % args))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003788
Gavin Makea2e3302023-03-11 06:46:20 +00003789 def fail(self, project, err=None):
3790 self._failures.append(_Failure(project, err))
David Rileye0684ad2017-04-05 00:02:59 -07003791 self._MarkUnclean()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003792
Gavin Makea2e3302023-03-11 06:46:20 +00003793 def later1(self, project, what):
3794 self._later_queue1.append(_Later(project, what))
Mike Frysinger70d861f2019-08-26 15:22:36 -04003795
Gavin Makea2e3302023-03-11 06:46:20 +00003796 def later2(self, project, what):
3797 self._later_queue2.append(_Later(project, what))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003798
Gavin Makea2e3302023-03-11 06:46:20 +00003799 def Finish(self):
3800 self._PrintMessages()
3801 self._RunLater()
3802 self._PrintMessages()
3803 return self.clean
3804
3805 def Recently(self):
3806 recent_clean = self.recent_clean
3807 self.recent_clean = True
3808 return recent_clean
3809
3810 def _MarkUnclean(self):
3811 self.clean = False
3812 self.recent_clean = False
3813
3814 def _RunLater(self):
3815 for q in ["_later_queue1", "_later_queue2"]:
3816 if not self._RunQueue(q):
3817 return
3818
3819 def _RunQueue(self, queue):
3820 for m in getattr(self, queue):
3821 if not m.Run(self):
3822 self._MarkUnclean()
3823 return False
3824 setattr(self, queue, [])
3825 return True
3826
3827 def _PrintMessages(self):
3828 if self._messages or self._failures:
3829 if os.isatty(2):
3830 self.out.write(progress.CSI_ERASE_LINE)
3831 self.out.write("\r")
3832
3833 for m in self._messages:
3834 m.Print(self)
3835 for m in self._failures:
3836 m.Print(self)
3837
3838 self._messages = []
3839 self._failures = []
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003840
3841
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003842class MetaProject(Project):
Gavin Makea2e3302023-03-11 06:46:20 +00003843 """A special project housed under .repo."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003844
Gavin Makea2e3302023-03-11 06:46:20 +00003845 def __init__(self, manifest, name, gitdir, worktree):
3846 Project.__init__(
3847 self,
3848 manifest=manifest,
3849 name=name,
3850 gitdir=gitdir,
3851 objdir=gitdir,
3852 worktree=worktree,
3853 remote=RemoteSpec("origin"),
3854 relpath=".repo/%s" % name,
3855 revisionExpr="refs/heads/master",
3856 revisionId=None,
3857 groups=None,
3858 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003859
Gavin Makea2e3302023-03-11 06:46:20 +00003860 def PreSync(self):
3861 if self.Exists:
3862 cb = self.CurrentBranch
3863 if cb:
3864 base = self.GetBranch(cb).merge
3865 if base:
3866 self.revisionExpr = base
3867 self.revisionId = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003868
Gavin Makea2e3302023-03-11 06:46:20 +00003869 @property
3870 def HasChanges(self):
3871 """Has the remote received new commits not yet checked out?"""
3872 if not self.remote or not self.revisionExpr:
3873 return False
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003874
Gavin Makea2e3302023-03-11 06:46:20 +00003875 all_refs = self.bare_ref.all
3876 revid = self.GetRevisionId(all_refs)
3877 head = self.work_git.GetHead()
3878 if head.startswith(R_HEADS):
3879 try:
3880 head = all_refs[head]
3881 except KeyError:
3882 head = None
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003883
Gavin Makea2e3302023-03-11 06:46:20 +00003884 if revid == head:
3885 return False
3886 elif self._revlist(not_rev(HEAD), revid):
3887 return True
3888 return False
LaMont Jones9b72cf22022-03-29 21:54:22 +00003889
3890
3891class RepoProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003892 """The MetaProject for repo itself."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003893
Gavin Makea2e3302023-03-11 06:46:20 +00003894 @property
3895 def LastFetch(self):
3896 try:
3897 fh = os.path.join(self.gitdir, "FETCH_HEAD")
3898 return os.path.getmtime(fh)
3899 except OSError:
3900 return 0
LaMont Jones9b72cf22022-03-29 21:54:22 +00003901
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +01003902
LaMont Jones9b72cf22022-03-29 21:54:22 +00003903class ManifestProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003904 """The MetaProject for manifests."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003905
Gavin Makea2e3302023-03-11 06:46:20 +00003906 def MetaBranchSwitch(self, submodules=False):
3907 """Prepare for manifest branch switch."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003908
Gavin Makea2e3302023-03-11 06:46:20 +00003909 # detach and delete manifest branch, allowing a new
3910 # branch to take over
3911 syncbuf = SyncBuffer(self.config, detach_head=True)
3912 self.Sync_LocalHalf(syncbuf, submodules=submodules)
3913 syncbuf.Finish()
LaMont Jones9b72cf22022-03-29 21:54:22 +00003914
Gavin Makea2e3302023-03-11 06:46:20 +00003915 return (
3916 GitCommand(
3917 self,
3918 ["update-ref", "-d", "refs/heads/default"],
3919 capture_stdout=True,
3920 capture_stderr=True,
3921 ).Wait()
3922 == 0
3923 )
LaMont Jones9b03f152022-03-29 23:01:18 +00003924
Gavin Makea2e3302023-03-11 06:46:20 +00003925 @property
3926 def standalone_manifest_url(self):
3927 """The URL of the standalone manifest, or None."""
3928 return self.config.GetString("manifest.standalone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003929
Gavin Makea2e3302023-03-11 06:46:20 +00003930 @property
3931 def manifest_groups(self):
3932 """The manifest groups string."""
3933 return self.config.GetString("manifest.groups")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003934
Gavin Makea2e3302023-03-11 06:46:20 +00003935 @property
3936 def reference(self):
3937 """The --reference for this manifest."""
3938 return self.config.GetString("repo.reference")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003939
Gavin Makea2e3302023-03-11 06:46:20 +00003940 @property
3941 def dissociate(self):
3942 """Whether to dissociate."""
3943 return self.config.GetBoolean("repo.dissociate")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003944
Gavin Makea2e3302023-03-11 06:46:20 +00003945 @property
3946 def archive(self):
3947 """Whether we use archive."""
3948 return self.config.GetBoolean("repo.archive")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003949
Gavin Makea2e3302023-03-11 06:46:20 +00003950 @property
3951 def mirror(self):
3952 """Whether we use mirror."""
3953 return self.config.GetBoolean("repo.mirror")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003954
Gavin Makea2e3302023-03-11 06:46:20 +00003955 @property
3956 def use_worktree(self):
3957 """Whether we use worktree."""
3958 return self.config.GetBoolean("repo.worktree")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003959
Gavin Makea2e3302023-03-11 06:46:20 +00003960 @property
3961 def clone_bundle(self):
3962 """Whether we use clone_bundle."""
3963 return self.config.GetBoolean("repo.clonebundle")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003964
Gavin Makea2e3302023-03-11 06:46:20 +00003965 @property
3966 def submodules(self):
3967 """Whether we use submodules."""
3968 return self.config.GetBoolean("repo.submodules")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003969
Gavin Makea2e3302023-03-11 06:46:20 +00003970 @property
3971 def git_lfs(self):
3972 """Whether we use git_lfs."""
3973 return self.config.GetBoolean("repo.git-lfs")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003974
Gavin Makea2e3302023-03-11 06:46:20 +00003975 @property
3976 def use_superproject(self):
3977 """Whether we use superproject."""
3978 return self.config.GetBoolean("repo.superproject")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003979
Gavin Makea2e3302023-03-11 06:46:20 +00003980 @property
3981 def partial_clone(self):
3982 """Whether this is a partial clone."""
3983 return self.config.GetBoolean("repo.partialclone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003984
Gavin Makea2e3302023-03-11 06:46:20 +00003985 @property
3986 def depth(self):
3987 """Partial clone depth."""
3988 return self.config.GetString("repo.depth")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003989
Gavin Makea2e3302023-03-11 06:46:20 +00003990 @property
3991 def clone_filter(self):
3992 """The clone filter."""
3993 return self.config.GetString("repo.clonefilter")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003994
Gavin Makea2e3302023-03-11 06:46:20 +00003995 @property
3996 def partial_clone_exclude(self):
3997 """Partial clone exclude string"""
3998 return self.config.GetString("repo.partialcloneexclude")
LaMont Jones4ada0432022-04-14 15:10:43 +00003999
Gavin Makea2e3302023-03-11 06:46:20 +00004000 @property
Jason Chang17833322023-05-23 13:06:55 -07004001 def clone_filter_for_depth(self):
4002 """Replace shallow clone with partial clone."""
4003 return self.config.GetString("repo.clonefilterfordepth")
4004
4005 @property
Gavin Makea2e3302023-03-11 06:46:20 +00004006 def manifest_platform(self):
4007 """The --platform argument from `repo init`."""
4008 return self.config.GetString("manifest.platform")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004009
Gavin Makea2e3302023-03-11 06:46:20 +00004010 @property
4011 def _platform_name(self):
4012 """Return the name of the platform."""
4013 return platform.system().lower()
LaMont Jones9b03f152022-03-29 23:01:18 +00004014
Gavin Makea2e3302023-03-11 06:46:20 +00004015 def SyncWithPossibleInit(
4016 self,
4017 submanifest,
4018 verbose=False,
4019 current_branch_only=False,
4020 tags="",
4021 git_event_log=None,
4022 ):
4023 """Sync a manifestProject, possibly for the first time.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004024
Gavin Makea2e3302023-03-11 06:46:20 +00004025 Call Sync() with arguments from the most recent `repo init`. If this is
4026 a new sub manifest, then inherit options from the parent's
4027 manifestProject.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004028
Gavin Makea2e3302023-03-11 06:46:20 +00004029 This is used by subcmds.Sync() to do an initial download of new sub
4030 manifests.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004031
Gavin Makea2e3302023-03-11 06:46:20 +00004032 Args:
4033 submanifest: an XmlSubmanifest, the submanifest to re-sync.
4034 verbose: a boolean, whether to show all output, rather than only
4035 errors.
4036 current_branch_only: a boolean, whether to only fetch the current
4037 manifest branch from the server.
4038 tags: a boolean, whether to fetch tags.
4039 git_event_log: an EventLog, for git tracing.
4040 """
4041 # TODO(lamontjones): when refactoring sync (and init?) consider how to
4042 # better get the init options that we should use for new submanifests
4043 # that are added when syncing an existing workspace.
4044 git_event_log = git_event_log or EventLog()
LaMont Jonesb90a4222022-04-14 15:00:09 +00004045 spec = submanifest.ToSubmanifestSpec()
Gavin Makea2e3302023-03-11 06:46:20 +00004046 # Use the init options from the existing manifestProject, or the parent
4047 # if it doesn't exist.
4048 #
4049 # Today, we only support changing manifest_groups on the sub-manifest,
4050 # with no supported-for-the-user way to change the other arguments from
4051 # those specified by the outermost manifest.
4052 #
4053 # TODO(lamontjones): determine which of these should come from the
4054 # outermost manifest and which should come from the parent manifest.
4055 mp = self if self.Exists else submanifest.parent.manifestProject
4056 return self.Sync(
LaMont Jones55ee3042022-04-06 17:10:21 +00004057 manifest_url=spec.manifestUrl,
4058 manifest_branch=spec.revision,
Gavin Makea2e3302023-03-11 06:46:20 +00004059 standalone_manifest=mp.standalone_manifest_url,
4060 groups=mp.manifest_groups,
4061 platform=mp.manifest_platform,
4062 mirror=mp.mirror,
4063 dissociate=mp.dissociate,
4064 reference=mp.reference,
4065 worktree=mp.use_worktree,
4066 submodules=mp.submodules,
4067 archive=mp.archive,
4068 partial_clone=mp.partial_clone,
4069 clone_filter=mp.clone_filter,
4070 partial_clone_exclude=mp.partial_clone_exclude,
4071 clone_bundle=mp.clone_bundle,
4072 git_lfs=mp.git_lfs,
4073 use_superproject=mp.use_superproject,
LaMont Jones55ee3042022-04-06 17:10:21 +00004074 verbose=verbose,
4075 current_branch_only=current_branch_only,
4076 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00004077 depth=mp.depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004078 git_event_log=git_event_log,
4079 manifest_name=spec.manifestName,
Gavin Makea2e3302023-03-11 06:46:20 +00004080 this_manifest_only=True,
LaMont Jones55ee3042022-04-06 17:10:21 +00004081 outer_manifest=False,
Jason Chang17833322023-05-23 13:06:55 -07004082 clone_filter_for_depth=mp.clone_filter_for_depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004083 )
LaMont Jones409407a2022-04-05 21:21:56 +00004084
Gavin Makea2e3302023-03-11 06:46:20 +00004085 def Sync(
4086 self,
4087 _kwargs_only=(),
4088 manifest_url="",
4089 manifest_branch=None,
4090 standalone_manifest=False,
4091 groups="",
4092 mirror=False,
4093 reference="",
4094 dissociate=False,
4095 worktree=False,
4096 submodules=False,
4097 archive=False,
4098 partial_clone=None,
4099 depth=None,
4100 clone_filter="blob:none",
4101 partial_clone_exclude=None,
4102 clone_bundle=None,
4103 git_lfs=None,
4104 use_superproject=None,
4105 verbose=False,
4106 current_branch_only=False,
4107 git_event_log=None,
4108 platform="",
4109 manifest_name="default.xml",
4110 tags="",
4111 this_manifest_only=False,
4112 outer_manifest=True,
Jason Chang17833322023-05-23 13:06:55 -07004113 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00004114 ):
4115 """Sync the manifest and all submanifests.
LaMont Jones409407a2022-04-05 21:21:56 +00004116
Gavin Makea2e3302023-03-11 06:46:20 +00004117 Args:
4118 manifest_url: a string, the URL of the manifest project.
4119 manifest_branch: a string, the manifest branch to use.
4120 standalone_manifest: a boolean, whether to store the manifest as a
4121 static file.
4122 groups: a string, restricts the checkout to projects with the
4123 specified groups.
4124 mirror: a boolean, whether to create a mirror of the remote
4125 repository.
4126 reference: a string, location of a repo instance to use as a
4127 reference.
4128 dissociate: a boolean, whether to dissociate from reference mirrors
4129 after clone.
4130 worktree: a boolean, whether to use git-worktree to manage projects.
4131 submodules: a boolean, whether sync submodules associated with the
4132 manifest project.
4133 archive: a boolean, whether to checkout each project as an archive.
4134 See git-archive.
4135 partial_clone: a boolean, whether to perform a partial clone.
4136 depth: an int, how deep of a shallow clone to create.
4137 clone_filter: a string, filter to use with partial_clone.
4138 partial_clone_exclude : a string, comma-delimeted list of project
4139 names to exclude from partial clone.
4140 clone_bundle: a boolean, whether to enable /clone.bundle on
4141 HTTP/HTTPS.
4142 git_lfs: a boolean, whether to enable git LFS support.
4143 use_superproject: a boolean, whether to use the manifest
4144 superproject to sync projects.
4145 verbose: a boolean, whether to show all output, rather than only
4146 errors.
4147 current_branch_only: a boolean, whether to only fetch the current
4148 manifest branch from the server.
4149 platform: a string, restrict the checkout to projects with the
4150 specified platform group.
4151 git_event_log: an EventLog, for git tracing.
4152 tags: a boolean, whether to fetch tags.
4153 manifest_name: a string, the name of the manifest file to use.
4154 this_manifest_only: a boolean, whether to only operate on the
4155 current sub manifest.
4156 outer_manifest: a boolean, whether to start at the outermost
4157 manifest.
Jason Chang17833322023-05-23 13:06:55 -07004158 clone_filter_for_depth: a string, when specified replaces shallow
4159 clones with partial.
LaMont Jones9b03f152022-03-29 23:01:18 +00004160
Gavin Makea2e3302023-03-11 06:46:20 +00004161 Returns:
4162 a boolean, whether the sync was successful.
4163 """
4164 assert _kwargs_only == (), "Sync only accepts keyword arguments."
LaMont Jones9b03f152022-03-29 23:01:18 +00004165
Gavin Makea2e3302023-03-11 06:46:20 +00004166 groups = groups or self.manifest.GetDefaultGroupsStr(
4167 with_platform=False
4168 )
4169 platform = platform or "auto"
4170 git_event_log = git_event_log or EventLog()
4171 if outer_manifest and self.manifest.is_submanifest:
4172 # In a multi-manifest checkout, use the outer manifest unless we are
4173 # told not to.
4174 return self.client.outer_manifest.manifestProject.Sync(
4175 manifest_url=manifest_url,
4176 manifest_branch=manifest_branch,
4177 standalone_manifest=standalone_manifest,
4178 groups=groups,
4179 platform=platform,
4180 mirror=mirror,
4181 dissociate=dissociate,
4182 reference=reference,
4183 worktree=worktree,
4184 submodules=submodules,
4185 archive=archive,
4186 partial_clone=partial_clone,
4187 clone_filter=clone_filter,
4188 partial_clone_exclude=partial_clone_exclude,
4189 clone_bundle=clone_bundle,
4190 git_lfs=git_lfs,
4191 use_superproject=use_superproject,
4192 verbose=verbose,
4193 current_branch_only=current_branch_only,
4194 tags=tags,
4195 depth=depth,
4196 git_event_log=git_event_log,
4197 manifest_name=manifest_name,
4198 this_manifest_only=this_manifest_only,
4199 outer_manifest=False,
4200 )
LaMont Jones9b03f152022-03-29 23:01:18 +00004201
Gavin Makea2e3302023-03-11 06:46:20 +00004202 # If repo has already been initialized, we take -u with the absence of
4203 # --standalone-manifest to mean "transition to a standard repo set up",
4204 # which necessitates starting fresh.
4205 # If --standalone-manifest is set, we always tear everything down and
4206 # start anew.
4207 if self.Exists:
4208 was_standalone_manifest = self.config.GetString(
4209 "manifest.standalone"
4210 )
4211 if was_standalone_manifest and not manifest_url:
4212 print(
4213 "fatal: repo was initialized with a standlone manifest, "
4214 "cannot be re-initialized without --manifest-url/-u"
4215 )
4216 return False
4217
4218 if standalone_manifest or (
4219 was_standalone_manifest and manifest_url
4220 ):
4221 self.config.ClearCache()
4222 if self.gitdir and os.path.exists(self.gitdir):
4223 platform_utils.rmtree(self.gitdir)
4224 if self.worktree and os.path.exists(self.worktree):
4225 platform_utils.rmtree(self.worktree)
4226
4227 is_new = not self.Exists
4228 if is_new:
4229 if not manifest_url:
4230 print("fatal: manifest url is required.", file=sys.stderr)
4231 return False
4232
4233 if verbose:
4234 print(
4235 "Downloading manifest from %s"
4236 % (GitConfig.ForUser().UrlInsteadOf(manifest_url),),
4237 file=sys.stderr,
4238 )
4239
4240 # The manifest project object doesn't keep track of the path on the
4241 # server where this git is located, so let's save that here.
4242 mirrored_manifest_git = None
4243 if reference:
4244 manifest_git_path = urllib.parse.urlparse(manifest_url).path[1:]
4245 mirrored_manifest_git = os.path.join(
4246 reference, manifest_git_path
4247 )
4248 if not mirrored_manifest_git.endswith(".git"):
4249 mirrored_manifest_git += ".git"
4250 if not os.path.exists(mirrored_manifest_git):
4251 mirrored_manifest_git = os.path.join(
4252 reference, ".repo/manifests.git"
4253 )
4254
4255 self._InitGitDir(mirror_git=mirrored_manifest_git)
4256
4257 # If standalone_manifest is set, mark the project as "standalone" --
4258 # we'll still do much of the manifests.git set up, but will avoid actual
4259 # syncs to a remote.
4260 if standalone_manifest:
4261 self.config.SetString("manifest.standalone", manifest_url)
4262 elif not manifest_url and not manifest_branch:
4263 # If -u is set and --standalone-manifest is not, then we're not in
4264 # standalone mode. Otherwise, use config to infer what we were in
4265 # the last init.
4266 standalone_manifest = bool(
4267 self.config.GetString("manifest.standalone")
4268 )
4269 if not standalone_manifest:
4270 self.config.SetString("manifest.standalone", None)
4271
4272 self._ConfigureDepth(depth)
4273
4274 # Set the remote URL before the remote branch as we might need it below.
4275 if manifest_url:
4276 r = self.GetRemote()
4277 r.url = manifest_url
4278 r.ResetFetch()
4279 r.Save()
4280
4281 if not standalone_manifest:
4282 if manifest_branch:
4283 if manifest_branch == "HEAD":
4284 manifest_branch = self.ResolveRemoteHead()
4285 if manifest_branch is None:
4286 print("fatal: unable to resolve HEAD", file=sys.stderr)
4287 return False
4288 self.revisionExpr = manifest_branch
4289 else:
4290 if is_new:
4291 default_branch = self.ResolveRemoteHead()
4292 if default_branch is None:
4293 # If the remote doesn't have HEAD configured, default to
4294 # master.
4295 default_branch = "refs/heads/master"
4296 self.revisionExpr = default_branch
4297 else:
4298 self.PreSync()
4299
4300 groups = re.split(r"[,\s]+", groups or "")
4301 all_platforms = ["linux", "darwin", "windows"]
4302 platformize = lambda x: "platform-" + x
4303 if platform == "auto":
4304 if not mirror and not self.mirror:
4305 groups.append(platformize(self._platform_name))
4306 elif platform == "all":
4307 groups.extend(map(platformize, all_platforms))
4308 elif platform in all_platforms:
4309 groups.append(platformize(platform))
4310 elif platform != "none":
4311 print("fatal: invalid platform flag", file=sys.stderr)
4312 return False
4313 self.config.SetString("manifest.platform", platform)
4314
4315 groups = [x for x in groups if x]
4316 groupstr = ",".join(groups)
4317 if (
4318 platform == "auto"
4319 and groupstr == self.manifest.GetDefaultGroupsStr()
4320 ):
4321 groupstr = None
4322 self.config.SetString("manifest.groups", groupstr)
4323
4324 if reference:
4325 self.config.SetString("repo.reference", reference)
4326
4327 if dissociate:
4328 self.config.SetBoolean("repo.dissociate", dissociate)
4329
4330 if worktree:
4331 if mirror:
4332 print(
4333 "fatal: --mirror and --worktree are incompatible",
4334 file=sys.stderr,
4335 )
4336 return False
4337 if submodules:
4338 print(
4339 "fatal: --submodules and --worktree are incompatible",
4340 file=sys.stderr,
4341 )
4342 return False
4343 self.config.SetBoolean("repo.worktree", worktree)
4344 if is_new:
4345 self.use_git_worktrees = True
4346 print("warning: --worktree is experimental!", file=sys.stderr)
4347
4348 if archive:
4349 if is_new:
4350 self.config.SetBoolean("repo.archive", archive)
4351 else:
4352 print(
4353 "fatal: --archive is only supported when initializing a "
4354 "new workspace.",
4355 file=sys.stderr,
4356 )
4357 print(
4358 "Either delete the .repo folder in this workspace, or "
4359 "initialize in another location.",
4360 file=sys.stderr,
4361 )
4362 return False
4363
4364 if mirror:
4365 if is_new:
4366 self.config.SetBoolean("repo.mirror", mirror)
4367 else:
4368 print(
4369 "fatal: --mirror is only supported when initializing a new "
4370 "workspace.",
4371 file=sys.stderr,
4372 )
4373 print(
4374 "Either delete the .repo folder in this workspace, or "
4375 "initialize in another location.",
4376 file=sys.stderr,
4377 )
4378 return False
4379
4380 if partial_clone is not None:
4381 if mirror:
4382 print(
4383 "fatal: --mirror and --partial-clone are mutually "
4384 "exclusive",
4385 file=sys.stderr,
4386 )
4387 return False
4388 self.config.SetBoolean("repo.partialclone", partial_clone)
4389 if clone_filter:
4390 self.config.SetString("repo.clonefilter", clone_filter)
4391 elif self.partial_clone:
4392 clone_filter = self.clone_filter
4393 else:
4394 clone_filter = None
4395
4396 if partial_clone_exclude is not None:
4397 self.config.SetString(
4398 "repo.partialcloneexclude", partial_clone_exclude
4399 )
4400
4401 if clone_bundle is None:
4402 clone_bundle = False if partial_clone else True
4403 else:
4404 self.config.SetBoolean("repo.clonebundle", clone_bundle)
4405
4406 if submodules:
4407 self.config.SetBoolean("repo.submodules", submodules)
4408
4409 if git_lfs is not None:
4410 if git_lfs:
4411 git_require((2, 17, 0), fail=True, msg="Git LFS support")
4412
4413 self.config.SetBoolean("repo.git-lfs", git_lfs)
4414 if not is_new:
4415 print(
4416 "warning: Changing --git-lfs settings will only affect new "
4417 "project checkouts.\n"
4418 " Existing projects will require manual updates.\n",
4419 file=sys.stderr,
4420 )
4421
Jason Chang17833322023-05-23 13:06:55 -07004422 if clone_filter_for_depth is not None:
4423 self.ConfigureCloneFilterForDepth(clone_filter_for_depth)
4424
Gavin Makea2e3302023-03-11 06:46:20 +00004425 if use_superproject is not None:
4426 self.config.SetBoolean("repo.superproject", use_superproject)
4427
4428 if not standalone_manifest:
4429 success = self.Sync_NetworkHalf(
4430 is_new=is_new,
4431 quiet=not verbose,
4432 verbose=verbose,
4433 clone_bundle=clone_bundle,
4434 current_branch_only=current_branch_only,
4435 tags=tags,
4436 submodules=submodules,
4437 clone_filter=clone_filter,
4438 partial_clone_exclude=self.manifest.PartialCloneExclude,
Jason Chang17833322023-05-23 13:06:55 -07004439 clone_filter_for_depth=self.manifest.CloneFilterForDepth,
Gavin Makea2e3302023-03-11 06:46:20 +00004440 ).success
4441 if not success:
4442 r = self.GetRemote()
4443 print(
4444 "fatal: cannot obtain manifest %s" % r.url, file=sys.stderr
4445 )
4446
4447 # Better delete the manifest git dir if we created it; otherwise
4448 # next time (when user fixes problems) we won't go through the
4449 # "is_new" logic.
4450 if is_new:
4451 platform_utils.rmtree(self.gitdir)
4452 return False
4453
4454 if manifest_branch:
4455 self.MetaBranchSwitch(submodules=submodules)
4456
4457 syncbuf = SyncBuffer(self.config)
4458 self.Sync_LocalHalf(syncbuf, submodules=submodules)
4459 syncbuf.Finish()
4460
4461 if is_new or self.CurrentBranch is None:
Jason Chang1a3612f2023-08-08 14:12:53 -07004462 try:
4463 self.StartBranch("default")
4464 except GitError as e:
4465 msg = str(e)
Gavin Makea2e3302023-03-11 06:46:20 +00004466 print(
Jason Chang1a3612f2023-08-08 14:12:53 -07004467 f"fatal: cannot create default in manifest {msg}",
Gavin Makea2e3302023-03-11 06:46:20 +00004468 file=sys.stderr,
4469 )
4470 return False
4471
4472 if not manifest_name:
4473 print("fatal: manifest name (-m) is required.", file=sys.stderr)
4474 return False
4475
4476 elif is_new:
4477 # This is a new standalone manifest.
4478 manifest_name = "default.xml"
4479 manifest_data = fetch.fetch_file(manifest_url, verbose=verbose)
4480 dest = os.path.join(self.worktree, manifest_name)
4481 os.makedirs(os.path.dirname(dest), exist_ok=True)
4482 with open(dest, "wb") as f:
4483 f.write(manifest_data)
4484
4485 try:
4486 self.manifest.Link(manifest_name)
4487 except ManifestParseError as e:
4488 print(
4489 "fatal: manifest '%s' not available" % manifest_name,
4490 file=sys.stderr,
4491 )
4492 print("fatal: %s" % str(e), file=sys.stderr)
4493 return False
4494
4495 if not this_manifest_only:
4496 for submanifest in self.manifest.submanifests.values():
4497 spec = submanifest.ToSubmanifestSpec()
4498 submanifest.repo_client.manifestProject.Sync(
4499 manifest_url=spec.manifestUrl,
4500 manifest_branch=spec.revision,
4501 standalone_manifest=standalone_manifest,
4502 groups=self.manifest_groups,
4503 platform=platform,
4504 mirror=mirror,
4505 dissociate=dissociate,
4506 reference=reference,
4507 worktree=worktree,
4508 submodules=submodules,
4509 archive=archive,
4510 partial_clone=partial_clone,
4511 clone_filter=clone_filter,
4512 partial_clone_exclude=partial_clone_exclude,
4513 clone_bundle=clone_bundle,
4514 git_lfs=git_lfs,
4515 use_superproject=use_superproject,
4516 verbose=verbose,
4517 current_branch_only=current_branch_only,
4518 tags=tags,
4519 depth=depth,
4520 git_event_log=git_event_log,
4521 manifest_name=spec.manifestName,
4522 this_manifest_only=False,
4523 outer_manifest=False,
4524 )
4525
4526 # Lastly, if the manifest has a <superproject> then have the
4527 # superproject sync it (if it will be used).
4528 if git_superproject.UseSuperproject(use_superproject, self.manifest):
4529 sync_result = self.manifest.superproject.Sync(git_event_log)
4530 if not sync_result.success:
4531 submanifest = ""
4532 if self.manifest.path_prefix:
4533 submanifest = f"for {self.manifest.path_prefix} "
4534 print(
4535 f"warning: git update of superproject {submanifest}failed, "
4536 "repo sync will not use superproject to fetch source; "
4537 "while this error is not fatal, and you can continue to "
4538 "run repo sync, please run repo init with the "
4539 "--no-use-superproject option to stop seeing this warning",
4540 file=sys.stderr,
4541 )
4542 if sync_result.fatal and use_superproject is not None:
4543 return False
4544
4545 return True
4546
Jason Chang17833322023-05-23 13:06:55 -07004547 def ConfigureCloneFilterForDepth(self, clone_filter_for_depth):
4548 """Configure clone filter to replace shallow clones.
4549
4550 Args:
4551 clone_filter_for_depth: a string or None, e.g. 'blob:none' will
4552 disable shallow clones and replace with partial clone. None will
4553 enable shallow clones.
4554 """
4555 self.config.SetString(
4556 "repo.clonefilterfordepth", clone_filter_for_depth
4557 )
4558
Gavin Makea2e3302023-03-11 06:46:20 +00004559 def _ConfigureDepth(self, depth):
4560 """Configure the depth we'll sync down.
4561
4562 Args:
4563 depth: an int, how deep of a partial clone to create.
4564 """
4565 # Opt.depth will be non-None if user actually passed --depth to repo
4566 # init.
4567 if depth is not None:
4568 if depth > 0:
4569 # Positive values will set the depth.
4570 depth = str(depth)
4571 else:
4572 # Negative numbers will clear the depth; passing None to
4573 # SetString will do that.
4574 depth = None
4575
4576 # We store the depth in the main manifest project.
4577 self.config.SetString("repo.depth", depth)