blob: ff018393c2133a7682fd6c3642ed508dd4342b7e [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
LaMont Jones1eddca82022-09-01 15:15:04 +000029from typing import NamedTuple
Mike Frysingeracf63b22019-06-13 02:24:21 -040030import urllib.parse
Shawn O. Pearcedf5ee522011-10-11 14:05:21 -070031
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070032from color import Coloring
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
Remy Bohmer16c13282020-09-10 10:38:04 +020044from error import GitError, UploadError, DownloadError
Mike Frysingere6a202f2019-08-02 15:57:57 -040045from error import ManifestInvalidRevisionError, ManifestInvalidPathError
LaMont Jones409407a2022-04-05 21:21:56 +000046from error import NoManifestException, ManifestParseError
Renaud Paquayd5cec5e2016-11-01 11:24:03 -070047import platform_utils
Mike Frysinger70d861f2019-08-26 15:22:36 -040048import progress
Joanna Wanga6c52f52022-11-03 16:51:19 -040049from repo_trace import Trace
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070050
Mike Frysinger21b7fbe2020-02-26 23:53:36 -050051from 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 -070052
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -070053
LaMont Jones1eddca82022-09-01 15:15:04 +000054class SyncNetworkHalfResult(NamedTuple):
Gavin Makea2e3302023-03-11 06:46:20 +000055 """Sync_NetworkHalf return value."""
56
57 # True if successful.
58 success: bool
59 # Did we query the remote? False when optimized_fetch is True and we have
60 # the commit already present.
61 remote_fetched: bool
LaMont Jones1eddca82022-09-01 15:15:04 +000062
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +010063
George Engelbrecht9bc283e2020-04-02 12:36:09 -060064# Maximum sleep time allowed during retries.
65MAXIMUM_RETRY_SLEEP_SEC = 3600.0
66# +-10% random jitter is added to each Fetches retry sleep duration.
67RETRY_JITTER_PERCENT = 0.1
68
LaMont Jonesfa8d9392022-11-02 22:01:29 +000069# Whether to use alternates. Switching back and forth is *NOT* supported.
Mike Frysinger1d00a7e2021-12-21 00:40:31 -050070# TODO(vapier): Remove knob once behavior is verified.
Gavin Makea2e3302023-03-11 06:46:20 +000071_ALTERNATES = os.environ.get("REPO_USE_ALTERNATES") == "1"
George Engelbrecht9bc283e2020-04-02 12:36:09 -060072
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +010073
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -070074def _lwrite(path, content):
Gavin Makea2e3302023-03-11 06:46:20 +000075 lock = "%s.lock" % path
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -070076
Gavin Makea2e3302023-03-11 06:46:20 +000077 # Maintain Unix line endings on all OS's to match git behavior.
78 with open(lock, "w", newline="\n") as fd:
79 fd.write(content)
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -070080
Gavin Makea2e3302023-03-11 06:46:20 +000081 try:
82 platform_utils.rename(lock, path)
83 except OSError:
84 platform_utils.remove(lock)
85 raise
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -070086
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -070087
Shawn O. Pearce48244782009-04-16 08:25:57 -070088def _error(fmt, *args):
Gavin Makea2e3302023-03-11 06:46:20 +000089 msg = fmt % args
90 print("error: %s" % msg, file=sys.stderr)
Shawn O. Pearce48244782009-04-16 08:25:57 -070091
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -070092
David Pursehousef33929d2015-08-24 14:39:14 +090093def _warn(fmt, *args):
Gavin Makea2e3302023-03-11 06:46:20 +000094 msg = fmt % args
95 print("warn: %s" % msg, file=sys.stderr)
David Pursehousef33929d2015-08-24 14:39:14 +090096
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -070097
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070098def not_rev(r):
Gavin Makea2e3302023-03-11 06:46:20 +000099 return "^" + r
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700100
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700101
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800102def sq(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000103 return "'" + r.replace("'", "'''") + "'"
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -0800104
David Pursehouse819827a2020-02-12 15:20:19 +0900105
Jonathan Nieder93719792015-03-17 11:29:58 -0700106_project_hook_list = None
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700107
108
Jonathan Nieder93719792015-03-17 11:29:58 -0700109def _ProjectHooks():
Gavin Makea2e3302023-03-11 06:46:20 +0000110 """List the hooks present in the 'hooks' directory.
Jonathan Nieder93719792015-03-17 11:29:58 -0700111
Gavin Makea2e3302023-03-11 06:46:20 +0000112 These hooks are project hooks and are copied to the '.git/hooks' directory
113 of all subprojects.
Jonathan Nieder93719792015-03-17 11:29:58 -0700114
Gavin Makea2e3302023-03-11 06:46:20 +0000115 This function caches the list of hooks (based on the contents of the
116 'repo/hooks' directory) on the first call.
Jonathan Nieder93719792015-03-17 11:29:58 -0700117
Gavin Makea2e3302023-03-11 06:46:20 +0000118 Returns:
119 A list of absolute paths to all of the files in the hooks directory.
120 """
121 global _project_hook_list
122 if _project_hook_list is None:
123 d = platform_utils.realpath(os.path.abspath(os.path.dirname(__file__)))
124 d = os.path.join(d, "hooks")
125 _project_hook_list = [
126 os.path.join(d, x) for x in platform_utils.listdir(d)
127 ]
128 return _project_hook_list
Jonathan Nieder93719792015-03-17 11:29:58 -0700129
130
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700131class DownloadedChange(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000132 _commit_cache = None
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700133
Gavin Makea2e3302023-03-11 06:46:20 +0000134 def __init__(self, project, base, change_id, ps_id, commit):
135 self.project = project
136 self.base = base
137 self.change_id = change_id
138 self.ps_id = ps_id
139 self.commit = commit
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700140
Gavin Makea2e3302023-03-11 06:46:20 +0000141 @property
142 def commits(self):
143 if self._commit_cache is None:
144 self._commit_cache = self.project.bare_git.rev_list(
145 "--abbrev=8",
146 "--abbrev-commit",
147 "--pretty=oneline",
148 "--reverse",
149 "--date-order",
150 not_rev(self.base),
151 self.commit,
152 "--",
153 )
154 return self._commit_cache
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700155
156
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700157class ReviewableBranch(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000158 _commit_cache = None
159 _base_exists = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700160
Gavin Makea2e3302023-03-11 06:46:20 +0000161 def __init__(self, project, branch, base):
162 self.project = project
163 self.branch = branch
164 self.base = base
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700165
Gavin Makea2e3302023-03-11 06:46:20 +0000166 @property
167 def name(self):
168 return self.branch.name
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700169
Gavin Makea2e3302023-03-11 06:46:20 +0000170 @property
171 def commits(self):
172 if self._commit_cache is None:
173 args = (
174 "--abbrev=8",
175 "--abbrev-commit",
176 "--pretty=oneline",
177 "--reverse",
178 "--date-order",
179 not_rev(self.base),
180 R_HEADS + self.name,
181 "--",
182 )
183 try:
184 self._commit_cache = self.project.bare_git.rev_list(*args)
185 except GitError:
186 # We weren't able to probe the commits for this branch. Was it
187 # tracking a branch that no longer exists? If so, return no
188 # commits. Otherwise, rethrow the error as we don't know what's
189 # going on.
190 if self.base_exists:
191 raise
Mike Frysinger6da17752019-09-11 18:43:17 -0400192
Gavin Makea2e3302023-03-11 06:46:20 +0000193 self._commit_cache = []
Mike Frysinger6da17752019-09-11 18:43:17 -0400194
Gavin Makea2e3302023-03-11 06:46:20 +0000195 return self._commit_cache
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700196
Gavin Makea2e3302023-03-11 06:46:20 +0000197 @property
198 def unabbrev_commits(self):
199 r = dict()
200 for commit in self.project.bare_git.rev_list(
201 not_rev(self.base), R_HEADS + self.name, "--"
202 ):
203 r[commit[0:8]] = commit
204 return r
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800205
Gavin Makea2e3302023-03-11 06:46:20 +0000206 @property
207 def date(self):
208 return self.project.bare_git.log(
209 "--pretty=format:%cd", "-n", "1", R_HEADS + self.name, "--"
210 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700211
Gavin Makea2e3302023-03-11 06:46:20 +0000212 @property
213 def base_exists(self):
214 """Whether the branch we're tracking exists.
Mike Frysinger6da17752019-09-11 18:43:17 -0400215
Gavin Makea2e3302023-03-11 06:46:20 +0000216 Normally it should, but sometimes branches we track can get deleted.
217 """
218 if self._base_exists is None:
219 try:
220 self.project.bare_git.rev_parse("--verify", not_rev(self.base))
221 # If we're still here, the base branch exists.
222 self._base_exists = True
223 except GitError:
224 # If we failed to verify, the base branch doesn't exist.
225 self._base_exists = False
Mike Frysinger6da17752019-09-11 18:43:17 -0400226
Gavin Makea2e3302023-03-11 06:46:20 +0000227 return self._base_exists
Mike Frysinger6da17752019-09-11 18:43:17 -0400228
Gavin Makea2e3302023-03-11 06:46:20 +0000229 def UploadForReview(
230 self,
231 people,
232 dryrun=False,
233 auto_topic=False,
234 hashtags=(),
235 labels=(),
236 private=False,
237 notify=None,
238 wip=False,
239 ready=False,
240 dest_branch=None,
241 validate_certs=True,
242 push_options=None,
243 ):
244 self.project.UploadForReview(
245 branch=self.name,
246 people=people,
247 dryrun=dryrun,
248 auto_topic=auto_topic,
249 hashtags=hashtags,
250 labels=labels,
251 private=private,
252 notify=notify,
253 wip=wip,
254 ready=ready,
255 dest_branch=dest_branch,
256 validate_certs=validate_certs,
257 push_options=push_options,
258 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700259
Gavin Makea2e3302023-03-11 06:46:20 +0000260 def GetPublishedRefs(self):
261 refs = {}
262 output = self.project.bare_git.ls_remote(
263 self.branch.remote.SshReviewUrl(self.project.UserEmail),
264 "refs/changes/*",
265 )
266 for line in output.split("\n"):
267 try:
268 (sha, ref) = line.split()
269 refs[sha] = ref
270 except ValueError:
271 pass
Ficus Kirkpatrickbc7ef672009-05-04 12:45:11 -0700272
Gavin Makea2e3302023-03-11 06:46:20 +0000273 return refs
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700274
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700275
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700276class StatusColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000277 def __init__(self, config):
278 super().__init__(config, "status")
279 self.project = self.printer("header", attr="bold")
280 self.branch = self.printer("header", attr="bold")
281 self.nobranch = self.printer("nobranch", fg="red")
282 self.important = self.printer("important", fg="red")
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700283
Gavin Makea2e3302023-03-11 06:46:20 +0000284 self.added = self.printer("added", fg="green")
285 self.changed = self.printer("changed", fg="red")
286 self.untracked = self.printer("untracked", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700287
288
289class DiffColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000290 def __init__(self, config):
291 super().__init__(config, "diff")
292 self.project = self.printer("header", attr="bold")
293 self.fail = self.printer("fail", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700294
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700295
Jack Neus6ea0cae2021-07-20 20:52:33 +0000296class Annotation(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000297 def __init__(self, name, value, keep):
298 self.name = name
299 self.value = value
300 self.keep = keep
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700301
Gavin Makea2e3302023-03-11 06:46:20 +0000302 def __eq__(self, other):
303 if not isinstance(other, Annotation):
304 return False
305 return self.__dict__ == other.__dict__
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700306
Gavin Makea2e3302023-03-11 06:46:20 +0000307 def __lt__(self, other):
308 # This exists just so that lists of Annotation objects can be sorted,
309 # for use in comparisons.
310 if not isinstance(other, Annotation):
311 raise ValueError("comparison is not between two Annotation objects")
312 if self.name == other.name:
313 if self.value == other.value:
314 return self.keep < other.keep
315 return self.value < other.value
316 return self.name < other.name
Jack Neus6ea0cae2021-07-20 20:52:33 +0000317
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700318
Mike Frysingere6a202f2019-08-02 15:57:57 -0400319def _SafeExpandPath(base, subpath, skipfinal=False):
Gavin Makea2e3302023-03-11 06:46:20 +0000320 """Make sure |subpath| is completely safe under |base|.
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700321
Gavin Makea2e3302023-03-11 06:46:20 +0000322 We make sure no intermediate symlinks are traversed, and that the final path
323 is not a special file (e.g. not a socket or fifo).
Mike Frysingere6a202f2019-08-02 15:57:57 -0400324
Gavin Makea2e3302023-03-11 06:46:20 +0000325 NB: We rely on a number of paths already being filtered out while parsing
326 the manifest. See the validation logic in manifest_xml.py for more details.
327 """
328 # Split up the path by its components. We can't use os.path.sep exclusively
329 # as some platforms (like Windows) will convert / to \ and that bypasses all
330 # our constructed logic here. Especially since manifest authors only use
331 # / in their paths.
332 resep = re.compile(r"[/%s]" % re.escape(os.path.sep))
333 components = resep.split(subpath)
334 if skipfinal:
335 # Whether the caller handles the final component itself.
336 finalpart = components.pop()
Mike Frysingere6a202f2019-08-02 15:57:57 -0400337
Gavin Makea2e3302023-03-11 06:46:20 +0000338 path = base
339 for part in components:
340 if part in {".", ".."}:
341 raise ManifestInvalidPathError(
342 '%s: "%s" not allowed in paths' % (subpath, part)
343 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400344
Gavin Makea2e3302023-03-11 06:46:20 +0000345 path = os.path.join(path, part)
346 if platform_utils.islink(path):
347 raise ManifestInvalidPathError(
348 "%s: traversing symlinks not allow" % (path,)
349 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400350
Gavin Makea2e3302023-03-11 06:46:20 +0000351 if os.path.exists(path):
352 if not os.path.isfile(path) and not platform_utils.isdir(path):
353 raise ManifestInvalidPathError(
354 "%s: only regular files & directories allowed" % (path,)
355 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400356
Gavin Makea2e3302023-03-11 06:46:20 +0000357 if skipfinal:
358 path = os.path.join(path, finalpart)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400359
Gavin Makea2e3302023-03-11 06:46:20 +0000360 return path
Mike Frysingere6a202f2019-08-02 15:57:57 -0400361
362
363class _CopyFile(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000364 """Container for <copyfile> manifest element."""
Mike Frysingere6a202f2019-08-02 15:57:57 -0400365
Gavin Makea2e3302023-03-11 06:46:20 +0000366 def __init__(self, git_worktree, src, topdir, dest):
367 """Register a <copyfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400368
Gavin Makea2e3302023-03-11 06:46:20 +0000369 Args:
370 git_worktree: Absolute path to the git project checkout.
371 src: Relative path under |git_worktree| of file to read.
372 topdir: Absolute path to the top of the repo client checkout.
373 dest: Relative path under |topdir| of file to write.
374 """
375 self.git_worktree = git_worktree
376 self.topdir = topdir
377 self.src = src
378 self.dest = dest
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700379
Gavin Makea2e3302023-03-11 06:46:20 +0000380 def _Copy(self):
381 src = _SafeExpandPath(self.git_worktree, self.src)
382 dest = _SafeExpandPath(self.topdir, self.dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400383
Gavin Makea2e3302023-03-11 06:46:20 +0000384 if platform_utils.isdir(src):
385 raise ManifestInvalidPathError(
386 "%s: copying from directory not supported" % (self.src,)
387 )
388 if platform_utils.isdir(dest):
389 raise ManifestInvalidPathError(
390 "%s: copying to directory not allowed" % (self.dest,)
391 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400392
Gavin Makea2e3302023-03-11 06:46:20 +0000393 # Copy file if it does not exist or is out of date.
394 if not os.path.exists(dest) or not filecmp.cmp(src, dest):
395 try:
396 # Remove existing file first, since it might be read-only.
397 if os.path.exists(dest):
398 platform_utils.remove(dest)
399 else:
400 dest_dir = os.path.dirname(dest)
401 if not platform_utils.isdir(dest_dir):
402 os.makedirs(dest_dir)
403 shutil.copy(src, dest)
404 # Make the file read-only.
405 mode = os.stat(dest)[stat.ST_MODE]
406 mode = mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
407 os.chmod(dest, mode)
408 except IOError:
409 _error("Cannot copy file %s to %s", src, dest)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700410
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700411
Anthony King7bdac712014-07-16 12:56:40 +0100412class _LinkFile(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000413 """Container for <linkfile> manifest element."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700414
Gavin Makea2e3302023-03-11 06:46:20 +0000415 def __init__(self, git_worktree, src, topdir, dest):
416 """Register a <linkfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400417
Gavin Makea2e3302023-03-11 06:46:20 +0000418 Args:
419 git_worktree: Absolute path to the git project checkout.
420 src: Target of symlink relative to path under |git_worktree|.
421 topdir: Absolute path to the top of the repo client checkout.
422 dest: Relative path under |topdir| of symlink to create.
423 """
424 self.git_worktree = git_worktree
425 self.topdir = topdir
426 self.src = src
427 self.dest = dest
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500428
Gavin Makea2e3302023-03-11 06:46:20 +0000429 def __linkIt(self, relSrc, absDest):
430 # Link file if it does not exist or is out of date.
431 if not platform_utils.islink(absDest) or (
432 platform_utils.readlink(absDest) != relSrc
433 ):
434 try:
435 # Remove existing file first, since it might be read-only.
436 if os.path.lexists(absDest):
437 platform_utils.remove(absDest)
438 else:
439 dest_dir = os.path.dirname(absDest)
440 if not platform_utils.isdir(dest_dir):
441 os.makedirs(dest_dir)
442 platform_utils.symlink(relSrc, absDest)
443 except IOError:
444 _error("Cannot link file %s to %s", relSrc, absDest)
445
446 def _Link(self):
447 """Link the self.src & self.dest paths.
448
449 Handles wild cards on the src linking all of the files in the source in
450 to the destination directory.
451 """
452 # Some people use src="." to create stable links to projects. Let's
453 # allow that but reject all other uses of "." to keep things simple.
454 if self.src == ".":
455 src = self.git_worktree
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500456 else:
Gavin Makea2e3302023-03-11 06:46:20 +0000457 src = _SafeExpandPath(self.git_worktree, self.src)
Wink Saville4c426ef2015-06-03 08:05:17 -0700458
Gavin Makea2e3302023-03-11 06:46:20 +0000459 if not glob.has_magic(src):
460 # Entity does not contain a wild card so just a simple one to one
461 # link operation.
462 dest = _SafeExpandPath(self.topdir, self.dest, skipfinal=True)
463 # dest & src are absolute paths at this point. Make sure the target
464 # of the symlink is relative in the context of the repo client
465 # checkout.
466 relpath = os.path.relpath(src, os.path.dirname(dest))
467 self.__linkIt(relpath, dest)
468 else:
469 dest = _SafeExpandPath(self.topdir, self.dest)
470 # Entity contains a wild card.
471 if os.path.exists(dest) and not platform_utils.isdir(dest):
472 _error(
473 "Link error: src with wildcard, %s must be a directory",
474 dest,
475 )
476 else:
477 for absSrcFile in glob.glob(src):
478 # Create a releative path from source dir to destination
479 # dir.
480 absSrcDir = os.path.dirname(absSrcFile)
481 relSrcDir = os.path.relpath(absSrcDir, dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400482
Gavin Makea2e3302023-03-11 06:46:20 +0000483 # Get the source file name.
484 srcFile = os.path.basename(absSrcFile)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400485
Gavin Makea2e3302023-03-11 06:46:20 +0000486 # Now form the final full paths to srcFile. They will be
487 # absolute for the desintaiton and relative for the source.
488 absDest = os.path.join(dest, srcFile)
489 relSrc = os.path.join(relSrcDir, srcFile)
490 self.__linkIt(relSrc, absDest)
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500491
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700492
Shawn O. Pearced1f70d92009-05-19 14:58:02 -0700493class RemoteSpec(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000494 def __init__(
495 self,
496 name,
497 url=None,
498 pushUrl=None,
499 review=None,
500 revision=None,
501 orig_name=None,
502 fetchUrl=None,
503 ):
504 self.name = name
505 self.url = url
506 self.pushUrl = pushUrl
507 self.review = review
508 self.revision = revision
509 self.orig_name = orig_name
510 self.fetchUrl = fetchUrl
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700511
Ian Kasprzak0286e312021-02-05 10:06:18 -0800512
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700513class Project(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000514 # These objects can be shared between several working trees.
515 @property
516 def shareable_dirs(self):
517 """Return the shareable directories"""
518 if self.UseAlternates:
519 return ["hooks", "rr-cache"]
520 else:
521 return ["hooks", "objects", "rr-cache"]
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700522
Gavin Makea2e3302023-03-11 06:46:20 +0000523 def __init__(
524 self,
525 manifest,
526 name,
527 remote,
528 gitdir,
529 objdir,
530 worktree,
531 relpath,
532 revisionExpr,
533 revisionId,
534 rebase=True,
535 groups=None,
536 sync_c=False,
537 sync_s=False,
538 sync_tags=True,
539 clone_depth=None,
540 upstream=None,
541 parent=None,
542 use_git_worktrees=False,
543 is_derived=False,
544 dest_branch=None,
545 optimized_fetch=False,
546 retry_fetches=0,
547 old_revision=None,
548 ):
549 """Init a Project object.
Doug Anderson3ba5f952011-04-07 12:51:04 -0700550
551 Args:
Gavin Makea2e3302023-03-11 06:46:20 +0000552 manifest: The XmlManifest object.
553 name: The `name` attribute of manifest.xml's project element.
554 remote: RemoteSpec object specifying its remote's properties.
555 gitdir: Absolute path of git directory.
556 objdir: Absolute path of directory to store git objects.
557 worktree: Absolute path of git working tree.
558 relpath: Relative path of git working tree to repo's top directory.
559 revisionExpr: The `revision` attribute of manifest.xml's project
560 element.
561 revisionId: git commit id for checking out.
562 rebase: The `rebase` attribute of manifest.xml's project element.
563 groups: The `groups` attribute of manifest.xml's project element.
564 sync_c: The `sync-c` attribute of manifest.xml's project element.
565 sync_s: The `sync-s` attribute of manifest.xml's project element.
566 sync_tags: The `sync-tags` attribute of manifest.xml's project
567 element.
568 upstream: The `upstream` attribute of manifest.xml's project
569 element.
570 parent: The parent Project object.
571 use_git_worktrees: Whether to use `git worktree` for this project.
572 is_derived: False if the project was explicitly defined in the
573 manifest; True if the project is a discovered submodule.
574 dest_branch: The branch to which to push changes for review by
575 default.
576 optimized_fetch: If True, when a project is set to a sha1 revision,
577 only fetch from the remote if the sha1 is not present locally.
578 retry_fetches: Retry remote fetches n times upon receiving transient
579 error with exponential backoff and jitter.
580 old_revision: saved git commit id for open GITC projects.
581 """
582 self.client = self.manifest = manifest
583 self.name = name
584 self.remote = remote
585 self.UpdatePaths(relpath, worktree, gitdir, objdir)
586 self.SetRevision(revisionExpr, revisionId=revisionId)
587
588 self.rebase = rebase
589 self.groups = groups
590 self.sync_c = sync_c
591 self.sync_s = sync_s
592 self.sync_tags = sync_tags
593 self.clone_depth = clone_depth
594 self.upstream = upstream
595 self.parent = parent
596 # NB: Do not use this setting in __init__ to change behavior so that the
597 # manifest.git checkout can inspect & change it after instantiating.
598 # See the XmlManifest init code for more info.
599 self.use_git_worktrees = use_git_worktrees
600 self.is_derived = is_derived
601 self.optimized_fetch = optimized_fetch
602 self.retry_fetches = max(0, retry_fetches)
603 self.subprojects = []
604
605 self.snapshots = {}
606 self.copyfiles = []
607 self.linkfiles = []
608 self.annotations = []
609 self.dest_branch = dest_branch
610 self.old_revision = old_revision
611
612 # This will be filled in if a project is later identified to be the
613 # project containing repo hooks.
614 self.enabled_repo_hooks = []
615
616 def RelPath(self, local=True):
617 """Return the path for the project relative to a manifest.
618
619 Args:
620 local: a boolean, if True, the path is relative to the local
621 (sub)manifest. If false, the path is relative to the outermost
622 manifest.
623 """
624 if local:
625 return self.relpath
626 return os.path.join(self.manifest.path_prefix, self.relpath)
627
628 def SetRevision(self, revisionExpr, revisionId=None):
629 """Set revisionId based on revision expression and id"""
630 self.revisionExpr = revisionExpr
631 if revisionId is None and revisionExpr and IsId(revisionExpr):
632 self.revisionId = self.revisionExpr
633 else:
634 self.revisionId = revisionId
635
636 def UpdatePaths(self, relpath, worktree, gitdir, objdir):
637 """Update paths used by this project"""
638 self.gitdir = gitdir.replace("\\", "/")
639 self.objdir = objdir.replace("\\", "/")
640 if worktree:
641 self.worktree = os.path.normpath(worktree).replace("\\", "/")
642 else:
643 self.worktree = None
644 self.relpath = relpath
645
646 self.config = GitConfig.ForRepository(
647 gitdir=self.gitdir, defaults=self.manifest.globalConfig
648 )
649
650 if self.worktree:
651 self.work_git = self._GitGetByExec(
652 self, bare=False, gitdir=self.gitdir
653 )
654 else:
655 self.work_git = None
656 self.bare_git = self._GitGetByExec(self, bare=True, gitdir=self.gitdir)
657 self.bare_ref = GitRefs(self.gitdir)
658 self.bare_objdir = self._GitGetByExec(
659 self, bare=True, gitdir=self.objdir
660 )
661
662 @property
663 def UseAlternates(self):
664 """Whether git alternates are in use.
665
666 This will be removed once migration to alternates is complete.
667 """
668 return _ALTERNATES or self.manifest.is_multimanifest
669
670 @property
671 def Derived(self):
672 return self.is_derived
673
674 @property
675 def Exists(self):
676 return platform_utils.isdir(self.gitdir) and platform_utils.isdir(
677 self.objdir
678 )
679
680 @property
681 def CurrentBranch(self):
682 """Obtain the name of the currently checked out branch.
683
684 The branch name omits the 'refs/heads/' prefix.
685 None is returned if the project is on a detached HEAD, or if the
686 work_git is otheriwse inaccessible (e.g. an incomplete sync).
687 """
688 try:
689 b = self.work_git.GetHead()
690 except NoManifestException:
691 # If the local checkout is in a bad state, don't barf. Let the
692 # callers process this like the head is unreadable.
693 return None
694 if b.startswith(R_HEADS):
695 return b[len(R_HEADS) :]
696 return None
697
698 def IsRebaseInProgress(self):
699 return (
700 os.path.exists(self.work_git.GetDotgitPath("rebase-apply"))
701 or os.path.exists(self.work_git.GetDotgitPath("rebase-merge"))
702 or os.path.exists(os.path.join(self.worktree, ".dotest"))
703 )
704
705 def IsDirty(self, consider_untracked=True):
706 """Is the working directory modified in some way?"""
707 self.work_git.update_index(
708 "-q", "--unmerged", "--ignore-missing", "--refresh"
709 )
710 if self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD):
711 return True
712 if self.work_git.DiffZ("diff-files"):
713 return True
714 if consider_untracked and self.UntrackedFiles():
715 return True
716 return False
717
718 _userident_name = None
719 _userident_email = None
720
721 @property
722 def UserName(self):
723 """Obtain the user's personal name."""
724 if self._userident_name is None:
725 self._LoadUserIdentity()
726 return self._userident_name
727
728 @property
729 def UserEmail(self):
730 """Obtain the user's email address. This is very likely
731 to be their Gerrit login.
732 """
733 if self._userident_email is None:
734 self._LoadUserIdentity()
735 return self._userident_email
736
737 def _LoadUserIdentity(self):
738 u = self.bare_git.var("GIT_COMMITTER_IDENT")
739 m = re.compile("^(.*) <([^>]*)> ").match(u)
740 if m:
741 self._userident_name = m.group(1)
742 self._userident_email = m.group(2)
743 else:
744 self._userident_name = ""
745 self._userident_email = ""
746
747 def GetRemote(self, name=None):
748 """Get the configuration for a single remote.
749
750 Defaults to the current project's remote.
751 """
752 if name is None:
753 name = self.remote.name
754 return self.config.GetRemote(name)
755
756 def GetBranch(self, name):
757 """Get the configuration for a single branch."""
758 return self.config.GetBranch(name)
759
760 def GetBranches(self):
761 """Get all existing local branches."""
762 current = self.CurrentBranch
763 all_refs = self._allrefs
764 heads = {}
765
766 for name, ref_id in all_refs.items():
767 if name.startswith(R_HEADS):
768 name = name[len(R_HEADS) :]
769 b = self.GetBranch(name)
770 b.current = name == current
771 b.published = None
772 b.revision = ref_id
773 heads[name] = b
774
775 for name, ref_id in all_refs.items():
776 if name.startswith(R_PUB):
777 name = name[len(R_PUB) :]
778 b = heads.get(name)
779 if b:
780 b.published = ref_id
781
782 return heads
783
784 def MatchesGroups(self, manifest_groups):
785 """Returns true if the manifest groups specified at init should cause
786 this project to be synced.
787 Prefixing a manifest group with "-" inverts the meaning of a group.
788 All projects are implicitly labelled with "all".
789
790 labels are resolved in order. In the example case of
791 project_groups: "all,group1,group2"
792 manifest_groups: "-group1,group2"
793 the project will be matched.
794
795 The special manifest group "default" will match any project that
796 does not have the special project group "notdefault"
797 """
798 default_groups = self.manifest.default_groups or ["default"]
799 expanded_manifest_groups = manifest_groups or default_groups
800 expanded_project_groups = ["all"] + (self.groups or [])
801 if "notdefault" not in expanded_project_groups:
802 expanded_project_groups += ["default"]
803
804 matched = False
805 for group in expanded_manifest_groups:
806 if group.startswith("-") and group[1:] in expanded_project_groups:
807 matched = False
808 elif group in expanded_project_groups:
809 matched = True
810
811 return matched
812
813 def UncommitedFiles(self, get_all=True):
814 """Returns a list of strings, uncommitted files in the git tree.
815
816 Args:
817 get_all: a boolean, if True - get information about all different
818 uncommitted files. If False - return as soon as any kind of
819 uncommitted files is detected.
820 """
821 details = []
822 self.work_git.update_index(
823 "-q", "--unmerged", "--ignore-missing", "--refresh"
824 )
825 if self.IsRebaseInProgress():
826 details.append("rebase in progress")
827 if not get_all:
828 return details
829
830 changes = self.work_git.DiffZ("diff-index", "--cached", HEAD).keys()
831 if changes:
832 details.extend(changes)
833 if not get_all:
834 return details
835
836 changes = self.work_git.DiffZ("diff-files").keys()
837 if changes:
838 details.extend(changes)
839 if not get_all:
840 return details
841
842 changes = self.UntrackedFiles()
843 if changes:
844 details.extend(changes)
845
846 return details
847
848 def UntrackedFiles(self):
849 """Returns a list of strings, untracked files in the git tree."""
850 return self.work_git.LsOthers()
851
852 def HasChanges(self):
853 """Returns true if there are uncommitted changes."""
854 return bool(self.UncommitedFiles(get_all=False))
855
856 def PrintWorkTreeStatus(self, output_redir=None, quiet=False, local=False):
857 """Prints the status of the repository to stdout.
858
859 Args:
860 output_redir: If specified, redirect the output to this object.
861 quiet: If True then only print the project name. Do not print
862 the modified files, branch name, etc.
863 local: a boolean, if True, the path is relative to the local
864 (sub)manifest. If false, the path is relative to the outermost
865 manifest.
866 """
867 if not platform_utils.isdir(self.worktree):
868 if output_redir is None:
869 output_redir = sys.stdout
870 print(file=output_redir)
871 print("project %s/" % self.RelPath(local), file=output_redir)
872 print(' missing (run "repo sync")', file=output_redir)
873 return
874
875 self.work_git.update_index(
876 "-q", "--unmerged", "--ignore-missing", "--refresh"
877 )
878 rb = self.IsRebaseInProgress()
879 di = self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD)
880 df = self.work_git.DiffZ("diff-files")
881 do = self.work_git.LsOthers()
882 if not rb and not di and not df and not do and not self.CurrentBranch:
883 return "CLEAN"
884
885 out = StatusColoring(self.config)
886 if output_redir is not None:
887 out.redirect(output_redir)
888 out.project("project %-40s", self.RelPath(local) + "/ ")
889
890 if quiet:
891 out.nl()
892 return "DIRTY"
893
894 branch = self.CurrentBranch
895 if branch is None:
896 out.nobranch("(*** NO BRANCH ***)")
897 else:
898 out.branch("branch %s", branch)
899 out.nl()
900
901 if rb:
902 out.important("prior sync failed; rebase still in progress")
903 out.nl()
904
905 paths = list()
906 paths.extend(di.keys())
907 paths.extend(df.keys())
908 paths.extend(do)
909
910 for p in sorted(set(paths)):
911 try:
912 i = di[p]
913 except KeyError:
914 i = None
915
916 try:
917 f = df[p]
918 except KeyError:
919 f = None
920
921 if i:
922 i_status = i.status.upper()
923 else:
924 i_status = "-"
925
926 if f:
927 f_status = f.status.lower()
928 else:
929 f_status = "-"
930
931 if i and i.src_path:
932 line = " %s%s\t%s => %s (%s%%)" % (
933 i_status,
934 f_status,
935 i.src_path,
936 p,
937 i.level,
938 )
939 else:
940 line = " %s%s\t%s" % (i_status, f_status, p)
941
942 if i and not f:
943 out.added("%s", line)
944 elif (i and f) or (not i and f):
945 out.changed("%s", line)
946 elif not i and not f:
947 out.untracked("%s", line)
948 else:
949 out.write("%s", line)
950 out.nl()
951
952 return "DIRTY"
953
954 def PrintWorkTreeDiff(
955 self, absolute_paths=False, output_redir=None, local=False
956 ):
957 """Prints the status of the repository to stdout."""
958 out = DiffColoring(self.config)
959 if output_redir:
960 out.redirect(output_redir)
961 cmd = ["diff"]
962 if out.is_on:
963 cmd.append("--color")
964 cmd.append(HEAD)
965 if absolute_paths:
966 cmd.append("--src-prefix=a/%s/" % self.RelPath(local))
967 cmd.append("--dst-prefix=b/%s/" % self.RelPath(local))
968 cmd.append("--")
969 try:
970 p = GitCommand(self, cmd, capture_stdout=True, capture_stderr=True)
971 p.Wait()
972 except GitError as e:
973 out.nl()
974 out.project("project %s/" % self.RelPath(local))
975 out.nl()
976 out.fail("%s", str(e))
977 out.nl()
978 return False
979 if p.stdout:
980 out.nl()
981 out.project("project %s/" % self.RelPath(local))
982 out.nl()
983 out.write("%s", p.stdout)
984 return p.Wait() == 0
985
986 def WasPublished(self, branch, all_refs=None):
987 """Was the branch published (uploaded) for code review?
988 If so, returns the SHA-1 hash of the last published
989 state for the branch.
990 """
991 key = R_PUB + branch
992 if all_refs is None:
993 try:
994 return self.bare_git.rev_parse(key)
995 except GitError:
996 return None
997 else:
998 try:
999 return all_refs[key]
1000 except KeyError:
1001 return None
1002
1003 def CleanPublishedCache(self, all_refs=None):
1004 """Prunes any stale published refs."""
1005 if all_refs is None:
1006 all_refs = self._allrefs
1007 heads = set()
1008 canrm = {}
1009 for name, ref_id in all_refs.items():
1010 if name.startswith(R_HEADS):
1011 heads.add(name)
1012 elif name.startswith(R_PUB):
1013 canrm[name] = ref_id
1014
1015 for name, ref_id in canrm.items():
1016 n = name[len(R_PUB) :]
1017 if R_HEADS + n not in heads:
1018 self.bare_git.DeleteRef(name, ref_id)
1019
1020 def GetUploadableBranches(self, selected_branch=None):
1021 """List any branches which can be uploaded for review."""
1022 heads = {}
1023 pubed = {}
1024
1025 for name, ref_id in self._allrefs.items():
1026 if name.startswith(R_HEADS):
1027 heads[name[len(R_HEADS) :]] = ref_id
1028 elif name.startswith(R_PUB):
1029 pubed[name[len(R_PUB) :]] = ref_id
1030
1031 ready = []
1032 for branch, ref_id in heads.items():
1033 if branch in pubed and pubed[branch] == ref_id:
1034 continue
1035 if selected_branch and branch != selected_branch:
1036 continue
1037
1038 rb = self.GetUploadableBranch(branch)
1039 if rb:
1040 ready.append(rb)
1041 return ready
1042
1043 def GetUploadableBranch(self, branch_name):
1044 """Get a single uploadable branch, or None."""
1045 branch = self.GetBranch(branch_name)
1046 base = branch.LocalMerge
1047 if branch.LocalMerge:
1048 rb = ReviewableBranch(self, branch, base)
1049 if rb.commits:
1050 return rb
1051 return None
1052
1053 def UploadForReview(
1054 self,
1055 branch=None,
1056 people=([], []),
1057 dryrun=False,
1058 auto_topic=False,
1059 hashtags=(),
1060 labels=(),
1061 private=False,
1062 notify=None,
1063 wip=False,
1064 ready=False,
1065 dest_branch=None,
1066 validate_certs=True,
1067 push_options=None,
1068 ):
1069 """Uploads the named branch for code review."""
1070 if branch is None:
1071 branch = self.CurrentBranch
1072 if branch is None:
1073 raise GitError("not currently on a branch")
1074
1075 branch = self.GetBranch(branch)
1076 if not branch.LocalMerge:
1077 raise GitError("branch %s does not track a remote" % branch.name)
1078 if not branch.remote.review:
1079 raise GitError("remote %s has no review url" % branch.remote.name)
1080
1081 # Basic validity check on label syntax.
1082 for label in labels:
1083 if not re.match(r"^.+[+-][0-9]+$", label):
1084 raise UploadError(
1085 f'invalid label syntax "{label}": labels use forms like '
1086 "CodeReview+1 or Verified-1"
1087 )
1088
1089 if dest_branch is None:
1090 dest_branch = self.dest_branch
1091 if dest_branch is None:
1092 dest_branch = branch.merge
1093 if not dest_branch.startswith(R_HEADS):
1094 dest_branch = R_HEADS + dest_branch
1095
1096 if not branch.remote.projectname:
1097 branch.remote.projectname = self.name
1098 branch.remote.Save()
1099
1100 url = branch.remote.ReviewUrl(self.UserEmail, validate_certs)
1101 if url is None:
1102 raise UploadError("review not configured")
1103 cmd = ["push"]
1104 if dryrun:
1105 cmd.append("-n")
1106
1107 if url.startswith("ssh://"):
1108 cmd.append("--receive-pack=gerrit receive-pack")
1109
Aravind Vasudevan99ebf622023-04-04 23:44:37 +00001110 # This stops git from pushing all reachable annotated tags when
1111 # push.followTags is configured. Gerrit does not accept any tags
1112 # pushed to a CL.
1113 if git_require((1, 8, 3)):
1114 cmd.append("--no-follow-tags")
1115
Gavin Makea2e3302023-03-11 06:46:20 +00001116 for push_option in push_options or []:
1117 cmd.append("-o")
1118 cmd.append(push_option)
1119
1120 cmd.append(url)
1121
1122 if dest_branch.startswith(R_HEADS):
1123 dest_branch = dest_branch[len(R_HEADS) :]
1124
1125 ref_spec = "%s:refs/for/%s" % (R_HEADS + branch.name, dest_branch)
1126 opts = []
1127 if auto_topic:
1128 opts += ["topic=" + branch.name]
1129 opts += ["t=%s" % p for p in hashtags]
1130 # NB: No need to encode labels as they've been validated above.
1131 opts += ["l=%s" % p for p in labels]
1132
1133 opts += ["r=%s" % p for p in people[0]]
1134 opts += ["cc=%s" % p for p in people[1]]
1135 if notify:
1136 opts += ["notify=" + notify]
1137 if private:
1138 opts += ["private"]
1139 if wip:
1140 opts += ["wip"]
1141 if ready:
1142 opts += ["ready"]
1143 if opts:
1144 ref_spec = ref_spec + "%" + ",".join(opts)
1145 cmd.append(ref_spec)
1146
1147 if GitCommand(self, cmd, bare=True).Wait() != 0:
1148 raise UploadError("Upload failed")
1149
1150 if not dryrun:
1151 msg = "posted to %s for %s" % (branch.remote.review, dest_branch)
1152 self.bare_git.UpdateRef(
1153 R_PUB + branch.name, R_HEADS + branch.name, message=msg
1154 )
1155
1156 def _ExtractArchive(self, tarpath, path=None):
1157 """Extract the given tar on its current location
1158
1159 Args:
1160 tarpath: The path to the actual tar file
1161
1162 """
1163 try:
1164 with tarfile.open(tarpath, "r") as tar:
1165 tar.extractall(path=path)
1166 return True
1167 except (IOError, tarfile.TarError) as e:
1168 _error("Cannot extract archive %s: %s", tarpath, str(e))
1169 return False
1170
1171 def Sync_NetworkHalf(
1172 self,
1173 quiet=False,
1174 verbose=False,
1175 output_redir=None,
1176 is_new=None,
1177 current_branch_only=None,
1178 force_sync=False,
1179 clone_bundle=True,
1180 tags=None,
1181 archive=False,
1182 optimized_fetch=False,
1183 retry_fetches=0,
1184 prune=False,
1185 submodules=False,
1186 ssh_proxy=None,
1187 clone_filter=None,
1188 partial_clone_exclude=set(),
1189 ):
1190 """Perform only the network IO portion of the sync process.
1191 Local working directory/branch state is not affected.
1192 """
1193 if archive and not isinstance(self, MetaProject):
1194 if self.remote.url.startswith(("http://", "https://")):
1195 _error(
1196 "%s: Cannot fetch archives from http/https remotes.",
1197 self.name,
1198 )
1199 return SyncNetworkHalfResult(False, False)
1200
1201 name = self.relpath.replace("\\", "/")
1202 name = name.replace("/", "_")
1203 tarpath = "%s.tar" % name
1204 topdir = self.manifest.topdir
1205
1206 try:
1207 self._FetchArchive(tarpath, cwd=topdir)
1208 except GitError as e:
1209 _error("%s", e)
1210 return SyncNetworkHalfResult(False, False)
1211
1212 # From now on, we only need absolute tarpath.
1213 tarpath = os.path.join(topdir, tarpath)
1214
1215 if not self._ExtractArchive(tarpath, path=topdir):
1216 return SyncNetworkHalfResult(False, True)
1217 try:
1218 platform_utils.remove(tarpath)
1219 except OSError as e:
1220 _warn("Cannot remove archive %s: %s", tarpath, str(e))
1221 self._CopyAndLinkFiles()
1222 return SyncNetworkHalfResult(True, True)
1223
1224 # If the shared object dir already exists, don't try to rebootstrap with
1225 # a clone bundle download. We should have the majority of objects
1226 # already.
1227 if clone_bundle and os.path.exists(self.objdir):
1228 clone_bundle = False
1229
1230 if self.name in partial_clone_exclude:
1231 clone_bundle = True
1232 clone_filter = None
1233
1234 if is_new is None:
1235 is_new = not self.Exists
1236 if is_new:
1237 self._InitGitDir(force_sync=force_sync, quiet=quiet)
1238 else:
1239 self._UpdateHooks(quiet=quiet)
1240 self._InitRemote()
1241
1242 if self.UseAlternates:
1243 # If gitdir/objects is a symlink, migrate it from the old layout.
1244 gitdir_objects = os.path.join(self.gitdir, "objects")
1245 if platform_utils.islink(gitdir_objects):
1246 platform_utils.remove(gitdir_objects, missing_ok=True)
1247 gitdir_alt = os.path.join(self.gitdir, "objects/info/alternates")
1248 if not os.path.exists(gitdir_alt):
1249 os.makedirs(os.path.dirname(gitdir_alt), exist_ok=True)
1250 _lwrite(
1251 gitdir_alt,
1252 os.path.join(
1253 os.path.relpath(self.objdir, gitdir_objects), "objects"
1254 )
1255 + "\n",
1256 )
1257
1258 if is_new:
1259 alt = os.path.join(self.objdir, "objects/info/alternates")
1260 try:
1261 with open(alt) as fd:
1262 # This works for both absolute and relative alternate
1263 # directories.
1264 alt_dir = os.path.join(
1265 self.objdir, "objects", fd.readline().rstrip()
1266 )
1267 except IOError:
1268 alt_dir = None
1269 else:
1270 alt_dir = None
1271
1272 if (
1273 clone_bundle
1274 and alt_dir is None
1275 and self._ApplyCloneBundle(
1276 initial=is_new, quiet=quiet, verbose=verbose
1277 )
1278 ):
1279 is_new = False
1280
1281 if current_branch_only is None:
1282 if self.sync_c:
1283 current_branch_only = True
1284 elif not self.manifest._loaded:
1285 # Manifest cannot check defaults until it syncs.
1286 current_branch_only = False
1287 elif self.manifest.default.sync_c:
1288 current_branch_only = True
1289
1290 if tags is None:
1291 tags = self.sync_tags
1292
1293 if self.clone_depth:
1294 depth = self.clone_depth
1295 else:
1296 depth = self.manifest.manifestProject.depth
1297
1298 # See if we can skip the network fetch entirely.
1299 remote_fetched = False
1300 if not (
1301 optimized_fetch
1302 and (
1303 ID_RE.match(self.revisionExpr)
1304 and self._CheckForImmutableRevision()
1305 )
1306 ):
1307 remote_fetched = True
1308 if not self._RemoteFetch(
1309 initial=is_new,
1310 quiet=quiet,
1311 verbose=verbose,
1312 output_redir=output_redir,
1313 alt_dir=alt_dir,
1314 current_branch_only=current_branch_only,
1315 tags=tags,
1316 prune=prune,
1317 depth=depth,
1318 submodules=submodules,
1319 force_sync=force_sync,
1320 ssh_proxy=ssh_proxy,
1321 clone_filter=clone_filter,
1322 retry_fetches=retry_fetches,
1323 ):
1324 return SyncNetworkHalfResult(False, remote_fetched)
1325
1326 mp = self.manifest.manifestProject
1327 dissociate = mp.dissociate
1328 if dissociate:
1329 alternates_file = os.path.join(
1330 self.objdir, "objects/info/alternates"
1331 )
1332 if os.path.exists(alternates_file):
1333 cmd = ["repack", "-a", "-d"]
1334 p = GitCommand(
1335 self,
1336 cmd,
1337 bare=True,
1338 capture_stdout=bool(output_redir),
1339 merge_output=bool(output_redir),
1340 )
1341 if p.stdout and output_redir:
1342 output_redir.write(p.stdout)
1343 if p.Wait() != 0:
1344 return SyncNetworkHalfResult(False, remote_fetched)
1345 platform_utils.remove(alternates_file)
1346
1347 if self.worktree:
1348 self._InitMRef()
1349 else:
1350 self._InitMirrorHead()
1351 platform_utils.remove(
1352 os.path.join(self.gitdir, "FETCH_HEAD"), missing_ok=True
1353 )
1354 return SyncNetworkHalfResult(True, remote_fetched)
1355
1356 def PostRepoUpgrade(self):
1357 self._InitHooks()
1358
1359 def _CopyAndLinkFiles(self):
1360 if self.client.isGitcClient:
1361 return
1362 for copyfile in self.copyfiles:
1363 copyfile._Copy()
1364 for linkfile in self.linkfiles:
1365 linkfile._Link()
1366
1367 def GetCommitRevisionId(self):
1368 """Get revisionId of a commit.
1369
1370 Use this method instead of GetRevisionId to get the id of the commit
1371 rather than the id of the current git object (for example, a tag)
1372
1373 """
1374 if not self.revisionExpr.startswith(R_TAGS):
1375 return self.GetRevisionId(self._allrefs)
1376
1377 try:
1378 return self.bare_git.rev_list(self.revisionExpr, "-1")[0]
1379 except GitError:
1380 raise ManifestInvalidRevisionError(
1381 "revision %s in %s not found" % (self.revisionExpr, self.name)
1382 )
1383
1384 def GetRevisionId(self, all_refs=None):
1385 if self.revisionId:
1386 return self.revisionId
1387
1388 rem = self.GetRemote()
1389 rev = rem.ToLocal(self.revisionExpr)
1390
1391 if all_refs is not None and rev in all_refs:
1392 return all_refs[rev]
1393
1394 try:
1395 return self.bare_git.rev_parse("--verify", "%s^0" % rev)
1396 except GitError:
1397 raise ManifestInvalidRevisionError(
1398 "revision %s in %s not found" % (self.revisionExpr, self.name)
1399 )
1400
1401 def SetRevisionId(self, revisionId):
1402 if self.revisionExpr:
1403 self.upstream = self.revisionExpr
1404
1405 self.revisionId = revisionId
1406
1407 def Sync_LocalHalf(self, syncbuf, force_sync=False, submodules=False):
1408 """Perform only the local IO portion of the sync process.
1409
1410 Network access is not required.
1411 """
1412 if not os.path.exists(self.gitdir):
1413 syncbuf.fail(
1414 self,
1415 "Cannot checkout %s due to missing network sync; Run "
1416 "`repo sync -n %s` first." % (self.name, self.name),
1417 )
1418 return
1419
1420 self._InitWorkTree(force_sync=force_sync, submodules=submodules)
1421 all_refs = self.bare_ref.all
1422 self.CleanPublishedCache(all_refs)
1423 revid = self.GetRevisionId(all_refs)
1424
1425 # Special case the root of the repo client checkout. Make sure it
1426 # doesn't contain files being checked out to dirs we don't allow.
1427 if self.relpath == ".":
1428 PROTECTED_PATHS = {".repo"}
1429 paths = set(
1430 self.work_git.ls_tree("-z", "--name-only", "--", revid).split(
1431 "\0"
1432 )
1433 )
1434 bad_paths = paths & PROTECTED_PATHS
1435 if bad_paths:
1436 syncbuf.fail(
1437 self,
1438 "Refusing to checkout project that writes to protected "
1439 "paths: %s" % (", ".join(bad_paths),),
1440 )
1441 return
1442
1443 def _doff():
1444 self._FastForward(revid)
1445 self._CopyAndLinkFiles()
1446
1447 def _dosubmodules():
1448 self._SyncSubmodules(quiet=True)
1449
1450 head = self.work_git.GetHead()
1451 if head.startswith(R_HEADS):
1452 branch = head[len(R_HEADS) :]
1453 try:
1454 head = all_refs[head]
1455 except KeyError:
1456 head = None
1457 else:
1458 branch = None
1459
1460 if branch is None or syncbuf.detach_head:
1461 # Currently on a detached HEAD. The user is assumed to
1462 # not have any local modifications worth worrying about.
1463 if self.IsRebaseInProgress():
1464 syncbuf.fail(self, _PriorSyncFailedError())
1465 return
1466
1467 if head == revid:
1468 # No changes; don't do anything further.
1469 # Except if the head needs to be detached.
1470 if not syncbuf.detach_head:
1471 # The copy/linkfile config may have changed.
1472 self._CopyAndLinkFiles()
1473 return
1474 else:
1475 lost = self._revlist(not_rev(revid), HEAD)
1476 if lost:
1477 syncbuf.info(self, "discarding %d commits", len(lost))
1478
1479 try:
1480 self._Checkout(revid, quiet=True)
1481 if submodules:
1482 self._SyncSubmodules(quiet=True)
1483 except GitError as e:
1484 syncbuf.fail(self, e)
1485 return
1486 self._CopyAndLinkFiles()
1487 return
1488
1489 if head == revid:
1490 # No changes; don't do anything further.
1491 #
1492 # The copy/linkfile config may have changed.
1493 self._CopyAndLinkFiles()
1494 return
1495
1496 branch = self.GetBranch(branch)
1497
1498 if not branch.LocalMerge:
1499 # The current branch has no tracking configuration.
1500 # Jump off it to a detached HEAD.
1501 syncbuf.info(
1502 self, "leaving %s; does not track upstream", branch.name
1503 )
1504 try:
1505 self._Checkout(revid, quiet=True)
1506 if submodules:
1507 self._SyncSubmodules(quiet=True)
1508 except GitError as e:
1509 syncbuf.fail(self, e)
1510 return
1511 self._CopyAndLinkFiles()
1512 return
1513
1514 upstream_gain = self._revlist(not_rev(HEAD), revid)
1515
1516 # See if we can perform a fast forward merge. This can happen if our
1517 # branch isn't in the exact same state as we last published.
1518 try:
1519 self.work_git.merge_base("--is-ancestor", HEAD, revid)
1520 # Skip the published logic.
1521 pub = False
1522 except GitError:
1523 pub = self.WasPublished(branch.name, all_refs)
1524
1525 if pub:
1526 not_merged = self._revlist(not_rev(revid), pub)
1527 if not_merged:
1528 if upstream_gain:
1529 # The user has published this branch and some of those
1530 # commits are not yet merged upstream. We do not want
1531 # to rewrite the published commits so we punt.
1532 syncbuf.fail(
1533 self,
1534 "branch %s is published (but not merged) and is now "
1535 "%d commits behind" % (branch.name, len(upstream_gain)),
1536 )
1537 return
1538 elif pub == head:
1539 # All published commits are merged, and thus we are a
1540 # strict subset. We can fast-forward safely.
1541 syncbuf.later1(self, _doff)
1542 if submodules:
1543 syncbuf.later1(self, _dosubmodules)
1544 return
1545
1546 # Examine the local commits not in the remote. Find the
1547 # last one attributed to this user, if any.
1548 local_changes = self._revlist(not_rev(revid), HEAD, format="%H %ce")
1549 last_mine = None
1550 cnt_mine = 0
1551 for commit in local_changes:
1552 commit_id, committer_email = commit.split(" ", 1)
1553 if committer_email == self.UserEmail:
1554 last_mine = commit_id
1555 cnt_mine += 1
1556
1557 if not upstream_gain and cnt_mine == len(local_changes):
1558 # The copy/linkfile config may have changed.
1559 self._CopyAndLinkFiles()
1560 return
1561
1562 if self.IsDirty(consider_untracked=False):
1563 syncbuf.fail(self, _DirtyError())
1564 return
1565
1566 # If the upstream switched on us, warn the user.
1567 if branch.merge != self.revisionExpr:
1568 if branch.merge and self.revisionExpr:
1569 syncbuf.info(
1570 self,
1571 "manifest switched %s...%s",
1572 branch.merge,
1573 self.revisionExpr,
1574 )
1575 elif branch.merge:
1576 syncbuf.info(self, "manifest no longer tracks %s", branch.merge)
1577
1578 if cnt_mine < len(local_changes):
1579 # Upstream rebased. Not everything in HEAD was created by this user.
1580 syncbuf.info(
1581 self,
1582 "discarding %d commits removed from upstream",
1583 len(local_changes) - cnt_mine,
1584 )
1585
1586 branch.remote = self.GetRemote()
1587 if not ID_RE.match(self.revisionExpr):
1588 # In case of manifest sync the revisionExpr might be a SHA1.
1589 branch.merge = self.revisionExpr
1590 if not branch.merge.startswith("refs/"):
1591 branch.merge = R_HEADS + branch.merge
1592 branch.Save()
1593
1594 if cnt_mine > 0 and self.rebase:
1595
1596 def _docopyandlink():
1597 self._CopyAndLinkFiles()
1598
1599 def _dorebase():
1600 self._Rebase(upstream="%s^1" % last_mine, onto=revid)
1601
1602 syncbuf.later2(self, _dorebase)
1603 if submodules:
1604 syncbuf.later2(self, _dosubmodules)
1605 syncbuf.later2(self, _docopyandlink)
1606 elif local_changes:
1607 try:
1608 self._ResetHard(revid)
1609 if submodules:
1610 self._SyncSubmodules(quiet=True)
1611 self._CopyAndLinkFiles()
1612 except GitError as e:
1613 syncbuf.fail(self, e)
1614 return
1615 else:
1616 syncbuf.later1(self, _doff)
1617 if submodules:
1618 syncbuf.later1(self, _dosubmodules)
1619
1620 def AddCopyFile(self, src, dest, topdir):
1621 """Mark |src| for copying to |dest| (relative to |topdir|).
1622
1623 No filesystem changes occur here. Actual copying happens later on.
1624
1625 Paths should have basic validation run on them before being queued.
1626 Further checking will be handled when the actual copy happens.
1627 """
1628 self.copyfiles.append(_CopyFile(self.worktree, src, topdir, dest))
1629
1630 def AddLinkFile(self, src, dest, topdir):
1631 """Mark |dest| to create a symlink (relative to |topdir|) pointing to
1632 |src|.
1633
1634 No filesystem changes occur here. Actual linking happens later on.
1635
1636 Paths should have basic validation run on them before being queued.
1637 Further checking will be handled when the actual link happens.
1638 """
1639 self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest))
1640
1641 def AddAnnotation(self, name, value, keep):
1642 self.annotations.append(Annotation(name, value, keep))
1643
1644 def DownloadPatchSet(self, change_id, patch_id):
1645 """Download a single patch set of a single change to FETCH_HEAD."""
1646 remote = self.GetRemote()
1647
1648 cmd = ["fetch", remote.name]
1649 cmd.append(
1650 "refs/changes/%2.2d/%d/%d" % (change_id % 100, change_id, patch_id)
1651 )
1652 if GitCommand(self, cmd, bare=True).Wait() != 0:
1653 return None
1654 return DownloadedChange(
1655 self,
1656 self.GetRevisionId(),
1657 change_id,
1658 patch_id,
1659 self.bare_git.rev_parse("FETCH_HEAD"),
1660 )
1661
1662 def DeleteWorktree(self, quiet=False, force=False):
1663 """Delete the source checkout and any other housekeeping tasks.
1664
1665 This currently leaves behind the internal .repo/ cache state. This
1666 helps when switching branches or manifest changes get reverted as we
1667 don't have to redownload all the git objects. But we should do some GC
1668 at some point.
1669
1670 Args:
1671 quiet: Whether to hide normal messages.
1672 force: Always delete tree even if dirty.
Doug Anderson3ba5f952011-04-07 12:51:04 -07001673
1674 Returns:
Gavin Makea2e3302023-03-11 06:46:20 +00001675 True if the worktree was completely cleaned out.
1676 """
1677 if self.IsDirty():
1678 if force:
1679 print(
1680 "warning: %s: Removing dirty project: uncommitted changes "
1681 "lost." % (self.RelPath(local=False),),
1682 file=sys.stderr,
1683 )
1684 else:
1685 print(
1686 "error: %s: Cannot remove project: uncommitted changes are "
1687 "present.\n" % (self.RelPath(local=False),),
1688 file=sys.stderr,
1689 )
1690 return False
Wink Saville02d79452009-04-10 13:01:24 -07001691
Gavin Makea2e3302023-03-11 06:46:20 +00001692 if not quiet:
1693 print(
1694 "%s: Deleting obsolete checkout." % (self.RelPath(local=False),)
1695 )
Wink Saville02d79452009-04-10 13:01:24 -07001696
Gavin Makea2e3302023-03-11 06:46:20 +00001697 # Unlock and delink from the main worktree. We don't use git's worktree
1698 # remove because it will recursively delete projects -- we handle that
1699 # ourselves below. https://crbug.com/git/48
1700 if self.use_git_worktrees:
1701 needle = platform_utils.realpath(self.gitdir)
1702 # Find the git worktree commondir under .repo/worktrees/.
1703 output = self.bare_git.worktree("list", "--porcelain").splitlines()[
1704 0
1705 ]
1706 assert output.startswith("worktree "), output
1707 commondir = output[9:]
1708 # Walk each of the git worktrees to see where they point.
1709 configs = os.path.join(commondir, "worktrees")
1710 for name in os.listdir(configs):
1711 gitdir = os.path.join(configs, name, "gitdir")
1712 with open(gitdir) as fp:
1713 relpath = fp.read().strip()
1714 # Resolve the checkout path and see if it matches this project.
1715 fullpath = platform_utils.realpath(
1716 os.path.join(configs, name, relpath)
1717 )
1718 if fullpath == needle:
1719 platform_utils.rmtree(os.path.join(configs, name))
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001720
Gavin Makea2e3302023-03-11 06:46:20 +00001721 # Delete the .git directory first, so we're less likely to have a
1722 # partially working git repository around. There shouldn't be any git
1723 # projects here, so rmtree works.
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001724
Gavin Makea2e3302023-03-11 06:46:20 +00001725 # Try to remove plain files first in case of git worktrees. If this
1726 # fails for any reason, we'll fall back to rmtree, and that'll display
1727 # errors if it can't remove things either.
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001728 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001729 platform_utils.remove(self.gitdir)
1730 except OSError:
1731 pass
1732 try:
1733 platform_utils.rmtree(self.gitdir)
1734 except OSError as e:
1735 if e.errno != errno.ENOENT:
1736 print("error: %s: %s" % (self.gitdir, e), file=sys.stderr)
1737 print(
1738 "error: %s: Failed to delete obsolete checkout; remove "
1739 "manually, then run `repo sync -l`."
1740 % (self.RelPath(local=False),),
1741 file=sys.stderr,
1742 )
1743 return False
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001744
Gavin Makea2e3302023-03-11 06:46:20 +00001745 # Delete everything under the worktree, except for directories that
1746 # contain another git project.
1747 dirs_to_remove = []
1748 failed = False
1749 for root, dirs, files in platform_utils.walk(self.worktree):
1750 for f in files:
1751 path = os.path.join(root, f)
1752 try:
1753 platform_utils.remove(path)
1754 except OSError as e:
1755 if e.errno != errno.ENOENT:
1756 print(
1757 "error: %s: Failed to remove: %s" % (path, e),
1758 file=sys.stderr,
1759 )
1760 failed = True
1761 dirs[:] = [
1762 d
1763 for d in dirs
1764 if not os.path.lexists(os.path.join(root, d, ".git"))
1765 ]
1766 dirs_to_remove += [
1767 os.path.join(root, d)
1768 for d in dirs
1769 if os.path.join(root, d) not in dirs_to_remove
1770 ]
1771 for d in reversed(dirs_to_remove):
1772 if platform_utils.islink(d):
1773 try:
1774 platform_utils.remove(d)
1775 except OSError as e:
1776 if e.errno != errno.ENOENT:
1777 print(
1778 "error: %s: Failed to remove: %s" % (d, e),
1779 file=sys.stderr,
1780 )
1781 failed = True
1782 elif not platform_utils.listdir(d):
1783 try:
1784 platform_utils.rmdir(d)
1785 except OSError as e:
1786 if e.errno != errno.ENOENT:
1787 print(
1788 "error: %s: Failed to remove: %s" % (d, e),
1789 file=sys.stderr,
1790 )
1791 failed = True
1792 if failed:
1793 print(
1794 "error: %s: Failed to delete obsolete checkout."
1795 % (self.RelPath(local=False),),
1796 file=sys.stderr,
1797 )
1798 print(
1799 " Remove manually, then run `repo sync -l`.",
1800 file=sys.stderr,
1801 )
1802 return False
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07001803
Gavin Makea2e3302023-03-11 06:46:20 +00001804 # Try deleting parent dirs if they are empty.
1805 path = self.worktree
1806 while path != self.manifest.topdir:
1807 try:
1808 platform_utils.rmdir(path)
1809 except OSError as e:
1810 if e.errno != errno.ENOENT:
1811 break
1812 path = os.path.dirname(path)
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001813
Gavin Makea2e3302023-03-11 06:46:20 +00001814 return True
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001815
Gavin Makea2e3302023-03-11 06:46:20 +00001816 def StartBranch(self, name, branch_merge="", revision=None):
1817 """Create a new branch off the manifest's revision."""
1818 if not branch_merge:
1819 branch_merge = self.revisionExpr
1820 head = self.work_git.GetHead()
1821 if head == (R_HEADS + name):
1822 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001823
David Pursehouse8a68ff92012-09-24 12:15:13 +09001824 all_refs = self.bare_ref.all
Gavin Makea2e3302023-03-11 06:46:20 +00001825 if R_HEADS + name in all_refs:
1826 return GitCommand(self, ["checkout", "-q", name, "--"]).Wait() == 0
Shawn O. Pearce88443382010-10-08 10:02:09 +02001827
Gavin Makea2e3302023-03-11 06:46:20 +00001828 branch = self.GetBranch(name)
1829 branch.remote = self.GetRemote()
1830 branch.merge = branch_merge
1831 if not branch.merge.startswith("refs/") and not ID_RE.match(
1832 branch_merge
1833 ):
1834 branch.merge = R_HEADS + branch_merge
Shawn O. Pearce88443382010-10-08 10:02:09 +02001835
Gavin Makea2e3302023-03-11 06:46:20 +00001836 if revision is None:
1837 revid = self.GetRevisionId(all_refs)
Shawn O. Pearce88443382010-10-08 10:02:09 +02001838 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001839 revid = self.work_git.rev_parse(revision)
Brian Harring14a66742012-09-28 20:21:57 -07001840
Gavin Makea2e3302023-03-11 06:46:20 +00001841 if head.startswith(R_HEADS):
Kevin Degiabaa7f32014-11-12 11:27:45 -07001842 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001843 head = all_refs[head]
1844 except KeyError:
1845 head = None
1846 if revid and head and revid == head:
1847 ref = R_HEADS + name
1848 self.work_git.update_ref(ref, revid)
1849 self.work_git.symbolic_ref(HEAD, ref)
1850 branch.Save()
1851 return True
Kevin Degib1a07b82015-07-27 13:33:43 -06001852
Gavin Makea2e3302023-03-11 06:46:20 +00001853 if (
1854 GitCommand(
1855 self, ["checkout", "-q", "-b", branch.name, revid]
1856 ).Wait()
1857 == 0
1858 ):
1859 branch.Save()
1860 return True
1861 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06001862
Gavin Makea2e3302023-03-11 06:46:20 +00001863 def CheckoutBranch(self, name):
1864 """Checkout a local topic branch.
Shawn O. Pearce2816d4f2009-03-03 17:53:18 -08001865
Gavin Makea2e3302023-03-11 06:46:20 +00001866 Args:
1867 name: The name of the branch to checkout.
Shawn O. Pearce88443382010-10-08 10:02:09 +02001868
Gavin Makea2e3302023-03-11 06:46:20 +00001869 Returns:
1870 True if the checkout succeeded; False if it didn't; None if the
1871 branch didn't exist.
1872 """
1873 rev = R_HEADS + name
1874 head = self.work_git.GetHead()
1875 if head == rev:
1876 # Already on the branch.
1877 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001878
Gavin Makea2e3302023-03-11 06:46:20 +00001879 all_refs = self.bare_ref.all
1880 try:
1881 revid = all_refs[rev]
1882 except KeyError:
1883 # Branch does not exist in this project.
1884 return None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001885
Gavin Makea2e3302023-03-11 06:46:20 +00001886 if head.startswith(R_HEADS):
1887 try:
1888 head = all_refs[head]
1889 except KeyError:
1890 head = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001891
Gavin Makea2e3302023-03-11 06:46:20 +00001892 if head == revid:
1893 # Same revision; just update HEAD to point to the new
1894 # target branch, but otherwise take no other action.
1895 _lwrite(
1896 self.work_git.GetDotgitPath(subpath=HEAD),
1897 "ref: %s%s\n" % (R_HEADS, name),
1898 )
1899 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05001900
Gavin Makea2e3302023-03-11 06:46:20 +00001901 return (
1902 GitCommand(
1903 self,
1904 ["checkout", name, "--"],
1905 capture_stdout=True,
1906 capture_stderr=True,
1907 ).Wait()
1908 == 0
1909 )
Mike Frysinger98bb7652021-12-20 21:15:59 -05001910
Gavin Makea2e3302023-03-11 06:46:20 +00001911 def AbandonBranch(self, name):
1912 """Destroy a local topic branch.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07001913
Gavin Makea2e3302023-03-11 06:46:20 +00001914 Args:
1915 name: The name of the branch to abandon.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07001916
Gavin Makea2e3302023-03-11 06:46:20 +00001917 Returns:
1918 True if the abandon succeeded; False if it didn't; None if the
1919 branch didn't exist.
1920 """
1921 rev = R_HEADS + name
1922 all_refs = self.bare_ref.all
1923 if rev not in all_refs:
1924 # Doesn't exist
1925 return None
1926
1927 head = self.work_git.GetHead()
1928 if head == rev:
1929 # We can't destroy the branch while we are sitting
1930 # on it. Switch to a detached HEAD.
1931 head = all_refs[head]
1932
1933 revid = self.GetRevisionId(all_refs)
1934 if head == revid:
1935 _lwrite(
1936 self.work_git.GetDotgitPath(subpath=HEAD), "%s\n" % revid
1937 )
1938 else:
1939 self._Checkout(revid, quiet=True)
1940
1941 return (
1942 GitCommand(
1943 self,
1944 ["branch", "-D", name],
1945 capture_stdout=True,
1946 capture_stderr=True,
1947 ).Wait()
1948 == 0
1949 )
1950
1951 def PruneHeads(self):
1952 """Prune any topic branches already merged into upstream."""
1953 cb = self.CurrentBranch
1954 kill = []
1955 left = self._allrefs
1956 for name in left.keys():
1957 if name.startswith(R_HEADS):
1958 name = name[len(R_HEADS) :]
1959 if cb is None or name != cb:
1960 kill.append(name)
1961
1962 # Minor optimization: If there's nothing to prune, then don't try to
1963 # read any project state.
1964 if not kill and not cb:
1965 return []
1966
1967 rev = self.GetRevisionId(left)
1968 if (
1969 cb is not None
1970 and not self._revlist(HEAD + "..." + rev)
1971 and not self.IsDirty(consider_untracked=False)
1972 ):
1973 self.work_git.DetachHead(HEAD)
1974 kill.append(cb)
1975
1976 if kill:
1977 old = self.bare_git.GetHead()
1978
1979 try:
1980 self.bare_git.DetachHead(rev)
1981
1982 b = ["branch", "-d"]
1983 b.extend(kill)
1984 b = GitCommand(
1985 self, b, bare=True, capture_stdout=True, capture_stderr=True
1986 )
1987 b.Wait()
1988 finally:
1989 if ID_RE.match(old):
1990 self.bare_git.DetachHead(old)
1991 else:
1992 self.bare_git.SetHead(old)
1993 left = self._allrefs
1994
1995 for branch in kill:
1996 if (R_HEADS + branch) not in left:
1997 self.CleanPublishedCache()
1998 break
1999
2000 if cb and cb not in kill:
2001 kill.append(cb)
2002 kill.sort()
2003
2004 kept = []
2005 for branch in kill:
2006 if R_HEADS + branch in left:
2007 branch = self.GetBranch(branch)
2008 base = branch.LocalMerge
2009 if not base:
2010 base = rev
2011 kept.append(ReviewableBranch(self, branch, base))
2012 return kept
2013
2014 def GetRegisteredSubprojects(self):
2015 result = []
2016
2017 def rec(subprojects):
2018 if not subprojects:
2019 return
2020 result.extend(subprojects)
2021 for p in subprojects:
2022 rec(p.subprojects)
2023
2024 rec(self.subprojects)
2025 return result
2026
2027 def _GetSubmodules(self):
2028 # Unfortunately we cannot call `git submodule status --recursive` here
2029 # because the working tree might not exist yet, and it cannot be used
2030 # without a working tree in its current implementation.
2031
2032 def get_submodules(gitdir, rev):
2033 # Parse .gitmodules for submodule sub_paths and sub_urls.
2034 sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
2035 if not sub_paths:
2036 return []
2037 # Run `git ls-tree` to read SHAs of submodule object, which happen
2038 # to be revision of submodule repository.
2039 sub_revs = git_ls_tree(gitdir, rev, sub_paths)
2040 submodules = []
2041 for sub_path, sub_url in zip(sub_paths, sub_urls):
2042 try:
2043 sub_rev = sub_revs[sub_path]
2044 except KeyError:
2045 # Ignore non-exist submodules.
2046 continue
2047 submodules.append((sub_rev, sub_path, sub_url))
2048 return submodules
2049
2050 re_path = re.compile(r"^submodule\.(.+)\.path=(.*)$")
2051 re_url = re.compile(r"^submodule\.(.+)\.url=(.*)$")
2052
2053 def parse_gitmodules(gitdir, rev):
2054 cmd = ["cat-file", "blob", "%s:.gitmodules" % rev]
2055 try:
2056 p = GitCommand(
2057 None,
2058 cmd,
2059 capture_stdout=True,
2060 capture_stderr=True,
2061 bare=True,
2062 gitdir=gitdir,
2063 )
2064 except GitError:
2065 return [], []
2066 if p.Wait() != 0:
2067 return [], []
2068
2069 gitmodules_lines = []
2070 fd, temp_gitmodules_path = tempfile.mkstemp()
2071 try:
2072 os.write(fd, p.stdout.encode("utf-8"))
2073 os.close(fd)
2074 cmd = ["config", "--file", temp_gitmodules_path, "--list"]
2075 p = GitCommand(
2076 None,
2077 cmd,
2078 capture_stdout=True,
2079 capture_stderr=True,
2080 bare=True,
2081 gitdir=gitdir,
2082 )
2083 if p.Wait() != 0:
2084 return [], []
2085 gitmodules_lines = p.stdout.split("\n")
2086 except GitError:
2087 return [], []
2088 finally:
2089 platform_utils.remove(temp_gitmodules_path)
2090
2091 names = set()
2092 paths = {}
2093 urls = {}
2094 for line in gitmodules_lines:
2095 if not line:
2096 continue
2097 m = re_path.match(line)
2098 if m:
2099 names.add(m.group(1))
2100 paths[m.group(1)] = m.group(2)
2101 continue
2102 m = re_url.match(line)
2103 if m:
2104 names.add(m.group(1))
2105 urls[m.group(1)] = m.group(2)
2106 continue
2107 names = sorted(names)
2108 return (
2109 [paths.get(name, "") for name in names],
2110 [urls.get(name, "") for name in names],
2111 )
2112
2113 def git_ls_tree(gitdir, rev, paths):
2114 cmd = ["ls-tree", rev, "--"]
2115 cmd.extend(paths)
2116 try:
2117 p = GitCommand(
2118 None,
2119 cmd,
2120 capture_stdout=True,
2121 capture_stderr=True,
2122 bare=True,
2123 gitdir=gitdir,
2124 )
2125 except GitError:
2126 return []
2127 if p.Wait() != 0:
2128 return []
2129 objects = {}
2130 for line in p.stdout.split("\n"):
2131 if not line.strip():
2132 continue
2133 object_rev, object_path = line.split()[2:4]
2134 objects[object_path] = object_rev
2135 return objects
2136
2137 try:
2138 rev = self.GetRevisionId()
2139 except GitError:
2140 return []
2141 return get_submodules(self.gitdir, rev)
2142
2143 def GetDerivedSubprojects(self):
2144 result = []
2145 if not self.Exists:
2146 # If git repo does not exist yet, querying its submodules will
2147 # mess up its states; so return here.
2148 return result
2149 for rev, path, url in self._GetSubmodules():
2150 name = self.manifest.GetSubprojectName(self, path)
2151 (
2152 relpath,
2153 worktree,
2154 gitdir,
2155 objdir,
2156 ) = self.manifest.GetSubprojectPaths(self, name, path)
2157 project = self.manifest.paths.get(relpath)
2158 if project:
2159 result.extend(project.GetDerivedSubprojects())
2160 continue
2161
2162 if url.startswith(".."):
2163 url = urllib.parse.urljoin("%s/" % self.remote.url, url)
2164 remote = RemoteSpec(
2165 self.remote.name,
2166 url=url,
2167 pushUrl=self.remote.pushUrl,
2168 review=self.remote.review,
2169 revision=self.remote.revision,
2170 )
2171 subproject = Project(
2172 manifest=self.manifest,
2173 name=name,
2174 remote=remote,
2175 gitdir=gitdir,
2176 objdir=objdir,
2177 worktree=worktree,
2178 relpath=relpath,
2179 revisionExpr=rev,
2180 revisionId=rev,
2181 rebase=self.rebase,
2182 groups=self.groups,
2183 sync_c=self.sync_c,
2184 sync_s=self.sync_s,
2185 sync_tags=self.sync_tags,
2186 parent=self,
2187 is_derived=True,
2188 )
2189 result.append(subproject)
2190 result.extend(subproject.GetDerivedSubprojects())
2191 return result
2192
2193 def EnableRepositoryExtension(self, key, value="true", version=1):
2194 """Enable git repository extension |key| with |value|.
2195
2196 Args:
2197 key: The extension to enabled. Omit the "extensions." prefix.
2198 value: The value to use for the extension.
2199 version: The minimum git repository version needed.
2200 """
2201 # Make sure the git repo version is new enough already.
2202 found_version = self.config.GetInt("core.repositoryFormatVersion")
2203 if found_version is None:
2204 found_version = 0
2205 if found_version < version:
2206 self.config.SetString("core.repositoryFormatVersion", str(version))
2207
2208 # Enable the extension!
2209 self.config.SetString("extensions.%s" % (key,), value)
2210
2211 def ResolveRemoteHead(self, name=None):
2212 """Find out what the default branch (HEAD) points to.
2213
2214 Normally this points to refs/heads/master, but projects are moving to
2215 main. Support whatever the server uses rather than hardcoding "master"
2216 ourselves.
2217 """
2218 if name is None:
2219 name = self.remote.name
2220
2221 # The output will look like (NB: tabs are separators):
2222 # ref: refs/heads/master HEAD
2223 # 5f6803b100bb3cd0f534e96e88c91373e8ed1c44 HEAD
2224 output = self.bare_git.ls_remote(
2225 "-q", "--symref", "--exit-code", name, "HEAD"
2226 )
2227
2228 for line in output.splitlines():
2229 lhs, rhs = line.split("\t", 1)
2230 if rhs == "HEAD" and lhs.startswith("ref:"):
2231 return lhs[4:].strip()
2232
2233 return None
2234
2235 def _CheckForImmutableRevision(self):
2236 try:
2237 # if revision (sha or tag) is not present then following function
2238 # throws an error.
2239 self.bare_git.rev_list(
2240 "-1", "--missing=allow-any", "%s^0" % self.revisionExpr, "--"
2241 )
2242 if self.upstream:
2243 rev = self.GetRemote().ToLocal(self.upstream)
2244 self.bare_git.rev_list(
2245 "-1", "--missing=allow-any", "%s^0" % rev, "--"
2246 )
2247 self.bare_git.merge_base(
2248 "--is-ancestor", self.revisionExpr, rev
2249 )
2250 return True
2251 except GitError:
2252 # There is no such persistent revision. We have to fetch it.
2253 return False
2254
2255 def _FetchArchive(self, tarpath, cwd=None):
2256 cmd = ["archive", "-v", "-o", tarpath]
2257 cmd.append("--remote=%s" % self.remote.url)
2258 cmd.append("--prefix=%s/" % self.RelPath(local=False))
2259 cmd.append(self.revisionExpr)
2260
2261 command = GitCommand(
2262 self, cmd, cwd=cwd, capture_stdout=True, capture_stderr=True
2263 )
2264
2265 if command.Wait() != 0:
2266 raise GitError("git archive %s: %s" % (self.name, command.stderr))
2267
2268 def _RemoteFetch(
2269 self,
2270 name=None,
2271 current_branch_only=False,
2272 initial=False,
2273 quiet=False,
2274 verbose=False,
2275 output_redir=None,
2276 alt_dir=None,
2277 tags=True,
2278 prune=False,
2279 depth=None,
2280 submodules=False,
2281 ssh_proxy=None,
2282 force_sync=False,
2283 clone_filter=None,
2284 retry_fetches=2,
2285 retry_sleep_initial_sec=4.0,
2286 retry_exp_factor=2.0,
2287 ):
2288 is_sha1 = False
2289 tag_name = None
2290 # The depth should not be used when fetching to a mirror because
2291 # it will result in a shallow repository that cannot be cloned or
2292 # fetched from.
2293 # The repo project should also never be synced with partial depth.
2294 if self.manifest.IsMirror or self.relpath == ".repo/repo":
2295 depth = None
2296
2297 if depth:
2298 current_branch_only = True
2299
2300 if ID_RE.match(self.revisionExpr) is not None:
2301 is_sha1 = True
2302
2303 if current_branch_only:
2304 if self.revisionExpr.startswith(R_TAGS):
2305 # This is a tag and its commit id should never change.
2306 tag_name = self.revisionExpr[len(R_TAGS) :]
2307 elif self.upstream and self.upstream.startswith(R_TAGS):
2308 # This is a tag and its commit id should never change.
2309 tag_name = self.upstream[len(R_TAGS) :]
2310
2311 if is_sha1 or tag_name is not None:
2312 if self._CheckForImmutableRevision():
2313 if verbose:
2314 print(
2315 "Skipped fetching project %s (already have "
2316 "persistent ref)" % self.name
2317 )
2318 return True
2319 if is_sha1 and not depth:
2320 # When syncing a specific commit and --depth is not set:
2321 # * if upstream is explicitly specified and is not a sha1, fetch
2322 # only upstream as users expect only upstream to be fetch.
2323 # Note: The commit might not be in upstream in which case the
2324 # sync will fail.
2325 # * otherwise, fetch all branches to make sure we end up with
2326 # the specific commit.
2327 if self.upstream:
2328 current_branch_only = not ID_RE.match(self.upstream)
2329 else:
2330 current_branch_only = False
2331
2332 if not name:
2333 name = self.remote.name
2334
2335 remote = self.GetRemote(name)
2336 if not remote.PreConnectFetch(ssh_proxy):
2337 ssh_proxy = None
2338
2339 if initial:
2340 if alt_dir and "objects" == os.path.basename(alt_dir):
2341 ref_dir = os.path.dirname(alt_dir)
2342 packed_refs = os.path.join(self.gitdir, "packed-refs")
2343
2344 all_refs = self.bare_ref.all
2345 ids = set(all_refs.values())
2346 tmp = set()
2347
2348 for r, ref_id in GitRefs(ref_dir).all.items():
2349 if r not in all_refs:
2350 if r.startswith(R_TAGS) or remote.WritesTo(r):
2351 all_refs[r] = ref_id
2352 ids.add(ref_id)
2353 continue
2354
2355 if ref_id in ids:
2356 continue
2357
2358 r = "refs/_alt/%s" % ref_id
2359 all_refs[r] = ref_id
2360 ids.add(ref_id)
2361 tmp.add(r)
2362
2363 tmp_packed_lines = []
2364 old_packed_lines = []
2365
2366 for r in sorted(all_refs):
2367 line = "%s %s\n" % (all_refs[r], r)
2368 tmp_packed_lines.append(line)
2369 if r not in tmp:
2370 old_packed_lines.append(line)
2371
2372 tmp_packed = "".join(tmp_packed_lines)
2373 old_packed = "".join(old_packed_lines)
2374 _lwrite(packed_refs, tmp_packed)
2375 else:
2376 alt_dir = None
2377
2378 cmd = ["fetch"]
2379
2380 if clone_filter:
2381 git_require((2, 19, 0), fail=True, msg="partial clones")
2382 cmd.append("--filter=%s" % clone_filter)
2383 self.EnableRepositoryExtension("partialclone", self.remote.name)
2384
2385 if depth:
2386 cmd.append("--depth=%s" % depth)
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002387 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002388 # If this repo has shallow objects, then we don't know which refs
2389 # have shallow objects or not. Tell git to unshallow all fetched
2390 # refs. Don't do this with projects that don't have shallow
2391 # objects, since it is less efficient.
2392 if os.path.exists(os.path.join(self.gitdir, "shallow")):
2393 cmd.append("--depth=2147483647")
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002394
Gavin Makea2e3302023-03-11 06:46:20 +00002395 if not verbose:
2396 cmd.append("--quiet")
2397 if not quiet and sys.stdout.isatty():
2398 cmd.append("--progress")
2399 if not self.worktree:
2400 cmd.append("--update-head-ok")
2401 cmd.append(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002402
Gavin Makea2e3302023-03-11 06:46:20 +00002403 if force_sync:
2404 cmd.append("--force")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002405
Gavin Makea2e3302023-03-11 06:46:20 +00002406 if prune:
2407 cmd.append("--prune")
Mike Frysinger29626b42021-05-01 09:37:13 -04002408
Gavin Makea2e3302023-03-11 06:46:20 +00002409 # Always pass something for --recurse-submodules, git with GIT_DIR
2410 # behaves incorrectly when not given `--recurse-submodules=no`.
2411 # (b/218891912)
2412 cmd.append(
2413 f'--recurse-submodules={"on-demand" if submodules else "no"}'
2414 )
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002415
Gavin Makea2e3302023-03-11 06:46:20 +00002416 spec = []
2417 if not current_branch_only:
2418 # Fetch whole repo.
2419 spec.append(
2420 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2421 )
2422 elif tag_name is not None:
2423 spec.append("tag")
2424 spec.append(tag_name)
Remy Böhmer1469c282020-12-15 18:49:02 +01002425
Gavin Makea2e3302023-03-11 06:46:20 +00002426 if self.manifest.IsMirror and not current_branch_only:
2427 branch = None
Remy Böhmer1469c282020-12-15 18:49:02 +01002428 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002429 branch = self.revisionExpr
2430 if (
2431 not self.manifest.IsMirror
2432 and is_sha1
2433 and depth
2434 and git_require((1, 8, 3))
2435 ):
2436 # Shallow checkout of a specific commit, fetch from that commit and
2437 # not the heads only as the commit might be deeper in the history.
2438 spec.append(branch)
2439 if self.upstream:
2440 spec.append(self.upstream)
David James8d201162013-10-11 17:03:19 -07002441 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002442 if is_sha1:
2443 branch = self.upstream
2444 if branch is not None and branch.strip():
2445 if not branch.startswith("refs/"):
2446 branch = R_HEADS + branch
2447 spec.append(str(("+%s:" % branch) + remote.ToLocal(branch)))
David James8d201162013-10-11 17:03:19 -07002448
Gavin Makea2e3302023-03-11 06:46:20 +00002449 # If mirroring repo and we cannot deduce the tag or branch to fetch,
2450 # fetch whole repo.
2451 if self.manifest.IsMirror and not spec:
2452 spec.append(
2453 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2454 )
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002455
Gavin Makea2e3302023-03-11 06:46:20 +00002456 # If using depth then we should not get all the tags since they may
2457 # be outside of the depth.
2458 if not tags or depth:
2459 cmd.append("--no-tags")
2460 else:
2461 cmd.append("--tags")
2462 spec.append(str(("+refs/tags/*:") + remote.ToLocal("refs/tags/*")))
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002463
Gavin Makea2e3302023-03-11 06:46:20 +00002464 cmd.extend(spec)
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002465
Gavin Makea2e3302023-03-11 06:46:20 +00002466 # At least one retry minimum due to git remote prune.
2467 retry_fetches = max(retry_fetches, 2)
2468 retry_cur_sleep = retry_sleep_initial_sec
2469 ok = prune_tried = False
2470 for try_n in range(retry_fetches):
2471 gitcmd = GitCommand(
2472 self,
2473 cmd,
2474 bare=True,
2475 objdir=os.path.join(self.objdir, "objects"),
2476 ssh_proxy=ssh_proxy,
2477 merge_output=True,
2478 capture_stdout=quiet or bool(output_redir),
2479 )
2480 if gitcmd.stdout and not quiet and output_redir:
2481 output_redir.write(gitcmd.stdout)
2482 ret = gitcmd.Wait()
2483 if ret == 0:
2484 ok = True
2485 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002486
Gavin Makea2e3302023-03-11 06:46:20 +00002487 # Retry later due to HTTP 429 Too Many Requests.
2488 elif (
2489 gitcmd.stdout
2490 and "error:" in gitcmd.stdout
2491 and "HTTP 429" in gitcmd.stdout
2492 ):
2493 # Fallthru to sleep+retry logic at the bottom.
2494 pass
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002495
Gavin Makea2e3302023-03-11 06:46:20 +00002496 # Try to prune remote branches once in case there are conflicts.
2497 # For example, if the remote had refs/heads/upstream, but deleted
2498 # that and now has refs/heads/upstream/foo.
2499 elif (
2500 gitcmd.stdout
2501 and "error:" in gitcmd.stdout
2502 and "git remote prune" in gitcmd.stdout
2503 and not prune_tried
2504 ):
2505 prune_tried = True
2506 prunecmd = GitCommand(
2507 self,
2508 ["remote", "prune", name],
2509 bare=True,
2510 ssh_proxy=ssh_proxy,
2511 )
2512 ret = prunecmd.Wait()
2513 if ret:
2514 break
2515 print(
2516 "retrying fetch after pruning remote branches",
2517 file=output_redir,
2518 )
2519 # Continue right away so we don't sleep as we shouldn't need to.
2520 continue
2521 elif current_branch_only and is_sha1 and ret == 128:
2522 # Exit code 128 means "couldn't find the ref you asked for"; if
2523 # we're in sha1 mode, we just tried sync'ing from the upstream
2524 # field; it doesn't exist, thus abort the optimization attempt
2525 # and do a full sync.
2526 break
2527 elif ret < 0:
2528 # Git died with a signal, exit immediately.
2529 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002530
Gavin Makea2e3302023-03-11 06:46:20 +00002531 # Figure out how long to sleep before the next attempt, if there is
2532 # one.
2533 if not verbose and gitcmd.stdout:
2534 print(
2535 "\n%s:\n%s" % (self.name, gitcmd.stdout),
2536 end="",
2537 file=output_redir,
2538 )
2539 if try_n < retry_fetches - 1:
2540 print(
2541 "%s: sleeping %s seconds before retrying"
2542 % (self.name, retry_cur_sleep),
2543 file=output_redir,
2544 )
2545 time.sleep(retry_cur_sleep)
2546 retry_cur_sleep = min(
2547 retry_exp_factor * retry_cur_sleep, MAXIMUM_RETRY_SLEEP_SEC
2548 )
2549 retry_cur_sleep *= 1 - random.uniform(
2550 -RETRY_JITTER_PERCENT, RETRY_JITTER_PERCENT
2551 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002552
Gavin Makea2e3302023-03-11 06:46:20 +00002553 if initial:
2554 if alt_dir:
2555 if old_packed != "":
2556 _lwrite(packed_refs, old_packed)
2557 else:
2558 platform_utils.remove(packed_refs)
2559 self.bare_git.pack_refs("--all", "--prune")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002560
Gavin Makea2e3302023-03-11 06:46:20 +00002561 if is_sha1 and current_branch_only:
2562 # We just synced the upstream given branch; verify we
2563 # got what we wanted, else trigger a second run of all
2564 # refs.
2565 if not self._CheckForImmutableRevision():
2566 # Sync the current branch only with depth set to None.
2567 # We always pass depth=None down to avoid infinite recursion.
2568 return self._RemoteFetch(
2569 name=name,
2570 quiet=quiet,
2571 verbose=verbose,
2572 output_redir=output_redir,
2573 current_branch_only=current_branch_only and depth,
2574 initial=False,
2575 alt_dir=alt_dir,
Nasser Grainawiaafed292023-05-24 12:51:03 -06002576 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00002577 depth=None,
2578 ssh_proxy=ssh_proxy,
2579 clone_filter=clone_filter,
2580 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002581
Gavin Makea2e3302023-03-11 06:46:20 +00002582 return ok
Mike Frysingerf4545122019-11-11 04:34:16 -05002583
Gavin Makea2e3302023-03-11 06:46:20 +00002584 def _ApplyCloneBundle(self, initial=False, quiet=False, verbose=False):
2585 if initial and (
2586 self.manifest.manifestProject.depth or self.clone_depth
2587 ):
2588 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002589
Gavin Makea2e3302023-03-11 06:46:20 +00002590 remote = self.GetRemote()
2591 bundle_url = remote.url + "/clone.bundle"
2592 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
2593 if GetSchemeFromUrl(bundle_url) not in (
2594 "http",
2595 "https",
2596 "persistent-http",
2597 "persistent-https",
2598 ):
2599 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06002600
Gavin Makea2e3302023-03-11 06:46:20 +00002601 bundle_dst = os.path.join(self.gitdir, "clone.bundle")
2602 bundle_tmp = os.path.join(self.gitdir, "clone.bundle.tmp")
2603
2604 exist_dst = os.path.exists(bundle_dst)
2605 exist_tmp = os.path.exists(bundle_tmp)
2606
2607 if not initial and not exist_dst and not exist_tmp:
2608 return False
2609
2610 if not exist_dst:
2611 exist_dst = self._FetchBundle(
2612 bundle_url, bundle_tmp, bundle_dst, quiet, verbose
2613 )
2614 if not exist_dst:
2615 return False
2616
2617 cmd = ["fetch"]
2618 if not verbose:
2619 cmd.append("--quiet")
2620 if not quiet and sys.stdout.isatty():
2621 cmd.append("--progress")
2622 if not self.worktree:
2623 cmd.append("--update-head-ok")
2624 cmd.append(bundle_dst)
2625 for f in remote.fetch:
2626 cmd.append(str(f))
2627 cmd.append("+refs/tags/*:refs/tags/*")
2628
2629 ok = (
2630 GitCommand(
2631 self,
2632 cmd,
2633 bare=True,
2634 objdir=os.path.join(self.objdir, "objects"),
2635 ).Wait()
2636 == 0
2637 )
2638 platform_utils.remove(bundle_dst, missing_ok=True)
2639 platform_utils.remove(bundle_tmp, missing_ok=True)
2640 return ok
2641
2642 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
2643 platform_utils.remove(dstPath, missing_ok=True)
2644
2645 cmd = ["curl", "--fail", "--output", tmpPath, "--netrc", "--location"]
2646 if quiet:
2647 cmd += ["--silent", "--show-error"]
2648 if os.path.exists(tmpPath):
2649 size = os.stat(tmpPath).st_size
2650 if size >= 1024:
2651 cmd += ["--continue-at", "%d" % (size,)]
2652 else:
2653 platform_utils.remove(tmpPath)
2654 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
2655 if cookiefile:
2656 cmd += ["--cookie", cookiefile]
2657 if proxy:
2658 cmd += ["--proxy", proxy]
2659 elif "http_proxy" in os.environ and "darwin" == sys.platform:
2660 cmd += ["--proxy", os.environ["http_proxy"]]
2661 if srcUrl.startswith("persistent-https"):
2662 srcUrl = "http" + srcUrl[len("persistent-https") :]
2663 elif srcUrl.startswith("persistent-http"):
2664 srcUrl = "http" + srcUrl[len("persistent-http") :]
2665 cmd += [srcUrl]
2666
2667 proc = None
2668 with Trace("Fetching bundle: %s", " ".join(cmd)):
2669 if verbose:
2670 print("%s: Downloading bundle: %s" % (self.name, srcUrl))
2671 stdout = None if verbose else subprocess.PIPE
2672 stderr = None if verbose else subprocess.STDOUT
2673 try:
2674 proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
2675 except OSError:
2676 return False
2677
2678 (output, _) = proc.communicate()
2679 curlret = proc.returncode
2680
2681 if curlret == 22:
2682 # From curl man page:
2683 # 22: HTTP page not retrieved. The requested url was not found
2684 # or returned another error with the HTTP error code being 400
2685 # or above. This return code only appears if -f, --fail is used.
2686 if verbose:
2687 print(
2688 "%s: Unable to retrieve clone.bundle; ignoring."
2689 % self.name
2690 )
2691 if output:
2692 print("Curl output:\n%s" % output)
2693 return False
2694 elif curlret and not verbose and output:
2695 print("%s" % output, file=sys.stderr)
2696
2697 if os.path.exists(tmpPath):
2698 if curlret == 0 and self._IsValidBundle(tmpPath, quiet):
2699 platform_utils.rename(tmpPath, dstPath)
2700 return True
2701 else:
2702 platform_utils.remove(tmpPath)
2703 return False
2704 else:
2705 return False
2706
2707 def _IsValidBundle(self, path, quiet):
2708 try:
2709 with open(path, "rb") as f:
2710 if f.read(16) == b"# v2 git bundle\n":
2711 return True
2712 else:
2713 if not quiet:
2714 print(
2715 "Invalid clone.bundle file; ignoring.",
2716 file=sys.stderr,
2717 )
2718 return False
2719 except OSError:
2720 return False
2721
2722 def _Checkout(self, rev, quiet=False):
2723 cmd = ["checkout"]
2724 if quiet:
2725 cmd.append("-q")
2726 cmd.append(rev)
2727 cmd.append("--")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002728 if GitCommand(self, cmd).Wait() != 0:
Gavin Makea2e3302023-03-11 06:46:20 +00002729 if self._allrefs:
2730 raise GitError("%s checkout %s " % (self.name, rev))
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002731
Gavin Makea2e3302023-03-11 06:46:20 +00002732 def _CherryPick(self, rev, ffonly=False, record_origin=False):
2733 cmd = ["cherry-pick"]
2734 if ffonly:
2735 cmd.append("--ff")
2736 if record_origin:
2737 cmd.append("-x")
2738 cmd.append(rev)
2739 cmd.append("--")
2740 if GitCommand(self, cmd).Wait() != 0:
2741 if self._allrefs:
2742 raise GitError("%s cherry-pick %s " % (self.name, rev))
Victor Boivie0960b5b2010-11-26 13:42:13 +01002743
Gavin Makea2e3302023-03-11 06:46:20 +00002744 def _LsRemote(self, refs):
2745 cmd = ["ls-remote", self.remote.name, refs]
2746 p = GitCommand(self, cmd, capture_stdout=True)
2747 if p.Wait() == 0:
2748 return p.stdout
2749 return None
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002750
Gavin Makea2e3302023-03-11 06:46:20 +00002751 def _Revert(self, rev):
2752 cmd = ["revert"]
2753 cmd.append("--no-edit")
2754 cmd.append(rev)
2755 cmd.append("--")
2756 if GitCommand(self, cmd).Wait() != 0:
2757 if self._allrefs:
2758 raise GitError("%s revert %s " % (self.name, rev))
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002759
Gavin Makea2e3302023-03-11 06:46:20 +00002760 def _ResetHard(self, rev, quiet=True):
2761 cmd = ["reset", "--hard"]
2762 if quiet:
2763 cmd.append("-q")
2764 cmd.append(rev)
2765 if GitCommand(self, cmd).Wait() != 0:
2766 raise GitError("%s reset --hard %s " % (self.name, rev))
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002767
Gavin Makea2e3302023-03-11 06:46:20 +00002768 def _SyncSubmodules(self, quiet=True):
2769 cmd = ["submodule", "update", "--init", "--recursive"]
2770 if quiet:
2771 cmd.append("-q")
2772 if GitCommand(self, cmd).Wait() != 0:
2773 raise GitError(
2774 "%s submodule update --init --recursive " % self.name
2775 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002776
Gavin Makea2e3302023-03-11 06:46:20 +00002777 def _Rebase(self, upstream, onto=None):
2778 cmd = ["rebase"]
2779 if onto is not None:
2780 cmd.extend(["--onto", onto])
2781 cmd.append(upstream)
2782 if GitCommand(self, cmd).Wait() != 0:
2783 raise GitError("%s rebase %s " % (self.name, upstream))
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002784
Gavin Makea2e3302023-03-11 06:46:20 +00002785 def _FastForward(self, head, ffonly=False):
2786 cmd = ["merge", "--no-stat", head]
2787 if ffonly:
2788 cmd.append("--ff-only")
2789 if GitCommand(self, cmd).Wait() != 0:
2790 raise GitError("%s merge %s " % (self.name, head))
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002791
Gavin Makea2e3302023-03-11 06:46:20 +00002792 def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False):
2793 init_git_dir = not os.path.exists(self.gitdir)
2794 init_obj_dir = not os.path.exists(self.objdir)
2795 try:
2796 # Initialize the bare repository, which contains all of the objects.
2797 if init_obj_dir:
2798 os.makedirs(self.objdir)
2799 self.bare_objdir.init()
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002800
Gavin Makea2e3302023-03-11 06:46:20 +00002801 self._UpdateHooks(quiet=quiet)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002802
Gavin Makea2e3302023-03-11 06:46:20 +00002803 if self.use_git_worktrees:
2804 # Enable per-worktree config file support if possible. This
2805 # is more a nice-to-have feature for users rather than a
2806 # hard requirement.
2807 if git_require((2, 20, 0)):
2808 self.EnableRepositoryExtension("worktreeConfig")
Renaud Paquay788e9622017-01-27 11:41:12 -08002809
Gavin Makea2e3302023-03-11 06:46:20 +00002810 # If we have a separate directory to hold refs, initialize it as
2811 # well.
2812 if self.objdir != self.gitdir:
2813 if init_git_dir:
2814 os.makedirs(self.gitdir)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002815
Gavin Makea2e3302023-03-11 06:46:20 +00002816 if init_obj_dir or init_git_dir:
2817 self._ReferenceGitDir(
2818 self.objdir, self.gitdir, copy_all=True
2819 )
2820 try:
2821 self._CheckDirReference(self.objdir, self.gitdir)
2822 except GitError as e:
2823 if force_sync:
2824 print(
2825 "Retrying clone after deleting %s" % self.gitdir,
2826 file=sys.stderr,
2827 )
2828 try:
2829 platform_utils.rmtree(
2830 platform_utils.realpath(self.gitdir)
2831 )
2832 if self.worktree and os.path.exists(
2833 platform_utils.realpath(self.worktree)
2834 ):
2835 platform_utils.rmtree(
2836 platform_utils.realpath(self.worktree)
2837 )
2838 return self._InitGitDir(
2839 mirror_git=mirror_git,
2840 force_sync=False,
2841 quiet=quiet,
2842 )
2843 except Exception:
2844 raise e
2845 raise e
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002846
Gavin Makea2e3302023-03-11 06:46:20 +00002847 if init_git_dir:
2848 mp = self.manifest.manifestProject
2849 ref_dir = mp.reference or ""
Julien Camperguedd654222014-01-09 16:21:37 +01002850
Gavin Makea2e3302023-03-11 06:46:20 +00002851 def _expanded_ref_dirs():
2852 """Iterate through possible git reference dir paths."""
2853 name = self.name + ".git"
2854 yield mirror_git or os.path.join(ref_dir, name)
2855 for prefix in "", self.remote.name:
2856 yield os.path.join(
2857 ref_dir, ".repo", "project-objects", prefix, name
2858 )
2859 yield os.path.join(
2860 ref_dir, ".repo", "worktrees", prefix, name
2861 )
2862
2863 if ref_dir or mirror_git:
2864 found_ref_dir = None
2865 for path in _expanded_ref_dirs():
2866 if os.path.exists(path):
2867 found_ref_dir = path
2868 break
2869 ref_dir = found_ref_dir
2870
2871 if ref_dir:
2872 if not os.path.isabs(ref_dir):
2873 # The alternate directory is relative to the object
2874 # database.
2875 ref_dir = os.path.relpath(
2876 ref_dir, os.path.join(self.objdir, "objects")
2877 )
2878 _lwrite(
2879 os.path.join(
2880 self.objdir, "objects/info/alternates"
2881 ),
2882 os.path.join(ref_dir, "objects") + "\n",
2883 )
2884
2885 m = self.manifest.manifestProject.config
2886 for key in ["user.name", "user.email"]:
2887 if m.Has(key, include_defaults=False):
2888 self.config.SetString(key, m.GetString(key))
2889 if not self.manifest.EnableGitLfs:
2890 self.config.SetString(
2891 "filter.lfs.smudge", "git-lfs smudge --skip -- %f"
2892 )
2893 self.config.SetString(
2894 "filter.lfs.process", "git-lfs filter-process --skip"
2895 )
2896 self.config.SetBoolean(
2897 "core.bare", True if self.manifest.IsMirror else None
2898 )
2899 except Exception:
2900 if init_obj_dir and os.path.exists(self.objdir):
2901 platform_utils.rmtree(self.objdir)
2902 if init_git_dir and os.path.exists(self.gitdir):
2903 platform_utils.rmtree(self.gitdir)
2904 raise
2905
2906 def _UpdateHooks(self, quiet=False):
2907 if os.path.exists(self.objdir):
2908 self._InitHooks(quiet=quiet)
2909
2910 def _InitHooks(self, quiet=False):
2911 hooks = platform_utils.realpath(os.path.join(self.objdir, "hooks"))
2912 if not os.path.exists(hooks):
2913 os.makedirs(hooks)
2914
2915 # Delete sample hooks. They're noise.
2916 for hook in glob.glob(os.path.join(hooks, "*.sample")):
2917 try:
2918 platform_utils.remove(hook, missing_ok=True)
2919 except PermissionError:
2920 pass
2921
2922 for stock_hook in _ProjectHooks():
2923 name = os.path.basename(stock_hook)
2924
2925 if (
2926 name in ("commit-msg",)
2927 and not self.remote.review
2928 and self is not self.manifest.manifestProject
2929 ):
2930 # Don't install a Gerrit Code Review hook if this
2931 # project does not appear to use it for reviews.
2932 #
2933 # Since the manifest project is one of those, but also
2934 # managed through gerrit, it's excluded.
2935 continue
2936
2937 dst = os.path.join(hooks, name)
2938 if platform_utils.islink(dst):
2939 continue
2940 if os.path.exists(dst):
2941 # If the files are the same, we'll leave it alone. We create
2942 # symlinks below by default but fallback to hardlinks if the OS
2943 # blocks them. So if we're here, it's probably because we made a
2944 # hardlink below.
2945 if not filecmp.cmp(stock_hook, dst, shallow=False):
2946 if not quiet:
2947 _warn(
2948 "%s: Not replacing locally modified %s hook",
2949 self.RelPath(local=False),
2950 name,
2951 )
2952 continue
2953 try:
2954 platform_utils.symlink(
2955 os.path.relpath(stock_hook, os.path.dirname(dst)), dst
2956 )
2957 except OSError as e:
2958 if e.errno == errno.EPERM:
2959 try:
2960 os.link(stock_hook, dst)
2961 except OSError:
2962 raise GitError(self._get_symlink_error_message())
2963 else:
2964 raise
2965
2966 def _InitRemote(self):
2967 if self.remote.url:
2968 remote = self.GetRemote()
2969 remote.url = self.remote.url
2970 remote.pushUrl = self.remote.pushUrl
2971 remote.review = self.remote.review
2972 remote.projectname = self.name
2973
2974 if self.worktree:
2975 remote.ResetFetch(mirror=False)
2976 else:
2977 remote.ResetFetch(mirror=True)
2978 remote.Save()
2979
2980 def _InitMRef(self):
2981 """Initialize the pseudo m/<manifest branch> ref."""
2982 if self.manifest.branch:
2983 if self.use_git_worktrees:
2984 # Set up the m/ space to point to the worktree-specific ref
2985 # space. We'll update the worktree-specific ref space on each
2986 # checkout.
2987 ref = R_M + self.manifest.branch
2988 if not self.bare_ref.symref(ref):
2989 self.bare_git.symbolic_ref(
2990 "-m",
2991 "redirecting to worktree scope",
2992 ref,
2993 R_WORKTREE_M + self.manifest.branch,
2994 )
2995
2996 # We can't update this ref with git worktrees until it exists.
2997 # We'll wait until the initial checkout to set it.
2998 if not os.path.exists(self.worktree):
2999 return
3000
3001 base = R_WORKTREE_M
3002 active_git = self.work_git
3003
3004 self._InitAnyMRef(HEAD, self.bare_git, detach=True)
3005 else:
3006 base = R_M
3007 active_git = self.bare_git
3008
3009 self._InitAnyMRef(base + self.manifest.branch, active_git)
3010
3011 def _InitMirrorHead(self):
3012 self._InitAnyMRef(HEAD, self.bare_git)
3013
3014 def _InitAnyMRef(self, ref, active_git, detach=False):
3015 """Initialize |ref| in |active_git| to the value in the manifest.
3016
3017 This points |ref| to the <project> setting in the manifest.
3018
3019 Args:
3020 ref: The branch to update.
3021 active_git: The git repository to make updates in.
3022 detach: Whether to update target of symbolic refs, or overwrite the
3023 ref directly (and thus make it non-symbolic).
3024 """
3025 cur = self.bare_ref.symref(ref)
3026
3027 if self.revisionId:
3028 if cur != "" or self.bare_ref.get(ref) != self.revisionId:
3029 msg = "manifest set to %s" % self.revisionId
3030 dst = self.revisionId + "^0"
3031 active_git.UpdateRef(ref, dst, message=msg, detach=True)
Julien Camperguedd654222014-01-09 16:21:37 +01003032 else:
Gavin Makea2e3302023-03-11 06:46:20 +00003033 remote = self.GetRemote()
3034 dst = remote.ToLocal(self.revisionExpr)
3035 if cur != dst:
3036 msg = "manifest set to %s" % self.revisionExpr
3037 if detach:
3038 active_git.UpdateRef(ref, dst, message=msg, detach=True)
3039 else:
3040 active_git.symbolic_ref("-m", msg, ref, dst)
Julien Camperguedd654222014-01-09 16:21:37 +01003041
Gavin Makea2e3302023-03-11 06:46:20 +00003042 def _CheckDirReference(self, srcdir, destdir):
3043 # Git worktrees don't use symlinks to share at all.
3044 if self.use_git_worktrees:
3045 return
Julien Camperguedd654222014-01-09 16:21:37 +01003046
Gavin Makea2e3302023-03-11 06:46:20 +00003047 for name in self.shareable_dirs:
3048 # Try to self-heal a bit in simple cases.
3049 dst_path = os.path.join(destdir, name)
3050 src_path = os.path.join(srcdir, name)
Julien Camperguedd654222014-01-09 16:21:37 +01003051
Gavin Makea2e3302023-03-11 06:46:20 +00003052 dst = platform_utils.realpath(dst_path)
3053 if os.path.lexists(dst):
3054 src = platform_utils.realpath(src_path)
3055 # Fail if the links are pointing to the wrong place.
3056 if src != dst:
3057 _error("%s is different in %s vs %s", name, destdir, srcdir)
3058 raise GitError(
3059 "--force-sync not enabled; cannot overwrite a local "
3060 "work tree. If you're comfortable with the "
3061 "possibility of losing the work tree's git metadata,"
3062 " use `repo sync --force-sync {0}` to "
3063 "proceed.".format(self.RelPath(local=False))
3064 )
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003065
Gavin Makea2e3302023-03-11 06:46:20 +00003066 def _ReferenceGitDir(self, gitdir, dotgit, copy_all):
3067 """Update |dotgit| to reference |gitdir|, using symlinks where possible.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003068
Gavin Makea2e3302023-03-11 06:46:20 +00003069 Args:
3070 gitdir: The bare git repository. Must already be initialized.
3071 dotgit: The repository you would like to initialize.
3072 copy_all: If true, copy all remaining files from |gitdir| ->
3073 |dotgit|. This saves you the effort of initializing |dotgit|
3074 yourself.
3075 """
3076 symlink_dirs = self.shareable_dirs[:]
3077 to_symlink = symlink_dirs
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003078
Gavin Makea2e3302023-03-11 06:46:20 +00003079 to_copy = []
3080 if copy_all:
3081 to_copy = platform_utils.listdir(gitdir)
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003082
Gavin Makea2e3302023-03-11 06:46:20 +00003083 dotgit = platform_utils.realpath(dotgit)
3084 for name in set(to_copy).union(to_symlink):
3085 try:
3086 src = platform_utils.realpath(os.path.join(gitdir, name))
3087 dst = os.path.join(dotgit, name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003088
Gavin Makea2e3302023-03-11 06:46:20 +00003089 if os.path.lexists(dst):
3090 continue
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003091
Gavin Makea2e3302023-03-11 06:46:20 +00003092 # If the source dir doesn't exist, create an empty dir.
3093 if name in symlink_dirs and not os.path.lexists(src):
3094 os.makedirs(src)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003095
Gavin Makea2e3302023-03-11 06:46:20 +00003096 if name in to_symlink:
3097 platform_utils.symlink(
3098 os.path.relpath(src, os.path.dirname(dst)), dst
3099 )
3100 elif copy_all and not platform_utils.islink(dst):
3101 if platform_utils.isdir(src):
3102 shutil.copytree(src, dst)
3103 elif os.path.isfile(src):
3104 shutil.copy(src, dst)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003105
Gavin Makea2e3302023-03-11 06:46:20 +00003106 except OSError as e:
3107 if e.errno == errno.EPERM:
3108 raise DownloadError(self._get_symlink_error_message())
3109 else:
3110 raise
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003111
Gavin Makea2e3302023-03-11 06:46:20 +00003112 def _InitGitWorktree(self):
3113 """Init the project using git worktrees."""
3114 self.bare_git.worktree("prune")
3115 self.bare_git.worktree(
3116 "add",
3117 "-ff",
3118 "--checkout",
3119 "--detach",
3120 "--lock",
3121 self.worktree,
3122 self.GetRevisionId(),
3123 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003124
Gavin Makea2e3302023-03-11 06:46:20 +00003125 # Rewrite the internal state files to use relative paths between the
3126 # checkouts & worktrees.
3127 dotgit = os.path.join(self.worktree, ".git")
3128 with open(dotgit, "r") as fp:
3129 # Figure out the checkout->worktree path.
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003130 setting = fp.read()
Gavin Makea2e3302023-03-11 06:46:20 +00003131 assert setting.startswith("gitdir:")
3132 git_worktree_path = setting.split(":", 1)[1].strip()
3133 # Some platforms (e.g. Windows) won't let us update dotgit in situ
3134 # because of file permissions. Delete it and recreate it from scratch
3135 # to avoid.
3136 platform_utils.remove(dotgit)
3137 # Use relative path from checkout->worktree & maintain Unix line endings
3138 # on all OS's to match git behavior.
3139 with open(dotgit, "w", newline="\n") as fp:
3140 print(
3141 "gitdir:",
3142 os.path.relpath(git_worktree_path, self.worktree),
3143 file=fp,
3144 )
3145 # Use relative path from worktree->checkout & maintain Unix line endings
3146 # on all OS's to match git behavior.
3147 with open(
3148 os.path.join(git_worktree_path, "gitdir"), "w", newline="\n"
3149 ) as fp:
3150 print(os.path.relpath(dotgit, git_worktree_path), file=fp)
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003151
Gavin Makea2e3302023-03-11 06:46:20 +00003152 self._InitMRef()
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003153
Gavin Makea2e3302023-03-11 06:46:20 +00003154 def _InitWorkTree(self, force_sync=False, submodules=False):
3155 """Setup the worktree .git path.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003156
Gavin Makea2e3302023-03-11 06:46:20 +00003157 This is the user-visible path like src/foo/.git/.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003158
Gavin Makea2e3302023-03-11 06:46:20 +00003159 With non-git-worktrees, this will be a symlink to the .repo/projects/
3160 path. With git-worktrees, this will be a .git file using "gitdir: ..."
3161 syntax.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003162
Gavin Makea2e3302023-03-11 06:46:20 +00003163 Older checkouts had .git/ directories. If we see that, migrate it.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003164
Gavin Makea2e3302023-03-11 06:46:20 +00003165 This also handles changes in the manifest. Maybe this project was
3166 backed by "foo/bar" on the server, but now it's "new/foo/bar". We have
3167 to update the path we point to under .repo/projects/ to match.
3168 """
3169 dotgit = os.path.join(self.worktree, ".git")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003170
Gavin Makea2e3302023-03-11 06:46:20 +00003171 # If using an old layout style (a directory), migrate it.
3172 if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
3173 self._MigrateOldWorkTreeGitDir(dotgit)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003174
Gavin Makea2e3302023-03-11 06:46:20 +00003175 init_dotgit = not os.path.exists(dotgit)
3176 if self.use_git_worktrees:
3177 if init_dotgit:
3178 self._InitGitWorktree()
3179 self._CopyAndLinkFiles()
3180 else:
3181 if not init_dotgit:
3182 # See if the project has changed.
3183 if platform_utils.realpath(
3184 self.gitdir
3185 ) != platform_utils.realpath(dotgit):
3186 platform_utils.remove(dotgit)
Doug Anderson37282b42011-03-04 11:54:18 -08003187
Gavin Makea2e3302023-03-11 06:46:20 +00003188 if init_dotgit or not os.path.exists(dotgit):
3189 os.makedirs(self.worktree, exist_ok=True)
3190 platform_utils.symlink(
3191 os.path.relpath(self.gitdir, self.worktree), dotgit
3192 )
Doug Anderson37282b42011-03-04 11:54:18 -08003193
Gavin Makea2e3302023-03-11 06:46:20 +00003194 if init_dotgit:
3195 _lwrite(
3196 os.path.join(dotgit, HEAD), "%s\n" % self.GetRevisionId()
3197 )
Doug Anderson37282b42011-03-04 11:54:18 -08003198
Gavin Makea2e3302023-03-11 06:46:20 +00003199 # Finish checking out the worktree.
3200 cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
3201 if GitCommand(self, cmd).Wait() != 0:
3202 raise GitError(
3203 "Cannot initialize work tree for " + self.name
3204 )
Doug Anderson37282b42011-03-04 11:54:18 -08003205
Gavin Makea2e3302023-03-11 06:46:20 +00003206 if submodules:
3207 self._SyncSubmodules(quiet=True)
3208 self._CopyAndLinkFiles()
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003209
Gavin Makea2e3302023-03-11 06:46:20 +00003210 @classmethod
3211 def _MigrateOldWorkTreeGitDir(cls, dotgit):
3212 """Migrate the old worktree .git/ dir style to a symlink.
3213
3214 This logic specifically only uses state from |dotgit| to figure out
3215 where to move content and not |self|. This way if the backing project
3216 also changed places, we only do the .git/ dir to .git symlink migration
3217 here. The path updates will happen independently.
3218 """
3219 # Figure out where in .repo/projects/ it's pointing to.
3220 if not os.path.islink(os.path.join(dotgit, "refs")):
3221 raise GitError(f"{dotgit}: unsupported checkout state")
3222 gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, "refs")))
3223
3224 # Remove known symlink paths that exist in .repo/projects/.
3225 KNOWN_LINKS = {
3226 "config",
3227 "description",
3228 "hooks",
3229 "info",
3230 "logs",
3231 "objects",
3232 "packed-refs",
3233 "refs",
3234 "rr-cache",
3235 "shallow",
3236 "svn",
3237 }
3238 # Paths that we know will be in both, but are safe to clobber in
3239 # .repo/projects/.
3240 SAFE_TO_CLOBBER = {
3241 "COMMIT_EDITMSG",
3242 "FETCH_HEAD",
3243 "HEAD",
3244 "gc.log",
3245 "gitk.cache",
3246 "index",
3247 "ORIG_HEAD",
3248 }
3249
3250 # First see if we'd succeed before starting the migration.
3251 unknown_paths = []
3252 for name in platform_utils.listdir(dotgit):
3253 # Ignore all temporary/backup names. These are common with vim &
3254 # emacs.
3255 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3256 continue
3257
3258 dotgit_path = os.path.join(dotgit, name)
3259 if name in KNOWN_LINKS:
3260 if not platform_utils.islink(dotgit_path):
3261 unknown_paths.append(f"{dotgit_path}: should be a symlink")
3262 else:
3263 gitdir_path = os.path.join(gitdir, name)
3264 if name not in SAFE_TO_CLOBBER and os.path.exists(gitdir_path):
3265 unknown_paths.append(
3266 f"{dotgit_path}: unknown file; please file a bug"
3267 )
3268 if unknown_paths:
3269 raise GitError("Aborting migration: " + "\n".join(unknown_paths))
3270
3271 # Now walk the paths and sync the .git/ to .repo/projects/.
3272 for name in platform_utils.listdir(dotgit):
3273 dotgit_path = os.path.join(dotgit, name)
3274
3275 # Ignore all temporary/backup names. These are common with vim &
3276 # emacs.
3277 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3278 platform_utils.remove(dotgit_path)
3279 elif name in KNOWN_LINKS:
3280 platform_utils.remove(dotgit_path)
3281 else:
3282 gitdir_path = os.path.join(gitdir, name)
3283 platform_utils.remove(gitdir_path, missing_ok=True)
3284 platform_utils.rename(dotgit_path, gitdir_path)
3285
3286 # Now that the dir should be empty, clear it out, and symlink it over.
3287 platform_utils.rmdir(dotgit)
3288 platform_utils.symlink(
3289 os.path.relpath(gitdir, os.path.dirname(dotgit)), dotgit
3290 )
3291
3292 def _get_symlink_error_message(self):
3293 if platform_utils.isWindows():
3294 return (
3295 "Unable to create symbolic link. Please re-run the command as "
3296 "Administrator, or see "
3297 "https://github.com/git-for-windows/git/wiki/Symbolic-Links "
3298 "for other options."
3299 )
3300 return "filesystem must support symlinks"
3301
3302 def _revlist(self, *args, **kw):
3303 a = []
3304 a.extend(args)
3305 a.append("--")
3306 return self.work_git.rev_list(*a, **kw)
3307
3308 @property
3309 def _allrefs(self):
3310 return self.bare_ref.all
3311
3312 def _getLogs(
3313 self, rev1, rev2, oneline=False, color=True, pretty_format=None
3314 ):
3315 """Get logs between two revisions of this project."""
3316 comp = ".."
3317 if rev1:
3318 revs = [rev1]
3319 if rev2:
3320 revs.extend([comp, rev2])
3321 cmd = ["log", "".join(revs)]
3322 out = DiffColoring(self.config)
3323 if out.is_on and color:
3324 cmd.append("--color")
3325 if pretty_format is not None:
3326 cmd.append("--pretty=format:%s" % pretty_format)
3327 if oneline:
3328 cmd.append("--oneline")
3329
3330 try:
3331 log = GitCommand(
3332 self, cmd, capture_stdout=True, capture_stderr=True
3333 )
3334 if log.Wait() == 0:
3335 return log.stdout
3336 except GitError:
3337 # worktree may not exist if groups changed for example. In that
3338 # case, try in gitdir instead.
3339 if not os.path.exists(self.worktree):
3340 return self.bare_git.log(*cmd[1:])
3341 else:
3342 raise
3343 return None
3344
3345 def getAddedAndRemovedLogs(
3346 self, toProject, oneline=False, color=True, pretty_format=None
3347 ):
3348 """Get the list of logs from this revision to given revisionId"""
3349 logs = {}
3350 selfId = self.GetRevisionId(self._allrefs)
3351 toId = toProject.GetRevisionId(toProject._allrefs)
3352
3353 logs["added"] = self._getLogs(
3354 selfId,
3355 toId,
3356 oneline=oneline,
3357 color=color,
3358 pretty_format=pretty_format,
3359 )
3360 logs["removed"] = self._getLogs(
3361 toId,
3362 selfId,
3363 oneline=oneline,
3364 color=color,
3365 pretty_format=pretty_format,
3366 )
3367 return logs
3368
3369 class _GitGetByExec(object):
3370 def __init__(self, project, bare, gitdir):
3371 self._project = project
3372 self._bare = bare
3373 self._gitdir = gitdir
3374
3375 # __getstate__ and __setstate__ are required for pickling because
3376 # __getattr__ exists.
3377 def __getstate__(self):
3378 return (self._project, self._bare, self._gitdir)
3379
3380 def __setstate__(self, state):
3381 self._project, self._bare, self._gitdir = state
3382
3383 def LsOthers(self):
3384 p = GitCommand(
3385 self._project,
3386 ["ls-files", "-z", "--others", "--exclude-standard"],
3387 bare=False,
3388 gitdir=self._gitdir,
3389 capture_stdout=True,
3390 capture_stderr=True,
3391 )
3392 if p.Wait() == 0:
3393 out = p.stdout
3394 if out:
3395 # Backslash is not anomalous.
3396 return out[:-1].split("\0")
3397 return []
3398
3399 def DiffZ(self, name, *args):
3400 cmd = [name]
3401 cmd.append("-z")
3402 cmd.append("--ignore-submodules")
3403 cmd.extend(args)
3404 p = GitCommand(
3405 self._project,
3406 cmd,
3407 gitdir=self._gitdir,
3408 bare=False,
3409 capture_stdout=True,
3410 capture_stderr=True,
3411 )
3412 p.Wait()
3413 r = {}
3414 out = p.stdout
3415 if out:
3416 out = iter(out[:-1].split("\0"))
3417 while out:
3418 try:
3419 info = next(out)
3420 path = next(out)
3421 except StopIteration:
3422 break
3423
3424 class _Info(object):
3425 def __init__(self, path, omode, nmode, oid, nid, state):
3426 self.path = path
3427 self.src_path = None
3428 self.old_mode = omode
3429 self.new_mode = nmode
3430 self.old_id = oid
3431 self.new_id = nid
3432
3433 if len(state) == 1:
3434 self.status = state
3435 self.level = None
3436 else:
3437 self.status = state[:1]
3438 self.level = state[1:]
3439 while self.level.startswith("0"):
3440 self.level = self.level[1:]
3441
3442 info = info[1:].split(" ")
3443 info = _Info(path, *info)
3444 if info.status in ("R", "C"):
3445 info.src_path = info.path
3446 info.path = next(out)
3447 r[info.path] = info
3448 return r
3449
3450 def GetDotgitPath(self, subpath=None):
3451 """Return the full path to the .git dir.
3452
3453 As a convenience, append |subpath| if provided.
3454 """
3455 if self._bare:
3456 dotgit = self._gitdir
3457 else:
3458 dotgit = os.path.join(self._project.worktree, ".git")
3459 if os.path.isfile(dotgit):
3460 # Git worktrees use a "gitdir:" syntax to point to the
3461 # scratch space.
3462 with open(dotgit) as fp:
3463 setting = fp.read()
3464 assert setting.startswith("gitdir:")
3465 gitdir = setting.split(":", 1)[1].strip()
3466 dotgit = os.path.normpath(
3467 os.path.join(self._project.worktree, gitdir)
3468 )
3469
3470 return dotgit if subpath is None else os.path.join(dotgit, subpath)
3471
3472 def GetHead(self):
3473 """Return the ref that HEAD points to."""
3474 path = self.GetDotgitPath(subpath=HEAD)
3475 try:
3476 with open(path) as fd:
3477 line = fd.readline()
3478 except IOError as e:
3479 raise NoManifestException(path, str(e))
3480 try:
3481 line = line.decode()
3482 except AttributeError:
3483 pass
3484 if line.startswith("ref: "):
3485 return line[5:-1]
3486 return line[:-1]
3487
3488 def SetHead(self, ref, message=None):
3489 cmdv = []
3490 if message is not None:
3491 cmdv.extend(["-m", message])
3492 cmdv.append(HEAD)
3493 cmdv.append(ref)
3494 self.symbolic_ref(*cmdv)
3495
3496 def DetachHead(self, new, message=None):
3497 cmdv = ["--no-deref"]
3498 if message is not None:
3499 cmdv.extend(["-m", message])
3500 cmdv.append(HEAD)
3501 cmdv.append(new)
3502 self.update_ref(*cmdv)
3503
3504 def UpdateRef(self, name, new, old=None, message=None, detach=False):
3505 cmdv = []
3506 if message is not None:
3507 cmdv.extend(["-m", message])
3508 if detach:
3509 cmdv.append("--no-deref")
3510 cmdv.append(name)
3511 cmdv.append(new)
3512 if old is not None:
3513 cmdv.append(old)
3514 self.update_ref(*cmdv)
3515
3516 def DeleteRef(self, name, old=None):
3517 if not old:
3518 old = self.rev_parse(name)
3519 self.update_ref("-d", name, old)
3520 self._project.bare_ref.deleted(name)
3521
3522 def rev_list(self, *args, **kw):
3523 if "format" in kw:
3524 cmdv = ["log", "--pretty=format:%s" % kw["format"]]
3525 else:
3526 cmdv = ["rev-list"]
3527 cmdv.extend(args)
3528 p = GitCommand(
3529 self._project,
3530 cmdv,
3531 bare=self._bare,
3532 gitdir=self._gitdir,
3533 capture_stdout=True,
3534 capture_stderr=True,
3535 )
3536 if p.Wait() != 0:
3537 raise GitError(
3538 "%s rev-list %s: %s"
3539 % (self._project.name, str(args), p.stderr)
3540 )
3541 return p.stdout.splitlines()
3542
3543 def __getattr__(self, name):
3544 """Allow arbitrary git commands using pythonic syntax.
3545
3546 This allows you to do things like:
3547 git_obj.rev_parse('HEAD')
3548
3549 Since we don't have a 'rev_parse' method defined, the __getattr__
3550 will run. We'll replace the '_' with a '-' and try to run a git
3551 command. Any other positional arguments will be passed to the git
3552 command, and the following keyword arguments are supported:
3553 config: An optional dict of git config options to be passed with
3554 '-c'.
3555
3556 Args:
3557 name: The name of the git command to call. Any '_' characters
3558 will be replaced with '-'.
3559
3560 Returns:
3561 A callable object that will try to call git with the named
3562 command.
3563 """
3564 name = name.replace("_", "-")
3565
3566 def runner(*args, **kwargs):
3567 cmdv = []
3568 config = kwargs.pop("config", None)
3569 for k in kwargs:
3570 raise TypeError(
3571 "%s() got an unexpected keyword argument %r" % (name, k)
3572 )
3573 if config is not None:
3574 for k, v in config.items():
3575 cmdv.append("-c")
3576 cmdv.append("%s=%s" % (k, v))
3577 cmdv.append(name)
3578 cmdv.extend(args)
3579 p = GitCommand(
3580 self._project,
3581 cmdv,
3582 bare=self._bare,
3583 gitdir=self._gitdir,
3584 capture_stdout=True,
3585 capture_stderr=True,
3586 )
3587 if p.Wait() != 0:
3588 raise GitError(
3589 "%s %s: %s" % (self._project.name, name, p.stderr)
3590 )
3591 r = p.stdout
3592 if r.endswith("\n") and r.index("\n") == len(r) - 1:
3593 return r[:-1]
3594 return r
3595
3596 return runner
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003597
3598
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003599class _PriorSyncFailedError(Exception):
Gavin Makea2e3302023-03-11 06:46:20 +00003600 def __str__(self):
3601 return "prior sync failed; rebase still in progress"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003602
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003603
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003604class _DirtyError(Exception):
Gavin Makea2e3302023-03-11 06:46:20 +00003605 def __str__(self):
3606 return "contains uncommitted changes"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003607
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003608
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003609class _InfoMessage(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003610 def __init__(self, project, text):
3611 self.project = project
3612 self.text = text
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003613
Gavin Makea2e3302023-03-11 06:46:20 +00003614 def Print(self, syncbuf):
3615 syncbuf.out.info(
3616 "%s/: %s", self.project.RelPath(local=False), self.text
3617 )
3618 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003619
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003620
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003621class _Failure(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003622 def __init__(self, project, why):
3623 self.project = project
3624 self.why = why
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003625
Gavin Makea2e3302023-03-11 06:46:20 +00003626 def Print(self, syncbuf):
3627 syncbuf.out.fail(
3628 "error: %s/: %s", self.project.RelPath(local=False), str(self.why)
3629 )
3630 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003631
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003632
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003633class _Later(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003634 def __init__(self, project, action):
3635 self.project = project
3636 self.action = action
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003637
Gavin Makea2e3302023-03-11 06:46:20 +00003638 def Run(self, syncbuf):
3639 out = syncbuf.out
3640 out.project("project %s/", self.project.RelPath(local=False))
3641 out.nl()
3642 try:
3643 self.action()
3644 out.nl()
3645 return True
3646 except GitError:
3647 out.nl()
3648 return False
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003649
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003650
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003651class _SyncColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +00003652 def __init__(self, config):
3653 super().__init__(config, "reposync")
3654 self.project = self.printer("header", attr="bold")
3655 self.info = self.printer("info")
3656 self.fail = self.printer("fail", fg="red")
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003657
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003658
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003659class SyncBuffer(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003660 def __init__(self, config, detach_head=False):
3661 self._messages = []
3662 self._failures = []
3663 self._later_queue1 = []
3664 self._later_queue2 = []
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003665
Gavin Makea2e3302023-03-11 06:46:20 +00003666 self.out = _SyncColoring(config)
3667 self.out.redirect(sys.stderr)
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003668
Gavin Makea2e3302023-03-11 06:46:20 +00003669 self.detach_head = detach_head
3670 self.clean = True
3671 self.recent_clean = True
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003672
Gavin Makea2e3302023-03-11 06:46:20 +00003673 def info(self, project, fmt, *args):
3674 self._messages.append(_InfoMessage(project, fmt % args))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003675
Gavin Makea2e3302023-03-11 06:46:20 +00003676 def fail(self, project, err=None):
3677 self._failures.append(_Failure(project, err))
David Rileye0684ad2017-04-05 00:02:59 -07003678 self._MarkUnclean()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003679
Gavin Makea2e3302023-03-11 06:46:20 +00003680 def later1(self, project, what):
3681 self._later_queue1.append(_Later(project, what))
Mike Frysinger70d861f2019-08-26 15:22:36 -04003682
Gavin Makea2e3302023-03-11 06:46:20 +00003683 def later2(self, project, what):
3684 self._later_queue2.append(_Later(project, what))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003685
Gavin Makea2e3302023-03-11 06:46:20 +00003686 def Finish(self):
3687 self._PrintMessages()
3688 self._RunLater()
3689 self._PrintMessages()
3690 return self.clean
3691
3692 def Recently(self):
3693 recent_clean = self.recent_clean
3694 self.recent_clean = True
3695 return recent_clean
3696
3697 def _MarkUnclean(self):
3698 self.clean = False
3699 self.recent_clean = False
3700
3701 def _RunLater(self):
3702 for q in ["_later_queue1", "_later_queue2"]:
3703 if not self._RunQueue(q):
3704 return
3705
3706 def _RunQueue(self, queue):
3707 for m in getattr(self, queue):
3708 if not m.Run(self):
3709 self._MarkUnclean()
3710 return False
3711 setattr(self, queue, [])
3712 return True
3713
3714 def _PrintMessages(self):
3715 if self._messages or self._failures:
3716 if os.isatty(2):
3717 self.out.write(progress.CSI_ERASE_LINE)
3718 self.out.write("\r")
3719
3720 for m in self._messages:
3721 m.Print(self)
3722 for m in self._failures:
3723 m.Print(self)
3724
3725 self._messages = []
3726 self._failures = []
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003727
3728
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003729class MetaProject(Project):
Gavin Makea2e3302023-03-11 06:46:20 +00003730 """A special project housed under .repo."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003731
Gavin Makea2e3302023-03-11 06:46:20 +00003732 def __init__(self, manifest, name, gitdir, worktree):
3733 Project.__init__(
3734 self,
3735 manifest=manifest,
3736 name=name,
3737 gitdir=gitdir,
3738 objdir=gitdir,
3739 worktree=worktree,
3740 remote=RemoteSpec("origin"),
3741 relpath=".repo/%s" % name,
3742 revisionExpr="refs/heads/master",
3743 revisionId=None,
3744 groups=None,
3745 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003746
Gavin Makea2e3302023-03-11 06:46:20 +00003747 def PreSync(self):
3748 if self.Exists:
3749 cb = self.CurrentBranch
3750 if cb:
3751 base = self.GetBranch(cb).merge
3752 if base:
3753 self.revisionExpr = base
3754 self.revisionId = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003755
Gavin Makea2e3302023-03-11 06:46:20 +00003756 @property
3757 def HasChanges(self):
3758 """Has the remote received new commits not yet checked out?"""
3759 if not self.remote or not self.revisionExpr:
3760 return False
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003761
Gavin Makea2e3302023-03-11 06:46:20 +00003762 all_refs = self.bare_ref.all
3763 revid = self.GetRevisionId(all_refs)
3764 head = self.work_git.GetHead()
3765 if head.startswith(R_HEADS):
3766 try:
3767 head = all_refs[head]
3768 except KeyError:
3769 head = None
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003770
Gavin Makea2e3302023-03-11 06:46:20 +00003771 if revid == head:
3772 return False
3773 elif self._revlist(not_rev(HEAD), revid):
3774 return True
3775 return False
LaMont Jones9b72cf22022-03-29 21:54:22 +00003776
3777
3778class RepoProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003779 """The MetaProject for repo itself."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003780
Gavin Makea2e3302023-03-11 06:46:20 +00003781 @property
3782 def LastFetch(self):
3783 try:
3784 fh = os.path.join(self.gitdir, "FETCH_HEAD")
3785 return os.path.getmtime(fh)
3786 except OSError:
3787 return 0
LaMont Jones9b72cf22022-03-29 21:54:22 +00003788
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +01003789
LaMont Jones9b72cf22022-03-29 21:54:22 +00003790class ManifestProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003791 """The MetaProject for manifests."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003792
Gavin Makea2e3302023-03-11 06:46:20 +00003793 def MetaBranchSwitch(self, submodules=False):
3794 """Prepare for manifest branch switch."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003795
Gavin Makea2e3302023-03-11 06:46:20 +00003796 # detach and delete manifest branch, allowing a new
3797 # branch to take over
3798 syncbuf = SyncBuffer(self.config, detach_head=True)
3799 self.Sync_LocalHalf(syncbuf, submodules=submodules)
3800 syncbuf.Finish()
LaMont Jones9b72cf22022-03-29 21:54:22 +00003801
Gavin Makea2e3302023-03-11 06:46:20 +00003802 return (
3803 GitCommand(
3804 self,
3805 ["update-ref", "-d", "refs/heads/default"],
3806 capture_stdout=True,
3807 capture_stderr=True,
3808 ).Wait()
3809 == 0
3810 )
LaMont Jones9b03f152022-03-29 23:01:18 +00003811
Gavin Makea2e3302023-03-11 06:46:20 +00003812 @property
3813 def standalone_manifest_url(self):
3814 """The URL of the standalone manifest, or None."""
3815 return self.config.GetString("manifest.standalone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003816
Gavin Makea2e3302023-03-11 06:46:20 +00003817 @property
3818 def manifest_groups(self):
3819 """The manifest groups string."""
3820 return self.config.GetString("manifest.groups")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003821
Gavin Makea2e3302023-03-11 06:46:20 +00003822 @property
3823 def reference(self):
3824 """The --reference for this manifest."""
3825 return self.config.GetString("repo.reference")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003826
Gavin Makea2e3302023-03-11 06:46:20 +00003827 @property
3828 def dissociate(self):
3829 """Whether to dissociate."""
3830 return self.config.GetBoolean("repo.dissociate")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003831
Gavin Makea2e3302023-03-11 06:46:20 +00003832 @property
3833 def archive(self):
3834 """Whether we use archive."""
3835 return self.config.GetBoolean("repo.archive")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003836
Gavin Makea2e3302023-03-11 06:46:20 +00003837 @property
3838 def mirror(self):
3839 """Whether we use mirror."""
3840 return self.config.GetBoolean("repo.mirror")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003841
Gavin Makea2e3302023-03-11 06:46:20 +00003842 @property
3843 def use_worktree(self):
3844 """Whether we use worktree."""
3845 return self.config.GetBoolean("repo.worktree")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003846
Gavin Makea2e3302023-03-11 06:46:20 +00003847 @property
3848 def clone_bundle(self):
3849 """Whether we use clone_bundle."""
3850 return self.config.GetBoolean("repo.clonebundle")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003851
Gavin Makea2e3302023-03-11 06:46:20 +00003852 @property
3853 def submodules(self):
3854 """Whether we use submodules."""
3855 return self.config.GetBoolean("repo.submodules")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003856
Gavin Makea2e3302023-03-11 06:46:20 +00003857 @property
3858 def git_lfs(self):
3859 """Whether we use git_lfs."""
3860 return self.config.GetBoolean("repo.git-lfs")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003861
Gavin Makea2e3302023-03-11 06:46:20 +00003862 @property
3863 def use_superproject(self):
3864 """Whether we use superproject."""
3865 return self.config.GetBoolean("repo.superproject")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003866
Gavin Makea2e3302023-03-11 06:46:20 +00003867 @property
3868 def partial_clone(self):
3869 """Whether this is a partial clone."""
3870 return self.config.GetBoolean("repo.partialclone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003871
Gavin Makea2e3302023-03-11 06:46:20 +00003872 @property
3873 def depth(self):
3874 """Partial clone depth."""
3875 return self.config.GetString("repo.depth")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003876
Gavin Makea2e3302023-03-11 06:46:20 +00003877 @property
3878 def clone_filter(self):
3879 """The clone filter."""
3880 return self.config.GetString("repo.clonefilter")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003881
Gavin Makea2e3302023-03-11 06:46:20 +00003882 @property
3883 def partial_clone_exclude(self):
3884 """Partial clone exclude string"""
3885 return self.config.GetString("repo.partialcloneexclude")
LaMont Jones4ada0432022-04-14 15:10:43 +00003886
Gavin Makea2e3302023-03-11 06:46:20 +00003887 @property
3888 def manifest_platform(self):
3889 """The --platform argument from `repo init`."""
3890 return self.config.GetString("manifest.platform")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003891
Gavin Makea2e3302023-03-11 06:46:20 +00003892 @property
3893 def _platform_name(self):
3894 """Return the name of the platform."""
3895 return platform.system().lower()
LaMont Jones9b03f152022-03-29 23:01:18 +00003896
Gavin Makea2e3302023-03-11 06:46:20 +00003897 def SyncWithPossibleInit(
3898 self,
3899 submanifest,
3900 verbose=False,
3901 current_branch_only=False,
3902 tags="",
3903 git_event_log=None,
3904 ):
3905 """Sync a manifestProject, possibly for the first time.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00003906
Gavin Makea2e3302023-03-11 06:46:20 +00003907 Call Sync() with arguments from the most recent `repo init`. If this is
3908 a new sub manifest, then inherit options from the parent's
3909 manifestProject.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00003910
Gavin Makea2e3302023-03-11 06:46:20 +00003911 This is used by subcmds.Sync() to do an initial download of new sub
3912 manifests.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00003913
Gavin Makea2e3302023-03-11 06:46:20 +00003914 Args:
3915 submanifest: an XmlSubmanifest, the submanifest to re-sync.
3916 verbose: a boolean, whether to show all output, rather than only
3917 errors.
3918 current_branch_only: a boolean, whether to only fetch the current
3919 manifest branch from the server.
3920 tags: a boolean, whether to fetch tags.
3921 git_event_log: an EventLog, for git tracing.
3922 """
3923 # TODO(lamontjones): when refactoring sync (and init?) consider how to
3924 # better get the init options that we should use for new submanifests
3925 # that are added when syncing an existing workspace.
3926 git_event_log = git_event_log or EventLog()
LaMont Jonesb90a4222022-04-14 15:00:09 +00003927 spec = submanifest.ToSubmanifestSpec()
Gavin Makea2e3302023-03-11 06:46:20 +00003928 # Use the init options from the existing manifestProject, or the parent
3929 # if it doesn't exist.
3930 #
3931 # Today, we only support changing manifest_groups on the sub-manifest,
3932 # with no supported-for-the-user way to change the other arguments from
3933 # those specified by the outermost manifest.
3934 #
3935 # TODO(lamontjones): determine which of these should come from the
3936 # outermost manifest and which should come from the parent manifest.
3937 mp = self if self.Exists else submanifest.parent.manifestProject
3938 return self.Sync(
LaMont Jones55ee3042022-04-06 17:10:21 +00003939 manifest_url=spec.manifestUrl,
3940 manifest_branch=spec.revision,
Gavin Makea2e3302023-03-11 06:46:20 +00003941 standalone_manifest=mp.standalone_manifest_url,
3942 groups=mp.manifest_groups,
3943 platform=mp.manifest_platform,
3944 mirror=mp.mirror,
3945 dissociate=mp.dissociate,
3946 reference=mp.reference,
3947 worktree=mp.use_worktree,
3948 submodules=mp.submodules,
3949 archive=mp.archive,
3950 partial_clone=mp.partial_clone,
3951 clone_filter=mp.clone_filter,
3952 partial_clone_exclude=mp.partial_clone_exclude,
3953 clone_bundle=mp.clone_bundle,
3954 git_lfs=mp.git_lfs,
3955 use_superproject=mp.use_superproject,
LaMont Jones55ee3042022-04-06 17:10:21 +00003956 verbose=verbose,
3957 current_branch_only=current_branch_only,
3958 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00003959 depth=mp.depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00003960 git_event_log=git_event_log,
3961 manifest_name=spec.manifestName,
Gavin Makea2e3302023-03-11 06:46:20 +00003962 this_manifest_only=True,
LaMont Jones55ee3042022-04-06 17:10:21 +00003963 outer_manifest=False,
3964 )
LaMont Jones409407a2022-04-05 21:21:56 +00003965
Gavin Makea2e3302023-03-11 06:46:20 +00003966 def Sync(
3967 self,
3968 _kwargs_only=(),
3969 manifest_url="",
3970 manifest_branch=None,
3971 standalone_manifest=False,
3972 groups="",
3973 mirror=False,
3974 reference="",
3975 dissociate=False,
3976 worktree=False,
3977 submodules=False,
3978 archive=False,
3979 partial_clone=None,
3980 depth=None,
3981 clone_filter="blob:none",
3982 partial_clone_exclude=None,
3983 clone_bundle=None,
3984 git_lfs=None,
3985 use_superproject=None,
3986 verbose=False,
3987 current_branch_only=False,
3988 git_event_log=None,
3989 platform="",
3990 manifest_name="default.xml",
3991 tags="",
3992 this_manifest_only=False,
3993 outer_manifest=True,
3994 ):
3995 """Sync the manifest and all submanifests.
LaMont Jones409407a2022-04-05 21:21:56 +00003996
Gavin Makea2e3302023-03-11 06:46:20 +00003997 Args:
3998 manifest_url: a string, the URL of the manifest project.
3999 manifest_branch: a string, the manifest branch to use.
4000 standalone_manifest: a boolean, whether to store the manifest as a
4001 static file.
4002 groups: a string, restricts the checkout to projects with the
4003 specified groups.
4004 mirror: a boolean, whether to create a mirror of the remote
4005 repository.
4006 reference: a string, location of a repo instance to use as a
4007 reference.
4008 dissociate: a boolean, whether to dissociate from reference mirrors
4009 after clone.
4010 worktree: a boolean, whether to use git-worktree to manage projects.
4011 submodules: a boolean, whether sync submodules associated with the
4012 manifest project.
4013 archive: a boolean, whether to checkout each project as an archive.
4014 See git-archive.
4015 partial_clone: a boolean, whether to perform a partial clone.
4016 depth: an int, how deep of a shallow clone to create.
4017 clone_filter: a string, filter to use with partial_clone.
4018 partial_clone_exclude : a string, comma-delimeted list of project
4019 names to exclude from partial clone.
4020 clone_bundle: a boolean, whether to enable /clone.bundle on
4021 HTTP/HTTPS.
4022 git_lfs: a boolean, whether to enable git LFS support.
4023 use_superproject: a boolean, whether to use the manifest
4024 superproject to sync projects.
4025 verbose: a boolean, whether to show all output, rather than only
4026 errors.
4027 current_branch_only: a boolean, whether to only fetch the current
4028 manifest branch from the server.
4029 platform: a string, restrict the checkout to projects with the
4030 specified platform group.
4031 git_event_log: an EventLog, for git tracing.
4032 tags: a boolean, whether to fetch tags.
4033 manifest_name: a string, the name of the manifest file to use.
4034 this_manifest_only: a boolean, whether to only operate on the
4035 current sub manifest.
4036 outer_manifest: a boolean, whether to start at the outermost
4037 manifest.
LaMont Jones9b03f152022-03-29 23:01:18 +00004038
Gavin Makea2e3302023-03-11 06:46:20 +00004039 Returns:
4040 a boolean, whether the sync was successful.
4041 """
4042 assert _kwargs_only == (), "Sync only accepts keyword arguments."
LaMont Jones9b03f152022-03-29 23:01:18 +00004043
Gavin Makea2e3302023-03-11 06:46:20 +00004044 groups = groups or self.manifest.GetDefaultGroupsStr(
4045 with_platform=False
4046 )
4047 platform = platform or "auto"
4048 git_event_log = git_event_log or EventLog()
4049 if outer_manifest and self.manifest.is_submanifest:
4050 # In a multi-manifest checkout, use the outer manifest unless we are
4051 # told not to.
4052 return self.client.outer_manifest.manifestProject.Sync(
4053 manifest_url=manifest_url,
4054 manifest_branch=manifest_branch,
4055 standalone_manifest=standalone_manifest,
4056 groups=groups,
4057 platform=platform,
4058 mirror=mirror,
4059 dissociate=dissociate,
4060 reference=reference,
4061 worktree=worktree,
4062 submodules=submodules,
4063 archive=archive,
4064 partial_clone=partial_clone,
4065 clone_filter=clone_filter,
4066 partial_clone_exclude=partial_clone_exclude,
4067 clone_bundle=clone_bundle,
4068 git_lfs=git_lfs,
4069 use_superproject=use_superproject,
4070 verbose=verbose,
4071 current_branch_only=current_branch_only,
4072 tags=tags,
4073 depth=depth,
4074 git_event_log=git_event_log,
4075 manifest_name=manifest_name,
4076 this_manifest_only=this_manifest_only,
4077 outer_manifest=False,
4078 )
LaMont Jones9b03f152022-03-29 23:01:18 +00004079
Gavin Makea2e3302023-03-11 06:46:20 +00004080 # If repo has already been initialized, we take -u with the absence of
4081 # --standalone-manifest to mean "transition to a standard repo set up",
4082 # which necessitates starting fresh.
4083 # If --standalone-manifest is set, we always tear everything down and
4084 # start anew.
4085 if self.Exists:
4086 was_standalone_manifest = self.config.GetString(
4087 "manifest.standalone"
4088 )
4089 if was_standalone_manifest and not manifest_url:
4090 print(
4091 "fatal: repo was initialized with a standlone manifest, "
4092 "cannot be re-initialized without --manifest-url/-u"
4093 )
4094 return False
4095
4096 if standalone_manifest or (
4097 was_standalone_manifest and manifest_url
4098 ):
4099 self.config.ClearCache()
4100 if self.gitdir and os.path.exists(self.gitdir):
4101 platform_utils.rmtree(self.gitdir)
4102 if self.worktree and os.path.exists(self.worktree):
4103 platform_utils.rmtree(self.worktree)
4104
4105 is_new = not self.Exists
4106 if is_new:
4107 if not manifest_url:
4108 print("fatal: manifest url is required.", file=sys.stderr)
4109 return False
4110
4111 if verbose:
4112 print(
4113 "Downloading manifest from %s"
4114 % (GitConfig.ForUser().UrlInsteadOf(manifest_url),),
4115 file=sys.stderr,
4116 )
4117
4118 # The manifest project object doesn't keep track of the path on the
4119 # server where this git is located, so let's save that here.
4120 mirrored_manifest_git = None
4121 if reference:
4122 manifest_git_path = urllib.parse.urlparse(manifest_url).path[1:]
4123 mirrored_manifest_git = os.path.join(
4124 reference, manifest_git_path
4125 )
4126 if not mirrored_manifest_git.endswith(".git"):
4127 mirrored_manifest_git += ".git"
4128 if not os.path.exists(mirrored_manifest_git):
4129 mirrored_manifest_git = os.path.join(
4130 reference, ".repo/manifests.git"
4131 )
4132
4133 self._InitGitDir(mirror_git=mirrored_manifest_git)
4134
4135 # If standalone_manifest is set, mark the project as "standalone" --
4136 # we'll still do much of the manifests.git set up, but will avoid actual
4137 # syncs to a remote.
4138 if standalone_manifest:
4139 self.config.SetString("manifest.standalone", manifest_url)
4140 elif not manifest_url and not manifest_branch:
4141 # If -u is set and --standalone-manifest is not, then we're not in
4142 # standalone mode. Otherwise, use config to infer what we were in
4143 # the last init.
4144 standalone_manifest = bool(
4145 self.config.GetString("manifest.standalone")
4146 )
4147 if not standalone_manifest:
4148 self.config.SetString("manifest.standalone", None)
4149
4150 self._ConfigureDepth(depth)
4151
4152 # Set the remote URL before the remote branch as we might need it below.
4153 if manifest_url:
4154 r = self.GetRemote()
4155 r.url = manifest_url
4156 r.ResetFetch()
4157 r.Save()
4158
4159 if not standalone_manifest:
4160 if manifest_branch:
4161 if manifest_branch == "HEAD":
4162 manifest_branch = self.ResolveRemoteHead()
4163 if manifest_branch is None:
4164 print("fatal: unable to resolve HEAD", file=sys.stderr)
4165 return False
4166 self.revisionExpr = manifest_branch
4167 else:
4168 if is_new:
4169 default_branch = self.ResolveRemoteHead()
4170 if default_branch is None:
4171 # If the remote doesn't have HEAD configured, default to
4172 # master.
4173 default_branch = "refs/heads/master"
4174 self.revisionExpr = default_branch
4175 else:
4176 self.PreSync()
4177
4178 groups = re.split(r"[,\s]+", groups or "")
4179 all_platforms = ["linux", "darwin", "windows"]
4180 platformize = lambda x: "platform-" + x
4181 if platform == "auto":
4182 if not mirror and not self.mirror:
4183 groups.append(platformize(self._platform_name))
4184 elif platform == "all":
4185 groups.extend(map(platformize, all_platforms))
4186 elif platform in all_platforms:
4187 groups.append(platformize(platform))
4188 elif platform != "none":
4189 print("fatal: invalid platform flag", file=sys.stderr)
4190 return False
4191 self.config.SetString("manifest.platform", platform)
4192
4193 groups = [x for x in groups if x]
4194 groupstr = ",".join(groups)
4195 if (
4196 platform == "auto"
4197 and groupstr == self.manifest.GetDefaultGroupsStr()
4198 ):
4199 groupstr = None
4200 self.config.SetString("manifest.groups", groupstr)
4201
4202 if reference:
4203 self.config.SetString("repo.reference", reference)
4204
4205 if dissociate:
4206 self.config.SetBoolean("repo.dissociate", dissociate)
4207
4208 if worktree:
4209 if mirror:
4210 print(
4211 "fatal: --mirror and --worktree are incompatible",
4212 file=sys.stderr,
4213 )
4214 return False
4215 if submodules:
4216 print(
4217 "fatal: --submodules and --worktree are incompatible",
4218 file=sys.stderr,
4219 )
4220 return False
4221 self.config.SetBoolean("repo.worktree", worktree)
4222 if is_new:
4223 self.use_git_worktrees = True
4224 print("warning: --worktree is experimental!", file=sys.stderr)
4225
4226 if archive:
4227 if is_new:
4228 self.config.SetBoolean("repo.archive", archive)
4229 else:
4230 print(
4231 "fatal: --archive is only supported when initializing a "
4232 "new workspace.",
4233 file=sys.stderr,
4234 )
4235 print(
4236 "Either delete the .repo folder in this workspace, or "
4237 "initialize in another location.",
4238 file=sys.stderr,
4239 )
4240 return False
4241
4242 if mirror:
4243 if is_new:
4244 self.config.SetBoolean("repo.mirror", mirror)
4245 else:
4246 print(
4247 "fatal: --mirror is only supported when initializing a new "
4248 "workspace.",
4249 file=sys.stderr,
4250 )
4251 print(
4252 "Either delete the .repo folder in this workspace, or "
4253 "initialize in another location.",
4254 file=sys.stderr,
4255 )
4256 return False
4257
4258 if partial_clone is not None:
4259 if mirror:
4260 print(
4261 "fatal: --mirror and --partial-clone are mutually "
4262 "exclusive",
4263 file=sys.stderr,
4264 )
4265 return False
4266 self.config.SetBoolean("repo.partialclone", partial_clone)
4267 if clone_filter:
4268 self.config.SetString("repo.clonefilter", clone_filter)
4269 elif self.partial_clone:
4270 clone_filter = self.clone_filter
4271 else:
4272 clone_filter = None
4273
4274 if partial_clone_exclude is not None:
4275 self.config.SetString(
4276 "repo.partialcloneexclude", partial_clone_exclude
4277 )
4278
4279 if clone_bundle is None:
4280 clone_bundle = False if partial_clone else True
4281 else:
4282 self.config.SetBoolean("repo.clonebundle", clone_bundle)
4283
4284 if submodules:
4285 self.config.SetBoolean("repo.submodules", submodules)
4286
4287 if git_lfs is not None:
4288 if git_lfs:
4289 git_require((2, 17, 0), fail=True, msg="Git LFS support")
4290
4291 self.config.SetBoolean("repo.git-lfs", git_lfs)
4292 if not is_new:
4293 print(
4294 "warning: Changing --git-lfs settings will only affect new "
4295 "project checkouts.\n"
4296 " Existing projects will require manual updates.\n",
4297 file=sys.stderr,
4298 )
4299
4300 if use_superproject is not None:
4301 self.config.SetBoolean("repo.superproject", use_superproject)
4302
4303 if not standalone_manifest:
4304 success = self.Sync_NetworkHalf(
4305 is_new=is_new,
4306 quiet=not verbose,
4307 verbose=verbose,
4308 clone_bundle=clone_bundle,
4309 current_branch_only=current_branch_only,
4310 tags=tags,
4311 submodules=submodules,
4312 clone_filter=clone_filter,
4313 partial_clone_exclude=self.manifest.PartialCloneExclude,
4314 ).success
4315 if not success:
4316 r = self.GetRemote()
4317 print(
4318 "fatal: cannot obtain manifest %s" % r.url, file=sys.stderr
4319 )
4320
4321 # Better delete the manifest git dir if we created it; otherwise
4322 # next time (when user fixes problems) we won't go through the
4323 # "is_new" logic.
4324 if is_new:
4325 platform_utils.rmtree(self.gitdir)
4326 return False
4327
4328 if manifest_branch:
4329 self.MetaBranchSwitch(submodules=submodules)
4330
4331 syncbuf = SyncBuffer(self.config)
4332 self.Sync_LocalHalf(syncbuf, submodules=submodules)
4333 syncbuf.Finish()
4334
4335 if is_new or self.CurrentBranch is None:
4336 if not self.StartBranch("default"):
4337 print(
4338 "fatal: cannot create default in manifest",
4339 file=sys.stderr,
4340 )
4341 return False
4342
4343 if not manifest_name:
4344 print("fatal: manifest name (-m) is required.", file=sys.stderr)
4345 return False
4346
4347 elif is_new:
4348 # This is a new standalone manifest.
4349 manifest_name = "default.xml"
4350 manifest_data = fetch.fetch_file(manifest_url, verbose=verbose)
4351 dest = os.path.join(self.worktree, manifest_name)
4352 os.makedirs(os.path.dirname(dest), exist_ok=True)
4353 with open(dest, "wb") as f:
4354 f.write(manifest_data)
4355
4356 try:
4357 self.manifest.Link(manifest_name)
4358 except ManifestParseError as e:
4359 print(
4360 "fatal: manifest '%s' not available" % manifest_name,
4361 file=sys.stderr,
4362 )
4363 print("fatal: %s" % str(e), file=sys.stderr)
4364 return False
4365
4366 if not this_manifest_only:
4367 for submanifest in self.manifest.submanifests.values():
4368 spec = submanifest.ToSubmanifestSpec()
4369 submanifest.repo_client.manifestProject.Sync(
4370 manifest_url=spec.manifestUrl,
4371 manifest_branch=spec.revision,
4372 standalone_manifest=standalone_manifest,
4373 groups=self.manifest_groups,
4374 platform=platform,
4375 mirror=mirror,
4376 dissociate=dissociate,
4377 reference=reference,
4378 worktree=worktree,
4379 submodules=submodules,
4380 archive=archive,
4381 partial_clone=partial_clone,
4382 clone_filter=clone_filter,
4383 partial_clone_exclude=partial_clone_exclude,
4384 clone_bundle=clone_bundle,
4385 git_lfs=git_lfs,
4386 use_superproject=use_superproject,
4387 verbose=verbose,
4388 current_branch_only=current_branch_only,
4389 tags=tags,
4390 depth=depth,
4391 git_event_log=git_event_log,
4392 manifest_name=spec.manifestName,
4393 this_manifest_only=False,
4394 outer_manifest=False,
4395 )
4396
4397 # Lastly, if the manifest has a <superproject> then have the
4398 # superproject sync it (if it will be used).
4399 if git_superproject.UseSuperproject(use_superproject, self.manifest):
4400 sync_result = self.manifest.superproject.Sync(git_event_log)
4401 if not sync_result.success:
4402 submanifest = ""
4403 if self.manifest.path_prefix:
4404 submanifest = f"for {self.manifest.path_prefix} "
4405 print(
4406 f"warning: git update of superproject {submanifest}failed, "
4407 "repo sync will not use superproject to fetch source; "
4408 "while this error is not fatal, and you can continue to "
4409 "run repo sync, please run repo init with the "
4410 "--no-use-superproject option to stop seeing this warning",
4411 file=sys.stderr,
4412 )
4413 if sync_result.fatal and use_superproject is not None:
4414 return False
4415
4416 return True
4417
4418 def _ConfigureDepth(self, depth):
4419 """Configure the depth we'll sync down.
4420
4421 Args:
4422 depth: an int, how deep of a partial clone to create.
4423 """
4424 # Opt.depth will be non-None if user actually passed --depth to repo
4425 # init.
4426 if depth is not None:
4427 if depth > 0:
4428 # Positive values will set the depth.
4429 depth = str(depth)
4430 else:
4431 # Negative numbers will clear the depth; passing None to
4432 # SetString will do that.
4433 depth = None
4434
4435 # We store the depth in the main manifest project.
4436 self.config.SetString("repo.depth", depth)