blob: 6e6a605ec250a6f77345c554e96b7ec2eb0b6e2b [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 '
1119 "CodeReview+1 or Verified-1"
1120 )
1121
1122 if dest_branch is None:
1123 dest_branch = self.dest_branch
1124 if dest_branch is None:
1125 dest_branch = branch.merge
1126 if not dest_branch.startswith(R_HEADS):
1127 dest_branch = R_HEADS + dest_branch
1128
1129 if not branch.remote.projectname:
1130 branch.remote.projectname = self.name
1131 branch.remote.Save()
1132
1133 url = branch.remote.ReviewUrl(self.UserEmail, validate_certs)
1134 if url is None:
1135 raise UploadError("review not configured")
1136 cmd = ["push"]
1137 if dryrun:
1138 cmd.append("-n")
1139
1140 if url.startswith("ssh://"):
1141 cmd.append("--receive-pack=gerrit receive-pack")
1142
Aravind Vasudevan99ebf622023-04-04 23:44:37 +00001143 # This stops git from pushing all reachable annotated tags when
1144 # push.followTags is configured. Gerrit does not accept any tags
1145 # pushed to a CL.
1146 if git_require((1, 8, 3)):
1147 cmd.append("--no-follow-tags")
1148
Gavin Makea2e3302023-03-11 06:46:20 +00001149 for push_option in push_options or []:
1150 cmd.append("-o")
1151 cmd.append(push_option)
1152
1153 cmd.append(url)
1154
1155 if dest_branch.startswith(R_HEADS):
1156 dest_branch = dest_branch[len(R_HEADS) :]
1157
1158 ref_spec = "%s:refs/for/%s" % (R_HEADS + branch.name, dest_branch)
1159 opts = []
1160 if auto_topic:
1161 opts += ["topic=" + branch.name]
1162 opts += ["t=%s" % p for p in hashtags]
1163 # NB: No need to encode labels as they've been validated above.
1164 opts += ["l=%s" % p for p in labels]
1165
1166 opts += ["r=%s" % p for p in people[0]]
1167 opts += ["cc=%s" % p for p in people[1]]
1168 if notify:
1169 opts += ["notify=" + notify]
1170 if private:
1171 opts += ["private"]
1172 if wip:
1173 opts += ["wip"]
1174 if ready:
1175 opts += ["ready"]
1176 if opts:
1177 ref_spec = ref_spec + "%" + ",".join(opts)
1178 cmd.append(ref_spec)
1179
1180 if GitCommand(self, cmd, bare=True).Wait() != 0:
1181 raise UploadError("Upload failed")
1182
1183 if not dryrun:
1184 msg = "posted to %s for %s" % (branch.remote.review, dest_branch)
1185 self.bare_git.UpdateRef(
1186 R_PUB + branch.name, R_HEADS + branch.name, message=msg
1187 )
1188
1189 def _ExtractArchive(self, tarpath, path=None):
1190 """Extract the given tar on its current location
1191
1192 Args:
1193 tarpath: The path to the actual tar file
1194
1195 """
1196 try:
1197 with tarfile.open(tarpath, "r") as tar:
1198 tar.extractall(path=path)
1199 return True
1200 except (IOError, tarfile.TarError) as e:
1201 _error("Cannot extract archive %s: %s", tarpath, str(e))
1202 return False
1203
1204 def Sync_NetworkHalf(
1205 self,
1206 quiet=False,
1207 verbose=False,
1208 output_redir=None,
1209 is_new=None,
1210 current_branch_only=None,
1211 force_sync=False,
1212 clone_bundle=True,
1213 tags=None,
1214 archive=False,
1215 optimized_fetch=False,
1216 retry_fetches=0,
1217 prune=False,
1218 submodules=False,
1219 ssh_proxy=None,
1220 clone_filter=None,
1221 partial_clone_exclude=set(),
Jason Chang17833322023-05-23 13:06:55 -07001222 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001223 ):
1224 """Perform only the network IO portion of the sync process.
1225 Local working directory/branch state is not affected.
1226 """
1227 if archive and not isinstance(self, MetaProject):
1228 if self.remote.url.startswith(("http://", "https://")):
Jason Chang32b59562023-07-14 16:45:35 -07001229 msg_template = (
1230 "%s: Cannot fetch archives from http/https remotes."
Gavin Makea2e3302023-03-11 06:46:20 +00001231 )
Jason Chang32b59562023-07-14 16:45:35 -07001232 msg_args = self.name
1233 msg = msg_template % msg_args
1234 _error(
1235 msg_template,
1236 msg_args,
1237 )
1238 return SyncNetworkHalfResult(
1239 False, SyncNetworkHalfError(msg, project=self.name)
1240 )
Gavin Makea2e3302023-03-11 06:46:20 +00001241
1242 name = self.relpath.replace("\\", "/")
1243 name = name.replace("/", "_")
1244 tarpath = "%s.tar" % name
1245 topdir = self.manifest.topdir
1246
1247 try:
1248 self._FetchArchive(tarpath, cwd=topdir)
1249 except GitError as e:
1250 _error("%s", e)
Jason Chang32b59562023-07-14 16:45:35 -07001251 return SyncNetworkHalfResult(False, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001252
1253 # From now on, we only need absolute tarpath.
1254 tarpath = os.path.join(topdir, tarpath)
1255
1256 if not self._ExtractArchive(tarpath, path=topdir):
Jason Chang32b59562023-07-14 16:45:35 -07001257 return SyncNetworkHalfResult(
1258 True,
1259 SyncNetworkHalfError(
1260 f"Unable to Extract Archive {tarpath}",
1261 project=self.name,
1262 ),
1263 )
Gavin Makea2e3302023-03-11 06:46:20 +00001264 try:
1265 platform_utils.remove(tarpath)
1266 except OSError as e:
1267 _warn("Cannot remove archive %s: %s", tarpath, str(e))
1268 self._CopyAndLinkFiles()
Jason Chang32b59562023-07-14 16:45:35 -07001269 return SyncNetworkHalfResult(True)
Gavin Makea2e3302023-03-11 06:46:20 +00001270
1271 # If the shared object dir already exists, don't try to rebootstrap with
1272 # a clone bundle download. We should have the majority of objects
1273 # already.
1274 if clone_bundle and os.path.exists(self.objdir):
1275 clone_bundle = False
1276
1277 if self.name in partial_clone_exclude:
1278 clone_bundle = True
1279 clone_filter = None
1280
1281 if is_new is None:
1282 is_new = not self.Exists
1283 if is_new:
1284 self._InitGitDir(force_sync=force_sync, quiet=quiet)
1285 else:
1286 self._UpdateHooks(quiet=quiet)
1287 self._InitRemote()
1288
1289 if self.UseAlternates:
1290 # If gitdir/objects is a symlink, migrate it from the old layout.
1291 gitdir_objects = os.path.join(self.gitdir, "objects")
1292 if platform_utils.islink(gitdir_objects):
1293 platform_utils.remove(gitdir_objects, missing_ok=True)
1294 gitdir_alt = os.path.join(self.gitdir, "objects/info/alternates")
1295 if not os.path.exists(gitdir_alt):
1296 os.makedirs(os.path.dirname(gitdir_alt), exist_ok=True)
1297 _lwrite(
1298 gitdir_alt,
1299 os.path.join(
1300 os.path.relpath(self.objdir, gitdir_objects), "objects"
1301 )
1302 + "\n",
1303 )
1304
1305 if is_new:
1306 alt = os.path.join(self.objdir, "objects/info/alternates")
1307 try:
1308 with open(alt) as fd:
1309 # This works for both absolute and relative alternate
1310 # directories.
1311 alt_dir = os.path.join(
1312 self.objdir, "objects", fd.readline().rstrip()
1313 )
1314 except IOError:
1315 alt_dir = None
1316 else:
1317 alt_dir = None
1318
1319 if (
1320 clone_bundle
1321 and alt_dir is None
1322 and self._ApplyCloneBundle(
1323 initial=is_new, quiet=quiet, verbose=verbose
1324 )
1325 ):
1326 is_new = False
1327
1328 if current_branch_only is None:
1329 if self.sync_c:
1330 current_branch_only = True
1331 elif not self.manifest._loaded:
1332 # Manifest cannot check defaults until it syncs.
1333 current_branch_only = False
1334 elif self.manifest.default.sync_c:
1335 current_branch_only = True
1336
1337 if tags is None:
1338 tags = self.sync_tags
1339
1340 if self.clone_depth:
1341 depth = self.clone_depth
1342 else:
1343 depth = self.manifest.manifestProject.depth
1344
Jason Chang17833322023-05-23 13:06:55 -07001345 if depth and clone_filter_for_depth:
1346 depth = None
1347 clone_filter = clone_filter_for_depth
1348
Gavin Makea2e3302023-03-11 06:46:20 +00001349 # See if we can skip the network fetch entirely.
1350 remote_fetched = False
1351 if not (
1352 optimized_fetch
1353 and (
1354 ID_RE.match(self.revisionExpr)
1355 and self._CheckForImmutableRevision()
1356 )
1357 ):
1358 remote_fetched = True
Jason Chang32b59562023-07-14 16:45:35 -07001359 try:
1360 if not self._RemoteFetch(
1361 initial=is_new,
1362 quiet=quiet,
1363 verbose=verbose,
1364 output_redir=output_redir,
1365 alt_dir=alt_dir,
1366 current_branch_only=current_branch_only,
1367 tags=tags,
1368 prune=prune,
1369 depth=depth,
1370 submodules=submodules,
1371 force_sync=force_sync,
1372 ssh_proxy=ssh_proxy,
1373 clone_filter=clone_filter,
1374 retry_fetches=retry_fetches,
1375 ):
1376 return SyncNetworkHalfResult(
1377 remote_fetched,
1378 SyncNetworkHalfError(
1379 f"Unable to remote fetch project {self.name}",
1380 project=self.name,
1381 ),
1382 )
1383 except RepoError as e:
1384 return SyncNetworkHalfResult(
1385 remote_fetched,
1386 e,
1387 )
Gavin Makea2e3302023-03-11 06:46:20 +00001388
1389 mp = self.manifest.manifestProject
1390 dissociate = mp.dissociate
1391 if dissociate:
1392 alternates_file = os.path.join(
1393 self.objdir, "objects/info/alternates"
1394 )
1395 if os.path.exists(alternates_file):
1396 cmd = ["repack", "-a", "-d"]
1397 p = GitCommand(
1398 self,
1399 cmd,
1400 bare=True,
1401 capture_stdout=bool(output_redir),
1402 merge_output=bool(output_redir),
1403 )
1404 if p.stdout and output_redir:
1405 output_redir.write(p.stdout)
1406 if p.Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07001407 return SyncNetworkHalfResult(
1408 remote_fetched,
1409 GitError(
1410 "Unable to repack alternates", project=self.name
1411 ),
1412 )
Gavin Makea2e3302023-03-11 06:46:20 +00001413 platform_utils.remove(alternates_file)
1414
1415 if self.worktree:
1416 self._InitMRef()
1417 else:
1418 self._InitMirrorHead()
1419 platform_utils.remove(
1420 os.path.join(self.gitdir, "FETCH_HEAD"), missing_ok=True
1421 )
Jason Chang32b59562023-07-14 16:45:35 -07001422 return SyncNetworkHalfResult(remote_fetched)
Gavin Makea2e3302023-03-11 06:46:20 +00001423
1424 def PostRepoUpgrade(self):
1425 self._InitHooks()
1426
1427 def _CopyAndLinkFiles(self):
1428 if self.client.isGitcClient:
1429 return
1430 for copyfile in self.copyfiles:
1431 copyfile._Copy()
1432 for linkfile in self.linkfiles:
1433 linkfile._Link()
1434
1435 def GetCommitRevisionId(self):
1436 """Get revisionId of a commit.
1437
1438 Use this method instead of GetRevisionId to get the id of the commit
1439 rather than the id of the current git object (for example, a tag)
1440
1441 """
1442 if not self.revisionExpr.startswith(R_TAGS):
1443 return self.GetRevisionId(self._allrefs)
1444
1445 try:
1446 return self.bare_git.rev_list(self.revisionExpr, "-1")[0]
1447 except GitError:
1448 raise ManifestInvalidRevisionError(
1449 "revision %s in %s not found" % (self.revisionExpr, self.name)
1450 )
1451
1452 def GetRevisionId(self, all_refs=None):
1453 if self.revisionId:
1454 return self.revisionId
1455
1456 rem = self.GetRemote()
1457 rev = rem.ToLocal(self.revisionExpr)
1458
1459 if all_refs is not None and rev in all_refs:
1460 return all_refs[rev]
1461
1462 try:
1463 return self.bare_git.rev_parse("--verify", "%s^0" % rev)
1464 except GitError:
1465 raise ManifestInvalidRevisionError(
1466 "revision %s in %s not found" % (self.revisionExpr, self.name)
1467 )
1468
1469 def SetRevisionId(self, revisionId):
1470 if self.revisionExpr:
1471 self.upstream = self.revisionExpr
1472
1473 self.revisionId = revisionId
1474
Jason Chang32b59562023-07-14 16:45:35 -07001475 def Sync_LocalHalf(
1476 self, syncbuf, force_sync=False, submodules=False, errors=None
1477 ):
Gavin Makea2e3302023-03-11 06:46:20 +00001478 """Perform only the local IO portion of the sync process.
1479
1480 Network access is not required.
1481 """
Jason Chang32b59562023-07-14 16:45:35 -07001482 if errors is None:
1483 errors = []
1484
1485 def fail(error: Exception):
1486 errors.append(error)
1487 syncbuf.fail(self, error)
1488
Gavin Makea2e3302023-03-11 06:46:20 +00001489 if not os.path.exists(self.gitdir):
Jason Chang32b59562023-07-14 16:45:35 -07001490 fail(
1491 LocalSyncFail(
1492 "Cannot checkout %s due to missing network sync; Run "
1493 "`repo sync -n %s` first." % (self.name, self.name),
1494 project=self.name,
1495 )
Gavin Makea2e3302023-03-11 06:46:20 +00001496 )
1497 return
1498
1499 self._InitWorkTree(force_sync=force_sync, submodules=submodules)
1500 all_refs = self.bare_ref.all
1501 self.CleanPublishedCache(all_refs)
1502 revid = self.GetRevisionId(all_refs)
1503
1504 # Special case the root of the repo client checkout. Make sure it
1505 # doesn't contain files being checked out to dirs we don't allow.
1506 if self.relpath == ".":
1507 PROTECTED_PATHS = {".repo"}
1508 paths = set(
1509 self.work_git.ls_tree("-z", "--name-only", "--", revid).split(
1510 "\0"
1511 )
1512 )
1513 bad_paths = paths & PROTECTED_PATHS
1514 if bad_paths:
Jason Chang32b59562023-07-14 16:45:35 -07001515 fail(
1516 LocalSyncFail(
1517 "Refusing to checkout project that writes to protected "
1518 "paths: %s" % (", ".join(bad_paths),),
1519 project=self.name,
1520 )
Gavin Makea2e3302023-03-11 06:46:20 +00001521 )
1522 return
1523
1524 def _doff():
1525 self._FastForward(revid)
1526 self._CopyAndLinkFiles()
1527
1528 def _dosubmodules():
1529 self._SyncSubmodules(quiet=True)
1530
1531 head = self.work_git.GetHead()
1532 if head.startswith(R_HEADS):
1533 branch = head[len(R_HEADS) :]
1534 try:
1535 head = all_refs[head]
1536 except KeyError:
1537 head = None
1538 else:
1539 branch = None
1540
1541 if branch is None or syncbuf.detach_head:
1542 # Currently on a detached HEAD. The user is assumed to
1543 # not have any local modifications worth worrying about.
1544 if self.IsRebaseInProgress():
Jason Chang32b59562023-07-14 16:45:35 -07001545 fail(_PriorSyncFailedError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001546 return
1547
1548 if head == revid:
1549 # No changes; don't do anything further.
1550 # Except if the head needs to be detached.
1551 if not syncbuf.detach_head:
1552 # The copy/linkfile config may have changed.
1553 self._CopyAndLinkFiles()
1554 return
1555 else:
1556 lost = self._revlist(not_rev(revid), HEAD)
1557 if lost:
1558 syncbuf.info(self, "discarding %d commits", len(lost))
1559
1560 try:
1561 self._Checkout(revid, quiet=True)
1562 if submodules:
1563 self._SyncSubmodules(quiet=True)
1564 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001565 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001566 return
1567 self._CopyAndLinkFiles()
1568 return
1569
1570 if head == revid:
1571 # No changes; don't do anything further.
1572 #
1573 # The copy/linkfile config may have changed.
1574 self._CopyAndLinkFiles()
1575 return
1576
1577 branch = self.GetBranch(branch)
1578
1579 if not branch.LocalMerge:
1580 # The current branch has no tracking configuration.
1581 # Jump off it to a detached HEAD.
1582 syncbuf.info(
1583 self, "leaving %s; does not track upstream", branch.name
1584 )
1585 try:
1586 self._Checkout(revid, quiet=True)
1587 if submodules:
1588 self._SyncSubmodules(quiet=True)
1589 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001590 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001591 return
1592 self._CopyAndLinkFiles()
1593 return
1594
1595 upstream_gain = self._revlist(not_rev(HEAD), revid)
1596
1597 # See if we can perform a fast forward merge. This can happen if our
1598 # branch isn't in the exact same state as we last published.
1599 try:
1600 self.work_git.merge_base("--is-ancestor", HEAD, revid)
1601 # Skip the published logic.
1602 pub = False
1603 except GitError:
1604 pub = self.WasPublished(branch.name, all_refs)
1605
1606 if pub:
1607 not_merged = self._revlist(not_rev(revid), pub)
1608 if not_merged:
1609 if upstream_gain:
1610 # The user has published this branch and some of those
1611 # commits are not yet merged upstream. We do not want
1612 # to rewrite the published commits so we punt.
Jason Chang32b59562023-07-14 16:45:35 -07001613 fail(
1614 LocalSyncFail(
1615 "branch %s is published (but not merged) and is "
1616 "now %d commits behind"
1617 % (branch.name, len(upstream_gain)),
1618 project=self.name,
1619 )
Gavin Makea2e3302023-03-11 06:46:20 +00001620 )
1621 return
1622 elif pub == head:
1623 # All published commits are merged, and thus we are a
1624 # strict subset. We can fast-forward safely.
1625 syncbuf.later1(self, _doff)
1626 if submodules:
1627 syncbuf.later1(self, _dosubmodules)
1628 return
1629
1630 # Examine the local commits not in the remote. Find the
1631 # last one attributed to this user, if any.
1632 local_changes = self._revlist(not_rev(revid), HEAD, format="%H %ce")
1633 last_mine = None
1634 cnt_mine = 0
1635 for commit in local_changes:
1636 commit_id, committer_email = commit.split(" ", 1)
1637 if committer_email == self.UserEmail:
1638 last_mine = commit_id
1639 cnt_mine += 1
1640
1641 if not upstream_gain and cnt_mine == len(local_changes):
1642 # The copy/linkfile config may have changed.
1643 self._CopyAndLinkFiles()
1644 return
1645
1646 if self.IsDirty(consider_untracked=False):
Jason Chang32b59562023-07-14 16:45:35 -07001647 fail(_DirtyError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001648 return
1649
1650 # If the upstream switched on us, warn the user.
1651 if branch.merge != self.revisionExpr:
1652 if branch.merge and self.revisionExpr:
1653 syncbuf.info(
1654 self,
1655 "manifest switched %s...%s",
1656 branch.merge,
1657 self.revisionExpr,
1658 )
1659 elif branch.merge:
1660 syncbuf.info(self, "manifest no longer tracks %s", branch.merge)
1661
1662 if cnt_mine < len(local_changes):
1663 # Upstream rebased. Not everything in HEAD was created by this user.
1664 syncbuf.info(
1665 self,
1666 "discarding %d commits removed from upstream",
1667 len(local_changes) - cnt_mine,
1668 )
1669
1670 branch.remote = self.GetRemote()
1671 if not ID_RE.match(self.revisionExpr):
1672 # In case of manifest sync the revisionExpr might be a SHA1.
1673 branch.merge = self.revisionExpr
1674 if not branch.merge.startswith("refs/"):
1675 branch.merge = R_HEADS + branch.merge
1676 branch.Save()
1677
1678 if cnt_mine > 0 and self.rebase:
1679
1680 def _docopyandlink():
1681 self._CopyAndLinkFiles()
1682
1683 def _dorebase():
1684 self._Rebase(upstream="%s^1" % last_mine, onto=revid)
1685
1686 syncbuf.later2(self, _dorebase)
1687 if submodules:
1688 syncbuf.later2(self, _dosubmodules)
1689 syncbuf.later2(self, _docopyandlink)
1690 elif local_changes:
1691 try:
1692 self._ResetHard(revid)
1693 if submodules:
1694 self._SyncSubmodules(quiet=True)
1695 self._CopyAndLinkFiles()
1696 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001697 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001698 return
1699 else:
1700 syncbuf.later1(self, _doff)
1701 if submodules:
1702 syncbuf.later1(self, _dosubmodules)
1703
1704 def AddCopyFile(self, src, dest, topdir):
1705 """Mark |src| for copying to |dest| (relative to |topdir|).
1706
1707 No filesystem changes occur here. Actual copying happens later on.
1708
1709 Paths should have basic validation run on them before being queued.
1710 Further checking will be handled when the actual copy happens.
1711 """
1712 self.copyfiles.append(_CopyFile(self.worktree, src, topdir, dest))
1713
1714 def AddLinkFile(self, src, dest, topdir):
1715 """Mark |dest| to create a symlink (relative to |topdir|) pointing to
1716 |src|.
1717
1718 No filesystem changes occur here. Actual linking happens later on.
1719
1720 Paths should have basic validation run on them before being queued.
1721 Further checking will be handled when the actual link happens.
1722 """
1723 self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest))
1724
1725 def AddAnnotation(self, name, value, keep):
1726 self.annotations.append(Annotation(name, value, keep))
1727
1728 def DownloadPatchSet(self, change_id, patch_id):
1729 """Download a single patch set of a single change to FETCH_HEAD."""
1730 remote = self.GetRemote()
1731
1732 cmd = ["fetch", remote.name]
1733 cmd.append(
1734 "refs/changes/%2.2d/%d/%d" % (change_id % 100, change_id, patch_id)
1735 )
Jason Chang1a3612f2023-08-08 14:12:53 -07001736 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001737 return DownloadedChange(
1738 self,
1739 self.GetRevisionId(),
1740 change_id,
1741 patch_id,
1742 self.bare_git.rev_parse("FETCH_HEAD"),
1743 )
1744
1745 def DeleteWorktree(self, quiet=False, force=False):
1746 """Delete the source checkout and any other housekeeping tasks.
1747
1748 This currently leaves behind the internal .repo/ cache state. This
1749 helps when switching branches or manifest changes get reverted as we
1750 don't have to redownload all the git objects. But we should do some GC
1751 at some point.
1752
1753 Args:
1754 quiet: Whether to hide normal messages.
1755 force: Always delete tree even if dirty.
Doug Anderson3ba5f952011-04-07 12:51:04 -07001756
1757 Returns:
Gavin Makea2e3302023-03-11 06:46:20 +00001758 True if the worktree was completely cleaned out.
1759 """
1760 if self.IsDirty():
1761 if force:
1762 print(
1763 "warning: %s: Removing dirty project: uncommitted changes "
1764 "lost." % (self.RelPath(local=False),),
1765 file=sys.stderr,
1766 )
1767 else:
Jason Chang32b59562023-07-14 16:45:35 -07001768 msg = (
1769 "error: %s: Cannot remove project: uncommitted"
1770 "changes are present.\n" % self.RelPath(local=False)
Gavin Makea2e3302023-03-11 06:46:20 +00001771 )
Jason Chang32b59562023-07-14 16:45:35 -07001772 print(msg, file=sys.stderr)
1773 raise DeleteDirtyWorktreeError(msg, project=self)
Wink Saville02d79452009-04-10 13:01:24 -07001774
Gavin Makea2e3302023-03-11 06:46:20 +00001775 if not quiet:
1776 print(
1777 "%s: Deleting obsolete checkout." % (self.RelPath(local=False),)
1778 )
Wink Saville02d79452009-04-10 13:01:24 -07001779
Gavin Makea2e3302023-03-11 06:46:20 +00001780 # Unlock and delink from the main worktree. We don't use git's worktree
1781 # remove because it will recursively delete projects -- we handle that
1782 # ourselves below. https://crbug.com/git/48
1783 if self.use_git_worktrees:
1784 needle = platform_utils.realpath(self.gitdir)
1785 # Find the git worktree commondir under .repo/worktrees/.
1786 output = self.bare_git.worktree("list", "--porcelain").splitlines()[
1787 0
1788 ]
1789 assert output.startswith("worktree "), output
1790 commondir = output[9:]
1791 # Walk each of the git worktrees to see where they point.
1792 configs = os.path.join(commondir, "worktrees")
1793 for name in os.listdir(configs):
1794 gitdir = os.path.join(configs, name, "gitdir")
1795 with open(gitdir) as fp:
1796 relpath = fp.read().strip()
1797 # Resolve the checkout path and see if it matches this project.
1798 fullpath = platform_utils.realpath(
1799 os.path.join(configs, name, relpath)
1800 )
1801 if fullpath == needle:
1802 platform_utils.rmtree(os.path.join(configs, name))
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001803
Gavin Makea2e3302023-03-11 06:46:20 +00001804 # Delete the .git directory first, so we're less likely to have a
1805 # partially working git repository around. There shouldn't be any git
1806 # projects here, so rmtree works.
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001807
Gavin Makea2e3302023-03-11 06:46:20 +00001808 # Try to remove plain files first in case of git worktrees. If this
1809 # fails for any reason, we'll fall back to rmtree, and that'll display
1810 # errors if it can't remove things either.
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001811 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001812 platform_utils.remove(self.gitdir)
1813 except OSError:
1814 pass
1815 try:
1816 platform_utils.rmtree(self.gitdir)
1817 except OSError as e:
1818 if e.errno != errno.ENOENT:
1819 print("error: %s: %s" % (self.gitdir, e), file=sys.stderr)
1820 print(
1821 "error: %s: Failed to delete obsolete checkout; remove "
1822 "manually, then run `repo sync -l`."
1823 % (self.RelPath(local=False),),
1824 file=sys.stderr,
1825 )
Jason Chang32b59562023-07-14 16:45:35 -07001826 raise DeleteWorktreeError(aggregate_errors=[e])
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001827
Gavin Makea2e3302023-03-11 06:46:20 +00001828 # Delete everything under the worktree, except for directories that
1829 # contain another git project.
1830 dirs_to_remove = []
1831 failed = False
Jason Chang32b59562023-07-14 16:45:35 -07001832 errors = []
Gavin Makea2e3302023-03-11 06:46:20 +00001833 for root, dirs, files in platform_utils.walk(self.worktree):
1834 for f in files:
1835 path = os.path.join(root, f)
1836 try:
1837 platform_utils.remove(path)
1838 except OSError as e:
1839 if e.errno != errno.ENOENT:
1840 print(
1841 "error: %s: Failed to remove: %s" % (path, e),
1842 file=sys.stderr,
1843 )
1844 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001845 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001846 dirs[:] = [
1847 d
1848 for d in dirs
1849 if not os.path.lexists(os.path.join(root, d, ".git"))
1850 ]
1851 dirs_to_remove += [
1852 os.path.join(root, d)
1853 for d in dirs
1854 if os.path.join(root, d) not in dirs_to_remove
1855 ]
1856 for d in reversed(dirs_to_remove):
1857 if platform_utils.islink(d):
1858 try:
1859 platform_utils.remove(d)
1860 except OSError as e:
1861 if e.errno != errno.ENOENT:
1862 print(
1863 "error: %s: Failed to remove: %s" % (d, e),
1864 file=sys.stderr,
1865 )
1866 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001867 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001868 elif not platform_utils.listdir(d):
1869 try:
1870 platform_utils.rmdir(d)
1871 except OSError as e:
1872 if e.errno != errno.ENOENT:
1873 print(
1874 "error: %s: Failed to remove: %s" % (d, e),
1875 file=sys.stderr,
1876 )
1877 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001878 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001879 if failed:
1880 print(
1881 "error: %s: Failed to delete obsolete checkout."
1882 % (self.RelPath(local=False),),
1883 file=sys.stderr,
1884 )
1885 print(
1886 " Remove manually, then run `repo sync -l`.",
1887 file=sys.stderr,
1888 )
Jason Chang32b59562023-07-14 16:45:35 -07001889 raise DeleteWorktreeError(aggregate_errors=errors)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07001890
Gavin Makea2e3302023-03-11 06:46:20 +00001891 # Try deleting parent dirs if they are empty.
1892 path = self.worktree
1893 while path != self.manifest.topdir:
1894 try:
1895 platform_utils.rmdir(path)
1896 except OSError as e:
1897 if e.errno != errno.ENOENT:
1898 break
1899 path = os.path.dirname(path)
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001900
Gavin Makea2e3302023-03-11 06:46:20 +00001901 return True
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001902
Gavin Makea2e3302023-03-11 06:46:20 +00001903 def StartBranch(self, name, branch_merge="", revision=None):
1904 """Create a new branch off the manifest's revision."""
1905 if not branch_merge:
1906 branch_merge = self.revisionExpr
1907 head = self.work_git.GetHead()
1908 if head == (R_HEADS + name):
1909 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001910
David Pursehouse8a68ff92012-09-24 12:15:13 +09001911 all_refs = self.bare_ref.all
Gavin Makea2e3302023-03-11 06:46:20 +00001912 if R_HEADS + name in all_refs:
Jason Chang1a3612f2023-08-08 14:12:53 -07001913 GitCommand(
1914 self, ["checkout", "-q", name, "--"], verify_command=True
1915 ).Wait()
1916 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001917
Gavin Makea2e3302023-03-11 06:46:20 +00001918 branch = self.GetBranch(name)
1919 branch.remote = self.GetRemote()
1920 branch.merge = branch_merge
1921 if not branch.merge.startswith("refs/") and not ID_RE.match(
1922 branch_merge
1923 ):
1924 branch.merge = R_HEADS + branch_merge
Shawn O. Pearce88443382010-10-08 10:02:09 +02001925
Gavin Makea2e3302023-03-11 06:46:20 +00001926 if revision is None:
1927 revid = self.GetRevisionId(all_refs)
Shawn O. Pearce88443382010-10-08 10:02:09 +02001928 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001929 revid = self.work_git.rev_parse(revision)
Brian Harring14a66742012-09-28 20:21:57 -07001930
Gavin Makea2e3302023-03-11 06:46:20 +00001931 if head.startswith(R_HEADS):
Kevin Degiabaa7f32014-11-12 11:27:45 -07001932 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001933 head = all_refs[head]
1934 except KeyError:
1935 head = None
1936 if revid and head and revid == head:
1937 ref = R_HEADS + name
1938 self.work_git.update_ref(ref, revid)
1939 self.work_git.symbolic_ref(HEAD, ref)
1940 branch.Save()
1941 return True
Kevin Degib1a07b82015-07-27 13:33:43 -06001942
Jason Chang1a3612f2023-08-08 14:12:53 -07001943 GitCommand(
1944 self,
1945 ["checkout", "-q", "-b", branch.name, revid],
1946 verify_command=True,
1947 ).Wait()
1948 branch.Save()
1949 return True
Kevin Degi384b3c52014-10-16 16:02:58 -06001950
Gavin Makea2e3302023-03-11 06:46:20 +00001951 def CheckoutBranch(self, name):
1952 """Checkout a local topic branch.
Shawn O. Pearce2816d4f2009-03-03 17:53:18 -08001953
Gavin Makea2e3302023-03-11 06:46:20 +00001954 Args:
1955 name: The name of the branch to checkout.
Shawn O. Pearce88443382010-10-08 10:02:09 +02001956
Gavin Makea2e3302023-03-11 06:46:20 +00001957 Returns:
Jason Chang1a3612f2023-08-08 14:12:53 -07001958 True if the checkout succeeded; False if the
1959 branch doesn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00001960 """
1961 rev = R_HEADS + name
1962 head = self.work_git.GetHead()
1963 if head == rev:
1964 # Already on the branch.
1965 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001966
Gavin Makea2e3302023-03-11 06:46:20 +00001967 all_refs = self.bare_ref.all
1968 try:
1969 revid = all_refs[rev]
1970 except KeyError:
1971 # Branch does not exist in this project.
Jason Chang1a3612f2023-08-08 14:12:53 -07001972 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001973
Gavin Makea2e3302023-03-11 06:46:20 +00001974 if head.startswith(R_HEADS):
1975 try:
1976 head = all_refs[head]
1977 except KeyError:
1978 head = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001979
Gavin Makea2e3302023-03-11 06:46:20 +00001980 if head == revid:
1981 # Same revision; just update HEAD to point to the new
1982 # target branch, but otherwise take no other action.
1983 _lwrite(
1984 self.work_git.GetDotgitPath(subpath=HEAD),
1985 "ref: %s%s\n" % (R_HEADS, name),
1986 )
1987 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05001988
Jason Chang1a3612f2023-08-08 14:12:53 -07001989 GitCommand(
1990 self,
1991 ["checkout", name, "--"],
1992 capture_stdout=True,
1993 capture_stderr=True,
1994 verify_command=True,
1995 ).Wait()
1996 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05001997
Gavin Makea2e3302023-03-11 06:46:20 +00001998 def AbandonBranch(self, name):
1999 """Destroy a local topic branch.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002000
Gavin Makea2e3302023-03-11 06:46:20 +00002001 Args:
2002 name: The name of the branch to abandon.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002003
Gavin Makea2e3302023-03-11 06:46:20 +00002004 Returns:
Jason Changf9aacd42023-08-03 14:38:00 -07002005 True if the abandon succeeded; Raises GitCommandError if it didn't;
2006 None if the branch didn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00002007 """
2008 rev = R_HEADS + name
2009 all_refs = self.bare_ref.all
2010 if rev not in all_refs:
2011 # Doesn't exist
2012 return None
2013
2014 head = self.work_git.GetHead()
2015 if head == rev:
2016 # We can't destroy the branch while we are sitting
2017 # on it. Switch to a detached HEAD.
2018 head = all_refs[head]
2019
2020 revid = self.GetRevisionId(all_refs)
2021 if head == revid:
2022 _lwrite(
2023 self.work_git.GetDotgitPath(subpath=HEAD), "%s\n" % revid
2024 )
2025 else:
2026 self._Checkout(revid, quiet=True)
Jason Changf9aacd42023-08-03 14:38:00 -07002027 GitCommand(
2028 self,
2029 ["branch", "-D", name],
2030 capture_stdout=True,
2031 capture_stderr=True,
2032 verify_command=True,
2033 ).Wait()
2034 return True
Gavin Makea2e3302023-03-11 06:46:20 +00002035
2036 def PruneHeads(self):
2037 """Prune any topic branches already merged into upstream."""
2038 cb = self.CurrentBranch
2039 kill = []
2040 left = self._allrefs
2041 for name in left.keys():
2042 if name.startswith(R_HEADS):
2043 name = name[len(R_HEADS) :]
2044 if cb is None or name != cb:
2045 kill.append(name)
2046
2047 # Minor optimization: If there's nothing to prune, then don't try to
2048 # read any project state.
2049 if not kill and not cb:
2050 return []
2051
2052 rev = self.GetRevisionId(left)
2053 if (
2054 cb is not None
2055 and not self._revlist(HEAD + "..." + rev)
2056 and not self.IsDirty(consider_untracked=False)
2057 ):
2058 self.work_git.DetachHead(HEAD)
2059 kill.append(cb)
2060
2061 if kill:
2062 old = self.bare_git.GetHead()
2063
2064 try:
2065 self.bare_git.DetachHead(rev)
2066
2067 b = ["branch", "-d"]
2068 b.extend(kill)
2069 b = GitCommand(
2070 self, b, bare=True, capture_stdout=True, capture_stderr=True
2071 )
2072 b.Wait()
2073 finally:
2074 if ID_RE.match(old):
2075 self.bare_git.DetachHead(old)
2076 else:
2077 self.bare_git.SetHead(old)
2078 left = self._allrefs
2079
2080 for branch in kill:
2081 if (R_HEADS + branch) not in left:
2082 self.CleanPublishedCache()
2083 break
2084
2085 if cb and cb not in kill:
2086 kill.append(cb)
2087 kill.sort()
2088
2089 kept = []
2090 for branch in kill:
2091 if R_HEADS + branch in left:
2092 branch = self.GetBranch(branch)
2093 base = branch.LocalMerge
2094 if not base:
2095 base = rev
2096 kept.append(ReviewableBranch(self, branch, base))
2097 return kept
2098
2099 def GetRegisteredSubprojects(self):
2100 result = []
2101
2102 def rec(subprojects):
2103 if not subprojects:
2104 return
2105 result.extend(subprojects)
2106 for p in subprojects:
2107 rec(p.subprojects)
2108
2109 rec(self.subprojects)
2110 return result
2111
2112 def _GetSubmodules(self):
2113 # Unfortunately we cannot call `git submodule status --recursive` here
2114 # because the working tree might not exist yet, and it cannot be used
2115 # without a working tree in its current implementation.
2116
2117 def get_submodules(gitdir, rev):
2118 # Parse .gitmodules for submodule sub_paths and sub_urls.
2119 sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
2120 if not sub_paths:
2121 return []
2122 # Run `git ls-tree` to read SHAs of submodule object, which happen
2123 # to be revision of submodule repository.
2124 sub_revs = git_ls_tree(gitdir, rev, sub_paths)
2125 submodules = []
2126 for sub_path, sub_url in zip(sub_paths, sub_urls):
2127 try:
2128 sub_rev = sub_revs[sub_path]
2129 except KeyError:
2130 # Ignore non-exist submodules.
2131 continue
2132 submodules.append((sub_rev, sub_path, sub_url))
2133 return submodules
2134
2135 re_path = re.compile(r"^submodule\.(.+)\.path=(.*)$")
2136 re_url = re.compile(r"^submodule\.(.+)\.url=(.*)$")
2137
2138 def parse_gitmodules(gitdir, rev):
2139 cmd = ["cat-file", "blob", "%s:.gitmodules" % rev]
2140 try:
2141 p = GitCommand(
2142 None,
2143 cmd,
2144 capture_stdout=True,
2145 capture_stderr=True,
2146 bare=True,
2147 gitdir=gitdir,
2148 )
2149 except GitError:
2150 return [], []
2151 if p.Wait() != 0:
2152 return [], []
2153
2154 gitmodules_lines = []
2155 fd, temp_gitmodules_path = tempfile.mkstemp()
2156 try:
2157 os.write(fd, p.stdout.encode("utf-8"))
2158 os.close(fd)
2159 cmd = ["config", "--file", temp_gitmodules_path, "--list"]
2160 p = GitCommand(
2161 None,
2162 cmd,
2163 capture_stdout=True,
2164 capture_stderr=True,
2165 bare=True,
2166 gitdir=gitdir,
2167 )
2168 if p.Wait() != 0:
2169 return [], []
2170 gitmodules_lines = p.stdout.split("\n")
2171 except GitError:
2172 return [], []
2173 finally:
2174 platform_utils.remove(temp_gitmodules_path)
2175
2176 names = set()
2177 paths = {}
2178 urls = {}
2179 for line in gitmodules_lines:
2180 if not line:
2181 continue
2182 m = re_path.match(line)
2183 if m:
2184 names.add(m.group(1))
2185 paths[m.group(1)] = m.group(2)
2186 continue
2187 m = re_url.match(line)
2188 if m:
2189 names.add(m.group(1))
2190 urls[m.group(1)] = m.group(2)
2191 continue
2192 names = sorted(names)
2193 return (
2194 [paths.get(name, "") for name in names],
2195 [urls.get(name, "") for name in names],
2196 )
2197
2198 def git_ls_tree(gitdir, rev, paths):
2199 cmd = ["ls-tree", rev, "--"]
2200 cmd.extend(paths)
2201 try:
2202 p = GitCommand(
2203 None,
2204 cmd,
2205 capture_stdout=True,
2206 capture_stderr=True,
2207 bare=True,
2208 gitdir=gitdir,
2209 )
2210 except GitError:
2211 return []
2212 if p.Wait() != 0:
2213 return []
2214 objects = {}
2215 for line in p.stdout.split("\n"):
2216 if not line.strip():
2217 continue
2218 object_rev, object_path = line.split()[2:4]
2219 objects[object_path] = object_rev
2220 return objects
2221
2222 try:
2223 rev = self.GetRevisionId()
2224 except GitError:
2225 return []
2226 return get_submodules(self.gitdir, rev)
2227
2228 def GetDerivedSubprojects(self):
2229 result = []
2230 if not self.Exists:
2231 # If git repo does not exist yet, querying its submodules will
2232 # mess up its states; so return here.
2233 return result
2234 for rev, path, url in self._GetSubmodules():
2235 name = self.manifest.GetSubprojectName(self, path)
2236 (
2237 relpath,
2238 worktree,
2239 gitdir,
2240 objdir,
2241 ) = self.manifest.GetSubprojectPaths(self, name, path)
2242 project = self.manifest.paths.get(relpath)
2243 if project:
2244 result.extend(project.GetDerivedSubprojects())
2245 continue
2246
2247 if url.startswith(".."):
2248 url = urllib.parse.urljoin("%s/" % self.remote.url, url)
2249 remote = RemoteSpec(
2250 self.remote.name,
2251 url=url,
2252 pushUrl=self.remote.pushUrl,
2253 review=self.remote.review,
2254 revision=self.remote.revision,
2255 )
2256 subproject = Project(
2257 manifest=self.manifest,
2258 name=name,
2259 remote=remote,
2260 gitdir=gitdir,
2261 objdir=objdir,
2262 worktree=worktree,
2263 relpath=relpath,
2264 revisionExpr=rev,
2265 revisionId=rev,
2266 rebase=self.rebase,
2267 groups=self.groups,
2268 sync_c=self.sync_c,
2269 sync_s=self.sync_s,
2270 sync_tags=self.sync_tags,
2271 parent=self,
2272 is_derived=True,
2273 )
2274 result.append(subproject)
2275 result.extend(subproject.GetDerivedSubprojects())
2276 return result
2277
2278 def EnableRepositoryExtension(self, key, value="true", version=1):
2279 """Enable git repository extension |key| with |value|.
2280
2281 Args:
2282 key: The extension to enabled. Omit the "extensions." prefix.
2283 value: The value to use for the extension.
2284 version: The minimum git repository version needed.
2285 """
2286 # Make sure the git repo version is new enough already.
2287 found_version = self.config.GetInt("core.repositoryFormatVersion")
2288 if found_version is None:
2289 found_version = 0
2290 if found_version < version:
2291 self.config.SetString("core.repositoryFormatVersion", str(version))
2292
2293 # Enable the extension!
2294 self.config.SetString("extensions.%s" % (key,), value)
2295
2296 def ResolveRemoteHead(self, name=None):
2297 """Find out what the default branch (HEAD) points to.
2298
2299 Normally this points to refs/heads/master, but projects are moving to
2300 main. Support whatever the server uses rather than hardcoding "master"
2301 ourselves.
2302 """
2303 if name is None:
2304 name = self.remote.name
2305
2306 # The output will look like (NB: tabs are separators):
2307 # ref: refs/heads/master HEAD
2308 # 5f6803b100bb3cd0f534e96e88c91373e8ed1c44 HEAD
2309 output = self.bare_git.ls_remote(
2310 "-q", "--symref", "--exit-code", name, "HEAD"
2311 )
2312
2313 for line in output.splitlines():
2314 lhs, rhs = line.split("\t", 1)
2315 if rhs == "HEAD" and lhs.startswith("ref:"):
2316 return lhs[4:].strip()
2317
2318 return None
2319
2320 def _CheckForImmutableRevision(self):
2321 try:
2322 # if revision (sha or tag) is not present then following function
2323 # throws an error.
2324 self.bare_git.rev_list(
2325 "-1", "--missing=allow-any", "%s^0" % self.revisionExpr, "--"
2326 )
2327 if self.upstream:
2328 rev = self.GetRemote().ToLocal(self.upstream)
2329 self.bare_git.rev_list(
2330 "-1", "--missing=allow-any", "%s^0" % rev, "--"
2331 )
2332 self.bare_git.merge_base(
2333 "--is-ancestor", self.revisionExpr, rev
2334 )
2335 return True
2336 except GitError:
2337 # There is no such persistent revision. We have to fetch it.
2338 return False
2339
2340 def _FetchArchive(self, tarpath, cwd=None):
2341 cmd = ["archive", "-v", "-o", tarpath]
2342 cmd.append("--remote=%s" % self.remote.url)
2343 cmd.append("--prefix=%s/" % self.RelPath(local=False))
2344 cmd.append(self.revisionExpr)
2345
2346 command = GitCommand(
Jason Chang32b59562023-07-14 16:45:35 -07002347 self,
2348 cmd,
2349 cwd=cwd,
2350 capture_stdout=True,
2351 capture_stderr=True,
2352 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00002353 )
Jason Chang32b59562023-07-14 16:45:35 -07002354 command.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00002355
2356 def _RemoteFetch(
2357 self,
2358 name=None,
2359 current_branch_only=False,
2360 initial=False,
2361 quiet=False,
2362 verbose=False,
2363 output_redir=None,
2364 alt_dir=None,
2365 tags=True,
2366 prune=False,
2367 depth=None,
2368 submodules=False,
2369 ssh_proxy=None,
2370 force_sync=False,
2371 clone_filter=None,
2372 retry_fetches=2,
2373 retry_sleep_initial_sec=4.0,
2374 retry_exp_factor=2.0,
Jason Chang32b59562023-07-14 16:45:35 -07002375 ) -> bool:
Gavin Makea2e3302023-03-11 06:46:20 +00002376 is_sha1 = False
2377 tag_name = None
2378 # The depth should not be used when fetching to a mirror because
2379 # it will result in a shallow repository that cannot be cloned or
2380 # fetched from.
2381 # The repo project should also never be synced with partial depth.
2382 if self.manifest.IsMirror or self.relpath == ".repo/repo":
2383 depth = None
2384
2385 if depth:
2386 current_branch_only = True
2387
2388 if ID_RE.match(self.revisionExpr) is not None:
2389 is_sha1 = True
2390
2391 if current_branch_only:
2392 if self.revisionExpr.startswith(R_TAGS):
2393 # This is a tag and its commit id should never change.
2394 tag_name = self.revisionExpr[len(R_TAGS) :]
2395 elif self.upstream and self.upstream.startswith(R_TAGS):
2396 # This is a tag and its commit id should never change.
2397 tag_name = self.upstream[len(R_TAGS) :]
2398
2399 if is_sha1 or tag_name is not None:
2400 if self._CheckForImmutableRevision():
2401 if verbose:
2402 print(
2403 "Skipped fetching project %s (already have "
2404 "persistent ref)" % self.name
2405 )
2406 return True
2407 if is_sha1 and not depth:
2408 # When syncing a specific commit and --depth is not set:
2409 # * if upstream is explicitly specified and is not a sha1, fetch
2410 # only upstream as users expect only upstream to be fetch.
2411 # Note: The commit might not be in upstream in which case the
2412 # sync will fail.
2413 # * otherwise, fetch all branches to make sure we end up with
2414 # the specific commit.
2415 if self.upstream:
2416 current_branch_only = not ID_RE.match(self.upstream)
2417 else:
2418 current_branch_only = False
2419
2420 if not name:
2421 name = self.remote.name
2422
2423 remote = self.GetRemote(name)
2424 if not remote.PreConnectFetch(ssh_proxy):
2425 ssh_proxy = None
2426
2427 if initial:
2428 if alt_dir and "objects" == os.path.basename(alt_dir):
2429 ref_dir = os.path.dirname(alt_dir)
2430 packed_refs = os.path.join(self.gitdir, "packed-refs")
2431
2432 all_refs = self.bare_ref.all
2433 ids = set(all_refs.values())
2434 tmp = set()
2435
2436 for r, ref_id in GitRefs(ref_dir).all.items():
2437 if r not in all_refs:
2438 if r.startswith(R_TAGS) or remote.WritesTo(r):
2439 all_refs[r] = ref_id
2440 ids.add(ref_id)
2441 continue
2442
2443 if ref_id in ids:
2444 continue
2445
2446 r = "refs/_alt/%s" % ref_id
2447 all_refs[r] = ref_id
2448 ids.add(ref_id)
2449 tmp.add(r)
2450
2451 tmp_packed_lines = []
2452 old_packed_lines = []
2453
2454 for r in sorted(all_refs):
2455 line = "%s %s\n" % (all_refs[r], r)
2456 tmp_packed_lines.append(line)
2457 if r not in tmp:
2458 old_packed_lines.append(line)
2459
2460 tmp_packed = "".join(tmp_packed_lines)
2461 old_packed = "".join(old_packed_lines)
2462 _lwrite(packed_refs, tmp_packed)
2463 else:
2464 alt_dir = None
2465
2466 cmd = ["fetch"]
2467
2468 if clone_filter:
2469 git_require((2, 19, 0), fail=True, msg="partial clones")
2470 cmd.append("--filter=%s" % clone_filter)
2471 self.EnableRepositoryExtension("partialclone", self.remote.name)
2472
2473 if depth:
2474 cmd.append("--depth=%s" % depth)
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002475 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002476 # If this repo has shallow objects, then we don't know which refs
2477 # have shallow objects or not. Tell git to unshallow all fetched
2478 # refs. Don't do this with projects that don't have shallow
2479 # objects, since it is less efficient.
2480 if os.path.exists(os.path.join(self.gitdir, "shallow")):
2481 cmd.append("--depth=2147483647")
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002482
Gavin Makea2e3302023-03-11 06:46:20 +00002483 if not verbose:
2484 cmd.append("--quiet")
2485 if not quiet and sys.stdout.isatty():
2486 cmd.append("--progress")
2487 if not self.worktree:
2488 cmd.append("--update-head-ok")
2489 cmd.append(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002490
Gavin Makea2e3302023-03-11 06:46:20 +00002491 if force_sync:
2492 cmd.append("--force")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002493
Gavin Makea2e3302023-03-11 06:46:20 +00002494 if prune:
2495 cmd.append("--prune")
Mike Frysinger29626b42021-05-01 09:37:13 -04002496
Gavin Makea2e3302023-03-11 06:46:20 +00002497 # Always pass something for --recurse-submodules, git with GIT_DIR
2498 # behaves incorrectly when not given `--recurse-submodules=no`.
2499 # (b/218891912)
2500 cmd.append(
2501 f'--recurse-submodules={"on-demand" if submodules else "no"}'
2502 )
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002503
Gavin Makea2e3302023-03-11 06:46:20 +00002504 spec = []
2505 if not current_branch_only:
2506 # Fetch whole repo.
2507 spec.append(
2508 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2509 )
2510 elif tag_name is not None:
2511 spec.append("tag")
2512 spec.append(tag_name)
Remy Böhmer1469c282020-12-15 18:49:02 +01002513
Gavin Makea2e3302023-03-11 06:46:20 +00002514 if self.manifest.IsMirror and not current_branch_only:
2515 branch = None
Remy Böhmer1469c282020-12-15 18:49:02 +01002516 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002517 branch = self.revisionExpr
2518 if (
2519 not self.manifest.IsMirror
2520 and is_sha1
2521 and depth
2522 and git_require((1, 8, 3))
2523 ):
2524 # Shallow checkout of a specific commit, fetch from that commit and
2525 # not the heads only as the commit might be deeper in the history.
2526 spec.append(branch)
2527 if self.upstream:
2528 spec.append(self.upstream)
David James8d201162013-10-11 17:03:19 -07002529 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002530 if is_sha1:
2531 branch = self.upstream
2532 if branch is not None and branch.strip():
2533 if not branch.startswith("refs/"):
2534 branch = R_HEADS + branch
2535 spec.append(str(("+%s:" % branch) + remote.ToLocal(branch)))
David James8d201162013-10-11 17:03:19 -07002536
Gavin Makea2e3302023-03-11 06:46:20 +00002537 # If mirroring repo and we cannot deduce the tag or branch to fetch,
2538 # fetch whole repo.
2539 if self.manifest.IsMirror and not spec:
2540 spec.append(
2541 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2542 )
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002543
Gavin Makea2e3302023-03-11 06:46:20 +00002544 # If using depth then we should not get all the tags since they may
2545 # be outside of the depth.
2546 if not tags or depth:
2547 cmd.append("--no-tags")
2548 else:
2549 cmd.append("--tags")
2550 spec.append(str(("+refs/tags/*:") + remote.ToLocal("refs/tags/*")))
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002551
Gavin Makea2e3302023-03-11 06:46:20 +00002552 cmd.extend(spec)
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002553
Gavin Makea2e3302023-03-11 06:46:20 +00002554 # At least one retry minimum due to git remote prune.
2555 retry_fetches = max(retry_fetches, 2)
2556 retry_cur_sleep = retry_sleep_initial_sec
2557 ok = prune_tried = False
2558 for try_n in range(retry_fetches):
Jason Chang32b59562023-07-14 16:45:35 -07002559 verify_command = try_n == retry_fetches - 1
Gavin Makea2e3302023-03-11 06:46:20 +00002560 gitcmd = GitCommand(
2561 self,
2562 cmd,
2563 bare=True,
2564 objdir=os.path.join(self.objdir, "objects"),
2565 ssh_proxy=ssh_proxy,
2566 merge_output=True,
2567 capture_stdout=quiet or bool(output_redir),
Jason Chang32b59562023-07-14 16:45:35 -07002568 verify_command=verify_command,
Gavin Makea2e3302023-03-11 06:46:20 +00002569 )
2570 if gitcmd.stdout and not quiet and output_redir:
2571 output_redir.write(gitcmd.stdout)
2572 ret = gitcmd.Wait()
2573 if ret == 0:
2574 ok = True
2575 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002576
Gavin Makea2e3302023-03-11 06:46:20 +00002577 # Retry later due to HTTP 429 Too Many Requests.
2578 elif (
2579 gitcmd.stdout
2580 and "error:" in gitcmd.stdout
2581 and "HTTP 429" in gitcmd.stdout
2582 ):
2583 # Fallthru to sleep+retry logic at the bottom.
2584 pass
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002585
Gavin Makea2e3302023-03-11 06:46:20 +00002586 # Try to prune remote branches once in case there are conflicts.
2587 # For example, if the remote had refs/heads/upstream, but deleted
2588 # that and now has refs/heads/upstream/foo.
2589 elif (
2590 gitcmd.stdout
2591 and "error:" in gitcmd.stdout
2592 and "git remote prune" in gitcmd.stdout
2593 and not prune_tried
2594 ):
2595 prune_tried = True
2596 prunecmd = GitCommand(
2597 self,
2598 ["remote", "prune", name],
2599 bare=True,
2600 ssh_proxy=ssh_proxy,
2601 )
2602 ret = prunecmd.Wait()
2603 if ret:
2604 break
2605 print(
2606 "retrying fetch after pruning remote branches",
2607 file=output_redir,
2608 )
2609 # Continue right away so we don't sleep as we shouldn't need to.
2610 continue
2611 elif current_branch_only and is_sha1 and ret == 128:
2612 # Exit code 128 means "couldn't find the ref you asked for"; if
2613 # we're in sha1 mode, we just tried sync'ing from the upstream
2614 # field; it doesn't exist, thus abort the optimization attempt
2615 # and do a full sync.
2616 break
2617 elif ret < 0:
2618 # Git died with a signal, exit immediately.
2619 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002620
Gavin Makea2e3302023-03-11 06:46:20 +00002621 # Figure out how long to sleep before the next attempt, if there is
2622 # one.
2623 if not verbose and gitcmd.stdout:
2624 print(
2625 "\n%s:\n%s" % (self.name, gitcmd.stdout),
2626 end="",
2627 file=output_redir,
2628 )
2629 if try_n < retry_fetches - 1:
2630 print(
2631 "%s: sleeping %s seconds before retrying"
2632 % (self.name, retry_cur_sleep),
2633 file=output_redir,
2634 )
2635 time.sleep(retry_cur_sleep)
2636 retry_cur_sleep = min(
2637 retry_exp_factor * retry_cur_sleep, MAXIMUM_RETRY_SLEEP_SEC
2638 )
2639 retry_cur_sleep *= 1 - random.uniform(
2640 -RETRY_JITTER_PERCENT, RETRY_JITTER_PERCENT
2641 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002642
Gavin Makea2e3302023-03-11 06:46:20 +00002643 if initial:
2644 if alt_dir:
2645 if old_packed != "":
2646 _lwrite(packed_refs, old_packed)
2647 else:
2648 platform_utils.remove(packed_refs)
2649 self.bare_git.pack_refs("--all", "--prune")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002650
Gavin Makea2e3302023-03-11 06:46:20 +00002651 if is_sha1 and current_branch_only:
2652 # We just synced the upstream given branch; verify we
2653 # got what we wanted, else trigger a second run of all
2654 # refs.
2655 if not self._CheckForImmutableRevision():
2656 # Sync the current branch only with depth set to None.
2657 # We always pass depth=None down to avoid infinite recursion.
2658 return self._RemoteFetch(
2659 name=name,
2660 quiet=quiet,
2661 verbose=verbose,
2662 output_redir=output_redir,
2663 current_branch_only=current_branch_only and depth,
2664 initial=False,
2665 alt_dir=alt_dir,
Nasser Grainawiaafed292023-05-24 12:51:03 -06002666 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00002667 depth=None,
2668 ssh_proxy=ssh_proxy,
2669 clone_filter=clone_filter,
2670 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002671
Gavin Makea2e3302023-03-11 06:46:20 +00002672 return ok
Mike Frysingerf4545122019-11-11 04:34:16 -05002673
Gavin Makea2e3302023-03-11 06:46:20 +00002674 def _ApplyCloneBundle(self, initial=False, quiet=False, verbose=False):
2675 if initial and (
2676 self.manifest.manifestProject.depth or self.clone_depth
2677 ):
2678 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002679
Gavin Makea2e3302023-03-11 06:46:20 +00002680 remote = self.GetRemote()
2681 bundle_url = remote.url + "/clone.bundle"
2682 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
2683 if GetSchemeFromUrl(bundle_url) not in (
2684 "http",
2685 "https",
2686 "persistent-http",
2687 "persistent-https",
2688 ):
2689 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06002690
Gavin Makea2e3302023-03-11 06:46:20 +00002691 bundle_dst = os.path.join(self.gitdir, "clone.bundle")
2692 bundle_tmp = os.path.join(self.gitdir, "clone.bundle.tmp")
2693
2694 exist_dst = os.path.exists(bundle_dst)
2695 exist_tmp = os.path.exists(bundle_tmp)
2696
2697 if not initial and not exist_dst and not exist_tmp:
2698 return False
2699
2700 if not exist_dst:
2701 exist_dst = self._FetchBundle(
2702 bundle_url, bundle_tmp, bundle_dst, quiet, verbose
2703 )
2704 if not exist_dst:
2705 return False
2706
2707 cmd = ["fetch"]
2708 if not verbose:
2709 cmd.append("--quiet")
2710 if not quiet and sys.stdout.isatty():
2711 cmd.append("--progress")
2712 if not self.worktree:
2713 cmd.append("--update-head-ok")
2714 cmd.append(bundle_dst)
2715 for f in remote.fetch:
2716 cmd.append(str(f))
2717 cmd.append("+refs/tags/*:refs/tags/*")
2718
2719 ok = (
2720 GitCommand(
2721 self,
2722 cmd,
2723 bare=True,
2724 objdir=os.path.join(self.objdir, "objects"),
2725 ).Wait()
2726 == 0
2727 )
2728 platform_utils.remove(bundle_dst, missing_ok=True)
2729 platform_utils.remove(bundle_tmp, missing_ok=True)
2730 return ok
2731
2732 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
2733 platform_utils.remove(dstPath, missing_ok=True)
2734
2735 cmd = ["curl", "--fail", "--output", tmpPath, "--netrc", "--location"]
2736 if quiet:
2737 cmd += ["--silent", "--show-error"]
2738 if os.path.exists(tmpPath):
2739 size = os.stat(tmpPath).st_size
2740 if size >= 1024:
2741 cmd += ["--continue-at", "%d" % (size,)]
2742 else:
2743 platform_utils.remove(tmpPath)
2744 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
2745 if cookiefile:
2746 cmd += ["--cookie", cookiefile]
2747 if proxy:
2748 cmd += ["--proxy", proxy]
2749 elif "http_proxy" in os.environ and "darwin" == sys.platform:
2750 cmd += ["--proxy", os.environ["http_proxy"]]
2751 if srcUrl.startswith("persistent-https"):
2752 srcUrl = "http" + srcUrl[len("persistent-https") :]
2753 elif srcUrl.startswith("persistent-http"):
2754 srcUrl = "http" + srcUrl[len("persistent-http") :]
2755 cmd += [srcUrl]
2756
2757 proc = None
2758 with Trace("Fetching bundle: %s", " ".join(cmd)):
2759 if verbose:
2760 print("%s: Downloading bundle: %s" % (self.name, srcUrl))
2761 stdout = None if verbose else subprocess.PIPE
2762 stderr = None if verbose else subprocess.STDOUT
2763 try:
2764 proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
2765 except OSError:
2766 return False
2767
2768 (output, _) = proc.communicate()
2769 curlret = proc.returncode
2770
2771 if curlret == 22:
2772 # From curl man page:
2773 # 22: HTTP page not retrieved. The requested url was not found
2774 # or returned another error with the HTTP error code being 400
2775 # or above. This return code only appears if -f, --fail is used.
2776 if verbose:
2777 print(
2778 "%s: Unable to retrieve clone.bundle; ignoring."
2779 % self.name
2780 )
2781 if output:
2782 print("Curl output:\n%s" % output)
2783 return False
2784 elif curlret and not verbose and output:
2785 print("%s" % output, file=sys.stderr)
2786
2787 if os.path.exists(tmpPath):
2788 if curlret == 0 and self._IsValidBundle(tmpPath, quiet):
2789 platform_utils.rename(tmpPath, dstPath)
2790 return True
2791 else:
2792 platform_utils.remove(tmpPath)
2793 return False
2794 else:
2795 return False
2796
2797 def _IsValidBundle(self, path, quiet):
2798 try:
2799 with open(path, "rb") as f:
2800 if f.read(16) == b"# v2 git bundle\n":
2801 return True
2802 else:
2803 if not quiet:
2804 print(
2805 "Invalid clone.bundle file; ignoring.",
2806 file=sys.stderr,
2807 )
2808 return False
2809 except OSError:
2810 return False
2811
2812 def _Checkout(self, rev, quiet=False):
2813 cmd = ["checkout"]
2814 if quiet:
2815 cmd.append("-q")
2816 cmd.append(rev)
2817 cmd.append("--")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002818 if GitCommand(self, cmd).Wait() != 0:
Gavin Makea2e3302023-03-11 06:46:20 +00002819 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002820 raise GitError(
2821 "%s checkout %s " % (self.name, rev), project=self.name
2822 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002823
Gavin Makea2e3302023-03-11 06:46:20 +00002824 def _CherryPick(self, rev, ffonly=False, record_origin=False):
2825 cmd = ["cherry-pick"]
2826 if ffonly:
2827 cmd.append("--ff")
2828 if record_origin:
2829 cmd.append("-x")
2830 cmd.append(rev)
2831 cmd.append("--")
2832 if GitCommand(self, cmd).Wait() != 0:
2833 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002834 raise GitError(
2835 "%s cherry-pick %s " % (self.name, rev), project=self.name
2836 )
Victor Boivie0960b5b2010-11-26 13:42:13 +01002837
Gavin Makea2e3302023-03-11 06:46:20 +00002838 def _LsRemote(self, refs):
2839 cmd = ["ls-remote", self.remote.name, refs]
2840 p = GitCommand(self, cmd, capture_stdout=True)
2841 if p.Wait() == 0:
2842 return p.stdout
2843 return None
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002844
Gavin Makea2e3302023-03-11 06:46:20 +00002845 def _Revert(self, rev):
2846 cmd = ["revert"]
2847 cmd.append("--no-edit")
2848 cmd.append(rev)
2849 cmd.append("--")
2850 if GitCommand(self, cmd).Wait() != 0:
2851 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002852 raise GitError(
2853 "%s revert %s " % (self.name, rev), project=self.name
2854 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002855
Gavin Makea2e3302023-03-11 06:46:20 +00002856 def _ResetHard(self, rev, quiet=True):
2857 cmd = ["reset", "--hard"]
2858 if quiet:
2859 cmd.append("-q")
2860 cmd.append(rev)
2861 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002862 raise GitError(
2863 "%s reset --hard %s " % (self.name, rev), project=self.name
2864 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002865
Gavin Makea2e3302023-03-11 06:46:20 +00002866 def _SyncSubmodules(self, quiet=True):
2867 cmd = ["submodule", "update", "--init", "--recursive"]
2868 if quiet:
2869 cmd.append("-q")
2870 if GitCommand(self, cmd).Wait() != 0:
2871 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07002872 "%s submodule update --init --recursive " % self.name,
2873 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00002874 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002875
Gavin Makea2e3302023-03-11 06:46:20 +00002876 def _Rebase(self, upstream, onto=None):
2877 cmd = ["rebase"]
2878 if onto is not None:
2879 cmd.extend(["--onto", onto])
2880 cmd.append(upstream)
2881 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002882 raise GitError(
2883 "%s rebase %s " % (self.name, upstream), project=self.name
2884 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002885
Gavin Makea2e3302023-03-11 06:46:20 +00002886 def _FastForward(self, head, ffonly=False):
2887 cmd = ["merge", "--no-stat", head]
2888 if ffonly:
2889 cmd.append("--ff-only")
2890 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002891 raise GitError(
2892 "%s merge %s " % (self.name, head), project=self.name
2893 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002894
Gavin Makea2e3302023-03-11 06:46:20 +00002895 def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False):
2896 init_git_dir = not os.path.exists(self.gitdir)
2897 init_obj_dir = not os.path.exists(self.objdir)
2898 try:
2899 # Initialize the bare repository, which contains all of the objects.
2900 if init_obj_dir:
2901 os.makedirs(self.objdir)
2902 self.bare_objdir.init()
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002903
Gavin Makea2e3302023-03-11 06:46:20 +00002904 self._UpdateHooks(quiet=quiet)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002905
Gavin Makea2e3302023-03-11 06:46:20 +00002906 if self.use_git_worktrees:
2907 # Enable per-worktree config file support if possible. This
2908 # is more a nice-to-have feature for users rather than a
2909 # hard requirement.
2910 if git_require((2, 20, 0)):
2911 self.EnableRepositoryExtension("worktreeConfig")
Renaud Paquay788e9622017-01-27 11:41:12 -08002912
Gavin Makea2e3302023-03-11 06:46:20 +00002913 # If we have a separate directory to hold refs, initialize it as
2914 # well.
2915 if self.objdir != self.gitdir:
2916 if init_git_dir:
2917 os.makedirs(self.gitdir)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002918
Gavin Makea2e3302023-03-11 06:46:20 +00002919 if init_obj_dir or init_git_dir:
2920 self._ReferenceGitDir(
2921 self.objdir, self.gitdir, copy_all=True
2922 )
2923 try:
2924 self._CheckDirReference(self.objdir, self.gitdir)
2925 except GitError as e:
2926 if force_sync:
2927 print(
2928 "Retrying clone after deleting %s" % self.gitdir,
2929 file=sys.stderr,
2930 )
2931 try:
2932 platform_utils.rmtree(
2933 platform_utils.realpath(self.gitdir)
2934 )
2935 if self.worktree and os.path.exists(
2936 platform_utils.realpath(self.worktree)
2937 ):
2938 platform_utils.rmtree(
2939 platform_utils.realpath(self.worktree)
2940 )
2941 return self._InitGitDir(
2942 mirror_git=mirror_git,
2943 force_sync=False,
2944 quiet=quiet,
2945 )
2946 except Exception:
2947 raise e
2948 raise e
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002949
Gavin Makea2e3302023-03-11 06:46:20 +00002950 if init_git_dir:
2951 mp = self.manifest.manifestProject
2952 ref_dir = mp.reference or ""
Julien Camperguedd654222014-01-09 16:21:37 +01002953
Gavin Makea2e3302023-03-11 06:46:20 +00002954 def _expanded_ref_dirs():
2955 """Iterate through possible git reference dir paths."""
2956 name = self.name + ".git"
2957 yield mirror_git or os.path.join(ref_dir, name)
2958 for prefix in "", self.remote.name:
2959 yield os.path.join(
2960 ref_dir, ".repo", "project-objects", prefix, name
2961 )
2962 yield os.path.join(
2963 ref_dir, ".repo", "worktrees", prefix, name
2964 )
2965
2966 if ref_dir or mirror_git:
2967 found_ref_dir = None
2968 for path in _expanded_ref_dirs():
2969 if os.path.exists(path):
2970 found_ref_dir = path
2971 break
2972 ref_dir = found_ref_dir
2973
2974 if ref_dir:
2975 if not os.path.isabs(ref_dir):
2976 # The alternate directory is relative to the object
2977 # database.
2978 ref_dir = os.path.relpath(
2979 ref_dir, os.path.join(self.objdir, "objects")
2980 )
2981 _lwrite(
2982 os.path.join(
2983 self.objdir, "objects/info/alternates"
2984 ),
2985 os.path.join(ref_dir, "objects") + "\n",
2986 )
2987
2988 m = self.manifest.manifestProject.config
2989 for key in ["user.name", "user.email"]:
2990 if m.Has(key, include_defaults=False):
2991 self.config.SetString(key, m.GetString(key))
2992 if not self.manifest.EnableGitLfs:
2993 self.config.SetString(
2994 "filter.lfs.smudge", "git-lfs smudge --skip -- %f"
2995 )
2996 self.config.SetString(
2997 "filter.lfs.process", "git-lfs filter-process --skip"
2998 )
2999 self.config.SetBoolean(
3000 "core.bare", True if self.manifest.IsMirror else None
3001 )
3002 except Exception:
3003 if init_obj_dir and os.path.exists(self.objdir):
3004 platform_utils.rmtree(self.objdir)
3005 if init_git_dir and os.path.exists(self.gitdir):
3006 platform_utils.rmtree(self.gitdir)
3007 raise
3008
3009 def _UpdateHooks(self, quiet=False):
3010 if os.path.exists(self.objdir):
3011 self._InitHooks(quiet=quiet)
3012
3013 def _InitHooks(self, quiet=False):
3014 hooks = platform_utils.realpath(os.path.join(self.objdir, "hooks"))
3015 if not os.path.exists(hooks):
3016 os.makedirs(hooks)
3017
3018 # Delete sample hooks. They're noise.
3019 for hook in glob.glob(os.path.join(hooks, "*.sample")):
3020 try:
3021 platform_utils.remove(hook, missing_ok=True)
3022 except PermissionError:
3023 pass
3024
3025 for stock_hook in _ProjectHooks():
3026 name = os.path.basename(stock_hook)
3027
3028 if (
3029 name in ("commit-msg",)
3030 and not self.remote.review
3031 and self is not self.manifest.manifestProject
3032 ):
3033 # Don't install a Gerrit Code Review hook if this
3034 # project does not appear to use it for reviews.
3035 #
3036 # Since the manifest project is one of those, but also
3037 # managed through gerrit, it's excluded.
3038 continue
3039
3040 dst = os.path.join(hooks, name)
3041 if platform_utils.islink(dst):
3042 continue
3043 if os.path.exists(dst):
3044 # If the files are the same, we'll leave it alone. We create
3045 # symlinks below by default but fallback to hardlinks if the OS
3046 # blocks them. So if we're here, it's probably because we made a
3047 # hardlink below.
3048 if not filecmp.cmp(stock_hook, dst, shallow=False):
3049 if not quiet:
3050 _warn(
3051 "%s: Not replacing locally modified %s hook",
3052 self.RelPath(local=False),
3053 name,
3054 )
3055 continue
3056 try:
3057 platform_utils.symlink(
3058 os.path.relpath(stock_hook, os.path.dirname(dst)), dst
3059 )
3060 except OSError as e:
3061 if e.errno == errno.EPERM:
3062 try:
3063 os.link(stock_hook, dst)
3064 except OSError:
Jason Chang32b59562023-07-14 16:45:35 -07003065 raise GitError(
3066 self._get_symlink_error_message(), project=self.name
3067 )
Gavin Makea2e3302023-03-11 06:46:20 +00003068 else:
3069 raise
3070
3071 def _InitRemote(self):
3072 if self.remote.url:
3073 remote = self.GetRemote()
3074 remote.url = self.remote.url
3075 remote.pushUrl = self.remote.pushUrl
3076 remote.review = self.remote.review
3077 remote.projectname = self.name
3078
3079 if self.worktree:
3080 remote.ResetFetch(mirror=False)
3081 else:
3082 remote.ResetFetch(mirror=True)
3083 remote.Save()
3084
3085 def _InitMRef(self):
3086 """Initialize the pseudo m/<manifest branch> ref."""
3087 if self.manifest.branch:
3088 if self.use_git_worktrees:
3089 # Set up the m/ space to point to the worktree-specific ref
3090 # space. We'll update the worktree-specific ref space on each
3091 # checkout.
3092 ref = R_M + self.manifest.branch
3093 if not self.bare_ref.symref(ref):
3094 self.bare_git.symbolic_ref(
3095 "-m",
3096 "redirecting to worktree scope",
3097 ref,
3098 R_WORKTREE_M + self.manifest.branch,
3099 )
3100
3101 # We can't update this ref with git worktrees until it exists.
3102 # We'll wait until the initial checkout to set it.
3103 if not os.path.exists(self.worktree):
3104 return
3105
3106 base = R_WORKTREE_M
3107 active_git = self.work_git
3108
3109 self._InitAnyMRef(HEAD, self.bare_git, detach=True)
3110 else:
3111 base = R_M
3112 active_git = self.bare_git
3113
3114 self._InitAnyMRef(base + self.manifest.branch, active_git)
3115
3116 def _InitMirrorHead(self):
3117 self._InitAnyMRef(HEAD, self.bare_git)
3118
3119 def _InitAnyMRef(self, ref, active_git, detach=False):
3120 """Initialize |ref| in |active_git| to the value in the manifest.
3121
3122 This points |ref| to the <project> setting in the manifest.
3123
3124 Args:
3125 ref: The branch to update.
3126 active_git: The git repository to make updates in.
3127 detach: Whether to update target of symbolic refs, or overwrite the
3128 ref directly (and thus make it non-symbolic).
3129 """
3130 cur = self.bare_ref.symref(ref)
3131
3132 if self.revisionId:
3133 if cur != "" or self.bare_ref.get(ref) != self.revisionId:
3134 msg = "manifest set to %s" % self.revisionId
3135 dst = self.revisionId + "^0"
3136 active_git.UpdateRef(ref, dst, message=msg, detach=True)
Julien Camperguedd654222014-01-09 16:21:37 +01003137 else:
Gavin Makea2e3302023-03-11 06:46:20 +00003138 remote = self.GetRemote()
3139 dst = remote.ToLocal(self.revisionExpr)
3140 if cur != dst:
3141 msg = "manifest set to %s" % self.revisionExpr
3142 if detach:
3143 active_git.UpdateRef(ref, dst, message=msg, detach=True)
3144 else:
3145 active_git.symbolic_ref("-m", msg, ref, dst)
Julien Camperguedd654222014-01-09 16:21:37 +01003146
Gavin Makea2e3302023-03-11 06:46:20 +00003147 def _CheckDirReference(self, srcdir, destdir):
3148 # Git worktrees don't use symlinks to share at all.
3149 if self.use_git_worktrees:
3150 return
Julien Camperguedd654222014-01-09 16:21:37 +01003151
Gavin Makea2e3302023-03-11 06:46:20 +00003152 for name in self.shareable_dirs:
3153 # Try to self-heal a bit in simple cases.
3154 dst_path = os.path.join(destdir, name)
3155 src_path = os.path.join(srcdir, name)
Julien Camperguedd654222014-01-09 16:21:37 +01003156
Gavin Makea2e3302023-03-11 06:46:20 +00003157 dst = platform_utils.realpath(dst_path)
3158 if os.path.lexists(dst):
3159 src = platform_utils.realpath(src_path)
3160 # Fail if the links are pointing to the wrong place.
3161 if src != dst:
3162 _error("%s is different in %s vs %s", name, destdir, srcdir)
3163 raise GitError(
3164 "--force-sync not enabled; cannot overwrite a local "
3165 "work tree. If you're comfortable with the "
3166 "possibility of losing the work tree's git metadata,"
3167 " use `repo sync --force-sync {0}` to "
Jason Chang32b59562023-07-14 16:45:35 -07003168 "proceed.".format(self.RelPath(local=False)),
3169 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003170 )
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003171
Gavin Makea2e3302023-03-11 06:46:20 +00003172 def _ReferenceGitDir(self, gitdir, dotgit, copy_all):
3173 """Update |dotgit| to reference |gitdir|, using symlinks where possible.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003174
Gavin Makea2e3302023-03-11 06:46:20 +00003175 Args:
3176 gitdir: The bare git repository. Must already be initialized.
3177 dotgit: The repository you would like to initialize.
3178 copy_all: If true, copy all remaining files from |gitdir| ->
3179 |dotgit|. This saves you the effort of initializing |dotgit|
3180 yourself.
3181 """
3182 symlink_dirs = self.shareable_dirs[:]
3183 to_symlink = symlink_dirs
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003184
Gavin Makea2e3302023-03-11 06:46:20 +00003185 to_copy = []
3186 if copy_all:
3187 to_copy = platform_utils.listdir(gitdir)
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003188
Gavin Makea2e3302023-03-11 06:46:20 +00003189 dotgit = platform_utils.realpath(dotgit)
3190 for name in set(to_copy).union(to_symlink):
3191 try:
3192 src = platform_utils.realpath(os.path.join(gitdir, name))
3193 dst = os.path.join(dotgit, name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003194
Gavin Makea2e3302023-03-11 06:46:20 +00003195 if os.path.lexists(dst):
3196 continue
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003197
Gavin Makea2e3302023-03-11 06:46:20 +00003198 # If the source dir doesn't exist, create an empty dir.
3199 if name in symlink_dirs and not os.path.lexists(src):
3200 os.makedirs(src)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003201
Gavin Makea2e3302023-03-11 06:46:20 +00003202 if name in to_symlink:
3203 platform_utils.symlink(
3204 os.path.relpath(src, os.path.dirname(dst)), dst
3205 )
3206 elif copy_all and not platform_utils.islink(dst):
3207 if platform_utils.isdir(src):
3208 shutil.copytree(src, dst)
3209 elif os.path.isfile(src):
3210 shutil.copy(src, dst)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003211
Gavin Makea2e3302023-03-11 06:46:20 +00003212 except OSError as e:
3213 if e.errno == errno.EPERM:
3214 raise DownloadError(self._get_symlink_error_message())
3215 else:
3216 raise
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003217
Gavin Makea2e3302023-03-11 06:46:20 +00003218 def _InitGitWorktree(self):
3219 """Init the project using git worktrees."""
3220 self.bare_git.worktree("prune")
3221 self.bare_git.worktree(
3222 "add",
3223 "-ff",
3224 "--checkout",
3225 "--detach",
3226 "--lock",
3227 self.worktree,
3228 self.GetRevisionId(),
3229 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003230
Gavin Makea2e3302023-03-11 06:46:20 +00003231 # Rewrite the internal state files to use relative paths between the
3232 # checkouts & worktrees.
3233 dotgit = os.path.join(self.worktree, ".git")
3234 with open(dotgit, "r") as fp:
3235 # Figure out the checkout->worktree path.
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003236 setting = fp.read()
Gavin Makea2e3302023-03-11 06:46:20 +00003237 assert setting.startswith("gitdir:")
3238 git_worktree_path = setting.split(":", 1)[1].strip()
3239 # Some platforms (e.g. Windows) won't let us update dotgit in situ
3240 # because of file permissions. Delete it and recreate it from scratch
3241 # to avoid.
3242 platform_utils.remove(dotgit)
3243 # Use relative path from checkout->worktree & maintain Unix line endings
3244 # on all OS's to match git behavior.
3245 with open(dotgit, "w", newline="\n") as fp:
3246 print(
3247 "gitdir:",
3248 os.path.relpath(git_worktree_path, self.worktree),
3249 file=fp,
3250 )
3251 # Use relative path from worktree->checkout & maintain Unix line endings
3252 # on all OS's to match git behavior.
3253 with open(
3254 os.path.join(git_worktree_path, "gitdir"), "w", newline="\n"
3255 ) as fp:
3256 print(os.path.relpath(dotgit, git_worktree_path), file=fp)
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003257
Gavin Makea2e3302023-03-11 06:46:20 +00003258 self._InitMRef()
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003259
Gavin Makea2e3302023-03-11 06:46:20 +00003260 def _InitWorkTree(self, force_sync=False, submodules=False):
3261 """Setup the worktree .git path.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003262
Gavin Makea2e3302023-03-11 06:46:20 +00003263 This is the user-visible path like src/foo/.git/.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003264
Gavin Makea2e3302023-03-11 06:46:20 +00003265 With non-git-worktrees, this will be a symlink to the .repo/projects/
3266 path. With git-worktrees, this will be a .git file using "gitdir: ..."
3267 syntax.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003268
Gavin Makea2e3302023-03-11 06:46:20 +00003269 Older checkouts had .git/ directories. If we see that, migrate it.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003270
Gavin Makea2e3302023-03-11 06:46:20 +00003271 This also handles changes in the manifest. Maybe this project was
3272 backed by "foo/bar" on the server, but now it's "new/foo/bar". We have
3273 to update the path we point to under .repo/projects/ to match.
3274 """
3275 dotgit = os.path.join(self.worktree, ".git")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003276
Gavin Makea2e3302023-03-11 06:46:20 +00003277 # If using an old layout style (a directory), migrate it.
3278 if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
Jason Chang32b59562023-07-14 16:45:35 -07003279 self._MigrateOldWorkTreeGitDir(dotgit, project=self.name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003280
Gavin Makea2e3302023-03-11 06:46:20 +00003281 init_dotgit = not os.path.exists(dotgit)
3282 if self.use_git_worktrees:
3283 if init_dotgit:
3284 self._InitGitWorktree()
3285 self._CopyAndLinkFiles()
3286 else:
3287 if not init_dotgit:
3288 # See if the project has changed.
3289 if platform_utils.realpath(
3290 self.gitdir
3291 ) != platform_utils.realpath(dotgit):
3292 platform_utils.remove(dotgit)
Doug Anderson37282b42011-03-04 11:54:18 -08003293
Gavin Makea2e3302023-03-11 06:46:20 +00003294 if init_dotgit or not os.path.exists(dotgit):
3295 os.makedirs(self.worktree, exist_ok=True)
3296 platform_utils.symlink(
3297 os.path.relpath(self.gitdir, self.worktree), dotgit
3298 )
Doug Anderson37282b42011-03-04 11:54:18 -08003299
Gavin Makea2e3302023-03-11 06:46:20 +00003300 if init_dotgit:
3301 _lwrite(
3302 os.path.join(dotgit, HEAD), "%s\n" % self.GetRevisionId()
3303 )
Doug Anderson37282b42011-03-04 11:54:18 -08003304
Gavin Makea2e3302023-03-11 06:46:20 +00003305 # Finish checking out the worktree.
3306 cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
3307 if GitCommand(self, cmd).Wait() != 0:
3308 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07003309 "Cannot initialize work tree for " + self.name,
3310 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003311 )
Doug Anderson37282b42011-03-04 11:54:18 -08003312
Gavin Makea2e3302023-03-11 06:46:20 +00003313 if submodules:
3314 self._SyncSubmodules(quiet=True)
3315 self._CopyAndLinkFiles()
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003316
Gavin Makea2e3302023-03-11 06:46:20 +00003317 @classmethod
Jason Chang32b59562023-07-14 16:45:35 -07003318 def _MigrateOldWorkTreeGitDir(cls, dotgit, project=None):
Gavin Makea2e3302023-03-11 06:46:20 +00003319 """Migrate the old worktree .git/ dir style to a symlink.
3320
3321 This logic specifically only uses state from |dotgit| to figure out
3322 where to move content and not |self|. This way if the backing project
3323 also changed places, we only do the .git/ dir to .git symlink migration
3324 here. The path updates will happen independently.
3325 """
3326 # Figure out where in .repo/projects/ it's pointing to.
3327 if not os.path.islink(os.path.join(dotgit, "refs")):
Jason Chang32b59562023-07-14 16:45:35 -07003328 raise GitError(
3329 f"{dotgit}: unsupported checkout state", project=project
3330 )
Gavin Makea2e3302023-03-11 06:46:20 +00003331 gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, "refs")))
3332
3333 # Remove known symlink paths that exist in .repo/projects/.
3334 KNOWN_LINKS = {
3335 "config",
3336 "description",
3337 "hooks",
3338 "info",
3339 "logs",
3340 "objects",
3341 "packed-refs",
3342 "refs",
3343 "rr-cache",
3344 "shallow",
3345 "svn",
3346 }
3347 # Paths that we know will be in both, but are safe to clobber in
3348 # .repo/projects/.
3349 SAFE_TO_CLOBBER = {
3350 "COMMIT_EDITMSG",
3351 "FETCH_HEAD",
3352 "HEAD",
3353 "gc.log",
3354 "gitk.cache",
3355 "index",
3356 "ORIG_HEAD",
3357 }
3358
3359 # First see if we'd succeed before starting the migration.
3360 unknown_paths = []
3361 for name in platform_utils.listdir(dotgit):
3362 # Ignore all temporary/backup names. These are common with vim &
3363 # emacs.
3364 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3365 continue
3366
3367 dotgit_path = os.path.join(dotgit, name)
3368 if name in KNOWN_LINKS:
3369 if not platform_utils.islink(dotgit_path):
3370 unknown_paths.append(f"{dotgit_path}: should be a symlink")
3371 else:
3372 gitdir_path = os.path.join(gitdir, name)
3373 if name not in SAFE_TO_CLOBBER and os.path.exists(gitdir_path):
3374 unknown_paths.append(
3375 f"{dotgit_path}: unknown file; please file a bug"
3376 )
3377 if unknown_paths:
Jason Chang32b59562023-07-14 16:45:35 -07003378 raise GitError(
3379 "Aborting migration: " + "\n".join(unknown_paths),
3380 project=project,
3381 )
Gavin Makea2e3302023-03-11 06:46:20 +00003382
3383 # Now walk the paths and sync the .git/ to .repo/projects/.
3384 for name in platform_utils.listdir(dotgit):
3385 dotgit_path = os.path.join(dotgit, name)
3386
3387 # Ignore all temporary/backup names. These are common with vim &
3388 # emacs.
3389 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3390 platform_utils.remove(dotgit_path)
3391 elif name in KNOWN_LINKS:
3392 platform_utils.remove(dotgit_path)
3393 else:
3394 gitdir_path = os.path.join(gitdir, name)
3395 platform_utils.remove(gitdir_path, missing_ok=True)
3396 platform_utils.rename(dotgit_path, gitdir_path)
3397
3398 # Now that the dir should be empty, clear it out, and symlink it over.
3399 platform_utils.rmdir(dotgit)
3400 platform_utils.symlink(
3401 os.path.relpath(gitdir, os.path.dirname(dotgit)), dotgit
3402 )
3403
3404 def _get_symlink_error_message(self):
3405 if platform_utils.isWindows():
3406 return (
3407 "Unable to create symbolic link. Please re-run the command as "
3408 "Administrator, or see "
3409 "https://github.com/git-for-windows/git/wiki/Symbolic-Links "
3410 "for other options."
3411 )
3412 return "filesystem must support symlinks"
3413
3414 def _revlist(self, *args, **kw):
3415 a = []
3416 a.extend(args)
3417 a.append("--")
3418 return self.work_git.rev_list(*a, **kw)
3419
3420 @property
3421 def _allrefs(self):
3422 return self.bare_ref.all
3423
3424 def _getLogs(
3425 self, rev1, rev2, oneline=False, color=True, pretty_format=None
3426 ):
3427 """Get logs between two revisions of this project."""
3428 comp = ".."
3429 if rev1:
3430 revs = [rev1]
3431 if rev2:
3432 revs.extend([comp, rev2])
3433 cmd = ["log", "".join(revs)]
3434 out = DiffColoring(self.config)
3435 if out.is_on and color:
3436 cmd.append("--color")
3437 if pretty_format is not None:
3438 cmd.append("--pretty=format:%s" % pretty_format)
3439 if oneline:
3440 cmd.append("--oneline")
3441
3442 try:
3443 log = GitCommand(
3444 self, cmd, capture_stdout=True, capture_stderr=True
3445 )
3446 if log.Wait() == 0:
3447 return log.stdout
3448 except GitError:
3449 # worktree may not exist if groups changed for example. In that
3450 # case, try in gitdir instead.
3451 if not os.path.exists(self.worktree):
3452 return self.bare_git.log(*cmd[1:])
3453 else:
3454 raise
3455 return None
3456
3457 def getAddedAndRemovedLogs(
3458 self, toProject, oneline=False, color=True, pretty_format=None
3459 ):
3460 """Get the list of logs from this revision to given revisionId"""
3461 logs = {}
3462 selfId = self.GetRevisionId(self._allrefs)
3463 toId = toProject.GetRevisionId(toProject._allrefs)
3464
3465 logs["added"] = self._getLogs(
3466 selfId,
3467 toId,
3468 oneline=oneline,
3469 color=color,
3470 pretty_format=pretty_format,
3471 )
3472 logs["removed"] = self._getLogs(
3473 toId,
3474 selfId,
3475 oneline=oneline,
3476 color=color,
3477 pretty_format=pretty_format,
3478 )
3479 return logs
3480
3481 class _GitGetByExec(object):
3482 def __init__(self, project, bare, gitdir):
3483 self._project = project
3484 self._bare = bare
3485 self._gitdir = gitdir
3486
3487 # __getstate__ and __setstate__ are required for pickling because
3488 # __getattr__ exists.
3489 def __getstate__(self):
3490 return (self._project, self._bare, self._gitdir)
3491
3492 def __setstate__(self, state):
3493 self._project, self._bare, self._gitdir = state
3494
3495 def LsOthers(self):
3496 p = GitCommand(
3497 self._project,
3498 ["ls-files", "-z", "--others", "--exclude-standard"],
3499 bare=False,
3500 gitdir=self._gitdir,
3501 capture_stdout=True,
3502 capture_stderr=True,
3503 )
3504 if p.Wait() == 0:
3505 out = p.stdout
3506 if out:
3507 # Backslash is not anomalous.
3508 return out[:-1].split("\0")
3509 return []
3510
3511 def DiffZ(self, name, *args):
3512 cmd = [name]
3513 cmd.append("-z")
3514 cmd.append("--ignore-submodules")
3515 cmd.extend(args)
3516 p = GitCommand(
3517 self._project,
3518 cmd,
3519 gitdir=self._gitdir,
3520 bare=False,
3521 capture_stdout=True,
3522 capture_stderr=True,
3523 )
3524 p.Wait()
3525 r = {}
3526 out = p.stdout
3527 if out:
3528 out = iter(out[:-1].split("\0"))
3529 while out:
3530 try:
3531 info = next(out)
3532 path = next(out)
3533 except StopIteration:
3534 break
3535
3536 class _Info(object):
3537 def __init__(self, path, omode, nmode, oid, nid, state):
3538 self.path = path
3539 self.src_path = None
3540 self.old_mode = omode
3541 self.new_mode = nmode
3542 self.old_id = oid
3543 self.new_id = nid
3544
3545 if len(state) == 1:
3546 self.status = state
3547 self.level = None
3548 else:
3549 self.status = state[:1]
3550 self.level = state[1:]
3551 while self.level.startswith("0"):
3552 self.level = self.level[1:]
3553
3554 info = info[1:].split(" ")
3555 info = _Info(path, *info)
3556 if info.status in ("R", "C"):
3557 info.src_path = info.path
3558 info.path = next(out)
3559 r[info.path] = info
3560 return r
3561
3562 def GetDotgitPath(self, subpath=None):
3563 """Return the full path to the .git dir.
3564
3565 As a convenience, append |subpath| if provided.
3566 """
3567 if self._bare:
3568 dotgit = self._gitdir
3569 else:
3570 dotgit = os.path.join(self._project.worktree, ".git")
3571 if os.path.isfile(dotgit):
3572 # Git worktrees use a "gitdir:" syntax to point to the
3573 # scratch space.
3574 with open(dotgit) as fp:
3575 setting = fp.read()
3576 assert setting.startswith("gitdir:")
3577 gitdir = setting.split(":", 1)[1].strip()
3578 dotgit = os.path.normpath(
3579 os.path.join(self._project.worktree, gitdir)
3580 )
3581
3582 return dotgit if subpath is None else os.path.join(dotgit, subpath)
3583
3584 def GetHead(self):
3585 """Return the ref that HEAD points to."""
3586 path = self.GetDotgitPath(subpath=HEAD)
3587 try:
3588 with open(path) as fd:
3589 line = fd.readline()
3590 except IOError as e:
3591 raise NoManifestException(path, str(e))
3592 try:
3593 line = line.decode()
3594 except AttributeError:
3595 pass
3596 if line.startswith("ref: "):
3597 return line[5:-1]
3598 return line[:-1]
3599
3600 def SetHead(self, ref, message=None):
3601 cmdv = []
3602 if message is not None:
3603 cmdv.extend(["-m", message])
3604 cmdv.append(HEAD)
3605 cmdv.append(ref)
3606 self.symbolic_ref(*cmdv)
3607
3608 def DetachHead(self, new, message=None):
3609 cmdv = ["--no-deref"]
3610 if message is not None:
3611 cmdv.extend(["-m", message])
3612 cmdv.append(HEAD)
3613 cmdv.append(new)
3614 self.update_ref(*cmdv)
3615
3616 def UpdateRef(self, name, new, old=None, message=None, detach=False):
3617 cmdv = []
3618 if message is not None:
3619 cmdv.extend(["-m", message])
3620 if detach:
3621 cmdv.append("--no-deref")
3622 cmdv.append(name)
3623 cmdv.append(new)
3624 if old is not None:
3625 cmdv.append(old)
3626 self.update_ref(*cmdv)
3627
3628 def DeleteRef(self, name, old=None):
3629 if not old:
3630 old = self.rev_parse(name)
3631 self.update_ref("-d", name, old)
3632 self._project.bare_ref.deleted(name)
3633
3634 def rev_list(self, *args, **kw):
3635 if "format" in kw:
3636 cmdv = ["log", "--pretty=format:%s" % kw["format"]]
3637 else:
3638 cmdv = ["rev-list"]
3639 cmdv.extend(args)
3640 p = GitCommand(
3641 self._project,
3642 cmdv,
3643 bare=self._bare,
3644 gitdir=self._gitdir,
3645 capture_stdout=True,
3646 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003647 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00003648 )
Jason Chang32b59562023-07-14 16:45:35 -07003649 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003650 return p.stdout.splitlines()
3651
3652 def __getattr__(self, name):
3653 """Allow arbitrary git commands using pythonic syntax.
3654
3655 This allows you to do things like:
3656 git_obj.rev_parse('HEAD')
3657
3658 Since we don't have a 'rev_parse' method defined, the __getattr__
3659 will run. We'll replace the '_' with a '-' and try to run a git
3660 command. Any other positional arguments will be passed to the git
3661 command, and the following keyword arguments are supported:
3662 config: An optional dict of git config options to be passed with
3663 '-c'.
3664
3665 Args:
3666 name: The name of the git command to call. Any '_' characters
3667 will be replaced with '-'.
3668
3669 Returns:
3670 A callable object that will try to call git with the named
3671 command.
3672 """
3673 name = name.replace("_", "-")
3674
3675 def runner(*args, **kwargs):
3676 cmdv = []
3677 config = kwargs.pop("config", None)
3678 for k in kwargs:
3679 raise TypeError(
3680 "%s() got an unexpected keyword argument %r" % (name, k)
3681 )
3682 if config is not None:
3683 for k, v in config.items():
3684 cmdv.append("-c")
3685 cmdv.append("%s=%s" % (k, v))
3686 cmdv.append(name)
3687 cmdv.extend(args)
3688 p = GitCommand(
3689 self._project,
3690 cmdv,
3691 bare=self._bare,
3692 gitdir=self._gitdir,
3693 capture_stdout=True,
3694 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003695 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00003696 )
Jason Chang32b59562023-07-14 16:45:35 -07003697 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003698 r = p.stdout
3699 if r.endswith("\n") and r.index("\n") == len(r) - 1:
3700 return r[:-1]
3701 return r
3702
3703 return runner
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003704
3705
Jason Chang32b59562023-07-14 16:45:35 -07003706class LocalSyncFail(RepoError):
3707 """Default error when there is an Sync_LocalHalf error."""
3708
3709
3710class _PriorSyncFailedError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003711 def __str__(self):
3712 return "prior sync failed; rebase still in progress"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003713
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003714
Jason Chang32b59562023-07-14 16:45:35 -07003715class _DirtyError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003716 def __str__(self):
3717 return "contains uncommitted changes"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003718
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003719
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003720class _InfoMessage(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003721 def __init__(self, project, text):
3722 self.project = project
3723 self.text = text
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003724
Gavin Makea2e3302023-03-11 06:46:20 +00003725 def Print(self, syncbuf):
3726 syncbuf.out.info(
3727 "%s/: %s", self.project.RelPath(local=False), self.text
3728 )
3729 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003730
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003731
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003732class _Failure(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003733 def __init__(self, project, why):
3734 self.project = project
3735 self.why = why
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003736
Gavin Makea2e3302023-03-11 06:46:20 +00003737 def Print(self, syncbuf):
3738 syncbuf.out.fail(
3739 "error: %s/: %s", self.project.RelPath(local=False), str(self.why)
3740 )
3741 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003742
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003743
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003744class _Later(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003745 def __init__(self, project, action):
3746 self.project = project
3747 self.action = action
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003748
Gavin Makea2e3302023-03-11 06:46:20 +00003749 def Run(self, syncbuf):
3750 out = syncbuf.out
3751 out.project("project %s/", self.project.RelPath(local=False))
3752 out.nl()
3753 try:
3754 self.action()
3755 out.nl()
3756 return True
3757 except GitError:
3758 out.nl()
3759 return False
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003760
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003761
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003762class _SyncColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +00003763 def __init__(self, config):
3764 super().__init__(config, "reposync")
3765 self.project = self.printer("header", attr="bold")
3766 self.info = self.printer("info")
3767 self.fail = self.printer("fail", fg="red")
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003768
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003769
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003770class SyncBuffer(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003771 def __init__(self, config, detach_head=False):
3772 self._messages = []
3773 self._failures = []
3774 self._later_queue1 = []
3775 self._later_queue2 = []
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003776
Gavin Makea2e3302023-03-11 06:46:20 +00003777 self.out = _SyncColoring(config)
3778 self.out.redirect(sys.stderr)
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003779
Gavin Makea2e3302023-03-11 06:46:20 +00003780 self.detach_head = detach_head
3781 self.clean = True
3782 self.recent_clean = True
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003783
Gavin Makea2e3302023-03-11 06:46:20 +00003784 def info(self, project, fmt, *args):
3785 self._messages.append(_InfoMessage(project, fmt % args))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003786
Gavin Makea2e3302023-03-11 06:46:20 +00003787 def fail(self, project, err=None):
3788 self._failures.append(_Failure(project, err))
David Rileye0684ad2017-04-05 00:02:59 -07003789 self._MarkUnclean()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003790
Gavin Makea2e3302023-03-11 06:46:20 +00003791 def later1(self, project, what):
3792 self._later_queue1.append(_Later(project, what))
Mike Frysinger70d861f2019-08-26 15:22:36 -04003793
Gavin Makea2e3302023-03-11 06:46:20 +00003794 def later2(self, project, what):
3795 self._later_queue2.append(_Later(project, what))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003796
Gavin Makea2e3302023-03-11 06:46:20 +00003797 def Finish(self):
3798 self._PrintMessages()
3799 self._RunLater()
3800 self._PrintMessages()
3801 return self.clean
3802
3803 def Recently(self):
3804 recent_clean = self.recent_clean
3805 self.recent_clean = True
3806 return recent_clean
3807
3808 def _MarkUnclean(self):
3809 self.clean = False
3810 self.recent_clean = False
3811
3812 def _RunLater(self):
3813 for q in ["_later_queue1", "_later_queue2"]:
3814 if not self._RunQueue(q):
3815 return
3816
3817 def _RunQueue(self, queue):
3818 for m in getattr(self, queue):
3819 if not m.Run(self):
3820 self._MarkUnclean()
3821 return False
3822 setattr(self, queue, [])
3823 return True
3824
3825 def _PrintMessages(self):
3826 if self._messages or self._failures:
3827 if os.isatty(2):
3828 self.out.write(progress.CSI_ERASE_LINE)
3829 self.out.write("\r")
3830
3831 for m in self._messages:
3832 m.Print(self)
3833 for m in self._failures:
3834 m.Print(self)
3835
3836 self._messages = []
3837 self._failures = []
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003838
3839
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003840class MetaProject(Project):
Gavin Makea2e3302023-03-11 06:46:20 +00003841 """A special project housed under .repo."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003842
Gavin Makea2e3302023-03-11 06:46:20 +00003843 def __init__(self, manifest, name, gitdir, worktree):
3844 Project.__init__(
3845 self,
3846 manifest=manifest,
3847 name=name,
3848 gitdir=gitdir,
3849 objdir=gitdir,
3850 worktree=worktree,
3851 remote=RemoteSpec("origin"),
3852 relpath=".repo/%s" % name,
3853 revisionExpr="refs/heads/master",
3854 revisionId=None,
3855 groups=None,
3856 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003857
Gavin Makea2e3302023-03-11 06:46:20 +00003858 def PreSync(self):
3859 if self.Exists:
3860 cb = self.CurrentBranch
3861 if cb:
3862 base = self.GetBranch(cb).merge
3863 if base:
3864 self.revisionExpr = base
3865 self.revisionId = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003866
Gavin Makea2e3302023-03-11 06:46:20 +00003867 @property
3868 def HasChanges(self):
3869 """Has the remote received new commits not yet checked out?"""
3870 if not self.remote or not self.revisionExpr:
3871 return False
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003872
Gavin Makea2e3302023-03-11 06:46:20 +00003873 all_refs = self.bare_ref.all
3874 revid = self.GetRevisionId(all_refs)
3875 head = self.work_git.GetHead()
3876 if head.startswith(R_HEADS):
3877 try:
3878 head = all_refs[head]
3879 except KeyError:
3880 head = None
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003881
Gavin Makea2e3302023-03-11 06:46:20 +00003882 if revid == head:
3883 return False
3884 elif self._revlist(not_rev(HEAD), revid):
3885 return True
3886 return False
LaMont Jones9b72cf22022-03-29 21:54:22 +00003887
3888
3889class RepoProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003890 """The MetaProject for repo itself."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003891
Gavin Makea2e3302023-03-11 06:46:20 +00003892 @property
3893 def LastFetch(self):
3894 try:
3895 fh = os.path.join(self.gitdir, "FETCH_HEAD")
3896 return os.path.getmtime(fh)
3897 except OSError:
3898 return 0
LaMont Jones9b72cf22022-03-29 21:54:22 +00003899
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +01003900
LaMont Jones9b72cf22022-03-29 21:54:22 +00003901class ManifestProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003902 """The MetaProject for manifests."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003903
Gavin Makea2e3302023-03-11 06:46:20 +00003904 def MetaBranchSwitch(self, submodules=False):
3905 """Prepare for manifest branch switch."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003906
Gavin Makea2e3302023-03-11 06:46:20 +00003907 # detach and delete manifest branch, allowing a new
3908 # branch to take over
3909 syncbuf = SyncBuffer(self.config, detach_head=True)
3910 self.Sync_LocalHalf(syncbuf, submodules=submodules)
3911 syncbuf.Finish()
LaMont Jones9b72cf22022-03-29 21:54:22 +00003912
Gavin Makea2e3302023-03-11 06:46:20 +00003913 return (
3914 GitCommand(
3915 self,
3916 ["update-ref", "-d", "refs/heads/default"],
3917 capture_stdout=True,
3918 capture_stderr=True,
3919 ).Wait()
3920 == 0
3921 )
LaMont Jones9b03f152022-03-29 23:01:18 +00003922
Gavin Makea2e3302023-03-11 06:46:20 +00003923 @property
3924 def standalone_manifest_url(self):
3925 """The URL of the standalone manifest, or None."""
3926 return self.config.GetString("manifest.standalone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003927
Gavin Makea2e3302023-03-11 06:46:20 +00003928 @property
3929 def manifest_groups(self):
3930 """The manifest groups string."""
3931 return self.config.GetString("manifest.groups")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003932
Gavin Makea2e3302023-03-11 06:46:20 +00003933 @property
3934 def reference(self):
3935 """The --reference for this manifest."""
3936 return self.config.GetString("repo.reference")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003937
Gavin Makea2e3302023-03-11 06:46:20 +00003938 @property
3939 def dissociate(self):
3940 """Whether to dissociate."""
3941 return self.config.GetBoolean("repo.dissociate")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003942
Gavin Makea2e3302023-03-11 06:46:20 +00003943 @property
3944 def archive(self):
3945 """Whether we use archive."""
3946 return self.config.GetBoolean("repo.archive")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003947
Gavin Makea2e3302023-03-11 06:46:20 +00003948 @property
3949 def mirror(self):
3950 """Whether we use mirror."""
3951 return self.config.GetBoolean("repo.mirror")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003952
Gavin Makea2e3302023-03-11 06:46:20 +00003953 @property
3954 def use_worktree(self):
3955 """Whether we use worktree."""
3956 return self.config.GetBoolean("repo.worktree")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003957
Gavin Makea2e3302023-03-11 06:46:20 +00003958 @property
3959 def clone_bundle(self):
3960 """Whether we use clone_bundle."""
3961 return self.config.GetBoolean("repo.clonebundle")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003962
Gavin Makea2e3302023-03-11 06:46:20 +00003963 @property
3964 def submodules(self):
3965 """Whether we use submodules."""
3966 return self.config.GetBoolean("repo.submodules")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003967
Gavin Makea2e3302023-03-11 06:46:20 +00003968 @property
3969 def git_lfs(self):
3970 """Whether we use git_lfs."""
3971 return self.config.GetBoolean("repo.git-lfs")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003972
Gavin Makea2e3302023-03-11 06:46:20 +00003973 @property
3974 def use_superproject(self):
3975 """Whether we use superproject."""
3976 return self.config.GetBoolean("repo.superproject")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003977
Gavin Makea2e3302023-03-11 06:46:20 +00003978 @property
3979 def partial_clone(self):
3980 """Whether this is a partial clone."""
3981 return self.config.GetBoolean("repo.partialclone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003982
Gavin Makea2e3302023-03-11 06:46:20 +00003983 @property
3984 def depth(self):
3985 """Partial clone depth."""
3986 return self.config.GetString("repo.depth")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003987
Gavin Makea2e3302023-03-11 06:46:20 +00003988 @property
3989 def clone_filter(self):
3990 """The clone filter."""
3991 return self.config.GetString("repo.clonefilter")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003992
Gavin Makea2e3302023-03-11 06:46:20 +00003993 @property
3994 def partial_clone_exclude(self):
3995 """Partial clone exclude string"""
3996 return self.config.GetString("repo.partialcloneexclude")
LaMont Jones4ada0432022-04-14 15:10:43 +00003997
Gavin Makea2e3302023-03-11 06:46:20 +00003998 @property
Jason Chang17833322023-05-23 13:06:55 -07003999 def clone_filter_for_depth(self):
4000 """Replace shallow clone with partial clone."""
4001 return self.config.GetString("repo.clonefilterfordepth")
4002
4003 @property
Gavin Makea2e3302023-03-11 06:46:20 +00004004 def manifest_platform(self):
4005 """The --platform argument from `repo init`."""
4006 return self.config.GetString("manifest.platform")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004007
Gavin Makea2e3302023-03-11 06:46:20 +00004008 @property
4009 def _platform_name(self):
4010 """Return the name of the platform."""
4011 return platform.system().lower()
LaMont Jones9b03f152022-03-29 23:01:18 +00004012
Gavin Makea2e3302023-03-11 06:46:20 +00004013 def SyncWithPossibleInit(
4014 self,
4015 submanifest,
4016 verbose=False,
4017 current_branch_only=False,
4018 tags="",
4019 git_event_log=None,
4020 ):
4021 """Sync a manifestProject, possibly for the first time.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004022
Gavin Makea2e3302023-03-11 06:46:20 +00004023 Call Sync() with arguments from the most recent `repo init`. If this is
4024 a new sub manifest, then inherit options from the parent's
4025 manifestProject.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004026
Gavin Makea2e3302023-03-11 06:46:20 +00004027 This is used by subcmds.Sync() to do an initial download of new sub
4028 manifests.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004029
Gavin Makea2e3302023-03-11 06:46:20 +00004030 Args:
4031 submanifest: an XmlSubmanifest, the submanifest to re-sync.
4032 verbose: a boolean, whether to show all output, rather than only
4033 errors.
4034 current_branch_only: a boolean, whether to only fetch the current
4035 manifest branch from the server.
4036 tags: a boolean, whether to fetch tags.
4037 git_event_log: an EventLog, for git tracing.
4038 """
4039 # TODO(lamontjones): when refactoring sync (and init?) consider how to
4040 # better get the init options that we should use for new submanifests
4041 # that are added when syncing an existing workspace.
4042 git_event_log = git_event_log or EventLog()
LaMont Jonesb90a4222022-04-14 15:00:09 +00004043 spec = submanifest.ToSubmanifestSpec()
Gavin Makea2e3302023-03-11 06:46:20 +00004044 # Use the init options from the existing manifestProject, or the parent
4045 # if it doesn't exist.
4046 #
4047 # Today, we only support changing manifest_groups on the sub-manifest,
4048 # with no supported-for-the-user way to change the other arguments from
4049 # those specified by the outermost manifest.
4050 #
4051 # TODO(lamontjones): determine which of these should come from the
4052 # outermost manifest and which should come from the parent manifest.
4053 mp = self if self.Exists else submanifest.parent.manifestProject
4054 return self.Sync(
LaMont Jones55ee3042022-04-06 17:10:21 +00004055 manifest_url=spec.manifestUrl,
4056 manifest_branch=spec.revision,
Gavin Makea2e3302023-03-11 06:46:20 +00004057 standalone_manifest=mp.standalone_manifest_url,
4058 groups=mp.manifest_groups,
4059 platform=mp.manifest_platform,
4060 mirror=mp.mirror,
4061 dissociate=mp.dissociate,
4062 reference=mp.reference,
4063 worktree=mp.use_worktree,
4064 submodules=mp.submodules,
4065 archive=mp.archive,
4066 partial_clone=mp.partial_clone,
4067 clone_filter=mp.clone_filter,
4068 partial_clone_exclude=mp.partial_clone_exclude,
4069 clone_bundle=mp.clone_bundle,
4070 git_lfs=mp.git_lfs,
4071 use_superproject=mp.use_superproject,
LaMont Jones55ee3042022-04-06 17:10:21 +00004072 verbose=verbose,
4073 current_branch_only=current_branch_only,
4074 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00004075 depth=mp.depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004076 git_event_log=git_event_log,
4077 manifest_name=spec.manifestName,
Gavin Makea2e3302023-03-11 06:46:20 +00004078 this_manifest_only=True,
LaMont Jones55ee3042022-04-06 17:10:21 +00004079 outer_manifest=False,
Jason Chang17833322023-05-23 13:06:55 -07004080 clone_filter_for_depth=mp.clone_filter_for_depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004081 )
LaMont Jones409407a2022-04-05 21:21:56 +00004082
Gavin Makea2e3302023-03-11 06:46:20 +00004083 def Sync(
4084 self,
4085 _kwargs_only=(),
4086 manifest_url="",
4087 manifest_branch=None,
4088 standalone_manifest=False,
4089 groups="",
4090 mirror=False,
4091 reference="",
4092 dissociate=False,
4093 worktree=False,
4094 submodules=False,
4095 archive=False,
4096 partial_clone=None,
4097 depth=None,
4098 clone_filter="blob:none",
4099 partial_clone_exclude=None,
4100 clone_bundle=None,
4101 git_lfs=None,
4102 use_superproject=None,
4103 verbose=False,
4104 current_branch_only=False,
4105 git_event_log=None,
4106 platform="",
4107 manifest_name="default.xml",
4108 tags="",
4109 this_manifest_only=False,
4110 outer_manifest=True,
Jason Chang17833322023-05-23 13:06:55 -07004111 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00004112 ):
4113 """Sync the manifest and all submanifests.
LaMont Jones409407a2022-04-05 21:21:56 +00004114
Gavin Makea2e3302023-03-11 06:46:20 +00004115 Args:
4116 manifest_url: a string, the URL of the manifest project.
4117 manifest_branch: a string, the manifest branch to use.
4118 standalone_manifest: a boolean, whether to store the manifest as a
4119 static file.
4120 groups: a string, restricts the checkout to projects with the
4121 specified groups.
4122 mirror: a boolean, whether to create a mirror of the remote
4123 repository.
4124 reference: a string, location of a repo instance to use as a
4125 reference.
4126 dissociate: a boolean, whether to dissociate from reference mirrors
4127 after clone.
4128 worktree: a boolean, whether to use git-worktree to manage projects.
4129 submodules: a boolean, whether sync submodules associated with the
4130 manifest project.
4131 archive: a boolean, whether to checkout each project as an archive.
4132 See git-archive.
4133 partial_clone: a boolean, whether to perform a partial clone.
4134 depth: an int, how deep of a shallow clone to create.
4135 clone_filter: a string, filter to use with partial_clone.
4136 partial_clone_exclude : a string, comma-delimeted list of project
4137 names to exclude from partial clone.
4138 clone_bundle: a boolean, whether to enable /clone.bundle on
4139 HTTP/HTTPS.
4140 git_lfs: a boolean, whether to enable git LFS support.
4141 use_superproject: a boolean, whether to use the manifest
4142 superproject to sync projects.
4143 verbose: a boolean, whether to show all output, rather than only
4144 errors.
4145 current_branch_only: a boolean, whether to only fetch the current
4146 manifest branch from the server.
4147 platform: a string, restrict the checkout to projects with the
4148 specified platform group.
4149 git_event_log: an EventLog, for git tracing.
4150 tags: a boolean, whether to fetch tags.
4151 manifest_name: a string, the name of the manifest file to use.
4152 this_manifest_only: a boolean, whether to only operate on the
4153 current sub manifest.
4154 outer_manifest: a boolean, whether to start at the outermost
4155 manifest.
Jason Chang17833322023-05-23 13:06:55 -07004156 clone_filter_for_depth: a string, when specified replaces shallow
4157 clones with partial.
LaMont Jones9b03f152022-03-29 23:01:18 +00004158
Gavin Makea2e3302023-03-11 06:46:20 +00004159 Returns:
4160 a boolean, whether the sync was successful.
4161 """
4162 assert _kwargs_only == (), "Sync only accepts keyword arguments."
LaMont Jones9b03f152022-03-29 23:01:18 +00004163
Gavin Makea2e3302023-03-11 06:46:20 +00004164 groups = groups or self.manifest.GetDefaultGroupsStr(
4165 with_platform=False
4166 )
4167 platform = platform or "auto"
4168 git_event_log = git_event_log or EventLog()
4169 if outer_manifest and self.manifest.is_submanifest:
4170 # In a multi-manifest checkout, use the outer manifest unless we are
4171 # told not to.
4172 return self.client.outer_manifest.manifestProject.Sync(
4173 manifest_url=manifest_url,
4174 manifest_branch=manifest_branch,
4175 standalone_manifest=standalone_manifest,
4176 groups=groups,
4177 platform=platform,
4178 mirror=mirror,
4179 dissociate=dissociate,
4180 reference=reference,
4181 worktree=worktree,
4182 submodules=submodules,
4183 archive=archive,
4184 partial_clone=partial_clone,
4185 clone_filter=clone_filter,
4186 partial_clone_exclude=partial_clone_exclude,
4187 clone_bundle=clone_bundle,
4188 git_lfs=git_lfs,
4189 use_superproject=use_superproject,
4190 verbose=verbose,
4191 current_branch_only=current_branch_only,
4192 tags=tags,
4193 depth=depth,
4194 git_event_log=git_event_log,
4195 manifest_name=manifest_name,
4196 this_manifest_only=this_manifest_only,
4197 outer_manifest=False,
4198 )
LaMont Jones9b03f152022-03-29 23:01:18 +00004199
Gavin Makea2e3302023-03-11 06:46:20 +00004200 # If repo has already been initialized, we take -u with the absence of
4201 # --standalone-manifest to mean "transition to a standard repo set up",
4202 # which necessitates starting fresh.
4203 # If --standalone-manifest is set, we always tear everything down and
4204 # start anew.
4205 if self.Exists:
4206 was_standalone_manifest = self.config.GetString(
4207 "manifest.standalone"
4208 )
4209 if was_standalone_manifest and not manifest_url:
4210 print(
4211 "fatal: repo was initialized with a standlone manifest, "
4212 "cannot be re-initialized without --manifest-url/-u"
4213 )
4214 return False
4215
4216 if standalone_manifest or (
4217 was_standalone_manifest and manifest_url
4218 ):
4219 self.config.ClearCache()
4220 if self.gitdir and os.path.exists(self.gitdir):
4221 platform_utils.rmtree(self.gitdir)
4222 if self.worktree and os.path.exists(self.worktree):
4223 platform_utils.rmtree(self.worktree)
4224
4225 is_new = not self.Exists
4226 if is_new:
4227 if not manifest_url:
4228 print("fatal: manifest url is required.", file=sys.stderr)
4229 return False
4230
4231 if verbose:
4232 print(
4233 "Downloading manifest from %s"
4234 % (GitConfig.ForUser().UrlInsteadOf(manifest_url),),
4235 file=sys.stderr,
4236 )
4237
4238 # The manifest project object doesn't keep track of the path on the
4239 # server where this git is located, so let's save that here.
4240 mirrored_manifest_git = None
4241 if reference:
4242 manifest_git_path = urllib.parse.urlparse(manifest_url).path[1:]
4243 mirrored_manifest_git = os.path.join(
4244 reference, manifest_git_path
4245 )
4246 if not mirrored_manifest_git.endswith(".git"):
4247 mirrored_manifest_git += ".git"
4248 if not os.path.exists(mirrored_manifest_git):
4249 mirrored_manifest_git = os.path.join(
4250 reference, ".repo/manifests.git"
4251 )
4252
4253 self._InitGitDir(mirror_git=mirrored_manifest_git)
4254
4255 # If standalone_manifest is set, mark the project as "standalone" --
4256 # we'll still do much of the manifests.git set up, but will avoid actual
4257 # syncs to a remote.
4258 if standalone_manifest:
4259 self.config.SetString("manifest.standalone", manifest_url)
4260 elif not manifest_url and not manifest_branch:
4261 # If -u is set and --standalone-manifest is not, then we're not in
4262 # standalone mode. Otherwise, use config to infer what we were in
4263 # the last init.
4264 standalone_manifest = bool(
4265 self.config.GetString("manifest.standalone")
4266 )
4267 if not standalone_manifest:
4268 self.config.SetString("manifest.standalone", None)
4269
4270 self._ConfigureDepth(depth)
4271
4272 # Set the remote URL before the remote branch as we might need it below.
4273 if manifest_url:
4274 r = self.GetRemote()
4275 r.url = manifest_url
4276 r.ResetFetch()
4277 r.Save()
4278
4279 if not standalone_manifest:
4280 if manifest_branch:
4281 if manifest_branch == "HEAD":
4282 manifest_branch = self.ResolveRemoteHead()
4283 if manifest_branch is None:
4284 print("fatal: unable to resolve HEAD", file=sys.stderr)
4285 return False
4286 self.revisionExpr = manifest_branch
4287 else:
4288 if is_new:
4289 default_branch = self.ResolveRemoteHead()
4290 if default_branch is None:
4291 # If the remote doesn't have HEAD configured, default to
4292 # master.
4293 default_branch = "refs/heads/master"
4294 self.revisionExpr = default_branch
4295 else:
4296 self.PreSync()
4297
4298 groups = re.split(r"[,\s]+", groups or "")
4299 all_platforms = ["linux", "darwin", "windows"]
4300 platformize = lambda x: "platform-" + x
4301 if platform == "auto":
4302 if not mirror and not self.mirror:
4303 groups.append(platformize(self._platform_name))
4304 elif platform == "all":
4305 groups.extend(map(platformize, all_platforms))
4306 elif platform in all_platforms:
4307 groups.append(platformize(platform))
4308 elif platform != "none":
4309 print("fatal: invalid platform flag", file=sys.stderr)
4310 return False
4311 self.config.SetString("manifest.platform", platform)
4312
4313 groups = [x for x in groups if x]
4314 groupstr = ",".join(groups)
4315 if (
4316 platform == "auto"
4317 and groupstr == self.manifest.GetDefaultGroupsStr()
4318 ):
4319 groupstr = None
4320 self.config.SetString("manifest.groups", groupstr)
4321
4322 if reference:
4323 self.config.SetString("repo.reference", reference)
4324
4325 if dissociate:
4326 self.config.SetBoolean("repo.dissociate", dissociate)
4327
4328 if worktree:
4329 if mirror:
4330 print(
4331 "fatal: --mirror and --worktree are incompatible",
4332 file=sys.stderr,
4333 )
4334 return False
4335 if submodules:
4336 print(
4337 "fatal: --submodules and --worktree are incompatible",
4338 file=sys.stderr,
4339 )
4340 return False
4341 self.config.SetBoolean("repo.worktree", worktree)
4342 if is_new:
4343 self.use_git_worktrees = True
4344 print("warning: --worktree is experimental!", file=sys.stderr)
4345
4346 if archive:
4347 if is_new:
4348 self.config.SetBoolean("repo.archive", archive)
4349 else:
4350 print(
4351 "fatal: --archive is only supported when initializing a "
4352 "new workspace.",
4353 file=sys.stderr,
4354 )
4355 print(
4356 "Either delete the .repo folder in this workspace, or "
4357 "initialize in another location.",
4358 file=sys.stderr,
4359 )
4360 return False
4361
4362 if mirror:
4363 if is_new:
4364 self.config.SetBoolean("repo.mirror", mirror)
4365 else:
4366 print(
4367 "fatal: --mirror is only supported when initializing a new "
4368 "workspace.",
4369 file=sys.stderr,
4370 )
4371 print(
4372 "Either delete the .repo folder in this workspace, or "
4373 "initialize in another location.",
4374 file=sys.stderr,
4375 )
4376 return False
4377
4378 if partial_clone is not None:
4379 if mirror:
4380 print(
4381 "fatal: --mirror and --partial-clone are mutually "
4382 "exclusive",
4383 file=sys.stderr,
4384 )
4385 return False
4386 self.config.SetBoolean("repo.partialclone", partial_clone)
4387 if clone_filter:
4388 self.config.SetString("repo.clonefilter", clone_filter)
4389 elif self.partial_clone:
4390 clone_filter = self.clone_filter
4391 else:
4392 clone_filter = None
4393
4394 if partial_clone_exclude is not None:
4395 self.config.SetString(
4396 "repo.partialcloneexclude", partial_clone_exclude
4397 )
4398
4399 if clone_bundle is None:
4400 clone_bundle = False if partial_clone else True
4401 else:
4402 self.config.SetBoolean("repo.clonebundle", clone_bundle)
4403
4404 if submodules:
4405 self.config.SetBoolean("repo.submodules", submodules)
4406
4407 if git_lfs is not None:
4408 if git_lfs:
4409 git_require((2, 17, 0), fail=True, msg="Git LFS support")
4410
4411 self.config.SetBoolean("repo.git-lfs", git_lfs)
4412 if not is_new:
4413 print(
4414 "warning: Changing --git-lfs settings will only affect new "
4415 "project checkouts.\n"
4416 " Existing projects will require manual updates.\n",
4417 file=sys.stderr,
4418 )
4419
Jason Chang17833322023-05-23 13:06:55 -07004420 if clone_filter_for_depth is not None:
4421 self.ConfigureCloneFilterForDepth(clone_filter_for_depth)
4422
Gavin Makea2e3302023-03-11 06:46:20 +00004423 if use_superproject is not None:
4424 self.config.SetBoolean("repo.superproject", use_superproject)
4425
4426 if not standalone_manifest:
4427 success = self.Sync_NetworkHalf(
4428 is_new=is_new,
4429 quiet=not verbose,
4430 verbose=verbose,
4431 clone_bundle=clone_bundle,
4432 current_branch_only=current_branch_only,
4433 tags=tags,
4434 submodules=submodules,
4435 clone_filter=clone_filter,
4436 partial_clone_exclude=self.manifest.PartialCloneExclude,
Jason Chang17833322023-05-23 13:06:55 -07004437 clone_filter_for_depth=self.manifest.CloneFilterForDepth,
Gavin Makea2e3302023-03-11 06:46:20 +00004438 ).success
4439 if not success:
4440 r = self.GetRemote()
4441 print(
4442 "fatal: cannot obtain manifest %s" % r.url, file=sys.stderr
4443 )
4444
4445 # Better delete the manifest git dir if we created it; otherwise
4446 # next time (when user fixes problems) we won't go through the
4447 # "is_new" logic.
4448 if is_new:
4449 platform_utils.rmtree(self.gitdir)
4450 return False
4451
4452 if manifest_branch:
4453 self.MetaBranchSwitch(submodules=submodules)
4454
4455 syncbuf = SyncBuffer(self.config)
4456 self.Sync_LocalHalf(syncbuf, submodules=submodules)
4457 syncbuf.Finish()
4458
4459 if is_new or self.CurrentBranch is None:
Jason Chang1a3612f2023-08-08 14:12:53 -07004460 try:
4461 self.StartBranch("default")
4462 except GitError as e:
4463 msg = str(e)
Gavin Makea2e3302023-03-11 06:46:20 +00004464 print(
Jason Chang1a3612f2023-08-08 14:12:53 -07004465 f"fatal: cannot create default in manifest {msg}",
Gavin Makea2e3302023-03-11 06:46:20 +00004466 file=sys.stderr,
4467 )
4468 return False
4469
4470 if not manifest_name:
4471 print("fatal: manifest name (-m) is required.", file=sys.stderr)
4472 return False
4473
4474 elif is_new:
4475 # This is a new standalone manifest.
4476 manifest_name = "default.xml"
4477 manifest_data = fetch.fetch_file(manifest_url, verbose=verbose)
4478 dest = os.path.join(self.worktree, manifest_name)
4479 os.makedirs(os.path.dirname(dest), exist_ok=True)
4480 with open(dest, "wb") as f:
4481 f.write(manifest_data)
4482
4483 try:
4484 self.manifest.Link(manifest_name)
4485 except ManifestParseError as e:
4486 print(
4487 "fatal: manifest '%s' not available" % manifest_name,
4488 file=sys.stderr,
4489 )
4490 print("fatal: %s" % str(e), file=sys.stderr)
4491 return False
4492
4493 if not this_manifest_only:
4494 for submanifest in self.manifest.submanifests.values():
4495 spec = submanifest.ToSubmanifestSpec()
4496 submanifest.repo_client.manifestProject.Sync(
4497 manifest_url=spec.manifestUrl,
4498 manifest_branch=spec.revision,
4499 standalone_manifest=standalone_manifest,
4500 groups=self.manifest_groups,
4501 platform=platform,
4502 mirror=mirror,
4503 dissociate=dissociate,
4504 reference=reference,
4505 worktree=worktree,
4506 submodules=submodules,
4507 archive=archive,
4508 partial_clone=partial_clone,
4509 clone_filter=clone_filter,
4510 partial_clone_exclude=partial_clone_exclude,
4511 clone_bundle=clone_bundle,
4512 git_lfs=git_lfs,
4513 use_superproject=use_superproject,
4514 verbose=verbose,
4515 current_branch_only=current_branch_only,
4516 tags=tags,
4517 depth=depth,
4518 git_event_log=git_event_log,
4519 manifest_name=spec.manifestName,
4520 this_manifest_only=False,
4521 outer_manifest=False,
4522 )
4523
4524 # Lastly, if the manifest has a <superproject> then have the
4525 # superproject sync it (if it will be used).
4526 if git_superproject.UseSuperproject(use_superproject, self.manifest):
4527 sync_result = self.manifest.superproject.Sync(git_event_log)
4528 if not sync_result.success:
4529 submanifest = ""
4530 if self.manifest.path_prefix:
4531 submanifest = f"for {self.manifest.path_prefix} "
4532 print(
4533 f"warning: git update of superproject {submanifest}failed, "
4534 "repo sync will not use superproject to fetch source; "
4535 "while this error is not fatal, and you can continue to "
4536 "run repo sync, please run repo init with the "
4537 "--no-use-superproject option to stop seeing this warning",
4538 file=sys.stderr,
4539 )
4540 if sync_result.fatal and use_superproject is not None:
4541 return False
4542
4543 return True
4544
Jason Chang17833322023-05-23 13:06:55 -07004545 def ConfigureCloneFilterForDepth(self, clone_filter_for_depth):
4546 """Configure clone filter to replace shallow clones.
4547
4548 Args:
4549 clone_filter_for_depth: a string or None, e.g. 'blob:none' will
4550 disable shallow clones and replace with partial clone. None will
4551 enable shallow clones.
4552 """
4553 self.config.SetString(
4554 "repo.clonefilterfordepth", clone_filter_for_depth
4555 )
4556
Gavin Makea2e3302023-03-11 06:46:20 +00004557 def _ConfigureDepth(self, depth):
4558 """Configure the depth we'll sync down.
4559
4560 Args:
4561 depth: an int, how deep of a partial clone to create.
4562 """
4563 # Opt.depth will be non-None if user actually passed --depth to repo
4564 # init.
4565 if depth is not None:
4566 if depth > 0:
4567 # Positive values will set the depth.
4568 depth = str(depth)
4569 else:
4570 # Negative numbers will clear the depth; passing None to
4571 # SetString will do that.
4572 depth = None
4573
4574 # We store the depth in the main manifest project.
4575 self.config.SetString("repo.depth", depth)