blob: c3eb09c96a79efbc4be1f591db99b7c7272d91b2 [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 )
1736 if GitCommand(self, cmd, bare=True).Wait() != 0:
1737 return None
1738 return DownloadedChange(
1739 self,
1740 self.GetRevisionId(),
1741 change_id,
1742 patch_id,
1743 self.bare_git.rev_parse("FETCH_HEAD"),
1744 )
1745
1746 def DeleteWorktree(self, quiet=False, force=False):
1747 """Delete the source checkout and any other housekeeping tasks.
1748
1749 This currently leaves behind the internal .repo/ cache state. This
1750 helps when switching branches or manifest changes get reverted as we
1751 don't have to redownload all the git objects. But we should do some GC
1752 at some point.
1753
1754 Args:
1755 quiet: Whether to hide normal messages.
1756 force: Always delete tree even if dirty.
Doug Anderson3ba5f952011-04-07 12:51:04 -07001757
1758 Returns:
Gavin Makea2e3302023-03-11 06:46:20 +00001759 True if the worktree was completely cleaned out.
1760 """
1761 if self.IsDirty():
1762 if force:
1763 print(
1764 "warning: %s: Removing dirty project: uncommitted changes "
1765 "lost." % (self.RelPath(local=False),),
1766 file=sys.stderr,
1767 )
1768 else:
Jason Chang32b59562023-07-14 16:45:35 -07001769 msg = (
1770 "error: %s: Cannot remove project: uncommitted"
1771 "changes are present.\n" % self.RelPath(local=False)
Gavin Makea2e3302023-03-11 06:46:20 +00001772 )
Jason Chang32b59562023-07-14 16:45:35 -07001773 print(msg, file=sys.stderr)
1774 raise DeleteDirtyWorktreeError(msg, project=self)
Wink Saville02d79452009-04-10 13:01:24 -07001775
Gavin Makea2e3302023-03-11 06:46:20 +00001776 if not quiet:
1777 print(
1778 "%s: Deleting obsolete checkout." % (self.RelPath(local=False),)
1779 )
Wink Saville02d79452009-04-10 13:01:24 -07001780
Gavin Makea2e3302023-03-11 06:46:20 +00001781 # Unlock and delink from the main worktree. We don't use git's worktree
1782 # remove because it will recursively delete projects -- we handle that
1783 # ourselves below. https://crbug.com/git/48
1784 if self.use_git_worktrees:
1785 needle = platform_utils.realpath(self.gitdir)
1786 # Find the git worktree commondir under .repo/worktrees/.
1787 output = self.bare_git.worktree("list", "--porcelain").splitlines()[
1788 0
1789 ]
1790 assert output.startswith("worktree "), output
1791 commondir = output[9:]
1792 # Walk each of the git worktrees to see where they point.
1793 configs = os.path.join(commondir, "worktrees")
1794 for name in os.listdir(configs):
1795 gitdir = os.path.join(configs, name, "gitdir")
1796 with open(gitdir) as fp:
1797 relpath = fp.read().strip()
1798 # Resolve the checkout path and see if it matches this project.
1799 fullpath = platform_utils.realpath(
1800 os.path.join(configs, name, relpath)
1801 )
1802 if fullpath == needle:
1803 platform_utils.rmtree(os.path.join(configs, name))
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001804
Gavin Makea2e3302023-03-11 06:46:20 +00001805 # Delete the .git directory first, so we're less likely to have a
1806 # partially working git repository around. There shouldn't be any git
1807 # projects here, so rmtree works.
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001808
Gavin Makea2e3302023-03-11 06:46:20 +00001809 # Try to remove plain files first in case of git worktrees. If this
1810 # fails for any reason, we'll fall back to rmtree, and that'll display
1811 # errors if it can't remove things either.
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001812 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001813 platform_utils.remove(self.gitdir)
1814 except OSError:
1815 pass
1816 try:
1817 platform_utils.rmtree(self.gitdir)
1818 except OSError as e:
1819 if e.errno != errno.ENOENT:
1820 print("error: %s: %s" % (self.gitdir, e), file=sys.stderr)
1821 print(
1822 "error: %s: Failed to delete obsolete checkout; remove "
1823 "manually, then run `repo sync -l`."
1824 % (self.RelPath(local=False),),
1825 file=sys.stderr,
1826 )
Jason Chang32b59562023-07-14 16:45:35 -07001827 raise DeleteWorktreeError(aggregate_errors=[e])
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001828
Gavin Makea2e3302023-03-11 06:46:20 +00001829 # Delete everything under the worktree, except for directories that
1830 # contain another git project.
1831 dirs_to_remove = []
1832 failed = False
Jason Chang32b59562023-07-14 16:45:35 -07001833 errors = []
Gavin Makea2e3302023-03-11 06:46:20 +00001834 for root, dirs, files in platform_utils.walk(self.worktree):
1835 for f in files:
1836 path = os.path.join(root, f)
1837 try:
1838 platform_utils.remove(path)
1839 except OSError as e:
1840 if e.errno != errno.ENOENT:
1841 print(
1842 "error: %s: Failed to remove: %s" % (path, e),
1843 file=sys.stderr,
1844 )
1845 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001846 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001847 dirs[:] = [
1848 d
1849 for d in dirs
1850 if not os.path.lexists(os.path.join(root, d, ".git"))
1851 ]
1852 dirs_to_remove += [
1853 os.path.join(root, d)
1854 for d in dirs
1855 if os.path.join(root, d) not in dirs_to_remove
1856 ]
1857 for d in reversed(dirs_to_remove):
1858 if platform_utils.islink(d):
1859 try:
1860 platform_utils.remove(d)
1861 except OSError as e:
1862 if e.errno != errno.ENOENT:
1863 print(
1864 "error: %s: Failed to remove: %s" % (d, e),
1865 file=sys.stderr,
1866 )
1867 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001868 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001869 elif not platform_utils.listdir(d):
1870 try:
1871 platform_utils.rmdir(d)
1872 except OSError as e:
1873 if e.errno != errno.ENOENT:
1874 print(
1875 "error: %s: Failed to remove: %s" % (d, e),
1876 file=sys.stderr,
1877 )
1878 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001879 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001880 if failed:
1881 print(
1882 "error: %s: Failed to delete obsolete checkout."
1883 % (self.RelPath(local=False),),
1884 file=sys.stderr,
1885 )
1886 print(
1887 " Remove manually, then run `repo sync -l`.",
1888 file=sys.stderr,
1889 )
Jason Chang32b59562023-07-14 16:45:35 -07001890 raise DeleteWorktreeError(aggregate_errors=errors)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07001891
Gavin Makea2e3302023-03-11 06:46:20 +00001892 # Try deleting parent dirs if they are empty.
1893 path = self.worktree
1894 while path != self.manifest.topdir:
1895 try:
1896 platform_utils.rmdir(path)
1897 except OSError as e:
1898 if e.errno != errno.ENOENT:
1899 break
1900 path = os.path.dirname(path)
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001901
Gavin Makea2e3302023-03-11 06:46:20 +00001902 return True
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001903
Gavin Makea2e3302023-03-11 06:46:20 +00001904 def StartBranch(self, name, branch_merge="", revision=None):
1905 """Create a new branch off the manifest's revision."""
1906 if not branch_merge:
1907 branch_merge = self.revisionExpr
1908 head = self.work_git.GetHead()
1909 if head == (R_HEADS + name):
1910 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001911
David Pursehouse8a68ff92012-09-24 12:15:13 +09001912 all_refs = self.bare_ref.all
Gavin Makea2e3302023-03-11 06:46:20 +00001913 if R_HEADS + name in all_refs:
1914 return GitCommand(self, ["checkout", "-q", name, "--"]).Wait() == 0
Shawn O. Pearce88443382010-10-08 10:02:09 +02001915
Gavin Makea2e3302023-03-11 06:46:20 +00001916 branch = self.GetBranch(name)
1917 branch.remote = self.GetRemote()
1918 branch.merge = branch_merge
1919 if not branch.merge.startswith("refs/") and not ID_RE.match(
1920 branch_merge
1921 ):
1922 branch.merge = R_HEADS + branch_merge
Shawn O. Pearce88443382010-10-08 10:02:09 +02001923
Gavin Makea2e3302023-03-11 06:46:20 +00001924 if revision is None:
1925 revid = self.GetRevisionId(all_refs)
Shawn O. Pearce88443382010-10-08 10:02:09 +02001926 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001927 revid = self.work_git.rev_parse(revision)
Brian Harring14a66742012-09-28 20:21:57 -07001928
Gavin Makea2e3302023-03-11 06:46:20 +00001929 if head.startswith(R_HEADS):
Kevin Degiabaa7f32014-11-12 11:27:45 -07001930 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001931 head = all_refs[head]
1932 except KeyError:
1933 head = None
1934 if revid and head and revid == head:
1935 ref = R_HEADS + name
1936 self.work_git.update_ref(ref, revid)
1937 self.work_git.symbolic_ref(HEAD, ref)
1938 branch.Save()
1939 return True
Kevin Degib1a07b82015-07-27 13:33:43 -06001940
Gavin Makea2e3302023-03-11 06:46:20 +00001941 if (
1942 GitCommand(
1943 self, ["checkout", "-q", "-b", branch.name, revid]
1944 ).Wait()
1945 == 0
1946 ):
1947 branch.Save()
1948 return True
1949 return False
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:
1958 True if the checkout succeeded; False if it didn't; None if the
1959 branch didn't exist.
1960 """
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.
1972 return None
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
Gavin Makea2e3302023-03-11 06:46:20 +00001989 return (
1990 GitCommand(
1991 self,
1992 ["checkout", name, "--"],
1993 capture_stdout=True,
1994 capture_stderr=True,
1995 ).Wait()
1996 == 0
1997 )
Mike Frysinger98bb7652021-12-20 21:15:59 -05001998
Gavin Makea2e3302023-03-11 06:46:20 +00001999 def AbandonBranch(self, name):
2000 """Destroy a local topic branch.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002001
Gavin Makea2e3302023-03-11 06:46:20 +00002002 Args:
2003 name: The name of the branch to abandon.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002004
Gavin Makea2e3302023-03-11 06:46:20 +00002005 Returns:
Jason Changf9aacd42023-08-03 14:38:00 -07002006 True if the abandon succeeded; Raises GitCommandError if it didn't;
2007 None if the branch didn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00002008 """
2009 rev = R_HEADS + name
2010 all_refs = self.bare_ref.all
2011 if rev not in all_refs:
2012 # Doesn't exist
2013 return None
2014
2015 head = self.work_git.GetHead()
2016 if head == rev:
2017 # We can't destroy the branch while we are sitting
2018 # on it. Switch to a detached HEAD.
2019 head = all_refs[head]
2020
2021 revid = self.GetRevisionId(all_refs)
2022 if head == revid:
2023 _lwrite(
2024 self.work_git.GetDotgitPath(subpath=HEAD), "%s\n" % revid
2025 )
2026 else:
2027 self._Checkout(revid, quiet=True)
Jason Changf9aacd42023-08-03 14:38:00 -07002028 GitCommand(
2029 self,
2030 ["branch", "-D", name],
2031 capture_stdout=True,
2032 capture_stderr=True,
2033 verify_command=True,
2034 ).Wait()
2035 return True
Gavin Makea2e3302023-03-11 06:46:20 +00002036
2037 def PruneHeads(self):
2038 """Prune any topic branches already merged into upstream."""
2039 cb = self.CurrentBranch
2040 kill = []
2041 left = self._allrefs
2042 for name in left.keys():
2043 if name.startswith(R_HEADS):
2044 name = name[len(R_HEADS) :]
2045 if cb is None or name != cb:
2046 kill.append(name)
2047
2048 # Minor optimization: If there's nothing to prune, then don't try to
2049 # read any project state.
2050 if not kill and not cb:
2051 return []
2052
2053 rev = self.GetRevisionId(left)
2054 if (
2055 cb is not None
2056 and not self._revlist(HEAD + "..." + rev)
2057 and not self.IsDirty(consider_untracked=False)
2058 ):
2059 self.work_git.DetachHead(HEAD)
2060 kill.append(cb)
2061
2062 if kill:
2063 old = self.bare_git.GetHead()
2064
2065 try:
2066 self.bare_git.DetachHead(rev)
2067
2068 b = ["branch", "-d"]
2069 b.extend(kill)
2070 b = GitCommand(
2071 self, b, bare=True, capture_stdout=True, capture_stderr=True
2072 )
2073 b.Wait()
2074 finally:
2075 if ID_RE.match(old):
2076 self.bare_git.DetachHead(old)
2077 else:
2078 self.bare_git.SetHead(old)
2079 left = self._allrefs
2080
2081 for branch in kill:
2082 if (R_HEADS + branch) not in left:
2083 self.CleanPublishedCache()
2084 break
2085
2086 if cb and cb not in kill:
2087 kill.append(cb)
2088 kill.sort()
2089
2090 kept = []
2091 for branch in kill:
2092 if R_HEADS + branch in left:
2093 branch = self.GetBranch(branch)
2094 base = branch.LocalMerge
2095 if not base:
2096 base = rev
2097 kept.append(ReviewableBranch(self, branch, base))
2098 return kept
2099
2100 def GetRegisteredSubprojects(self):
2101 result = []
2102
2103 def rec(subprojects):
2104 if not subprojects:
2105 return
2106 result.extend(subprojects)
2107 for p in subprojects:
2108 rec(p.subprojects)
2109
2110 rec(self.subprojects)
2111 return result
2112
2113 def _GetSubmodules(self):
2114 # Unfortunately we cannot call `git submodule status --recursive` here
2115 # because the working tree might not exist yet, and it cannot be used
2116 # without a working tree in its current implementation.
2117
2118 def get_submodules(gitdir, rev):
2119 # Parse .gitmodules for submodule sub_paths and sub_urls.
2120 sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
2121 if not sub_paths:
2122 return []
2123 # Run `git ls-tree` to read SHAs of submodule object, which happen
2124 # to be revision of submodule repository.
2125 sub_revs = git_ls_tree(gitdir, rev, sub_paths)
2126 submodules = []
2127 for sub_path, sub_url in zip(sub_paths, sub_urls):
2128 try:
2129 sub_rev = sub_revs[sub_path]
2130 except KeyError:
2131 # Ignore non-exist submodules.
2132 continue
2133 submodules.append((sub_rev, sub_path, sub_url))
2134 return submodules
2135
2136 re_path = re.compile(r"^submodule\.(.+)\.path=(.*)$")
2137 re_url = re.compile(r"^submodule\.(.+)\.url=(.*)$")
2138
2139 def parse_gitmodules(gitdir, rev):
2140 cmd = ["cat-file", "blob", "%s:.gitmodules" % rev]
2141 try:
2142 p = GitCommand(
2143 None,
2144 cmd,
2145 capture_stdout=True,
2146 capture_stderr=True,
2147 bare=True,
2148 gitdir=gitdir,
2149 )
2150 except GitError:
2151 return [], []
2152 if p.Wait() != 0:
2153 return [], []
2154
2155 gitmodules_lines = []
2156 fd, temp_gitmodules_path = tempfile.mkstemp()
2157 try:
2158 os.write(fd, p.stdout.encode("utf-8"))
2159 os.close(fd)
2160 cmd = ["config", "--file", temp_gitmodules_path, "--list"]
2161 p = GitCommand(
2162 None,
2163 cmd,
2164 capture_stdout=True,
2165 capture_stderr=True,
2166 bare=True,
2167 gitdir=gitdir,
2168 )
2169 if p.Wait() != 0:
2170 return [], []
2171 gitmodules_lines = p.stdout.split("\n")
2172 except GitError:
2173 return [], []
2174 finally:
2175 platform_utils.remove(temp_gitmodules_path)
2176
2177 names = set()
2178 paths = {}
2179 urls = {}
2180 for line in gitmodules_lines:
2181 if not line:
2182 continue
2183 m = re_path.match(line)
2184 if m:
2185 names.add(m.group(1))
2186 paths[m.group(1)] = m.group(2)
2187 continue
2188 m = re_url.match(line)
2189 if m:
2190 names.add(m.group(1))
2191 urls[m.group(1)] = m.group(2)
2192 continue
2193 names = sorted(names)
2194 return (
2195 [paths.get(name, "") for name in names],
2196 [urls.get(name, "") for name in names],
2197 )
2198
2199 def git_ls_tree(gitdir, rev, paths):
2200 cmd = ["ls-tree", rev, "--"]
2201 cmd.extend(paths)
2202 try:
2203 p = GitCommand(
2204 None,
2205 cmd,
2206 capture_stdout=True,
2207 capture_stderr=True,
2208 bare=True,
2209 gitdir=gitdir,
2210 )
2211 except GitError:
2212 return []
2213 if p.Wait() != 0:
2214 return []
2215 objects = {}
2216 for line in p.stdout.split("\n"):
2217 if not line.strip():
2218 continue
2219 object_rev, object_path = line.split()[2:4]
2220 objects[object_path] = object_rev
2221 return objects
2222
2223 try:
2224 rev = self.GetRevisionId()
2225 except GitError:
2226 return []
2227 return get_submodules(self.gitdir, rev)
2228
2229 def GetDerivedSubprojects(self):
2230 result = []
2231 if not self.Exists:
2232 # If git repo does not exist yet, querying its submodules will
2233 # mess up its states; so return here.
2234 return result
2235 for rev, path, url in self._GetSubmodules():
2236 name = self.manifest.GetSubprojectName(self, path)
2237 (
2238 relpath,
2239 worktree,
2240 gitdir,
2241 objdir,
2242 ) = self.manifest.GetSubprojectPaths(self, name, path)
2243 project = self.manifest.paths.get(relpath)
2244 if project:
2245 result.extend(project.GetDerivedSubprojects())
2246 continue
2247
2248 if url.startswith(".."):
2249 url = urllib.parse.urljoin("%s/" % self.remote.url, url)
2250 remote = RemoteSpec(
2251 self.remote.name,
2252 url=url,
2253 pushUrl=self.remote.pushUrl,
2254 review=self.remote.review,
2255 revision=self.remote.revision,
2256 )
2257 subproject = Project(
2258 manifest=self.manifest,
2259 name=name,
2260 remote=remote,
2261 gitdir=gitdir,
2262 objdir=objdir,
2263 worktree=worktree,
2264 relpath=relpath,
2265 revisionExpr=rev,
2266 revisionId=rev,
2267 rebase=self.rebase,
2268 groups=self.groups,
2269 sync_c=self.sync_c,
2270 sync_s=self.sync_s,
2271 sync_tags=self.sync_tags,
2272 parent=self,
2273 is_derived=True,
2274 )
2275 result.append(subproject)
2276 result.extend(subproject.GetDerivedSubprojects())
2277 return result
2278
2279 def EnableRepositoryExtension(self, key, value="true", version=1):
2280 """Enable git repository extension |key| with |value|.
2281
2282 Args:
2283 key: The extension to enabled. Omit the "extensions." prefix.
2284 value: The value to use for the extension.
2285 version: The minimum git repository version needed.
2286 """
2287 # Make sure the git repo version is new enough already.
2288 found_version = self.config.GetInt("core.repositoryFormatVersion")
2289 if found_version is None:
2290 found_version = 0
2291 if found_version < version:
2292 self.config.SetString("core.repositoryFormatVersion", str(version))
2293
2294 # Enable the extension!
2295 self.config.SetString("extensions.%s" % (key,), value)
2296
2297 def ResolveRemoteHead(self, name=None):
2298 """Find out what the default branch (HEAD) points to.
2299
2300 Normally this points to refs/heads/master, but projects are moving to
2301 main. Support whatever the server uses rather than hardcoding "master"
2302 ourselves.
2303 """
2304 if name is None:
2305 name = self.remote.name
2306
2307 # The output will look like (NB: tabs are separators):
2308 # ref: refs/heads/master HEAD
2309 # 5f6803b100bb3cd0f534e96e88c91373e8ed1c44 HEAD
2310 output = self.bare_git.ls_remote(
2311 "-q", "--symref", "--exit-code", name, "HEAD"
2312 )
2313
2314 for line in output.splitlines():
2315 lhs, rhs = line.split("\t", 1)
2316 if rhs == "HEAD" and lhs.startswith("ref:"):
2317 return lhs[4:].strip()
2318
2319 return None
2320
2321 def _CheckForImmutableRevision(self):
2322 try:
2323 # if revision (sha or tag) is not present then following function
2324 # throws an error.
2325 self.bare_git.rev_list(
2326 "-1", "--missing=allow-any", "%s^0" % self.revisionExpr, "--"
2327 )
2328 if self.upstream:
2329 rev = self.GetRemote().ToLocal(self.upstream)
2330 self.bare_git.rev_list(
2331 "-1", "--missing=allow-any", "%s^0" % rev, "--"
2332 )
2333 self.bare_git.merge_base(
2334 "--is-ancestor", self.revisionExpr, rev
2335 )
2336 return True
2337 except GitError:
2338 # There is no such persistent revision. We have to fetch it.
2339 return False
2340
2341 def _FetchArchive(self, tarpath, cwd=None):
2342 cmd = ["archive", "-v", "-o", tarpath]
2343 cmd.append("--remote=%s" % self.remote.url)
2344 cmd.append("--prefix=%s/" % self.RelPath(local=False))
2345 cmd.append(self.revisionExpr)
2346
2347 command = GitCommand(
Jason Chang32b59562023-07-14 16:45:35 -07002348 self,
2349 cmd,
2350 cwd=cwd,
2351 capture_stdout=True,
2352 capture_stderr=True,
2353 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00002354 )
Jason Chang32b59562023-07-14 16:45:35 -07002355 command.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00002356
2357 def _RemoteFetch(
2358 self,
2359 name=None,
2360 current_branch_only=False,
2361 initial=False,
2362 quiet=False,
2363 verbose=False,
2364 output_redir=None,
2365 alt_dir=None,
2366 tags=True,
2367 prune=False,
2368 depth=None,
2369 submodules=False,
2370 ssh_proxy=None,
2371 force_sync=False,
2372 clone_filter=None,
2373 retry_fetches=2,
2374 retry_sleep_initial_sec=4.0,
2375 retry_exp_factor=2.0,
Jason Chang32b59562023-07-14 16:45:35 -07002376 ) -> bool:
Gavin Makea2e3302023-03-11 06:46:20 +00002377 is_sha1 = False
2378 tag_name = None
2379 # The depth should not be used when fetching to a mirror because
2380 # it will result in a shallow repository that cannot be cloned or
2381 # fetched from.
2382 # The repo project should also never be synced with partial depth.
2383 if self.manifest.IsMirror or self.relpath == ".repo/repo":
2384 depth = None
2385
2386 if depth:
2387 current_branch_only = True
2388
2389 if ID_RE.match(self.revisionExpr) is not None:
2390 is_sha1 = True
2391
2392 if current_branch_only:
2393 if self.revisionExpr.startswith(R_TAGS):
2394 # This is a tag and its commit id should never change.
2395 tag_name = self.revisionExpr[len(R_TAGS) :]
2396 elif self.upstream and self.upstream.startswith(R_TAGS):
2397 # This is a tag and its commit id should never change.
2398 tag_name = self.upstream[len(R_TAGS) :]
2399
2400 if is_sha1 or tag_name is not None:
2401 if self._CheckForImmutableRevision():
2402 if verbose:
2403 print(
2404 "Skipped fetching project %s (already have "
2405 "persistent ref)" % self.name
2406 )
2407 return True
2408 if is_sha1 and not depth:
2409 # When syncing a specific commit and --depth is not set:
2410 # * if upstream is explicitly specified and is not a sha1, fetch
2411 # only upstream as users expect only upstream to be fetch.
2412 # Note: The commit might not be in upstream in which case the
2413 # sync will fail.
2414 # * otherwise, fetch all branches to make sure we end up with
2415 # the specific commit.
2416 if self.upstream:
2417 current_branch_only = not ID_RE.match(self.upstream)
2418 else:
2419 current_branch_only = False
2420
2421 if not name:
2422 name = self.remote.name
2423
2424 remote = self.GetRemote(name)
2425 if not remote.PreConnectFetch(ssh_proxy):
2426 ssh_proxy = None
2427
2428 if initial:
2429 if alt_dir and "objects" == os.path.basename(alt_dir):
2430 ref_dir = os.path.dirname(alt_dir)
2431 packed_refs = os.path.join(self.gitdir, "packed-refs")
2432
2433 all_refs = self.bare_ref.all
2434 ids = set(all_refs.values())
2435 tmp = set()
2436
2437 for r, ref_id in GitRefs(ref_dir).all.items():
2438 if r not in all_refs:
2439 if r.startswith(R_TAGS) or remote.WritesTo(r):
2440 all_refs[r] = ref_id
2441 ids.add(ref_id)
2442 continue
2443
2444 if ref_id in ids:
2445 continue
2446
2447 r = "refs/_alt/%s" % ref_id
2448 all_refs[r] = ref_id
2449 ids.add(ref_id)
2450 tmp.add(r)
2451
2452 tmp_packed_lines = []
2453 old_packed_lines = []
2454
2455 for r in sorted(all_refs):
2456 line = "%s %s\n" % (all_refs[r], r)
2457 tmp_packed_lines.append(line)
2458 if r not in tmp:
2459 old_packed_lines.append(line)
2460
2461 tmp_packed = "".join(tmp_packed_lines)
2462 old_packed = "".join(old_packed_lines)
2463 _lwrite(packed_refs, tmp_packed)
2464 else:
2465 alt_dir = None
2466
2467 cmd = ["fetch"]
2468
2469 if clone_filter:
2470 git_require((2, 19, 0), fail=True, msg="partial clones")
2471 cmd.append("--filter=%s" % clone_filter)
2472 self.EnableRepositoryExtension("partialclone", self.remote.name)
2473
2474 if depth:
2475 cmd.append("--depth=%s" % depth)
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002476 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002477 # If this repo has shallow objects, then we don't know which refs
2478 # have shallow objects or not. Tell git to unshallow all fetched
2479 # refs. Don't do this with projects that don't have shallow
2480 # objects, since it is less efficient.
2481 if os.path.exists(os.path.join(self.gitdir, "shallow")):
2482 cmd.append("--depth=2147483647")
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002483
Gavin Makea2e3302023-03-11 06:46:20 +00002484 if not verbose:
2485 cmd.append("--quiet")
2486 if not quiet and sys.stdout.isatty():
2487 cmd.append("--progress")
2488 if not self.worktree:
2489 cmd.append("--update-head-ok")
2490 cmd.append(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002491
Gavin Makea2e3302023-03-11 06:46:20 +00002492 if force_sync:
2493 cmd.append("--force")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002494
Gavin Makea2e3302023-03-11 06:46:20 +00002495 if prune:
2496 cmd.append("--prune")
Mike Frysinger29626b42021-05-01 09:37:13 -04002497
Gavin Makea2e3302023-03-11 06:46:20 +00002498 # Always pass something for --recurse-submodules, git with GIT_DIR
2499 # behaves incorrectly when not given `--recurse-submodules=no`.
2500 # (b/218891912)
2501 cmd.append(
2502 f'--recurse-submodules={"on-demand" if submodules else "no"}'
2503 )
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002504
Gavin Makea2e3302023-03-11 06:46:20 +00002505 spec = []
2506 if not current_branch_only:
2507 # Fetch whole repo.
2508 spec.append(
2509 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2510 )
2511 elif tag_name is not None:
2512 spec.append("tag")
2513 spec.append(tag_name)
Remy Böhmer1469c282020-12-15 18:49:02 +01002514
Gavin Makea2e3302023-03-11 06:46:20 +00002515 if self.manifest.IsMirror and not current_branch_only:
2516 branch = None
Remy Böhmer1469c282020-12-15 18:49:02 +01002517 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002518 branch = self.revisionExpr
2519 if (
2520 not self.manifest.IsMirror
2521 and is_sha1
2522 and depth
2523 and git_require((1, 8, 3))
2524 ):
2525 # Shallow checkout of a specific commit, fetch from that commit and
2526 # not the heads only as the commit might be deeper in the history.
2527 spec.append(branch)
2528 if self.upstream:
2529 spec.append(self.upstream)
David James8d201162013-10-11 17:03:19 -07002530 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002531 if is_sha1:
2532 branch = self.upstream
2533 if branch is not None and branch.strip():
2534 if not branch.startswith("refs/"):
2535 branch = R_HEADS + branch
2536 spec.append(str(("+%s:" % branch) + remote.ToLocal(branch)))
David James8d201162013-10-11 17:03:19 -07002537
Gavin Makea2e3302023-03-11 06:46:20 +00002538 # If mirroring repo and we cannot deduce the tag or branch to fetch,
2539 # fetch whole repo.
2540 if self.manifest.IsMirror and not spec:
2541 spec.append(
2542 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2543 )
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002544
Gavin Makea2e3302023-03-11 06:46:20 +00002545 # If using depth then we should not get all the tags since they may
2546 # be outside of the depth.
2547 if not tags or depth:
2548 cmd.append("--no-tags")
2549 else:
2550 cmd.append("--tags")
2551 spec.append(str(("+refs/tags/*:") + remote.ToLocal("refs/tags/*")))
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002552
Gavin Makea2e3302023-03-11 06:46:20 +00002553 cmd.extend(spec)
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002554
Gavin Makea2e3302023-03-11 06:46:20 +00002555 # At least one retry minimum due to git remote prune.
2556 retry_fetches = max(retry_fetches, 2)
2557 retry_cur_sleep = retry_sleep_initial_sec
2558 ok = prune_tried = False
2559 for try_n in range(retry_fetches):
Jason Chang32b59562023-07-14 16:45:35 -07002560 verify_command = try_n == retry_fetches - 1
Gavin Makea2e3302023-03-11 06:46:20 +00002561 gitcmd = GitCommand(
2562 self,
2563 cmd,
2564 bare=True,
2565 objdir=os.path.join(self.objdir, "objects"),
2566 ssh_proxy=ssh_proxy,
2567 merge_output=True,
2568 capture_stdout=quiet or bool(output_redir),
Jason Chang32b59562023-07-14 16:45:35 -07002569 verify_command=verify_command,
Gavin Makea2e3302023-03-11 06:46:20 +00002570 )
2571 if gitcmd.stdout and not quiet and output_redir:
2572 output_redir.write(gitcmd.stdout)
2573 ret = gitcmd.Wait()
2574 if ret == 0:
2575 ok = True
2576 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002577
Gavin Makea2e3302023-03-11 06:46:20 +00002578 # Retry later due to HTTP 429 Too Many Requests.
2579 elif (
2580 gitcmd.stdout
2581 and "error:" in gitcmd.stdout
2582 and "HTTP 429" in gitcmd.stdout
2583 ):
2584 # Fallthru to sleep+retry logic at the bottom.
2585 pass
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002586
Gavin Makea2e3302023-03-11 06:46:20 +00002587 # Try to prune remote branches once in case there are conflicts.
2588 # For example, if the remote had refs/heads/upstream, but deleted
2589 # that and now has refs/heads/upstream/foo.
2590 elif (
2591 gitcmd.stdout
2592 and "error:" in gitcmd.stdout
2593 and "git remote prune" in gitcmd.stdout
2594 and not prune_tried
2595 ):
2596 prune_tried = True
2597 prunecmd = GitCommand(
2598 self,
2599 ["remote", "prune", name],
2600 bare=True,
2601 ssh_proxy=ssh_proxy,
2602 )
2603 ret = prunecmd.Wait()
2604 if ret:
2605 break
2606 print(
2607 "retrying fetch after pruning remote branches",
2608 file=output_redir,
2609 )
2610 # Continue right away so we don't sleep as we shouldn't need to.
2611 continue
2612 elif current_branch_only and is_sha1 and ret == 128:
2613 # Exit code 128 means "couldn't find the ref you asked for"; if
2614 # we're in sha1 mode, we just tried sync'ing from the upstream
2615 # field; it doesn't exist, thus abort the optimization attempt
2616 # and do a full sync.
2617 break
2618 elif ret < 0:
2619 # Git died with a signal, exit immediately.
2620 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002621
Gavin Makea2e3302023-03-11 06:46:20 +00002622 # Figure out how long to sleep before the next attempt, if there is
2623 # one.
2624 if not verbose and gitcmd.stdout:
2625 print(
2626 "\n%s:\n%s" % (self.name, gitcmd.stdout),
2627 end="",
2628 file=output_redir,
2629 )
2630 if try_n < retry_fetches - 1:
2631 print(
2632 "%s: sleeping %s seconds before retrying"
2633 % (self.name, retry_cur_sleep),
2634 file=output_redir,
2635 )
2636 time.sleep(retry_cur_sleep)
2637 retry_cur_sleep = min(
2638 retry_exp_factor * retry_cur_sleep, MAXIMUM_RETRY_SLEEP_SEC
2639 )
2640 retry_cur_sleep *= 1 - random.uniform(
2641 -RETRY_JITTER_PERCENT, RETRY_JITTER_PERCENT
2642 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002643
Gavin Makea2e3302023-03-11 06:46:20 +00002644 if initial:
2645 if alt_dir:
2646 if old_packed != "":
2647 _lwrite(packed_refs, old_packed)
2648 else:
2649 platform_utils.remove(packed_refs)
2650 self.bare_git.pack_refs("--all", "--prune")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002651
Gavin Makea2e3302023-03-11 06:46:20 +00002652 if is_sha1 and current_branch_only:
2653 # We just synced the upstream given branch; verify we
2654 # got what we wanted, else trigger a second run of all
2655 # refs.
2656 if not self._CheckForImmutableRevision():
2657 # Sync the current branch only with depth set to None.
2658 # We always pass depth=None down to avoid infinite recursion.
2659 return self._RemoteFetch(
2660 name=name,
2661 quiet=quiet,
2662 verbose=verbose,
2663 output_redir=output_redir,
2664 current_branch_only=current_branch_only and depth,
2665 initial=False,
2666 alt_dir=alt_dir,
Nasser Grainawiaafed292023-05-24 12:51:03 -06002667 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00002668 depth=None,
2669 ssh_proxy=ssh_proxy,
2670 clone_filter=clone_filter,
2671 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002672
Gavin Makea2e3302023-03-11 06:46:20 +00002673 return ok
Mike Frysingerf4545122019-11-11 04:34:16 -05002674
Gavin Makea2e3302023-03-11 06:46:20 +00002675 def _ApplyCloneBundle(self, initial=False, quiet=False, verbose=False):
2676 if initial and (
2677 self.manifest.manifestProject.depth or self.clone_depth
2678 ):
2679 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002680
Gavin Makea2e3302023-03-11 06:46:20 +00002681 remote = self.GetRemote()
2682 bundle_url = remote.url + "/clone.bundle"
2683 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
2684 if GetSchemeFromUrl(bundle_url) not in (
2685 "http",
2686 "https",
2687 "persistent-http",
2688 "persistent-https",
2689 ):
2690 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06002691
Gavin Makea2e3302023-03-11 06:46:20 +00002692 bundle_dst = os.path.join(self.gitdir, "clone.bundle")
2693 bundle_tmp = os.path.join(self.gitdir, "clone.bundle.tmp")
2694
2695 exist_dst = os.path.exists(bundle_dst)
2696 exist_tmp = os.path.exists(bundle_tmp)
2697
2698 if not initial and not exist_dst and not exist_tmp:
2699 return False
2700
2701 if not exist_dst:
2702 exist_dst = self._FetchBundle(
2703 bundle_url, bundle_tmp, bundle_dst, quiet, verbose
2704 )
2705 if not exist_dst:
2706 return False
2707
2708 cmd = ["fetch"]
2709 if not verbose:
2710 cmd.append("--quiet")
2711 if not quiet and sys.stdout.isatty():
2712 cmd.append("--progress")
2713 if not self.worktree:
2714 cmd.append("--update-head-ok")
2715 cmd.append(bundle_dst)
2716 for f in remote.fetch:
2717 cmd.append(str(f))
2718 cmd.append("+refs/tags/*:refs/tags/*")
2719
2720 ok = (
2721 GitCommand(
2722 self,
2723 cmd,
2724 bare=True,
2725 objdir=os.path.join(self.objdir, "objects"),
2726 ).Wait()
2727 == 0
2728 )
2729 platform_utils.remove(bundle_dst, missing_ok=True)
2730 platform_utils.remove(bundle_tmp, missing_ok=True)
2731 return ok
2732
2733 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
2734 platform_utils.remove(dstPath, missing_ok=True)
2735
2736 cmd = ["curl", "--fail", "--output", tmpPath, "--netrc", "--location"]
2737 if quiet:
2738 cmd += ["--silent", "--show-error"]
2739 if os.path.exists(tmpPath):
2740 size = os.stat(tmpPath).st_size
2741 if size >= 1024:
2742 cmd += ["--continue-at", "%d" % (size,)]
2743 else:
2744 platform_utils.remove(tmpPath)
2745 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
2746 if cookiefile:
2747 cmd += ["--cookie", cookiefile]
2748 if proxy:
2749 cmd += ["--proxy", proxy]
2750 elif "http_proxy" in os.environ and "darwin" == sys.platform:
2751 cmd += ["--proxy", os.environ["http_proxy"]]
2752 if srcUrl.startswith("persistent-https"):
2753 srcUrl = "http" + srcUrl[len("persistent-https") :]
2754 elif srcUrl.startswith("persistent-http"):
2755 srcUrl = "http" + srcUrl[len("persistent-http") :]
2756 cmd += [srcUrl]
2757
2758 proc = None
2759 with Trace("Fetching bundle: %s", " ".join(cmd)):
2760 if verbose:
2761 print("%s: Downloading bundle: %s" % (self.name, srcUrl))
2762 stdout = None if verbose else subprocess.PIPE
2763 stderr = None if verbose else subprocess.STDOUT
2764 try:
2765 proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
2766 except OSError:
2767 return False
2768
2769 (output, _) = proc.communicate()
2770 curlret = proc.returncode
2771
2772 if curlret == 22:
2773 # From curl man page:
2774 # 22: HTTP page not retrieved. The requested url was not found
2775 # or returned another error with the HTTP error code being 400
2776 # or above. This return code only appears if -f, --fail is used.
2777 if verbose:
2778 print(
2779 "%s: Unable to retrieve clone.bundle; ignoring."
2780 % self.name
2781 )
2782 if output:
2783 print("Curl output:\n%s" % output)
2784 return False
2785 elif curlret and not verbose and output:
2786 print("%s" % output, file=sys.stderr)
2787
2788 if os.path.exists(tmpPath):
2789 if curlret == 0 and self._IsValidBundle(tmpPath, quiet):
2790 platform_utils.rename(tmpPath, dstPath)
2791 return True
2792 else:
2793 platform_utils.remove(tmpPath)
2794 return False
2795 else:
2796 return False
2797
2798 def _IsValidBundle(self, path, quiet):
2799 try:
2800 with open(path, "rb") as f:
2801 if f.read(16) == b"# v2 git bundle\n":
2802 return True
2803 else:
2804 if not quiet:
2805 print(
2806 "Invalid clone.bundle file; ignoring.",
2807 file=sys.stderr,
2808 )
2809 return False
2810 except OSError:
2811 return False
2812
2813 def _Checkout(self, rev, quiet=False):
2814 cmd = ["checkout"]
2815 if quiet:
2816 cmd.append("-q")
2817 cmd.append(rev)
2818 cmd.append("--")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002819 if GitCommand(self, cmd).Wait() != 0:
Gavin Makea2e3302023-03-11 06:46:20 +00002820 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002821 raise GitError(
2822 "%s checkout %s " % (self.name, rev), project=self.name
2823 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002824
Gavin Makea2e3302023-03-11 06:46:20 +00002825 def _CherryPick(self, rev, ffonly=False, record_origin=False):
2826 cmd = ["cherry-pick"]
2827 if ffonly:
2828 cmd.append("--ff")
2829 if record_origin:
2830 cmd.append("-x")
2831 cmd.append(rev)
2832 cmd.append("--")
2833 if GitCommand(self, cmd).Wait() != 0:
2834 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002835 raise GitError(
2836 "%s cherry-pick %s " % (self.name, rev), project=self.name
2837 )
Victor Boivie0960b5b2010-11-26 13:42:13 +01002838
Gavin Makea2e3302023-03-11 06:46:20 +00002839 def _LsRemote(self, refs):
2840 cmd = ["ls-remote", self.remote.name, refs]
2841 p = GitCommand(self, cmd, capture_stdout=True)
2842 if p.Wait() == 0:
2843 return p.stdout
2844 return None
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002845
Gavin Makea2e3302023-03-11 06:46:20 +00002846 def _Revert(self, rev):
2847 cmd = ["revert"]
2848 cmd.append("--no-edit")
2849 cmd.append(rev)
2850 cmd.append("--")
2851 if GitCommand(self, cmd).Wait() != 0:
2852 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002853 raise GitError(
2854 "%s revert %s " % (self.name, rev), project=self.name
2855 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002856
Gavin Makea2e3302023-03-11 06:46:20 +00002857 def _ResetHard(self, rev, quiet=True):
2858 cmd = ["reset", "--hard"]
2859 if quiet:
2860 cmd.append("-q")
2861 cmd.append(rev)
2862 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002863 raise GitError(
2864 "%s reset --hard %s " % (self.name, rev), project=self.name
2865 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002866
Gavin Makea2e3302023-03-11 06:46:20 +00002867 def _SyncSubmodules(self, quiet=True):
2868 cmd = ["submodule", "update", "--init", "--recursive"]
2869 if quiet:
2870 cmd.append("-q")
2871 if GitCommand(self, cmd).Wait() != 0:
2872 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07002873 "%s submodule update --init --recursive " % self.name,
2874 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00002875 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002876
Gavin Makea2e3302023-03-11 06:46:20 +00002877 def _Rebase(self, upstream, onto=None):
2878 cmd = ["rebase"]
2879 if onto is not None:
2880 cmd.extend(["--onto", onto])
2881 cmd.append(upstream)
2882 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002883 raise GitError(
2884 "%s rebase %s " % (self.name, upstream), project=self.name
2885 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002886
Gavin Makea2e3302023-03-11 06:46:20 +00002887 def _FastForward(self, head, ffonly=False):
2888 cmd = ["merge", "--no-stat", head]
2889 if ffonly:
2890 cmd.append("--ff-only")
2891 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002892 raise GitError(
2893 "%s merge %s " % (self.name, head), project=self.name
2894 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002895
Gavin Makea2e3302023-03-11 06:46:20 +00002896 def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False):
2897 init_git_dir = not os.path.exists(self.gitdir)
2898 init_obj_dir = not os.path.exists(self.objdir)
2899 try:
2900 # Initialize the bare repository, which contains all of the objects.
2901 if init_obj_dir:
2902 os.makedirs(self.objdir)
2903 self.bare_objdir.init()
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002904
Gavin Makea2e3302023-03-11 06:46:20 +00002905 self._UpdateHooks(quiet=quiet)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002906
Gavin Makea2e3302023-03-11 06:46:20 +00002907 if self.use_git_worktrees:
2908 # Enable per-worktree config file support if possible. This
2909 # is more a nice-to-have feature for users rather than a
2910 # hard requirement.
2911 if git_require((2, 20, 0)):
2912 self.EnableRepositoryExtension("worktreeConfig")
Renaud Paquay788e9622017-01-27 11:41:12 -08002913
Gavin Makea2e3302023-03-11 06:46:20 +00002914 # If we have a separate directory to hold refs, initialize it as
2915 # well.
2916 if self.objdir != self.gitdir:
2917 if init_git_dir:
2918 os.makedirs(self.gitdir)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002919
Gavin Makea2e3302023-03-11 06:46:20 +00002920 if init_obj_dir or init_git_dir:
2921 self._ReferenceGitDir(
2922 self.objdir, self.gitdir, copy_all=True
2923 )
2924 try:
2925 self._CheckDirReference(self.objdir, self.gitdir)
2926 except GitError as e:
2927 if force_sync:
2928 print(
2929 "Retrying clone after deleting %s" % self.gitdir,
2930 file=sys.stderr,
2931 )
2932 try:
2933 platform_utils.rmtree(
2934 platform_utils.realpath(self.gitdir)
2935 )
2936 if self.worktree and os.path.exists(
2937 platform_utils.realpath(self.worktree)
2938 ):
2939 platform_utils.rmtree(
2940 platform_utils.realpath(self.worktree)
2941 )
2942 return self._InitGitDir(
2943 mirror_git=mirror_git,
2944 force_sync=False,
2945 quiet=quiet,
2946 )
2947 except Exception:
2948 raise e
2949 raise e
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002950
Gavin Makea2e3302023-03-11 06:46:20 +00002951 if init_git_dir:
2952 mp = self.manifest.manifestProject
2953 ref_dir = mp.reference or ""
Julien Camperguedd654222014-01-09 16:21:37 +01002954
Gavin Makea2e3302023-03-11 06:46:20 +00002955 def _expanded_ref_dirs():
2956 """Iterate through possible git reference dir paths."""
2957 name = self.name + ".git"
2958 yield mirror_git or os.path.join(ref_dir, name)
2959 for prefix in "", self.remote.name:
2960 yield os.path.join(
2961 ref_dir, ".repo", "project-objects", prefix, name
2962 )
2963 yield os.path.join(
2964 ref_dir, ".repo", "worktrees", prefix, name
2965 )
2966
2967 if ref_dir or mirror_git:
2968 found_ref_dir = None
2969 for path in _expanded_ref_dirs():
2970 if os.path.exists(path):
2971 found_ref_dir = path
2972 break
2973 ref_dir = found_ref_dir
2974
2975 if ref_dir:
2976 if not os.path.isabs(ref_dir):
2977 # The alternate directory is relative to the object
2978 # database.
2979 ref_dir = os.path.relpath(
2980 ref_dir, os.path.join(self.objdir, "objects")
2981 )
2982 _lwrite(
2983 os.path.join(
2984 self.objdir, "objects/info/alternates"
2985 ),
2986 os.path.join(ref_dir, "objects") + "\n",
2987 )
2988
2989 m = self.manifest.manifestProject.config
2990 for key in ["user.name", "user.email"]:
2991 if m.Has(key, include_defaults=False):
2992 self.config.SetString(key, m.GetString(key))
2993 if not self.manifest.EnableGitLfs:
2994 self.config.SetString(
2995 "filter.lfs.smudge", "git-lfs smudge --skip -- %f"
2996 )
2997 self.config.SetString(
2998 "filter.lfs.process", "git-lfs filter-process --skip"
2999 )
3000 self.config.SetBoolean(
3001 "core.bare", True if self.manifest.IsMirror else None
3002 )
3003 except Exception:
3004 if init_obj_dir and os.path.exists(self.objdir):
3005 platform_utils.rmtree(self.objdir)
3006 if init_git_dir and os.path.exists(self.gitdir):
3007 platform_utils.rmtree(self.gitdir)
3008 raise
3009
3010 def _UpdateHooks(self, quiet=False):
3011 if os.path.exists(self.objdir):
3012 self._InitHooks(quiet=quiet)
3013
3014 def _InitHooks(self, quiet=False):
3015 hooks = platform_utils.realpath(os.path.join(self.objdir, "hooks"))
3016 if not os.path.exists(hooks):
3017 os.makedirs(hooks)
3018
3019 # Delete sample hooks. They're noise.
3020 for hook in glob.glob(os.path.join(hooks, "*.sample")):
3021 try:
3022 platform_utils.remove(hook, missing_ok=True)
3023 except PermissionError:
3024 pass
3025
3026 for stock_hook in _ProjectHooks():
3027 name = os.path.basename(stock_hook)
3028
3029 if (
3030 name in ("commit-msg",)
3031 and not self.remote.review
3032 and self is not self.manifest.manifestProject
3033 ):
3034 # Don't install a Gerrit Code Review hook if this
3035 # project does not appear to use it for reviews.
3036 #
3037 # Since the manifest project is one of those, but also
3038 # managed through gerrit, it's excluded.
3039 continue
3040
3041 dst = os.path.join(hooks, name)
3042 if platform_utils.islink(dst):
3043 continue
3044 if os.path.exists(dst):
3045 # If the files are the same, we'll leave it alone. We create
3046 # symlinks below by default but fallback to hardlinks if the OS
3047 # blocks them. So if we're here, it's probably because we made a
3048 # hardlink below.
3049 if not filecmp.cmp(stock_hook, dst, shallow=False):
3050 if not quiet:
3051 _warn(
3052 "%s: Not replacing locally modified %s hook",
3053 self.RelPath(local=False),
3054 name,
3055 )
3056 continue
3057 try:
3058 platform_utils.symlink(
3059 os.path.relpath(stock_hook, os.path.dirname(dst)), dst
3060 )
3061 except OSError as e:
3062 if e.errno == errno.EPERM:
3063 try:
3064 os.link(stock_hook, dst)
3065 except OSError:
Jason Chang32b59562023-07-14 16:45:35 -07003066 raise GitError(
3067 self._get_symlink_error_message(), project=self.name
3068 )
Gavin Makea2e3302023-03-11 06:46:20 +00003069 else:
3070 raise
3071
3072 def _InitRemote(self):
3073 if self.remote.url:
3074 remote = self.GetRemote()
3075 remote.url = self.remote.url
3076 remote.pushUrl = self.remote.pushUrl
3077 remote.review = self.remote.review
3078 remote.projectname = self.name
3079
3080 if self.worktree:
3081 remote.ResetFetch(mirror=False)
3082 else:
3083 remote.ResetFetch(mirror=True)
3084 remote.Save()
3085
3086 def _InitMRef(self):
3087 """Initialize the pseudo m/<manifest branch> ref."""
3088 if self.manifest.branch:
3089 if self.use_git_worktrees:
3090 # Set up the m/ space to point to the worktree-specific ref
3091 # space. We'll update the worktree-specific ref space on each
3092 # checkout.
3093 ref = R_M + self.manifest.branch
3094 if not self.bare_ref.symref(ref):
3095 self.bare_git.symbolic_ref(
3096 "-m",
3097 "redirecting to worktree scope",
3098 ref,
3099 R_WORKTREE_M + self.manifest.branch,
3100 )
3101
3102 # We can't update this ref with git worktrees until it exists.
3103 # We'll wait until the initial checkout to set it.
3104 if not os.path.exists(self.worktree):
3105 return
3106
3107 base = R_WORKTREE_M
3108 active_git = self.work_git
3109
3110 self._InitAnyMRef(HEAD, self.bare_git, detach=True)
3111 else:
3112 base = R_M
3113 active_git = self.bare_git
3114
3115 self._InitAnyMRef(base + self.manifest.branch, active_git)
3116
3117 def _InitMirrorHead(self):
3118 self._InitAnyMRef(HEAD, self.bare_git)
3119
3120 def _InitAnyMRef(self, ref, active_git, detach=False):
3121 """Initialize |ref| in |active_git| to the value in the manifest.
3122
3123 This points |ref| to the <project> setting in the manifest.
3124
3125 Args:
3126 ref: The branch to update.
3127 active_git: The git repository to make updates in.
3128 detach: Whether to update target of symbolic refs, or overwrite the
3129 ref directly (and thus make it non-symbolic).
3130 """
3131 cur = self.bare_ref.symref(ref)
3132
3133 if self.revisionId:
3134 if cur != "" or self.bare_ref.get(ref) != self.revisionId:
3135 msg = "manifest set to %s" % self.revisionId
3136 dst = self.revisionId + "^0"
3137 active_git.UpdateRef(ref, dst, message=msg, detach=True)
Julien Camperguedd654222014-01-09 16:21:37 +01003138 else:
Gavin Makea2e3302023-03-11 06:46:20 +00003139 remote = self.GetRemote()
3140 dst = remote.ToLocal(self.revisionExpr)
3141 if cur != dst:
3142 msg = "manifest set to %s" % self.revisionExpr
3143 if detach:
3144 active_git.UpdateRef(ref, dst, message=msg, detach=True)
3145 else:
3146 active_git.symbolic_ref("-m", msg, ref, dst)
Julien Camperguedd654222014-01-09 16:21:37 +01003147
Gavin Makea2e3302023-03-11 06:46:20 +00003148 def _CheckDirReference(self, srcdir, destdir):
3149 # Git worktrees don't use symlinks to share at all.
3150 if self.use_git_worktrees:
3151 return
Julien Camperguedd654222014-01-09 16:21:37 +01003152
Gavin Makea2e3302023-03-11 06:46:20 +00003153 for name in self.shareable_dirs:
3154 # Try to self-heal a bit in simple cases.
3155 dst_path = os.path.join(destdir, name)
3156 src_path = os.path.join(srcdir, name)
Julien Camperguedd654222014-01-09 16:21:37 +01003157
Gavin Makea2e3302023-03-11 06:46:20 +00003158 dst = platform_utils.realpath(dst_path)
3159 if os.path.lexists(dst):
3160 src = platform_utils.realpath(src_path)
3161 # Fail if the links are pointing to the wrong place.
3162 if src != dst:
3163 _error("%s is different in %s vs %s", name, destdir, srcdir)
3164 raise GitError(
3165 "--force-sync not enabled; cannot overwrite a local "
3166 "work tree. If you're comfortable with the "
3167 "possibility of losing the work tree's git metadata,"
3168 " use `repo sync --force-sync {0}` to "
Jason Chang32b59562023-07-14 16:45:35 -07003169 "proceed.".format(self.RelPath(local=False)),
3170 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003171 )
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003172
Gavin Makea2e3302023-03-11 06:46:20 +00003173 def _ReferenceGitDir(self, gitdir, dotgit, copy_all):
3174 """Update |dotgit| to reference |gitdir|, using symlinks where possible.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003175
Gavin Makea2e3302023-03-11 06:46:20 +00003176 Args:
3177 gitdir: The bare git repository. Must already be initialized.
3178 dotgit: The repository you would like to initialize.
3179 copy_all: If true, copy all remaining files from |gitdir| ->
3180 |dotgit|. This saves you the effort of initializing |dotgit|
3181 yourself.
3182 """
3183 symlink_dirs = self.shareable_dirs[:]
3184 to_symlink = symlink_dirs
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003185
Gavin Makea2e3302023-03-11 06:46:20 +00003186 to_copy = []
3187 if copy_all:
3188 to_copy = platform_utils.listdir(gitdir)
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003189
Gavin Makea2e3302023-03-11 06:46:20 +00003190 dotgit = platform_utils.realpath(dotgit)
3191 for name in set(to_copy).union(to_symlink):
3192 try:
3193 src = platform_utils.realpath(os.path.join(gitdir, name))
3194 dst = os.path.join(dotgit, name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003195
Gavin Makea2e3302023-03-11 06:46:20 +00003196 if os.path.lexists(dst):
3197 continue
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003198
Gavin Makea2e3302023-03-11 06:46:20 +00003199 # If the source dir doesn't exist, create an empty dir.
3200 if name in symlink_dirs and not os.path.lexists(src):
3201 os.makedirs(src)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003202
Gavin Makea2e3302023-03-11 06:46:20 +00003203 if name in to_symlink:
3204 platform_utils.symlink(
3205 os.path.relpath(src, os.path.dirname(dst)), dst
3206 )
3207 elif copy_all and not platform_utils.islink(dst):
3208 if platform_utils.isdir(src):
3209 shutil.copytree(src, dst)
3210 elif os.path.isfile(src):
3211 shutil.copy(src, dst)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003212
Gavin Makea2e3302023-03-11 06:46:20 +00003213 except OSError as e:
3214 if e.errno == errno.EPERM:
3215 raise DownloadError(self._get_symlink_error_message())
3216 else:
3217 raise
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003218
Gavin Makea2e3302023-03-11 06:46:20 +00003219 def _InitGitWorktree(self):
3220 """Init the project using git worktrees."""
3221 self.bare_git.worktree("prune")
3222 self.bare_git.worktree(
3223 "add",
3224 "-ff",
3225 "--checkout",
3226 "--detach",
3227 "--lock",
3228 self.worktree,
3229 self.GetRevisionId(),
3230 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003231
Gavin Makea2e3302023-03-11 06:46:20 +00003232 # Rewrite the internal state files to use relative paths between the
3233 # checkouts & worktrees.
3234 dotgit = os.path.join(self.worktree, ".git")
3235 with open(dotgit, "r") as fp:
3236 # Figure out the checkout->worktree path.
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003237 setting = fp.read()
Gavin Makea2e3302023-03-11 06:46:20 +00003238 assert setting.startswith("gitdir:")
3239 git_worktree_path = setting.split(":", 1)[1].strip()
3240 # Some platforms (e.g. Windows) won't let us update dotgit in situ
3241 # because of file permissions. Delete it and recreate it from scratch
3242 # to avoid.
3243 platform_utils.remove(dotgit)
3244 # Use relative path from checkout->worktree & maintain Unix line endings
3245 # on all OS's to match git behavior.
3246 with open(dotgit, "w", newline="\n") as fp:
3247 print(
3248 "gitdir:",
3249 os.path.relpath(git_worktree_path, self.worktree),
3250 file=fp,
3251 )
3252 # Use relative path from worktree->checkout & maintain Unix line endings
3253 # on all OS's to match git behavior.
3254 with open(
3255 os.path.join(git_worktree_path, "gitdir"), "w", newline="\n"
3256 ) as fp:
3257 print(os.path.relpath(dotgit, git_worktree_path), file=fp)
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003258
Gavin Makea2e3302023-03-11 06:46:20 +00003259 self._InitMRef()
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003260
Gavin Makea2e3302023-03-11 06:46:20 +00003261 def _InitWorkTree(self, force_sync=False, submodules=False):
3262 """Setup the worktree .git path.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003263
Gavin Makea2e3302023-03-11 06:46:20 +00003264 This is the user-visible path like src/foo/.git/.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003265
Gavin Makea2e3302023-03-11 06:46:20 +00003266 With non-git-worktrees, this will be a symlink to the .repo/projects/
3267 path. With git-worktrees, this will be a .git file using "gitdir: ..."
3268 syntax.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003269
Gavin Makea2e3302023-03-11 06:46:20 +00003270 Older checkouts had .git/ directories. If we see that, migrate it.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003271
Gavin Makea2e3302023-03-11 06:46:20 +00003272 This also handles changes in the manifest. Maybe this project was
3273 backed by "foo/bar" on the server, but now it's "new/foo/bar". We have
3274 to update the path we point to under .repo/projects/ to match.
3275 """
3276 dotgit = os.path.join(self.worktree, ".git")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003277
Gavin Makea2e3302023-03-11 06:46:20 +00003278 # If using an old layout style (a directory), migrate it.
3279 if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
Jason Chang32b59562023-07-14 16:45:35 -07003280 self._MigrateOldWorkTreeGitDir(dotgit, project=self.name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003281
Gavin Makea2e3302023-03-11 06:46:20 +00003282 init_dotgit = not os.path.exists(dotgit)
3283 if self.use_git_worktrees:
3284 if init_dotgit:
3285 self._InitGitWorktree()
3286 self._CopyAndLinkFiles()
3287 else:
3288 if not init_dotgit:
3289 # See if the project has changed.
3290 if platform_utils.realpath(
3291 self.gitdir
3292 ) != platform_utils.realpath(dotgit):
3293 platform_utils.remove(dotgit)
Doug Anderson37282b42011-03-04 11:54:18 -08003294
Gavin Makea2e3302023-03-11 06:46:20 +00003295 if init_dotgit or not os.path.exists(dotgit):
3296 os.makedirs(self.worktree, exist_ok=True)
3297 platform_utils.symlink(
3298 os.path.relpath(self.gitdir, self.worktree), dotgit
3299 )
Doug Anderson37282b42011-03-04 11:54:18 -08003300
Gavin Makea2e3302023-03-11 06:46:20 +00003301 if init_dotgit:
3302 _lwrite(
3303 os.path.join(dotgit, HEAD), "%s\n" % self.GetRevisionId()
3304 )
Doug Anderson37282b42011-03-04 11:54:18 -08003305
Gavin Makea2e3302023-03-11 06:46:20 +00003306 # Finish checking out the worktree.
3307 cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
3308 if GitCommand(self, cmd).Wait() != 0:
3309 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07003310 "Cannot initialize work tree for " + self.name,
3311 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003312 )
Doug Anderson37282b42011-03-04 11:54:18 -08003313
Gavin Makea2e3302023-03-11 06:46:20 +00003314 if submodules:
3315 self._SyncSubmodules(quiet=True)
3316 self._CopyAndLinkFiles()
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003317
Gavin Makea2e3302023-03-11 06:46:20 +00003318 @classmethod
Jason Chang32b59562023-07-14 16:45:35 -07003319 def _MigrateOldWorkTreeGitDir(cls, dotgit, project=None):
Gavin Makea2e3302023-03-11 06:46:20 +00003320 """Migrate the old worktree .git/ dir style to a symlink.
3321
3322 This logic specifically only uses state from |dotgit| to figure out
3323 where to move content and not |self|. This way if the backing project
3324 also changed places, we only do the .git/ dir to .git symlink migration
3325 here. The path updates will happen independently.
3326 """
3327 # Figure out where in .repo/projects/ it's pointing to.
3328 if not os.path.islink(os.path.join(dotgit, "refs")):
Jason Chang32b59562023-07-14 16:45:35 -07003329 raise GitError(
3330 f"{dotgit}: unsupported checkout state", project=project
3331 )
Gavin Makea2e3302023-03-11 06:46:20 +00003332 gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, "refs")))
3333
3334 # Remove known symlink paths that exist in .repo/projects/.
3335 KNOWN_LINKS = {
3336 "config",
3337 "description",
3338 "hooks",
3339 "info",
3340 "logs",
3341 "objects",
3342 "packed-refs",
3343 "refs",
3344 "rr-cache",
3345 "shallow",
3346 "svn",
3347 }
3348 # Paths that we know will be in both, but are safe to clobber in
3349 # .repo/projects/.
3350 SAFE_TO_CLOBBER = {
3351 "COMMIT_EDITMSG",
3352 "FETCH_HEAD",
3353 "HEAD",
3354 "gc.log",
3355 "gitk.cache",
3356 "index",
3357 "ORIG_HEAD",
3358 }
3359
3360 # First see if we'd succeed before starting the migration.
3361 unknown_paths = []
3362 for name in platform_utils.listdir(dotgit):
3363 # Ignore all temporary/backup names. These are common with vim &
3364 # emacs.
3365 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3366 continue
3367
3368 dotgit_path = os.path.join(dotgit, name)
3369 if name in KNOWN_LINKS:
3370 if not platform_utils.islink(dotgit_path):
3371 unknown_paths.append(f"{dotgit_path}: should be a symlink")
3372 else:
3373 gitdir_path = os.path.join(gitdir, name)
3374 if name not in SAFE_TO_CLOBBER and os.path.exists(gitdir_path):
3375 unknown_paths.append(
3376 f"{dotgit_path}: unknown file; please file a bug"
3377 )
3378 if unknown_paths:
Jason Chang32b59562023-07-14 16:45:35 -07003379 raise GitError(
3380 "Aborting migration: " + "\n".join(unknown_paths),
3381 project=project,
3382 )
Gavin Makea2e3302023-03-11 06:46:20 +00003383
3384 # Now walk the paths and sync the .git/ to .repo/projects/.
3385 for name in platform_utils.listdir(dotgit):
3386 dotgit_path = os.path.join(dotgit, name)
3387
3388 # Ignore all temporary/backup names. These are common with vim &
3389 # emacs.
3390 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3391 platform_utils.remove(dotgit_path)
3392 elif name in KNOWN_LINKS:
3393 platform_utils.remove(dotgit_path)
3394 else:
3395 gitdir_path = os.path.join(gitdir, name)
3396 platform_utils.remove(gitdir_path, missing_ok=True)
3397 platform_utils.rename(dotgit_path, gitdir_path)
3398
3399 # Now that the dir should be empty, clear it out, and symlink it over.
3400 platform_utils.rmdir(dotgit)
3401 platform_utils.symlink(
3402 os.path.relpath(gitdir, os.path.dirname(dotgit)), dotgit
3403 )
3404
3405 def _get_symlink_error_message(self):
3406 if platform_utils.isWindows():
3407 return (
3408 "Unable to create symbolic link. Please re-run the command as "
3409 "Administrator, or see "
3410 "https://github.com/git-for-windows/git/wiki/Symbolic-Links "
3411 "for other options."
3412 )
3413 return "filesystem must support symlinks"
3414
3415 def _revlist(self, *args, **kw):
3416 a = []
3417 a.extend(args)
3418 a.append("--")
3419 return self.work_git.rev_list(*a, **kw)
3420
3421 @property
3422 def _allrefs(self):
3423 return self.bare_ref.all
3424
3425 def _getLogs(
3426 self, rev1, rev2, oneline=False, color=True, pretty_format=None
3427 ):
3428 """Get logs between two revisions of this project."""
3429 comp = ".."
3430 if rev1:
3431 revs = [rev1]
3432 if rev2:
3433 revs.extend([comp, rev2])
3434 cmd = ["log", "".join(revs)]
3435 out = DiffColoring(self.config)
3436 if out.is_on and color:
3437 cmd.append("--color")
3438 if pretty_format is not None:
3439 cmd.append("--pretty=format:%s" % pretty_format)
3440 if oneline:
3441 cmd.append("--oneline")
3442
3443 try:
3444 log = GitCommand(
3445 self, cmd, capture_stdout=True, capture_stderr=True
3446 )
3447 if log.Wait() == 0:
3448 return log.stdout
3449 except GitError:
3450 # worktree may not exist if groups changed for example. In that
3451 # case, try in gitdir instead.
3452 if not os.path.exists(self.worktree):
3453 return self.bare_git.log(*cmd[1:])
3454 else:
3455 raise
3456 return None
3457
3458 def getAddedAndRemovedLogs(
3459 self, toProject, oneline=False, color=True, pretty_format=None
3460 ):
3461 """Get the list of logs from this revision to given revisionId"""
3462 logs = {}
3463 selfId = self.GetRevisionId(self._allrefs)
3464 toId = toProject.GetRevisionId(toProject._allrefs)
3465
3466 logs["added"] = self._getLogs(
3467 selfId,
3468 toId,
3469 oneline=oneline,
3470 color=color,
3471 pretty_format=pretty_format,
3472 )
3473 logs["removed"] = self._getLogs(
3474 toId,
3475 selfId,
3476 oneline=oneline,
3477 color=color,
3478 pretty_format=pretty_format,
3479 )
3480 return logs
3481
3482 class _GitGetByExec(object):
3483 def __init__(self, project, bare, gitdir):
3484 self._project = project
3485 self._bare = bare
3486 self._gitdir = gitdir
3487
3488 # __getstate__ and __setstate__ are required for pickling because
3489 # __getattr__ exists.
3490 def __getstate__(self):
3491 return (self._project, self._bare, self._gitdir)
3492
3493 def __setstate__(self, state):
3494 self._project, self._bare, self._gitdir = state
3495
3496 def LsOthers(self):
3497 p = GitCommand(
3498 self._project,
3499 ["ls-files", "-z", "--others", "--exclude-standard"],
3500 bare=False,
3501 gitdir=self._gitdir,
3502 capture_stdout=True,
3503 capture_stderr=True,
3504 )
3505 if p.Wait() == 0:
3506 out = p.stdout
3507 if out:
3508 # Backslash is not anomalous.
3509 return out[:-1].split("\0")
3510 return []
3511
3512 def DiffZ(self, name, *args):
3513 cmd = [name]
3514 cmd.append("-z")
3515 cmd.append("--ignore-submodules")
3516 cmd.extend(args)
3517 p = GitCommand(
3518 self._project,
3519 cmd,
3520 gitdir=self._gitdir,
3521 bare=False,
3522 capture_stdout=True,
3523 capture_stderr=True,
3524 )
3525 p.Wait()
3526 r = {}
3527 out = p.stdout
3528 if out:
3529 out = iter(out[:-1].split("\0"))
3530 while out:
3531 try:
3532 info = next(out)
3533 path = next(out)
3534 except StopIteration:
3535 break
3536
3537 class _Info(object):
3538 def __init__(self, path, omode, nmode, oid, nid, state):
3539 self.path = path
3540 self.src_path = None
3541 self.old_mode = omode
3542 self.new_mode = nmode
3543 self.old_id = oid
3544 self.new_id = nid
3545
3546 if len(state) == 1:
3547 self.status = state
3548 self.level = None
3549 else:
3550 self.status = state[:1]
3551 self.level = state[1:]
3552 while self.level.startswith("0"):
3553 self.level = self.level[1:]
3554
3555 info = info[1:].split(" ")
3556 info = _Info(path, *info)
3557 if info.status in ("R", "C"):
3558 info.src_path = info.path
3559 info.path = next(out)
3560 r[info.path] = info
3561 return r
3562
3563 def GetDotgitPath(self, subpath=None):
3564 """Return the full path to the .git dir.
3565
3566 As a convenience, append |subpath| if provided.
3567 """
3568 if self._bare:
3569 dotgit = self._gitdir
3570 else:
3571 dotgit = os.path.join(self._project.worktree, ".git")
3572 if os.path.isfile(dotgit):
3573 # Git worktrees use a "gitdir:" syntax to point to the
3574 # scratch space.
3575 with open(dotgit) as fp:
3576 setting = fp.read()
3577 assert setting.startswith("gitdir:")
3578 gitdir = setting.split(":", 1)[1].strip()
3579 dotgit = os.path.normpath(
3580 os.path.join(self._project.worktree, gitdir)
3581 )
3582
3583 return dotgit if subpath is None else os.path.join(dotgit, subpath)
3584
3585 def GetHead(self):
3586 """Return the ref that HEAD points to."""
3587 path = self.GetDotgitPath(subpath=HEAD)
3588 try:
3589 with open(path) as fd:
3590 line = fd.readline()
3591 except IOError as e:
3592 raise NoManifestException(path, str(e))
3593 try:
3594 line = line.decode()
3595 except AttributeError:
3596 pass
3597 if line.startswith("ref: "):
3598 return line[5:-1]
3599 return line[:-1]
3600
3601 def SetHead(self, ref, message=None):
3602 cmdv = []
3603 if message is not None:
3604 cmdv.extend(["-m", message])
3605 cmdv.append(HEAD)
3606 cmdv.append(ref)
3607 self.symbolic_ref(*cmdv)
3608
3609 def DetachHead(self, new, message=None):
3610 cmdv = ["--no-deref"]
3611 if message is not None:
3612 cmdv.extend(["-m", message])
3613 cmdv.append(HEAD)
3614 cmdv.append(new)
3615 self.update_ref(*cmdv)
3616
3617 def UpdateRef(self, name, new, old=None, message=None, detach=False):
3618 cmdv = []
3619 if message is not None:
3620 cmdv.extend(["-m", message])
3621 if detach:
3622 cmdv.append("--no-deref")
3623 cmdv.append(name)
3624 cmdv.append(new)
3625 if old is not None:
3626 cmdv.append(old)
3627 self.update_ref(*cmdv)
3628
3629 def DeleteRef(self, name, old=None):
3630 if not old:
3631 old = self.rev_parse(name)
3632 self.update_ref("-d", name, old)
3633 self._project.bare_ref.deleted(name)
3634
3635 def rev_list(self, *args, **kw):
3636 if "format" in kw:
3637 cmdv = ["log", "--pretty=format:%s" % kw["format"]]
3638 else:
3639 cmdv = ["rev-list"]
3640 cmdv.extend(args)
3641 p = GitCommand(
3642 self._project,
3643 cmdv,
3644 bare=self._bare,
3645 gitdir=self._gitdir,
3646 capture_stdout=True,
3647 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003648 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00003649 )
Jason Chang32b59562023-07-14 16:45:35 -07003650 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003651 return p.stdout.splitlines()
3652
3653 def __getattr__(self, name):
3654 """Allow arbitrary git commands using pythonic syntax.
3655
3656 This allows you to do things like:
3657 git_obj.rev_parse('HEAD')
3658
3659 Since we don't have a 'rev_parse' method defined, the __getattr__
3660 will run. We'll replace the '_' with a '-' and try to run a git
3661 command. Any other positional arguments will be passed to the git
3662 command, and the following keyword arguments are supported:
3663 config: An optional dict of git config options to be passed with
3664 '-c'.
3665
3666 Args:
3667 name: The name of the git command to call. Any '_' characters
3668 will be replaced with '-'.
3669
3670 Returns:
3671 A callable object that will try to call git with the named
3672 command.
3673 """
3674 name = name.replace("_", "-")
3675
3676 def runner(*args, **kwargs):
3677 cmdv = []
3678 config = kwargs.pop("config", None)
3679 for k in kwargs:
3680 raise TypeError(
3681 "%s() got an unexpected keyword argument %r" % (name, k)
3682 )
3683 if config is not None:
3684 for k, v in config.items():
3685 cmdv.append("-c")
3686 cmdv.append("%s=%s" % (k, v))
3687 cmdv.append(name)
3688 cmdv.extend(args)
3689 p = GitCommand(
3690 self._project,
3691 cmdv,
3692 bare=self._bare,
3693 gitdir=self._gitdir,
3694 capture_stdout=True,
3695 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003696 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00003697 )
Jason Chang32b59562023-07-14 16:45:35 -07003698 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003699 r = p.stdout
3700 if r.endswith("\n") and r.index("\n") == len(r) - 1:
3701 return r[:-1]
3702 return r
3703
3704 return runner
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003705
3706
Jason Chang32b59562023-07-14 16:45:35 -07003707class LocalSyncFail(RepoError):
3708 """Default error when there is an Sync_LocalHalf error."""
3709
3710
3711class _PriorSyncFailedError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003712 def __str__(self):
3713 return "prior sync failed; rebase still in progress"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003714
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003715
Jason Chang32b59562023-07-14 16:45:35 -07003716class _DirtyError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003717 def __str__(self):
3718 return "contains uncommitted changes"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003719
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003720
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003721class _InfoMessage(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003722 def __init__(self, project, text):
3723 self.project = project
3724 self.text = text
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003725
Gavin Makea2e3302023-03-11 06:46:20 +00003726 def Print(self, syncbuf):
3727 syncbuf.out.info(
3728 "%s/: %s", self.project.RelPath(local=False), self.text
3729 )
3730 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003731
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003732
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003733class _Failure(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003734 def __init__(self, project, why):
3735 self.project = project
3736 self.why = why
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003737
Gavin Makea2e3302023-03-11 06:46:20 +00003738 def Print(self, syncbuf):
3739 syncbuf.out.fail(
3740 "error: %s/: %s", self.project.RelPath(local=False), str(self.why)
3741 )
3742 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003743
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003744
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003745class _Later(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003746 def __init__(self, project, action):
3747 self.project = project
3748 self.action = action
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003749
Gavin Makea2e3302023-03-11 06:46:20 +00003750 def Run(self, syncbuf):
3751 out = syncbuf.out
3752 out.project("project %s/", self.project.RelPath(local=False))
3753 out.nl()
3754 try:
3755 self.action()
3756 out.nl()
3757 return True
3758 except GitError:
3759 out.nl()
3760 return False
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003761
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003762
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003763class _SyncColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +00003764 def __init__(self, config):
3765 super().__init__(config, "reposync")
3766 self.project = self.printer("header", attr="bold")
3767 self.info = self.printer("info")
3768 self.fail = self.printer("fail", fg="red")
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003769
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003770
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003771class SyncBuffer(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003772 def __init__(self, config, detach_head=False):
3773 self._messages = []
3774 self._failures = []
3775 self._later_queue1 = []
3776 self._later_queue2 = []
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003777
Gavin Makea2e3302023-03-11 06:46:20 +00003778 self.out = _SyncColoring(config)
3779 self.out.redirect(sys.stderr)
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003780
Gavin Makea2e3302023-03-11 06:46:20 +00003781 self.detach_head = detach_head
3782 self.clean = True
3783 self.recent_clean = True
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003784
Gavin Makea2e3302023-03-11 06:46:20 +00003785 def info(self, project, fmt, *args):
3786 self._messages.append(_InfoMessage(project, fmt % args))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003787
Gavin Makea2e3302023-03-11 06:46:20 +00003788 def fail(self, project, err=None):
3789 self._failures.append(_Failure(project, err))
David Rileye0684ad2017-04-05 00:02:59 -07003790 self._MarkUnclean()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003791
Gavin Makea2e3302023-03-11 06:46:20 +00003792 def later1(self, project, what):
3793 self._later_queue1.append(_Later(project, what))
Mike Frysinger70d861f2019-08-26 15:22:36 -04003794
Gavin Makea2e3302023-03-11 06:46:20 +00003795 def later2(self, project, what):
3796 self._later_queue2.append(_Later(project, what))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003797
Gavin Makea2e3302023-03-11 06:46:20 +00003798 def Finish(self):
3799 self._PrintMessages()
3800 self._RunLater()
3801 self._PrintMessages()
3802 return self.clean
3803
3804 def Recently(self):
3805 recent_clean = self.recent_clean
3806 self.recent_clean = True
3807 return recent_clean
3808
3809 def _MarkUnclean(self):
3810 self.clean = False
3811 self.recent_clean = False
3812
3813 def _RunLater(self):
3814 for q in ["_later_queue1", "_later_queue2"]:
3815 if not self._RunQueue(q):
3816 return
3817
3818 def _RunQueue(self, queue):
3819 for m in getattr(self, queue):
3820 if not m.Run(self):
3821 self._MarkUnclean()
3822 return False
3823 setattr(self, queue, [])
3824 return True
3825
3826 def _PrintMessages(self):
3827 if self._messages or self._failures:
3828 if os.isatty(2):
3829 self.out.write(progress.CSI_ERASE_LINE)
3830 self.out.write("\r")
3831
3832 for m in self._messages:
3833 m.Print(self)
3834 for m in self._failures:
3835 m.Print(self)
3836
3837 self._messages = []
3838 self._failures = []
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003839
3840
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003841class MetaProject(Project):
Gavin Makea2e3302023-03-11 06:46:20 +00003842 """A special project housed under .repo."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003843
Gavin Makea2e3302023-03-11 06:46:20 +00003844 def __init__(self, manifest, name, gitdir, worktree):
3845 Project.__init__(
3846 self,
3847 manifest=manifest,
3848 name=name,
3849 gitdir=gitdir,
3850 objdir=gitdir,
3851 worktree=worktree,
3852 remote=RemoteSpec("origin"),
3853 relpath=".repo/%s" % name,
3854 revisionExpr="refs/heads/master",
3855 revisionId=None,
3856 groups=None,
3857 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003858
Gavin Makea2e3302023-03-11 06:46:20 +00003859 def PreSync(self):
3860 if self.Exists:
3861 cb = self.CurrentBranch
3862 if cb:
3863 base = self.GetBranch(cb).merge
3864 if base:
3865 self.revisionExpr = base
3866 self.revisionId = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003867
Gavin Makea2e3302023-03-11 06:46:20 +00003868 @property
3869 def HasChanges(self):
3870 """Has the remote received new commits not yet checked out?"""
3871 if not self.remote or not self.revisionExpr:
3872 return False
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003873
Gavin Makea2e3302023-03-11 06:46:20 +00003874 all_refs = self.bare_ref.all
3875 revid = self.GetRevisionId(all_refs)
3876 head = self.work_git.GetHead()
3877 if head.startswith(R_HEADS):
3878 try:
3879 head = all_refs[head]
3880 except KeyError:
3881 head = None
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003882
Gavin Makea2e3302023-03-11 06:46:20 +00003883 if revid == head:
3884 return False
3885 elif self._revlist(not_rev(HEAD), revid):
3886 return True
3887 return False
LaMont Jones9b72cf22022-03-29 21:54:22 +00003888
3889
3890class RepoProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003891 """The MetaProject for repo itself."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003892
Gavin Makea2e3302023-03-11 06:46:20 +00003893 @property
3894 def LastFetch(self):
3895 try:
3896 fh = os.path.join(self.gitdir, "FETCH_HEAD")
3897 return os.path.getmtime(fh)
3898 except OSError:
3899 return 0
LaMont Jones9b72cf22022-03-29 21:54:22 +00003900
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +01003901
LaMont Jones9b72cf22022-03-29 21:54:22 +00003902class ManifestProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003903 """The MetaProject for manifests."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003904
Gavin Makea2e3302023-03-11 06:46:20 +00003905 def MetaBranchSwitch(self, submodules=False):
3906 """Prepare for manifest branch switch."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003907
Gavin Makea2e3302023-03-11 06:46:20 +00003908 # detach and delete manifest branch, allowing a new
3909 # branch to take over
3910 syncbuf = SyncBuffer(self.config, detach_head=True)
3911 self.Sync_LocalHalf(syncbuf, submodules=submodules)
3912 syncbuf.Finish()
LaMont Jones9b72cf22022-03-29 21:54:22 +00003913
Gavin Makea2e3302023-03-11 06:46:20 +00003914 return (
3915 GitCommand(
3916 self,
3917 ["update-ref", "-d", "refs/heads/default"],
3918 capture_stdout=True,
3919 capture_stderr=True,
3920 ).Wait()
3921 == 0
3922 )
LaMont Jones9b03f152022-03-29 23:01:18 +00003923
Gavin Makea2e3302023-03-11 06:46:20 +00003924 @property
3925 def standalone_manifest_url(self):
3926 """The URL of the standalone manifest, or None."""
3927 return self.config.GetString("manifest.standalone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003928
Gavin Makea2e3302023-03-11 06:46:20 +00003929 @property
3930 def manifest_groups(self):
3931 """The manifest groups string."""
3932 return self.config.GetString("manifest.groups")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003933
Gavin Makea2e3302023-03-11 06:46:20 +00003934 @property
3935 def reference(self):
3936 """The --reference for this manifest."""
3937 return self.config.GetString("repo.reference")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003938
Gavin Makea2e3302023-03-11 06:46:20 +00003939 @property
3940 def dissociate(self):
3941 """Whether to dissociate."""
3942 return self.config.GetBoolean("repo.dissociate")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003943
Gavin Makea2e3302023-03-11 06:46:20 +00003944 @property
3945 def archive(self):
3946 """Whether we use archive."""
3947 return self.config.GetBoolean("repo.archive")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003948
Gavin Makea2e3302023-03-11 06:46:20 +00003949 @property
3950 def mirror(self):
3951 """Whether we use mirror."""
3952 return self.config.GetBoolean("repo.mirror")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003953
Gavin Makea2e3302023-03-11 06:46:20 +00003954 @property
3955 def use_worktree(self):
3956 """Whether we use worktree."""
3957 return self.config.GetBoolean("repo.worktree")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003958
Gavin Makea2e3302023-03-11 06:46:20 +00003959 @property
3960 def clone_bundle(self):
3961 """Whether we use clone_bundle."""
3962 return self.config.GetBoolean("repo.clonebundle")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003963
Gavin Makea2e3302023-03-11 06:46:20 +00003964 @property
3965 def submodules(self):
3966 """Whether we use submodules."""
3967 return self.config.GetBoolean("repo.submodules")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003968
Gavin Makea2e3302023-03-11 06:46:20 +00003969 @property
3970 def git_lfs(self):
3971 """Whether we use git_lfs."""
3972 return self.config.GetBoolean("repo.git-lfs")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003973
Gavin Makea2e3302023-03-11 06:46:20 +00003974 @property
3975 def use_superproject(self):
3976 """Whether we use superproject."""
3977 return self.config.GetBoolean("repo.superproject")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003978
Gavin Makea2e3302023-03-11 06:46:20 +00003979 @property
3980 def partial_clone(self):
3981 """Whether this is a partial clone."""
3982 return self.config.GetBoolean("repo.partialclone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003983
Gavin Makea2e3302023-03-11 06:46:20 +00003984 @property
3985 def depth(self):
3986 """Partial clone depth."""
3987 return self.config.GetString("repo.depth")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003988
Gavin Makea2e3302023-03-11 06:46:20 +00003989 @property
3990 def clone_filter(self):
3991 """The clone filter."""
3992 return self.config.GetString("repo.clonefilter")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003993
Gavin Makea2e3302023-03-11 06:46:20 +00003994 @property
3995 def partial_clone_exclude(self):
3996 """Partial clone exclude string"""
3997 return self.config.GetString("repo.partialcloneexclude")
LaMont Jones4ada0432022-04-14 15:10:43 +00003998
Gavin Makea2e3302023-03-11 06:46:20 +00003999 @property
Jason Chang17833322023-05-23 13:06:55 -07004000 def clone_filter_for_depth(self):
4001 """Replace shallow clone with partial clone."""
4002 return self.config.GetString("repo.clonefilterfordepth")
4003
4004 @property
Gavin Makea2e3302023-03-11 06:46:20 +00004005 def manifest_platform(self):
4006 """The --platform argument from `repo init`."""
4007 return self.config.GetString("manifest.platform")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004008
Gavin Makea2e3302023-03-11 06:46:20 +00004009 @property
4010 def _platform_name(self):
4011 """Return the name of the platform."""
4012 return platform.system().lower()
LaMont Jones9b03f152022-03-29 23:01:18 +00004013
Gavin Makea2e3302023-03-11 06:46:20 +00004014 def SyncWithPossibleInit(
4015 self,
4016 submanifest,
4017 verbose=False,
4018 current_branch_only=False,
4019 tags="",
4020 git_event_log=None,
4021 ):
4022 """Sync a manifestProject, possibly for the first time.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004023
Gavin Makea2e3302023-03-11 06:46:20 +00004024 Call Sync() with arguments from the most recent `repo init`. If this is
4025 a new sub manifest, then inherit options from the parent's
4026 manifestProject.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004027
Gavin Makea2e3302023-03-11 06:46:20 +00004028 This is used by subcmds.Sync() to do an initial download of new sub
4029 manifests.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004030
Gavin Makea2e3302023-03-11 06:46:20 +00004031 Args:
4032 submanifest: an XmlSubmanifest, the submanifest to re-sync.
4033 verbose: a boolean, whether to show all output, rather than only
4034 errors.
4035 current_branch_only: a boolean, whether to only fetch the current
4036 manifest branch from the server.
4037 tags: a boolean, whether to fetch tags.
4038 git_event_log: an EventLog, for git tracing.
4039 """
4040 # TODO(lamontjones): when refactoring sync (and init?) consider how to
4041 # better get the init options that we should use for new submanifests
4042 # that are added when syncing an existing workspace.
4043 git_event_log = git_event_log or EventLog()
LaMont Jonesb90a4222022-04-14 15:00:09 +00004044 spec = submanifest.ToSubmanifestSpec()
Gavin Makea2e3302023-03-11 06:46:20 +00004045 # Use the init options from the existing manifestProject, or the parent
4046 # if it doesn't exist.
4047 #
4048 # Today, we only support changing manifest_groups on the sub-manifest,
4049 # with no supported-for-the-user way to change the other arguments from
4050 # those specified by the outermost manifest.
4051 #
4052 # TODO(lamontjones): determine which of these should come from the
4053 # outermost manifest and which should come from the parent manifest.
4054 mp = self if self.Exists else submanifest.parent.manifestProject
4055 return self.Sync(
LaMont Jones55ee3042022-04-06 17:10:21 +00004056 manifest_url=spec.manifestUrl,
4057 manifest_branch=spec.revision,
Gavin Makea2e3302023-03-11 06:46:20 +00004058 standalone_manifest=mp.standalone_manifest_url,
4059 groups=mp.manifest_groups,
4060 platform=mp.manifest_platform,
4061 mirror=mp.mirror,
4062 dissociate=mp.dissociate,
4063 reference=mp.reference,
4064 worktree=mp.use_worktree,
4065 submodules=mp.submodules,
4066 archive=mp.archive,
4067 partial_clone=mp.partial_clone,
4068 clone_filter=mp.clone_filter,
4069 partial_clone_exclude=mp.partial_clone_exclude,
4070 clone_bundle=mp.clone_bundle,
4071 git_lfs=mp.git_lfs,
4072 use_superproject=mp.use_superproject,
LaMont Jones55ee3042022-04-06 17:10:21 +00004073 verbose=verbose,
4074 current_branch_only=current_branch_only,
4075 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00004076 depth=mp.depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004077 git_event_log=git_event_log,
4078 manifest_name=spec.manifestName,
Gavin Makea2e3302023-03-11 06:46:20 +00004079 this_manifest_only=True,
LaMont Jones55ee3042022-04-06 17:10:21 +00004080 outer_manifest=False,
Jason Chang17833322023-05-23 13:06:55 -07004081 clone_filter_for_depth=mp.clone_filter_for_depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004082 )
LaMont Jones409407a2022-04-05 21:21:56 +00004083
Gavin Makea2e3302023-03-11 06:46:20 +00004084 def Sync(
4085 self,
4086 _kwargs_only=(),
4087 manifest_url="",
4088 manifest_branch=None,
4089 standalone_manifest=False,
4090 groups="",
4091 mirror=False,
4092 reference="",
4093 dissociate=False,
4094 worktree=False,
4095 submodules=False,
4096 archive=False,
4097 partial_clone=None,
4098 depth=None,
4099 clone_filter="blob:none",
4100 partial_clone_exclude=None,
4101 clone_bundle=None,
4102 git_lfs=None,
4103 use_superproject=None,
4104 verbose=False,
4105 current_branch_only=False,
4106 git_event_log=None,
4107 platform="",
4108 manifest_name="default.xml",
4109 tags="",
4110 this_manifest_only=False,
4111 outer_manifest=True,
Jason Chang17833322023-05-23 13:06:55 -07004112 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00004113 ):
4114 """Sync the manifest and all submanifests.
LaMont Jones409407a2022-04-05 21:21:56 +00004115
Gavin Makea2e3302023-03-11 06:46:20 +00004116 Args:
4117 manifest_url: a string, the URL of the manifest project.
4118 manifest_branch: a string, the manifest branch to use.
4119 standalone_manifest: a boolean, whether to store the manifest as a
4120 static file.
4121 groups: a string, restricts the checkout to projects with the
4122 specified groups.
4123 mirror: a boolean, whether to create a mirror of the remote
4124 repository.
4125 reference: a string, location of a repo instance to use as a
4126 reference.
4127 dissociate: a boolean, whether to dissociate from reference mirrors
4128 after clone.
4129 worktree: a boolean, whether to use git-worktree to manage projects.
4130 submodules: a boolean, whether sync submodules associated with the
4131 manifest project.
4132 archive: a boolean, whether to checkout each project as an archive.
4133 See git-archive.
4134 partial_clone: a boolean, whether to perform a partial clone.
4135 depth: an int, how deep of a shallow clone to create.
4136 clone_filter: a string, filter to use with partial_clone.
4137 partial_clone_exclude : a string, comma-delimeted list of project
4138 names to exclude from partial clone.
4139 clone_bundle: a boolean, whether to enable /clone.bundle on
4140 HTTP/HTTPS.
4141 git_lfs: a boolean, whether to enable git LFS support.
4142 use_superproject: a boolean, whether to use the manifest
4143 superproject to sync projects.
4144 verbose: a boolean, whether to show all output, rather than only
4145 errors.
4146 current_branch_only: a boolean, whether to only fetch the current
4147 manifest branch from the server.
4148 platform: a string, restrict the checkout to projects with the
4149 specified platform group.
4150 git_event_log: an EventLog, for git tracing.
4151 tags: a boolean, whether to fetch tags.
4152 manifest_name: a string, the name of the manifest file to use.
4153 this_manifest_only: a boolean, whether to only operate on the
4154 current sub manifest.
4155 outer_manifest: a boolean, whether to start at the outermost
4156 manifest.
Jason Chang17833322023-05-23 13:06:55 -07004157 clone_filter_for_depth: a string, when specified replaces shallow
4158 clones with partial.
LaMont Jones9b03f152022-03-29 23:01:18 +00004159
Gavin Makea2e3302023-03-11 06:46:20 +00004160 Returns:
4161 a boolean, whether the sync was successful.
4162 """
4163 assert _kwargs_only == (), "Sync only accepts keyword arguments."
LaMont Jones9b03f152022-03-29 23:01:18 +00004164
Gavin Makea2e3302023-03-11 06:46:20 +00004165 groups = groups or self.manifest.GetDefaultGroupsStr(
4166 with_platform=False
4167 )
4168 platform = platform or "auto"
4169 git_event_log = git_event_log or EventLog()
4170 if outer_manifest and self.manifest.is_submanifest:
4171 # In a multi-manifest checkout, use the outer manifest unless we are
4172 # told not to.
4173 return self.client.outer_manifest.manifestProject.Sync(
4174 manifest_url=manifest_url,
4175 manifest_branch=manifest_branch,
4176 standalone_manifest=standalone_manifest,
4177 groups=groups,
4178 platform=platform,
4179 mirror=mirror,
4180 dissociate=dissociate,
4181 reference=reference,
4182 worktree=worktree,
4183 submodules=submodules,
4184 archive=archive,
4185 partial_clone=partial_clone,
4186 clone_filter=clone_filter,
4187 partial_clone_exclude=partial_clone_exclude,
4188 clone_bundle=clone_bundle,
4189 git_lfs=git_lfs,
4190 use_superproject=use_superproject,
4191 verbose=verbose,
4192 current_branch_only=current_branch_only,
4193 tags=tags,
4194 depth=depth,
4195 git_event_log=git_event_log,
4196 manifest_name=manifest_name,
4197 this_manifest_only=this_manifest_only,
4198 outer_manifest=False,
4199 )
LaMont Jones9b03f152022-03-29 23:01:18 +00004200
Gavin Makea2e3302023-03-11 06:46:20 +00004201 # If repo has already been initialized, we take -u with the absence of
4202 # --standalone-manifest to mean "transition to a standard repo set up",
4203 # which necessitates starting fresh.
4204 # If --standalone-manifest is set, we always tear everything down and
4205 # start anew.
4206 if self.Exists:
4207 was_standalone_manifest = self.config.GetString(
4208 "manifest.standalone"
4209 )
4210 if was_standalone_manifest and not manifest_url:
4211 print(
4212 "fatal: repo was initialized with a standlone manifest, "
4213 "cannot be re-initialized without --manifest-url/-u"
4214 )
4215 return False
4216
4217 if standalone_manifest or (
4218 was_standalone_manifest and manifest_url
4219 ):
4220 self.config.ClearCache()
4221 if self.gitdir and os.path.exists(self.gitdir):
4222 platform_utils.rmtree(self.gitdir)
4223 if self.worktree and os.path.exists(self.worktree):
4224 platform_utils.rmtree(self.worktree)
4225
4226 is_new = not self.Exists
4227 if is_new:
4228 if not manifest_url:
4229 print("fatal: manifest url is required.", file=sys.stderr)
4230 return False
4231
4232 if verbose:
4233 print(
4234 "Downloading manifest from %s"
4235 % (GitConfig.ForUser().UrlInsteadOf(manifest_url),),
4236 file=sys.stderr,
4237 )
4238
4239 # The manifest project object doesn't keep track of the path on the
4240 # server where this git is located, so let's save that here.
4241 mirrored_manifest_git = None
4242 if reference:
4243 manifest_git_path = urllib.parse.urlparse(manifest_url).path[1:]
4244 mirrored_manifest_git = os.path.join(
4245 reference, manifest_git_path
4246 )
4247 if not mirrored_manifest_git.endswith(".git"):
4248 mirrored_manifest_git += ".git"
4249 if not os.path.exists(mirrored_manifest_git):
4250 mirrored_manifest_git = os.path.join(
4251 reference, ".repo/manifests.git"
4252 )
4253
4254 self._InitGitDir(mirror_git=mirrored_manifest_git)
4255
4256 # If standalone_manifest is set, mark the project as "standalone" --
4257 # we'll still do much of the manifests.git set up, but will avoid actual
4258 # syncs to a remote.
4259 if standalone_manifest:
4260 self.config.SetString("manifest.standalone", manifest_url)
4261 elif not manifest_url and not manifest_branch:
4262 # If -u is set and --standalone-manifest is not, then we're not in
4263 # standalone mode. Otherwise, use config to infer what we were in
4264 # the last init.
4265 standalone_manifest = bool(
4266 self.config.GetString("manifest.standalone")
4267 )
4268 if not standalone_manifest:
4269 self.config.SetString("manifest.standalone", None)
4270
4271 self._ConfigureDepth(depth)
4272
4273 # Set the remote URL before the remote branch as we might need it below.
4274 if manifest_url:
4275 r = self.GetRemote()
4276 r.url = manifest_url
4277 r.ResetFetch()
4278 r.Save()
4279
4280 if not standalone_manifest:
4281 if manifest_branch:
4282 if manifest_branch == "HEAD":
4283 manifest_branch = self.ResolveRemoteHead()
4284 if manifest_branch is None:
4285 print("fatal: unable to resolve HEAD", file=sys.stderr)
4286 return False
4287 self.revisionExpr = manifest_branch
4288 else:
4289 if is_new:
4290 default_branch = self.ResolveRemoteHead()
4291 if default_branch is None:
4292 # If the remote doesn't have HEAD configured, default to
4293 # master.
4294 default_branch = "refs/heads/master"
4295 self.revisionExpr = default_branch
4296 else:
4297 self.PreSync()
4298
4299 groups = re.split(r"[,\s]+", groups or "")
4300 all_platforms = ["linux", "darwin", "windows"]
4301 platformize = lambda x: "platform-" + x
4302 if platform == "auto":
4303 if not mirror and not self.mirror:
4304 groups.append(platformize(self._platform_name))
4305 elif platform == "all":
4306 groups.extend(map(platformize, all_platforms))
4307 elif platform in all_platforms:
4308 groups.append(platformize(platform))
4309 elif platform != "none":
4310 print("fatal: invalid platform flag", file=sys.stderr)
4311 return False
4312 self.config.SetString("manifest.platform", platform)
4313
4314 groups = [x for x in groups if x]
4315 groupstr = ",".join(groups)
4316 if (
4317 platform == "auto"
4318 and groupstr == self.manifest.GetDefaultGroupsStr()
4319 ):
4320 groupstr = None
4321 self.config.SetString("manifest.groups", groupstr)
4322
4323 if reference:
4324 self.config.SetString("repo.reference", reference)
4325
4326 if dissociate:
4327 self.config.SetBoolean("repo.dissociate", dissociate)
4328
4329 if worktree:
4330 if mirror:
4331 print(
4332 "fatal: --mirror and --worktree are incompatible",
4333 file=sys.stderr,
4334 )
4335 return False
4336 if submodules:
4337 print(
4338 "fatal: --submodules and --worktree are incompatible",
4339 file=sys.stderr,
4340 )
4341 return False
4342 self.config.SetBoolean("repo.worktree", worktree)
4343 if is_new:
4344 self.use_git_worktrees = True
4345 print("warning: --worktree is experimental!", file=sys.stderr)
4346
4347 if archive:
4348 if is_new:
4349 self.config.SetBoolean("repo.archive", archive)
4350 else:
4351 print(
4352 "fatal: --archive is only supported when initializing a "
4353 "new workspace.",
4354 file=sys.stderr,
4355 )
4356 print(
4357 "Either delete the .repo folder in this workspace, or "
4358 "initialize in another location.",
4359 file=sys.stderr,
4360 )
4361 return False
4362
4363 if mirror:
4364 if is_new:
4365 self.config.SetBoolean("repo.mirror", mirror)
4366 else:
4367 print(
4368 "fatal: --mirror is only supported when initializing a new "
4369 "workspace.",
4370 file=sys.stderr,
4371 )
4372 print(
4373 "Either delete the .repo folder in this workspace, or "
4374 "initialize in another location.",
4375 file=sys.stderr,
4376 )
4377 return False
4378
4379 if partial_clone is not None:
4380 if mirror:
4381 print(
4382 "fatal: --mirror and --partial-clone are mutually "
4383 "exclusive",
4384 file=sys.stderr,
4385 )
4386 return False
4387 self.config.SetBoolean("repo.partialclone", partial_clone)
4388 if clone_filter:
4389 self.config.SetString("repo.clonefilter", clone_filter)
4390 elif self.partial_clone:
4391 clone_filter = self.clone_filter
4392 else:
4393 clone_filter = None
4394
4395 if partial_clone_exclude is not None:
4396 self.config.SetString(
4397 "repo.partialcloneexclude", partial_clone_exclude
4398 )
4399
4400 if clone_bundle is None:
4401 clone_bundle = False if partial_clone else True
4402 else:
4403 self.config.SetBoolean("repo.clonebundle", clone_bundle)
4404
4405 if submodules:
4406 self.config.SetBoolean("repo.submodules", submodules)
4407
4408 if git_lfs is not None:
4409 if git_lfs:
4410 git_require((2, 17, 0), fail=True, msg="Git LFS support")
4411
4412 self.config.SetBoolean("repo.git-lfs", git_lfs)
4413 if not is_new:
4414 print(
4415 "warning: Changing --git-lfs settings will only affect new "
4416 "project checkouts.\n"
4417 " Existing projects will require manual updates.\n",
4418 file=sys.stderr,
4419 )
4420
Jason Chang17833322023-05-23 13:06:55 -07004421 if clone_filter_for_depth is not None:
4422 self.ConfigureCloneFilterForDepth(clone_filter_for_depth)
4423
Gavin Makea2e3302023-03-11 06:46:20 +00004424 if use_superproject is not None:
4425 self.config.SetBoolean("repo.superproject", use_superproject)
4426
4427 if not standalone_manifest:
4428 success = self.Sync_NetworkHalf(
4429 is_new=is_new,
4430 quiet=not verbose,
4431 verbose=verbose,
4432 clone_bundle=clone_bundle,
4433 current_branch_only=current_branch_only,
4434 tags=tags,
4435 submodules=submodules,
4436 clone_filter=clone_filter,
4437 partial_clone_exclude=self.manifest.PartialCloneExclude,
Jason Chang17833322023-05-23 13:06:55 -07004438 clone_filter_for_depth=self.manifest.CloneFilterForDepth,
Gavin Makea2e3302023-03-11 06:46:20 +00004439 ).success
4440 if not success:
4441 r = self.GetRemote()
4442 print(
4443 "fatal: cannot obtain manifest %s" % r.url, file=sys.stderr
4444 )
4445
4446 # Better delete the manifest git dir if we created it; otherwise
4447 # next time (when user fixes problems) we won't go through the
4448 # "is_new" logic.
4449 if is_new:
4450 platform_utils.rmtree(self.gitdir)
4451 return False
4452
4453 if manifest_branch:
4454 self.MetaBranchSwitch(submodules=submodules)
4455
4456 syncbuf = SyncBuffer(self.config)
4457 self.Sync_LocalHalf(syncbuf, submodules=submodules)
4458 syncbuf.Finish()
4459
4460 if is_new or self.CurrentBranch is None:
4461 if not self.StartBranch("default"):
4462 print(
4463 "fatal: cannot create default in manifest",
4464 file=sys.stderr,
4465 )
4466 return False
4467
4468 if not manifest_name:
4469 print("fatal: manifest name (-m) is required.", file=sys.stderr)
4470 return False
4471
4472 elif is_new:
4473 # This is a new standalone manifest.
4474 manifest_name = "default.xml"
4475 manifest_data = fetch.fetch_file(manifest_url, verbose=verbose)
4476 dest = os.path.join(self.worktree, manifest_name)
4477 os.makedirs(os.path.dirname(dest), exist_ok=True)
4478 with open(dest, "wb") as f:
4479 f.write(manifest_data)
4480
4481 try:
4482 self.manifest.Link(manifest_name)
4483 except ManifestParseError as e:
4484 print(
4485 "fatal: manifest '%s' not available" % manifest_name,
4486 file=sys.stderr,
4487 )
4488 print("fatal: %s" % str(e), file=sys.stderr)
4489 return False
4490
4491 if not this_manifest_only:
4492 for submanifest in self.manifest.submanifests.values():
4493 spec = submanifest.ToSubmanifestSpec()
4494 submanifest.repo_client.manifestProject.Sync(
4495 manifest_url=spec.manifestUrl,
4496 manifest_branch=spec.revision,
4497 standalone_manifest=standalone_manifest,
4498 groups=self.manifest_groups,
4499 platform=platform,
4500 mirror=mirror,
4501 dissociate=dissociate,
4502 reference=reference,
4503 worktree=worktree,
4504 submodules=submodules,
4505 archive=archive,
4506 partial_clone=partial_clone,
4507 clone_filter=clone_filter,
4508 partial_clone_exclude=partial_clone_exclude,
4509 clone_bundle=clone_bundle,
4510 git_lfs=git_lfs,
4511 use_superproject=use_superproject,
4512 verbose=verbose,
4513 current_branch_only=current_branch_only,
4514 tags=tags,
4515 depth=depth,
4516 git_event_log=git_event_log,
4517 manifest_name=spec.manifestName,
4518 this_manifest_only=False,
4519 outer_manifest=False,
4520 )
4521
4522 # Lastly, if the manifest has a <superproject> then have the
4523 # superproject sync it (if it will be used).
4524 if git_superproject.UseSuperproject(use_superproject, self.manifest):
4525 sync_result = self.manifest.superproject.Sync(git_event_log)
4526 if not sync_result.success:
4527 submanifest = ""
4528 if self.manifest.path_prefix:
4529 submanifest = f"for {self.manifest.path_prefix} "
4530 print(
4531 f"warning: git update of superproject {submanifest}failed, "
4532 "repo sync will not use superproject to fetch source; "
4533 "while this error is not fatal, and you can continue to "
4534 "run repo sync, please run repo init with the "
4535 "--no-use-superproject option to stop seeing this warning",
4536 file=sys.stderr,
4537 )
4538 if sync_result.fatal and use_superproject is not None:
4539 return False
4540
4541 return True
4542
Jason Chang17833322023-05-23 13:06:55 -07004543 def ConfigureCloneFilterForDepth(self, clone_filter_for_depth):
4544 """Configure clone filter to replace shallow clones.
4545
4546 Args:
4547 clone_filter_for_depth: a string or None, e.g. 'blob:none' will
4548 disable shallow clones and replace with partial clone. None will
4549 enable shallow clones.
4550 """
4551 self.config.SetString(
4552 "repo.clonefilterfordepth", clone_filter_for_depth
4553 )
4554
Gavin Makea2e3302023-03-11 06:46:20 +00004555 def _ConfigureDepth(self, depth):
4556 """Configure the depth we'll sync down.
4557
4558 Args:
4559 depth: an int, how deep of a partial clone to create.
4560 """
4561 # Opt.depth will be non-None if user actually passed --depth to repo
4562 # init.
4563 if depth is not None:
4564 if depth > 0:
4565 # Positive values will set the depth.
4566 depth = str(depth)
4567 else:
4568 # Negative numbers will clear the depth; passing None to
4569 # SetString will do that.
4570 depth = None
4571
4572 # We store the depth in the main manifest project.
4573 self.config.SetString("repo.depth", depth)