blob: 887fe83aa1c11f00d9b72a0534d8e937c86e380f [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
1110 for push_option in push_options or []:
1111 cmd.append("-o")
1112 cmd.append(push_option)
1113
1114 cmd.append(url)
1115
1116 if dest_branch.startswith(R_HEADS):
1117 dest_branch = dest_branch[len(R_HEADS) :]
1118
1119 ref_spec = "%s:refs/for/%s" % (R_HEADS + branch.name, dest_branch)
1120 opts = []
1121 if auto_topic:
1122 opts += ["topic=" + branch.name]
1123 opts += ["t=%s" % p for p in hashtags]
1124 # NB: No need to encode labels as they've been validated above.
1125 opts += ["l=%s" % p for p in labels]
1126
1127 opts += ["r=%s" % p for p in people[0]]
1128 opts += ["cc=%s" % p for p in people[1]]
1129 if notify:
1130 opts += ["notify=" + notify]
1131 if private:
1132 opts += ["private"]
1133 if wip:
1134 opts += ["wip"]
1135 if ready:
1136 opts += ["ready"]
1137 if opts:
1138 ref_spec = ref_spec + "%" + ",".join(opts)
1139 cmd.append(ref_spec)
1140
1141 if GitCommand(self, cmd, bare=True).Wait() != 0:
1142 raise UploadError("Upload failed")
1143
1144 if not dryrun:
1145 msg = "posted to %s for %s" % (branch.remote.review, dest_branch)
1146 self.bare_git.UpdateRef(
1147 R_PUB + branch.name, R_HEADS + branch.name, message=msg
1148 )
1149
1150 def _ExtractArchive(self, tarpath, path=None):
1151 """Extract the given tar on its current location
1152
1153 Args:
1154 tarpath: The path to the actual tar file
1155
1156 """
1157 try:
1158 with tarfile.open(tarpath, "r") as tar:
1159 tar.extractall(path=path)
1160 return True
1161 except (IOError, tarfile.TarError) as e:
1162 _error("Cannot extract archive %s: %s", tarpath, str(e))
1163 return False
1164
1165 def Sync_NetworkHalf(
1166 self,
1167 quiet=False,
1168 verbose=False,
1169 output_redir=None,
1170 is_new=None,
1171 current_branch_only=None,
1172 force_sync=False,
1173 clone_bundle=True,
1174 tags=None,
1175 archive=False,
1176 optimized_fetch=False,
1177 retry_fetches=0,
1178 prune=False,
1179 submodules=False,
1180 ssh_proxy=None,
1181 clone_filter=None,
1182 partial_clone_exclude=set(),
1183 ):
1184 """Perform only the network IO portion of the sync process.
1185 Local working directory/branch state is not affected.
1186 """
1187 if archive and not isinstance(self, MetaProject):
1188 if self.remote.url.startswith(("http://", "https://")):
1189 _error(
1190 "%s: Cannot fetch archives from http/https remotes.",
1191 self.name,
1192 )
1193 return SyncNetworkHalfResult(False, False)
1194
1195 name = self.relpath.replace("\\", "/")
1196 name = name.replace("/", "_")
1197 tarpath = "%s.tar" % name
1198 topdir = self.manifest.topdir
1199
1200 try:
1201 self._FetchArchive(tarpath, cwd=topdir)
1202 except GitError as e:
1203 _error("%s", e)
1204 return SyncNetworkHalfResult(False, False)
1205
1206 # From now on, we only need absolute tarpath.
1207 tarpath = os.path.join(topdir, tarpath)
1208
1209 if not self._ExtractArchive(tarpath, path=topdir):
1210 return SyncNetworkHalfResult(False, True)
1211 try:
1212 platform_utils.remove(tarpath)
1213 except OSError as e:
1214 _warn("Cannot remove archive %s: %s", tarpath, str(e))
1215 self._CopyAndLinkFiles()
1216 return SyncNetworkHalfResult(True, True)
1217
1218 # If the shared object dir already exists, don't try to rebootstrap with
1219 # a clone bundle download. We should have the majority of objects
1220 # already.
1221 if clone_bundle and os.path.exists(self.objdir):
1222 clone_bundle = False
1223
1224 if self.name in partial_clone_exclude:
1225 clone_bundle = True
1226 clone_filter = None
1227
1228 if is_new is None:
1229 is_new = not self.Exists
1230 if is_new:
1231 self._InitGitDir(force_sync=force_sync, quiet=quiet)
1232 else:
1233 self._UpdateHooks(quiet=quiet)
1234 self._InitRemote()
1235
1236 if self.UseAlternates:
1237 # If gitdir/objects is a symlink, migrate it from the old layout.
1238 gitdir_objects = os.path.join(self.gitdir, "objects")
1239 if platform_utils.islink(gitdir_objects):
1240 platform_utils.remove(gitdir_objects, missing_ok=True)
1241 gitdir_alt = os.path.join(self.gitdir, "objects/info/alternates")
1242 if not os.path.exists(gitdir_alt):
1243 os.makedirs(os.path.dirname(gitdir_alt), exist_ok=True)
1244 _lwrite(
1245 gitdir_alt,
1246 os.path.join(
1247 os.path.relpath(self.objdir, gitdir_objects), "objects"
1248 )
1249 + "\n",
1250 )
1251
1252 if is_new:
1253 alt = os.path.join(self.objdir, "objects/info/alternates")
1254 try:
1255 with open(alt) as fd:
1256 # This works for both absolute and relative alternate
1257 # directories.
1258 alt_dir = os.path.join(
1259 self.objdir, "objects", fd.readline().rstrip()
1260 )
1261 except IOError:
1262 alt_dir = None
1263 else:
1264 alt_dir = None
1265
1266 if (
1267 clone_bundle
1268 and alt_dir is None
1269 and self._ApplyCloneBundle(
1270 initial=is_new, quiet=quiet, verbose=verbose
1271 )
1272 ):
1273 is_new = False
1274
1275 if current_branch_only is None:
1276 if self.sync_c:
1277 current_branch_only = True
1278 elif not self.manifest._loaded:
1279 # Manifest cannot check defaults until it syncs.
1280 current_branch_only = False
1281 elif self.manifest.default.sync_c:
1282 current_branch_only = True
1283
1284 if tags is None:
1285 tags = self.sync_tags
1286
1287 if self.clone_depth:
1288 depth = self.clone_depth
1289 else:
1290 depth = self.manifest.manifestProject.depth
1291
1292 # See if we can skip the network fetch entirely.
1293 remote_fetched = False
1294 if not (
1295 optimized_fetch
1296 and (
1297 ID_RE.match(self.revisionExpr)
1298 and self._CheckForImmutableRevision()
1299 )
1300 ):
1301 remote_fetched = True
1302 if not self._RemoteFetch(
1303 initial=is_new,
1304 quiet=quiet,
1305 verbose=verbose,
1306 output_redir=output_redir,
1307 alt_dir=alt_dir,
1308 current_branch_only=current_branch_only,
1309 tags=tags,
1310 prune=prune,
1311 depth=depth,
1312 submodules=submodules,
1313 force_sync=force_sync,
1314 ssh_proxy=ssh_proxy,
1315 clone_filter=clone_filter,
1316 retry_fetches=retry_fetches,
1317 ):
1318 return SyncNetworkHalfResult(False, remote_fetched)
1319
1320 mp = self.manifest.manifestProject
1321 dissociate = mp.dissociate
1322 if dissociate:
1323 alternates_file = os.path.join(
1324 self.objdir, "objects/info/alternates"
1325 )
1326 if os.path.exists(alternates_file):
1327 cmd = ["repack", "-a", "-d"]
1328 p = GitCommand(
1329 self,
1330 cmd,
1331 bare=True,
1332 capture_stdout=bool(output_redir),
1333 merge_output=bool(output_redir),
1334 )
1335 if p.stdout and output_redir:
1336 output_redir.write(p.stdout)
1337 if p.Wait() != 0:
1338 return SyncNetworkHalfResult(False, remote_fetched)
1339 platform_utils.remove(alternates_file)
1340
1341 if self.worktree:
1342 self._InitMRef()
1343 else:
1344 self._InitMirrorHead()
1345 platform_utils.remove(
1346 os.path.join(self.gitdir, "FETCH_HEAD"), missing_ok=True
1347 )
1348 return SyncNetworkHalfResult(True, remote_fetched)
1349
1350 def PostRepoUpgrade(self):
1351 self._InitHooks()
1352
1353 def _CopyAndLinkFiles(self):
1354 if self.client.isGitcClient:
1355 return
1356 for copyfile in self.copyfiles:
1357 copyfile._Copy()
1358 for linkfile in self.linkfiles:
1359 linkfile._Link()
1360
1361 def GetCommitRevisionId(self):
1362 """Get revisionId of a commit.
1363
1364 Use this method instead of GetRevisionId to get the id of the commit
1365 rather than the id of the current git object (for example, a tag)
1366
1367 """
1368 if not self.revisionExpr.startswith(R_TAGS):
1369 return self.GetRevisionId(self._allrefs)
1370
1371 try:
1372 return self.bare_git.rev_list(self.revisionExpr, "-1")[0]
1373 except GitError:
1374 raise ManifestInvalidRevisionError(
1375 "revision %s in %s not found" % (self.revisionExpr, self.name)
1376 )
1377
1378 def GetRevisionId(self, all_refs=None):
1379 if self.revisionId:
1380 return self.revisionId
1381
1382 rem = self.GetRemote()
1383 rev = rem.ToLocal(self.revisionExpr)
1384
1385 if all_refs is not None and rev in all_refs:
1386 return all_refs[rev]
1387
1388 try:
1389 return self.bare_git.rev_parse("--verify", "%s^0" % rev)
1390 except GitError:
1391 raise ManifestInvalidRevisionError(
1392 "revision %s in %s not found" % (self.revisionExpr, self.name)
1393 )
1394
1395 def SetRevisionId(self, revisionId):
1396 if self.revisionExpr:
1397 self.upstream = self.revisionExpr
1398
1399 self.revisionId = revisionId
1400
1401 def Sync_LocalHalf(self, syncbuf, force_sync=False, submodules=False):
1402 """Perform only the local IO portion of the sync process.
1403
1404 Network access is not required.
1405 """
1406 if not os.path.exists(self.gitdir):
1407 syncbuf.fail(
1408 self,
1409 "Cannot checkout %s due to missing network sync; Run "
1410 "`repo sync -n %s` first." % (self.name, self.name),
1411 )
1412 return
1413
1414 self._InitWorkTree(force_sync=force_sync, submodules=submodules)
1415 all_refs = self.bare_ref.all
1416 self.CleanPublishedCache(all_refs)
1417 revid = self.GetRevisionId(all_refs)
1418
1419 # Special case the root of the repo client checkout. Make sure it
1420 # doesn't contain files being checked out to dirs we don't allow.
1421 if self.relpath == ".":
1422 PROTECTED_PATHS = {".repo"}
1423 paths = set(
1424 self.work_git.ls_tree("-z", "--name-only", "--", revid).split(
1425 "\0"
1426 )
1427 )
1428 bad_paths = paths & PROTECTED_PATHS
1429 if bad_paths:
1430 syncbuf.fail(
1431 self,
1432 "Refusing to checkout project that writes to protected "
1433 "paths: %s" % (", ".join(bad_paths),),
1434 )
1435 return
1436
1437 def _doff():
1438 self._FastForward(revid)
1439 self._CopyAndLinkFiles()
1440
1441 def _dosubmodules():
1442 self._SyncSubmodules(quiet=True)
1443
1444 head = self.work_git.GetHead()
1445 if head.startswith(R_HEADS):
1446 branch = head[len(R_HEADS) :]
1447 try:
1448 head = all_refs[head]
1449 except KeyError:
1450 head = None
1451 else:
1452 branch = None
1453
1454 if branch is None or syncbuf.detach_head:
1455 # Currently on a detached HEAD. The user is assumed to
1456 # not have any local modifications worth worrying about.
1457 if self.IsRebaseInProgress():
1458 syncbuf.fail(self, _PriorSyncFailedError())
1459 return
1460
1461 if head == revid:
1462 # No changes; don't do anything further.
1463 # Except if the head needs to be detached.
1464 if not syncbuf.detach_head:
1465 # The copy/linkfile config may have changed.
1466 self._CopyAndLinkFiles()
1467 return
1468 else:
1469 lost = self._revlist(not_rev(revid), HEAD)
1470 if lost:
1471 syncbuf.info(self, "discarding %d commits", len(lost))
1472
1473 try:
1474 self._Checkout(revid, quiet=True)
1475 if submodules:
1476 self._SyncSubmodules(quiet=True)
1477 except GitError as e:
1478 syncbuf.fail(self, e)
1479 return
1480 self._CopyAndLinkFiles()
1481 return
1482
1483 if head == revid:
1484 # No changes; don't do anything further.
1485 #
1486 # The copy/linkfile config may have changed.
1487 self._CopyAndLinkFiles()
1488 return
1489
1490 branch = self.GetBranch(branch)
1491
1492 if not branch.LocalMerge:
1493 # The current branch has no tracking configuration.
1494 # Jump off it to a detached HEAD.
1495 syncbuf.info(
1496 self, "leaving %s; does not track upstream", branch.name
1497 )
1498 try:
1499 self._Checkout(revid, quiet=True)
1500 if submodules:
1501 self._SyncSubmodules(quiet=True)
1502 except GitError as e:
1503 syncbuf.fail(self, e)
1504 return
1505 self._CopyAndLinkFiles()
1506 return
1507
1508 upstream_gain = self._revlist(not_rev(HEAD), revid)
1509
1510 # See if we can perform a fast forward merge. This can happen if our
1511 # branch isn't in the exact same state as we last published.
1512 try:
1513 self.work_git.merge_base("--is-ancestor", HEAD, revid)
1514 # Skip the published logic.
1515 pub = False
1516 except GitError:
1517 pub = self.WasPublished(branch.name, all_refs)
1518
1519 if pub:
1520 not_merged = self._revlist(not_rev(revid), pub)
1521 if not_merged:
1522 if upstream_gain:
1523 # The user has published this branch and some of those
1524 # commits are not yet merged upstream. We do not want
1525 # to rewrite the published commits so we punt.
1526 syncbuf.fail(
1527 self,
1528 "branch %s is published (but not merged) and is now "
1529 "%d commits behind" % (branch.name, len(upstream_gain)),
1530 )
1531 return
1532 elif pub == head:
1533 # All published commits are merged, and thus we are a
1534 # strict subset. We can fast-forward safely.
1535 syncbuf.later1(self, _doff)
1536 if submodules:
1537 syncbuf.later1(self, _dosubmodules)
1538 return
1539
1540 # Examine the local commits not in the remote. Find the
1541 # last one attributed to this user, if any.
1542 local_changes = self._revlist(not_rev(revid), HEAD, format="%H %ce")
1543 last_mine = None
1544 cnt_mine = 0
1545 for commit in local_changes:
1546 commit_id, committer_email = commit.split(" ", 1)
1547 if committer_email == self.UserEmail:
1548 last_mine = commit_id
1549 cnt_mine += 1
1550
1551 if not upstream_gain and cnt_mine == len(local_changes):
1552 # The copy/linkfile config may have changed.
1553 self._CopyAndLinkFiles()
1554 return
1555
1556 if self.IsDirty(consider_untracked=False):
1557 syncbuf.fail(self, _DirtyError())
1558 return
1559
1560 # If the upstream switched on us, warn the user.
1561 if branch.merge != self.revisionExpr:
1562 if branch.merge and self.revisionExpr:
1563 syncbuf.info(
1564 self,
1565 "manifest switched %s...%s",
1566 branch.merge,
1567 self.revisionExpr,
1568 )
1569 elif branch.merge:
1570 syncbuf.info(self, "manifest no longer tracks %s", branch.merge)
1571
1572 if cnt_mine < len(local_changes):
1573 # Upstream rebased. Not everything in HEAD was created by this user.
1574 syncbuf.info(
1575 self,
1576 "discarding %d commits removed from upstream",
1577 len(local_changes) - cnt_mine,
1578 )
1579
1580 branch.remote = self.GetRemote()
1581 if not ID_RE.match(self.revisionExpr):
1582 # In case of manifest sync the revisionExpr might be a SHA1.
1583 branch.merge = self.revisionExpr
1584 if not branch.merge.startswith("refs/"):
1585 branch.merge = R_HEADS + branch.merge
1586 branch.Save()
1587
1588 if cnt_mine > 0 and self.rebase:
1589
1590 def _docopyandlink():
1591 self._CopyAndLinkFiles()
1592
1593 def _dorebase():
1594 self._Rebase(upstream="%s^1" % last_mine, onto=revid)
1595
1596 syncbuf.later2(self, _dorebase)
1597 if submodules:
1598 syncbuf.later2(self, _dosubmodules)
1599 syncbuf.later2(self, _docopyandlink)
1600 elif local_changes:
1601 try:
1602 self._ResetHard(revid)
1603 if submodules:
1604 self._SyncSubmodules(quiet=True)
1605 self._CopyAndLinkFiles()
1606 except GitError as e:
1607 syncbuf.fail(self, e)
1608 return
1609 else:
1610 syncbuf.later1(self, _doff)
1611 if submodules:
1612 syncbuf.later1(self, _dosubmodules)
1613
1614 def AddCopyFile(self, src, dest, topdir):
1615 """Mark |src| for copying to |dest| (relative to |topdir|).
1616
1617 No filesystem changes occur here. Actual copying happens later on.
1618
1619 Paths should have basic validation run on them before being queued.
1620 Further checking will be handled when the actual copy happens.
1621 """
1622 self.copyfiles.append(_CopyFile(self.worktree, src, topdir, dest))
1623
1624 def AddLinkFile(self, src, dest, topdir):
1625 """Mark |dest| to create a symlink (relative to |topdir|) pointing to
1626 |src|.
1627
1628 No filesystem changes occur here. Actual linking happens later on.
1629
1630 Paths should have basic validation run on them before being queued.
1631 Further checking will be handled when the actual link happens.
1632 """
1633 self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest))
1634
1635 def AddAnnotation(self, name, value, keep):
1636 self.annotations.append(Annotation(name, value, keep))
1637
1638 def DownloadPatchSet(self, change_id, patch_id):
1639 """Download a single patch set of a single change to FETCH_HEAD."""
1640 remote = self.GetRemote()
1641
1642 cmd = ["fetch", remote.name]
1643 cmd.append(
1644 "refs/changes/%2.2d/%d/%d" % (change_id % 100, change_id, patch_id)
1645 )
1646 if GitCommand(self, cmd, bare=True).Wait() != 0:
1647 return None
1648 return DownloadedChange(
1649 self,
1650 self.GetRevisionId(),
1651 change_id,
1652 patch_id,
1653 self.bare_git.rev_parse("FETCH_HEAD"),
1654 )
1655
1656 def DeleteWorktree(self, quiet=False, force=False):
1657 """Delete the source checkout and any other housekeeping tasks.
1658
1659 This currently leaves behind the internal .repo/ cache state. This
1660 helps when switching branches or manifest changes get reverted as we
1661 don't have to redownload all the git objects. But we should do some GC
1662 at some point.
1663
1664 Args:
1665 quiet: Whether to hide normal messages.
1666 force: Always delete tree even if dirty.
Doug Anderson3ba5f952011-04-07 12:51:04 -07001667
1668 Returns:
Gavin Makea2e3302023-03-11 06:46:20 +00001669 True if the worktree was completely cleaned out.
1670 """
1671 if self.IsDirty():
1672 if force:
1673 print(
1674 "warning: %s: Removing dirty project: uncommitted changes "
1675 "lost." % (self.RelPath(local=False),),
1676 file=sys.stderr,
1677 )
1678 else:
1679 print(
1680 "error: %s: Cannot remove project: uncommitted changes are "
1681 "present.\n" % (self.RelPath(local=False),),
1682 file=sys.stderr,
1683 )
1684 return False
Wink Saville02d79452009-04-10 13:01:24 -07001685
Gavin Makea2e3302023-03-11 06:46:20 +00001686 if not quiet:
1687 print(
1688 "%s: Deleting obsolete checkout." % (self.RelPath(local=False),)
1689 )
Wink Saville02d79452009-04-10 13:01:24 -07001690
Gavin Makea2e3302023-03-11 06:46:20 +00001691 # Unlock and delink from the main worktree. We don't use git's worktree
1692 # remove because it will recursively delete projects -- we handle that
1693 # ourselves below. https://crbug.com/git/48
1694 if self.use_git_worktrees:
1695 needle = platform_utils.realpath(self.gitdir)
1696 # Find the git worktree commondir under .repo/worktrees/.
1697 output = self.bare_git.worktree("list", "--porcelain").splitlines()[
1698 0
1699 ]
1700 assert output.startswith("worktree "), output
1701 commondir = output[9:]
1702 # Walk each of the git worktrees to see where they point.
1703 configs = os.path.join(commondir, "worktrees")
1704 for name in os.listdir(configs):
1705 gitdir = os.path.join(configs, name, "gitdir")
1706 with open(gitdir) as fp:
1707 relpath = fp.read().strip()
1708 # Resolve the checkout path and see if it matches this project.
1709 fullpath = platform_utils.realpath(
1710 os.path.join(configs, name, relpath)
1711 )
1712 if fullpath == needle:
1713 platform_utils.rmtree(os.path.join(configs, name))
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001714
Gavin Makea2e3302023-03-11 06:46:20 +00001715 # Delete the .git directory first, so we're less likely to have a
1716 # partially working git repository around. There shouldn't be any git
1717 # projects here, so rmtree works.
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001718
Gavin Makea2e3302023-03-11 06:46:20 +00001719 # Try to remove plain files first in case of git worktrees. If this
1720 # fails for any reason, we'll fall back to rmtree, and that'll display
1721 # errors if it can't remove things either.
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001722 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001723 platform_utils.remove(self.gitdir)
1724 except OSError:
1725 pass
1726 try:
1727 platform_utils.rmtree(self.gitdir)
1728 except OSError as e:
1729 if e.errno != errno.ENOENT:
1730 print("error: %s: %s" % (self.gitdir, e), file=sys.stderr)
1731 print(
1732 "error: %s: Failed to delete obsolete checkout; remove "
1733 "manually, then run `repo sync -l`."
1734 % (self.RelPath(local=False),),
1735 file=sys.stderr,
1736 )
1737 return False
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001738
Gavin Makea2e3302023-03-11 06:46:20 +00001739 # Delete everything under the worktree, except for directories that
1740 # contain another git project.
1741 dirs_to_remove = []
1742 failed = False
1743 for root, dirs, files in platform_utils.walk(self.worktree):
1744 for f in files:
1745 path = os.path.join(root, f)
1746 try:
1747 platform_utils.remove(path)
1748 except OSError as e:
1749 if e.errno != errno.ENOENT:
1750 print(
1751 "error: %s: Failed to remove: %s" % (path, e),
1752 file=sys.stderr,
1753 )
1754 failed = True
1755 dirs[:] = [
1756 d
1757 for d in dirs
1758 if not os.path.lexists(os.path.join(root, d, ".git"))
1759 ]
1760 dirs_to_remove += [
1761 os.path.join(root, d)
1762 for d in dirs
1763 if os.path.join(root, d) not in dirs_to_remove
1764 ]
1765 for d in reversed(dirs_to_remove):
1766 if platform_utils.islink(d):
1767 try:
1768 platform_utils.remove(d)
1769 except OSError as e:
1770 if e.errno != errno.ENOENT:
1771 print(
1772 "error: %s: Failed to remove: %s" % (d, e),
1773 file=sys.stderr,
1774 )
1775 failed = True
1776 elif not platform_utils.listdir(d):
1777 try:
1778 platform_utils.rmdir(d)
1779 except OSError as e:
1780 if e.errno != errno.ENOENT:
1781 print(
1782 "error: %s: Failed to remove: %s" % (d, e),
1783 file=sys.stderr,
1784 )
1785 failed = True
1786 if failed:
1787 print(
1788 "error: %s: Failed to delete obsolete checkout."
1789 % (self.RelPath(local=False),),
1790 file=sys.stderr,
1791 )
1792 print(
1793 " Remove manually, then run `repo sync -l`.",
1794 file=sys.stderr,
1795 )
1796 return False
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07001797
Gavin Makea2e3302023-03-11 06:46:20 +00001798 # Try deleting parent dirs if they are empty.
1799 path = self.worktree
1800 while path != self.manifest.topdir:
1801 try:
1802 platform_utils.rmdir(path)
1803 except OSError as e:
1804 if e.errno != errno.ENOENT:
1805 break
1806 path = os.path.dirname(path)
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001807
Gavin Makea2e3302023-03-11 06:46:20 +00001808 return True
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001809
Gavin Makea2e3302023-03-11 06:46:20 +00001810 def StartBranch(self, name, branch_merge="", revision=None):
1811 """Create a new branch off the manifest's revision."""
1812 if not branch_merge:
1813 branch_merge = self.revisionExpr
1814 head = self.work_git.GetHead()
1815 if head == (R_HEADS + name):
1816 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001817
David Pursehouse8a68ff92012-09-24 12:15:13 +09001818 all_refs = self.bare_ref.all
Gavin Makea2e3302023-03-11 06:46:20 +00001819 if R_HEADS + name in all_refs:
1820 return GitCommand(self, ["checkout", "-q", name, "--"]).Wait() == 0
Shawn O. Pearce88443382010-10-08 10:02:09 +02001821
Gavin Makea2e3302023-03-11 06:46:20 +00001822 branch = self.GetBranch(name)
1823 branch.remote = self.GetRemote()
1824 branch.merge = branch_merge
1825 if not branch.merge.startswith("refs/") and not ID_RE.match(
1826 branch_merge
1827 ):
1828 branch.merge = R_HEADS + branch_merge
Shawn O. Pearce88443382010-10-08 10:02:09 +02001829
Gavin Makea2e3302023-03-11 06:46:20 +00001830 if revision is None:
1831 revid = self.GetRevisionId(all_refs)
Shawn O. Pearce88443382010-10-08 10:02:09 +02001832 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001833 revid = self.work_git.rev_parse(revision)
Brian Harring14a66742012-09-28 20:21:57 -07001834
Gavin Makea2e3302023-03-11 06:46:20 +00001835 if head.startswith(R_HEADS):
Kevin Degiabaa7f32014-11-12 11:27:45 -07001836 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001837 head = all_refs[head]
1838 except KeyError:
1839 head = None
1840 if revid and head and revid == head:
1841 ref = R_HEADS + name
1842 self.work_git.update_ref(ref, revid)
1843 self.work_git.symbolic_ref(HEAD, ref)
1844 branch.Save()
1845 return True
Kevin Degib1a07b82015-07-27 13:33:43 -06001846
Gavin Makea2e3302023-03-11 06:46:20 +00001847 if (
1848 GitCommand(
1849 self, ["checkout", "-q", "-b", branch.name, revid]
1850 ).Wait()
1851 == 0
1852 ):
1853 branch.Save()
1854 return True
1855 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06001856
Gavin Makea2e3302023-03-11 06:46:20 +00001857 def CheckoutBranch(self, name):
1858 """Checkout a local topic branch.
Shawn O. Pearce2816d4f2009-03-03 17:53:18 -08001859
Gavin Makea2e3302023-03-11 06:46:20 +00001860 Args:
1861 name: The name of the branch to checkout.
Shawn O. Pearce88443382010-10-08 10:02:09 +02001862
Gavin Makea2e3302023-03-11 06:46:20 +00001863 Returns:
1864 True if the checkout succeeded; False if it didn't; None if the
1865 branch didn't exist.
1866 """
1867 rev = R_HEADS + name
1868 head = self.work_git.GetHead()
1869 if head == rev:
1870 # Already on the branch.
1871 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001872
Gavin Makea2e3302023-03-11 06:46:20 +00001873 all_refs = self.bare_ref.all
1874 try:
1875 revid = all_refs[rev]
1876 except KeyError:
1877 # Branch does not exist in this project.
1878 return None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001879
Gavin Makea2e3302023-03-11 06:46:20 +00001880 if head.startswith(R_HEADS):
1881 try:
1882 head = all_refs[head]
1883 except KeyError:
1884 head = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001885
Gavin Makea2e3302023-03-11 06:46:20 +00001886 if head == revid:
1887 # Same revision; just update HEAD to point to the new
1888 # target branch, but otherwise take no other action.
1889 _lwrite(
1890 self.work_git.GetDotgitPath(subpath=HEAD),
1891 "ref: %s%s\n" % (R_HEADS, name),
1892 )
1893 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05001894
Gavin Makea2e3302023-03-11 06:46:20 +00001895 return (
1896 GitCommand(
1897 self,
1898 ["checkout", name, "--"],
1899 capture_stdout=True,
1900 capture_stderr=True,
1901 ).Wait()
1902 == 0
1903 )
Mike Frysinger98bb7652021-12-20 21:15:59 -05001904
Gavin Makea2e3302023-03-11 06:46:20 +00001905 def AbandonBranch(self, name):
1906 """Destroy a local topic branch.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07001907
Gavin Makea2e3302023-03-11 06:46:20 +00001908 Args:
1909 name: The name of the branch to abandon.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07001910
Gavin Makea2e3302023-03-11 06:46:20 +00001911 Returns:
1912 True if the abandon succeeded; False if it didn't; None if the
1913 branch didn't exist.
1914 """
1915 rev = R_HEADS + name
1916 all_refs = self.bare_ref.all
1917 if rev not in all_refs:
1918 # Doesn't exist
1919 return None
1920
1921 head = self.work_git.GetHead()
1922 if head == rev:
1923 # We can't destroy the branch while we are sitting
1924 # on it. Switch to a detached HEAD.
1925 head = all_refs[head]
1926
1927 revid = self.GetRevisionId(all_refs)
1928 if head == revid:
1929 _lwrite(
1930 self.work_git.GetDotgitPath(subpath=HEAD), "%s\n" % revid
1931 )
1932 else:
1933 self._Checkout(revid, quiet=True)
1934
1935 return (
1936 GitCommand(
1937 self,
1938 ["branch", "-D", name],
1939 capture_stdout=True,
1940 capture_stderr=True,
1941 ).Wait()
1942 == 0
1943 )
1944
1945 def PruneHeads(self):
1946 """Prune any topic branches already merged into upstream."""
1947 cb = self.CurrentBranch
1948 kill = []
1949 left = self._allrefs
1950 for name in left.keys():
1951 if name.startswith(R_HEADS):
1952 name = name[len(R_HEADS) :]
1953 if cb is None or name != cb:
1954 kill.append(name)
1955
1956 # Minor optimization: If there's nothing to prune, then don't try to
1957 # read any project state.
1958 if not kill and not cb:
1959 return []
1960
1961 rev = self.GetRevisionId(left)
1962 if (
1963 cb is not None
1964 and not self._revlist(HEAD + "..." + rev)
1965 and not self.IsDirty(consider_untracked=False)
1966 ):
1967 self.work_git.DetachHead(HEAD)
1968 kill.append(cb)
1969
1970 if kill:
1971 old = self.bare_git.GetHead()
1972
1973 try:
1974 self.bare_git.DetachHead(rev)
1975
1976 b = ["branch", "-d"]
1977 b.extend(kill)
1978 b = GitCommand(
1979 self, b, bare=True, capture_stdout=True, capture_stderr=True
1980 )
1981 b.Wait()
1982 finally:
1983 if ID_RE.match(old):
1984 self.bare_git.DetachHead(old)
1985 else:
1986 self.bare_git.SetHead(old)
1987 left = self._allrefs
1988
1989 for branch in kill:
1990 if (R_HEADS + branch) not in left:
1991 self.CleanPublishedCache()
1992 break
1993
1994 if cb and cb not in kill:
1995 kill.append(cb)
1996 kill.sort()
1997
1998 kept = []
1999 for branch in kill:
2000 if R_HEADS + branch in left:
2001 branch = self.GetBranch(branch)
2002 base = branch.LocalMerge
2003 if not base:
2004 base = rev
2005 kept.append(ReviewableBranch(self, branch, base))
2006 return kept
2007
2008 def GetRegisteredSubprojects(self):
2009 result = []
2010
2011 def rec(subprojects):
2012 if not subprojects:
2013 return
2014 result.extend(subprojects)
2015 for p in subprojects:
2016 rec(p.subprojects)
2017
2018 rec(self.subprojects)
2019 return result
2020
2021 def _GetSubmodules(self):
2022 # Unfortunately we cannot call `git submodule status --recursive` here
2023 # because the working tree might not exist yet, and it cannot be used
2024 # without a working tree in its current implementation.
2025
2026 def get_submodules(gitdir, rev):
2027 # Parse .gitmodules for submodule sub_paths and sub_urls.
2028 sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
2029 if not sub_paths:
2030 return []
2031 # Run `git ls-tree` to read SHAs of submodule object, which happen
2032 # to be revision of submodule repository.
2033 sub_revs = git_ls_tree(gitdir, rev, sub_paths)
2034 submodules = []
2035 for sub_path, sub_url in zip(sub_paths, sub_urls):
2036 try:
2037 sub_rev = sub_revs[sub_path]
2038 except KeyError:
2039 # Ignore non-exist submodules.
2040 continue
2041 submodules.append((sub_rev, sub_path, sub_url))
2042 return submodules
2043
2044 re_path = re.compile(r"^submodule\.(.+)\.path=(.*)$")
2045 re_url = re.compile(r"^submodule\.(.+)\.url=(.*)$")
2046
2047 def parse_gitmodules(gitdir, rev):
2048 cmd = ["cat-file", "blob", "%s:.gitmodules" % rev]
2049 try:
2050 p = GitCommand(
2051 None,
2052 cmd,
2053 capture_stdout=True,
2054 capture_stderr=True,
2055 bare=True,
2056 gitdir=gitdir,
2057 )
2058 except GitError:
2059 return [], []
2060 if p.Wait() != 0:
2061 return [], []
2062
2063 gitmodules_lines = []
2064 fd, temp_gitmodules_path = tempfile.mkstemp()
2065 try:
2066 os.write(fd, p.stdout.encode("utf-8"))
2067 os.close(fd)
2068 cmd = ["config", "--file", temp_gitmodules_path, "--list"]
2069 p = GitCommand(
2070 None,
2071 cmd,
2072 capture_stdout=True,
2073 capture_stderr=True,
2074 bare=True,
2075 gitdir=gitdir,
2076 )
2077 if p.Wait() != 0:
2078 return [], []
2079 gitmodules_lines = p.stdout.split("\n")
2080 except GitError:
2081 return [], []
2082 finally:
2083 platform_utils.remove(temp_gitmodules_path)
2084
2085 names = set()
2086 paths = {}
2087 urls = {}
2088 for line in gitmodules_lines:
2089 if not line:
2090 continue
2091 m = re_path.match(line)
2092 if m:
2093 names.add(m.group(1))
2094 paths[m.group(1)] = m.group(2)
2095 continue
2096 m = re_url.match(line)
2097 if m:
2098 names.add(m.group(1))
2099 urls[m.group(1)] = m.group(2)
2100 continue
2101 names = sorted(names)
2102 return (
2103 [paths.get(name, "") for name in names],
2104 [urls.get(name, "") for name in names],
2105 )
2106
2107 def git_ls_tree(gitdir, rev, paths):
2108 cmd = ["ls-tree", rev, "--"]
2109 cmd.extend(paths)
2110 try:
2111 p = GitCommand(
2112 None,
2113 cmd,
2114 capture_stdout=True,
2115 capture_stderr=True,
2116 bare=True,
2117 gitdir=gitdir,
2118 )
2119 except GitError:
2120 return []
2121 if p.Wait() != 0:
2122 return []
2123 objects = {}
2124 for line in p.stdout.split("\n"):
2125 if not line.strip():
2126 continue
2127 object_rev, object_path = line.split()[2:4]
2128 objects[object_path] = object_rev
2129 return objects
2130
2131 try:
2132 rev = self.GetRevisionId()
2133 except GitError:
2134 return []
2135 return get_submodules(self.gitdir, rev)
2136
2137 def GetDerivedSubprojects(self):
2138 result = []
2139 if not self.Exists:
2140 # If git repo does not exist yet, querying its submodules will
2141 # mess up its states; so return here.
2142 return result
2143 for rev, path, url in self._GetSubmodules():
2144 name = self.manifest.GetSubprojectName(self, path)
2145 (
2146 relpath,
2147 worktree,
2148 gitdir,
2149 objdir,
2150 ) = self.manifest.GetSubprojectPaths(self, name, path)
2151 project = self.manifest.paths.get(relpath)
2152 if project:
2153 result.extend(project.GetDerivedSubprojects())
2154 continue
2155
2156 if url.startswith(".."):
2157 url = urllib.parse.urljoin("%s/" % self.remote.url, url)
2158 remote = RemoteSpec(
2159 self.remote.name,
2160 url=url,
2161 pushUrl=self.remote.pushUrl,
2162 review=self.remote.review,
2163 revision=self.remote.revision,
2164 )
2165 subproject = Project(
2166 manifest=self.manifest,
2167 name=name,
2168 remote=remote,
2169 gitdir=gitdir,
2170 objdir=objdir,
2171 worktree=worktree,
2172 relpath=relpath,
2173 revisionExpr=rev,
2174 revisionId=rev,
2175 rebase=self.rebase,
2176 groups=self.groups,
2177 sync_c=self.sync_c,
2178 sync_s=self.sync_s,
2179 sync_tags=self.sync_tags,
2180 parent=self,
2181 is_derived=True,
2182 )
2183 result.append(subproject)
2184 result.extend(subproject.GetDerivedSubprojects())
2185 return result
2186
2187 def EnableRepositoryExtension(self, key, value="true", version=1):
2188 """Enable git repository extension |key| with |value|.
2189
2190 Args:
2191 key: The extension to enabled. Omit the "extensions." prefix.
2192 value: The value to use for the extension.
2193 version: The minimum git repository version needed.
2194 """
2195 # Make sure the git repo version is new enough already.
2196 found_version = self.config.GetInt("core.repositoryFormatVersion")
2197 if found_version is None:
2198 found_version = 0
2199 if found_version < version:
2200 self.config.SetString("core.repositoryFormatVersion", str(version))
2201
2202 # Enable the extension!
2203 self.config.SetString("extensions.%s" % (key,), value)
2204
2205 def ResolveRemoteHead(self, name=None):
2206 """Find out what the default branch (HEAD) points to.
2207
2208 Normally this points to refs/heads/master, but projects are moving to
2209 main. Support whatever the server uses rather than hardcoding "master"
2210 ourselves.
2211 """
2212 if name is None:
2213 name = self.remote.name
2214
2215 # The output will look like (NB: tabs are separators):
2216 # ref: refs/heads/master HEAD
2217 # 5f6803b100bb3cd0f534e96e88c91373e8ed1c44 HEAD
2218 output = self.bare_git.ls_remote(
2219 "-q", "--symref", "--exit-code", name, "HEAD"
2220 )
2221
2222 for line in output.splitlines():
2223 lhs, rhs = line.split("\t", 1)
2224 if rhs == "HEAD" and lhs.startswith("ref:"):
2225 return lhs[4:].strip()
2226
2227 return None
2228
2229 def _CheckForImmutableRevision(self):
2230 try:
2231 # if revision (sha or tag) is not present then following function
2232 # throws an error.
2233 self.bare_git.rev_list(
2234 "-1", "--missing=allow-any", "%s^0" % self.revisionExpr, "--"
2235 )
2236 if self.upstream:
2237 rev = self.GetRemote().ToLocal(self.upstream)
2238 self.bare_git.rev_list(
2239 "-1", "--missing=allow-any", "%s^0" % rev, "--"
2240 )
2241 self.bare_git.merge_base(
2242 "--is-ancestor", self.revisionExpr, rev
2243 )
2244 return True
2245 except GitError:
2246 # There is no such persistent revision. We have to fetch it.
2247 return False
2248
2249 def _FetchArchive(self, tarpath, cwd=None):
2250 cmd = ["archive", "-v", "-o", tarpath]
2251 cmd.append("--remote=%s" % self.remote.url)
2252 cmd.append("--prefix=%s/" % self.RelPath(local=False))
2253 cmd.append(self.revisionExpr)
2254
2255 command = GitCommand(
2256 self, cmd, cwd=cwd, capture_stdout=True, capture_stderr=True
2257 )
2258
2259 if command.Wait() != 0:
2260 raise GitError("git archive %s: %s" % (self.name, command.stderr))
2261
2262 def _RemoteFetch(
2263 self,
2264 name=None,
2265 current_branch_only=False,
2266 initial=False,
2267 quiet=False,
2268 verbose=False,
2269 output_redir=None,
2270 alt_dir=None,
2271 tags=True,
2272 prune=False,
2273 depth=None,
2274 submodules=False,
2275 ssh_proxy=None,
2276 force_sync=False,
2277 clone_filter=None,
2278 retry_fetches=2,
2279 retry_sleep_initial_sec=4.0,
2280 retry_exp_factor=2.0,
2281 ):
2282 is_sha1 = False
2283 tag_name = None
2284 # The depth should not be used when fetching to a mirror because
2285 # it will result in a shallow repository that cannot be cloned or
2286 # fetched from.
2287 # The repo project should also never be synced with partial depth.
2288 if self.manifest.IsMirror or self.relpath == ".repo/repo":
2289 depth = None
2290
2291 if depth:
2292 current_branch_only = True
2293
2294 if ID_RE.match(self.revisionExpr) is not None:
2295 is_sha1 = True
2296
2297 if current_branch_only:
2298 if self.revisionExpr.startswith(R_TAGS):
2299 # This is a tag and its commit id should never change.
2300 tag_name = self.revisionExpr[len(R_TAGS) :]
2301 elif self.upstream and self.upstream.startswith(R_TAGS):
2302 # This is a tag and its commit id should never change.
2303 tag_name = self.upstream[len(R_TAGS) :]
2304
2305 if is_sha1 or tag_name is not None:
2306 if self._CheckForImmutableRevision():
2307 if verbose:
2308 print(
2309 "Skipped fetching project %s (already have "
2310 "persistent ref)" % self.name
2311 )
2312 return True
2313 if is_sha1 and not depth:
2314 # When syncing a specific commit and --depth is not set:
2315 # * if upstream is explicitly specified and is not a sha1, fetch
2316 # only upstream as users expect only upstream to be fetch.
2317 # Note: The commit might not be in upstream in which case the
2318 # sync will fail.
2319 # * otherwise, fetch all branches to make sure we end up with
2320 # the specific commit.
2321 if self.upstream:
2322 current_branch_only = not ID_RE.match(self.upstream)
2323 else:
2324 current_branch_only = False
2325
2326 if not name:
2327 name = self.remote.name
2328
2329 remote = self.GetRemote(name)
2330 if not remote.PreConnectFetch(ssh_proxy):
2331 ssh_proxy = None
2332
2333 if initial:
2334 if alt_dir and "objects" == os.path.basename(alt_dir):
2335 ref_dir = os.path.dirname(alt_dir)
2336 packed_refs = os.path.join(self.gitdir, "packed-refs")
2337
2338 all_refs = self.bare_ref.all
2339 ids = set(all_refs.values())
2340 tmp = set()
2341
2342 for r, ref_id in GitRefs(ref_dir).all.items():
2343 if r not in all_refs:
2344 if r.startswith(R_TAGS) or remote.WritesTo(r):
2345 all_refs[r] = ref_id
2346 ids.add(ref_id)
2347 continue
2348
2349 if ref_id in ids:
2350 continue
2351
2352 r = "refs/_alt/%s" % ref_id
2353 all_refs[r] = ref_id
2354 ids.add(ref_id)
2355 tmp.add(r)
2356
2357 tmp_packed_lines = []
2358 old_packed_lines = []
2359
2360 for r in sorted(all_refs):
2361 line = "%s %s\n" % (all_refs[r], r)
2362 tmp_packed_lines.append(line)
2363 if r not in tmp:
2364 old_packed_lines.append(line)
2365
2366 tmp_packed = "".join(tmp_packed_lines)
2367 old_packed = "".join(old_packed_lines)
2368 _lwrite(packed_refs, tmp_packed)
2369 else:
2370 alt_dir = None
2371
2372 cmd = ["fetch"]
2373
2374 if clone_filter:
2375 git_require((2, 19, 0), fail=True, msg="partial clones")
2376 cmd.append("--filter=%s" % clone_filter)
2377 self.EnableRepositoryExtension("partialclone", self.remote.name)
2378
2379 if depth:
2380 cmd.append("--depth=%s" % depth)
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002381 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002382 # If this repo has shallow objects, then we don't know which refs
2383 # have shallow objects or not. Tell git to unshallow all fetched
2384 # refs. Don't do this with projects that don't have shallow
2385 # objects, since it is less efficient.
2386 if os.path.exists(os.path.join(self.gitdir, "shallow")):
2387 cmd.append("--depth=2147483647")
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002388
Gavin Makea2e3302023-03-11 06:46:20 +00002389 if not verbose:
2390 cmd.append("--quiet")
2391 if not quiet and sys.stdout.isatty():
2392 cmd.append("--progress")
2393 if not self.worktree:
2394 cmd.append("--update-head-ok")
2395 cmd.append(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002396
Gavin Makea2e3302023-03-11 06:46:20 +00002397 if force_sync:
2398 cmd.append("--force")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002399
Gavin Makea2e3302023-03-11 06:46:20 +00002400 if prune:
2401 cmd.append("--prune")
Mike Frysinger29626b42021-05-01 09:37:13 -04002402
Gavin Makea2e3302023-03-11 06:46:20 +00002403 # Always pass something for --recurse-submodules, git with GIT_DIR
2404 # behaves incorrectly when not given `--recurse-submodules=no`.
2405 # (b/218891912)
2406 cmd.append(
2407 f'--recurse-submodules={"on-demand" if submodules else "no"}'
2408 )
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002409
Gavin Makea2e3302023-03-11 06:46:20 +00002410 spec = []
2411 if not current_branch_only:
2412 # Fetch whole repo.
2413 spec.append(
2414 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2415 )
2416 elif tag_name is not None:
2417 spec.append("tag")
2418 spec.append(tag_name)
Remy Böhmer1469c282020-12-15 18:49:02 +01002419
Gavin Makea2e3302023-03-11 06:46:20 +00002420 if self.manifest.IsMirror and not current_branch_only:
2421 branch = None
Remy Böhmer1469c282020-12-15 18:49:02 +01002422 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002423 branch = self.revisionExpr
2424 if (
2425 not self.manifest.IsMirror
2426 and is_sha1
2427 and depth
2428 and git_require((1, 8, 3))
2429 ):
2430 # Shallow checkout of a specific commit, fetch from that commit and
2431 # not the heads only as the commit might be deeper in the history.
2432 spec.append(branch)
2433 if self.upstream:
2434 spec.append(self.upstream)
David James8d201162013-10-11 17:03:19 -07002435 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002436 if is_sha1:
2437 branch = self.upstream
2438 if branch is not None and branch.strip():
2439 if not branch.startswith("refs/"):
2440 branch = R_HEADS + branch
2441 spec.append(str(("+%s:" % branch) + remote.ToLocal(branch)))
David James8d201162013-10-11 17:03:19 -07002442
Gavin Makea2e3302023-03-11 06:46:20 +00002443 # If mirroring repo and we cannot deduce the tag or branch to fetch,
2444 # fetch whole repo.
2445 if self.manifest.IsMirror and not spec:
2446 spec.append(
2447 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2448 )
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002449
Gavin Makea2e3302023-03-11 06:46:20 +00002450 # If using depth then we should not get all the tags since they may
2451 # be outside of the depth.
2452 if not tags or depth:
2453 cmd.append("--no-tags")
2454 else:
2455 cmd.append("--tags")
2456 spec.append(str(("+refs/tags/*:") + remote.ToLocal("refs/tags/*")))
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002457
Gavin Makea2e3302023-03-11 06:46:20 +00002458 cmd.extend(spec)
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002459
Gavin Makea2e3302023-03-11 06:46:20 +00002460 # At least one retry minimum due to git remote prune.
2461 retry_fetches = max(retry_fetches, 2)
2462 retry_cur_sleep = retry_sleep_initial_sec
2463 ok = prune_tried = False
2464 for try_n in range(retry_fetches):
2465 gitcmd = GitCommand(
2466 self,
2467 cmd,
2468 bare=True,
2469 objdir=os.path.join(self.objdir, "objects"),
2470 ssh_proxy=ssh_proxy,
2471 merge_output=True,
2472 capture_stdout=quiet or bool(output_redir),
2473 )
2474 if gitcmd.stdout and not quiet and output_redir:
2475 output_redir.write(gitcmd.stdout)
2476 ret = gitcmd.Wait()
2477 if ret == 0:
2478 ok = True
2479 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002480
Gavin Makea2e3302023-03-11 06:46:20 +00002481 # Retry later due to HTTP 429 Too Many Requests.
2482 elif (
2483 gitcmd.stdout
2484 and "error:" in gitcmd.stdout
2485 and "HTTP 429" in gitcmd.stdout
2486 ):
2487 # Fallthru to sleep+retry logic at the bottom.
2488 pass
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002489
Gavin Makea2e3302023-03-11 06:46:20 +00002490 # Try to prune remote branches once in case there are conflicts.
2491 # For example, if the remote had refs/heads/upstream, but deleted
2492 # that and now has refs/heads/upstream/foo.
2493 elif (
2494 gitcmd.stdout
2495 and "error:" in gitcmd.stdout
2496 and "git remote prune" in gitcmd.stdout
2497 and not prune_tried
2498 ):
2499 prune_tried = True
2500 prunecmd = GitCommand(
2501 self,
2502 ["remote", "prune", name],
2503 bare=True,
2504 ssh_proxy=ssh_proxy,
2505 )
2506 ret = prunecmd.Wait()
2507 if ret:
2508 break
2509 print(
2510 "retrying fetch after pruning remote branches",
2511 file=output_redir,
2512 )
2513 # Continue right away so we don't sleep as we shouldn't need to.
2514 continue
2515 elif current_branch_only and is_sha1 and ret == 128:
2516 # Exit code 128 means "couldn't find the ref you asked for"; if
2517 # we're in sha1 mode, we just tried sync'ing from the upstream
2518 # field; it doesn't exist, thus abort the optimization attempt
2519 # and do a full sync.
2520 break
2521 elif ret < 0:
2522 # Git died with a signal, exit immediately.
2523 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002524
Gavin Makea2e3302023-03-11 06:46:20 +00002525 # Figure out how long to sleep before the next attempt, if there is
2526 # one.
2527 if not verbose and gitcmd.stdout:
2528 print(
2529 "\n%s:\n%s" % (self.name, gitcmd.stdout),
2530 end="",
2531 file=output_redir,
2532 )
2533 if try_n < retry_fetches - 1:
2534 print(
2535 "%s: sleeping %s seconds before retrying"
2536 % (self.name, retry_cur_sleep),
2537 file=output_redir,
2538 )
2539 time.sleep(retry_cur_sleep)
2540 retry_cur_sleep = min(
2541 retry_exp_factor * retry_cur_sleep, MAXIMUM_RETRY_SLEEP_SEC
2542 )
2543 retry_cur_sleep *= 1 - random.uniform(
2544 -RETRY_JITTER_PERCENT, RETRY_JITTER_PERCENT
2545 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002546
Gavin Makea2e3302023-03-11 06:46:20 +00002547 if initial:
2548 if alt_dir:
2549 if old_packed != "":
2550 _lwrite(packed_refs, old_packed)
2551 else:
2552 platform_utils.remove(packed_refs)
2553 self.bare_git.pack_refs("--all", "--prune")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002554
Gavin Makea2e3302023-03-11 06:46:20 +00002555 if is_sha1 and current_branch_only:
2556 # We just synced the upstream given branch; verify we
2557 # got what we wanted, else trigger a second run of all
2558 # refs.
2559 if not self._CheckForImmutableRevision():
2560 # Sync the current branch only with depth set to None.
2561 # We always pass depth=None down to avoid infinite recursion.
2562 return self._RemoteFetch(
2563 name=name,
2564 quiet=quiet,
2565 verbose=verbose,
2566 output_redir=output_redir,
2567 current_branch_only=current_branch_only and depth,
2568 initial=False,
2569 alt_dir=alt_dir,
2570 depth=None,
2571 ssh_proxy=ssh_proxy,
2572 clone_filter=clone_filter,
2573 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002574
Gavin Makea2e3302023-03-11 06:46:20 +00002575 return ok
Mike Frysingerf4545122019-11-11 04:34:16 -05002576
Gavin Makea2e3302023-03-11 06:46:20 +00002577 def _ApplyCloneBundle(self, initial=False, quiet=False, verbose=False):
2578 if initial and (
2579 self.manifest.manifestProject.depth or self.clone_depth
2580 ):
2581 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002582
Gavin Makea2e3302023-03-11 06:46:20 +00002583 remote = self.GetRemote()
2584 bundle_url = remote.url + "/clone.bundle"
2585 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
2586 if GetSchemeFromUrl(bundle_url) not in (
2587 "http",
2588 "https",
2589 "persistent-http",
2590 "persistent-https",
2591 ):
2592 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06002593
Gavin Makea2e3302023-03-11 06:46:20 +00002594 bundle_dst = os.path.join(self.gitdir, "clone.bundle")
2595 bundle_tmp = os.path.join(self.gitdir, "clone.bundle.tmp")
2596
2597 exist_dst = os.path.exists(bundle_dst)
2598 exist_tmp = os.path.exists(bundle_tmp)
2599
2600 if not initial and not exist_dst and not exist_tmp:
2601 return False
2602
2603 if not exist_dst:
2604 exist_dst = self._FetchBundle(
2605 bundle_url, bundle_tmp, bundle_dst, quiet, verbose
2606 )
2607 if not exist_dst:
2608 return False
2609
2610 cmd = ["fetch"]
2611 if not verbose:
2612 cmd.append("--quiet")
2613 if not quiet and sys.stdout.isatty():
2614 cmd.append("--progress")
2615 if not self.worktree:
2616 cmd.append("--update-head-ok")
2617 cmd.append(bundle_dst)
2618 for f in remote.fetch:
2619 cmd.append(str(f))
2620 cmd.append("+refs/tags/*:refs/tags/*")
2621
2622 ok = (
2623 GitCommand(
2624 self,
2625 cmd,
2626 bare=True,
2627 objdir=os.path.join(self.objdir, "objects"),
2628 ).Wait()
2629 == 0
2630 )
2631 platform_utils.remove(bundle_dst, missing_ok=True)
2632 platform_utils.remove(bundle_tmp, missing_ok=True)
2633 return ok
2634
2635 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
2636 platform_utils.remove(dstPath, missing_ok=True)
2637
2638 cmd = ["curl", "--fail", "--output", tmpPath, "--netrc", "--location"]
2639 if quiet:
2640 cmd += ["--silent", "--show-error"]
2641 if os.path.exists(tmpPath):
2642 size = os.stat(tmpPath).st_size
2643 if size >= 1024:
2644 cmd += ["--continue-at", "%d" % (size,)]
2645 else:
2646 platform_utils.remove(tmpPath)
2647 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
2648 if cookiefile:
2649 cmd += ["--cookie", cookiefile]
2650 if proxy:
2651 cmd += ["--proxy", proxy]
2652 elif "http_proxy" in os.environ and "darwin" == sys.platform:
2653 cmd += ["--proxy", os.environ["http_proxy"]]
2654 if srcUrl.startswith("persistent-https"):
2655 srcUrl = "http" + srcUrl[len("persistent-https") :]
2656 elif srcUrl.startswith("persistent-http"):
2657 srcUrl = "http" + srcUrl[len("persistent-http") :]
2658 cmd += [srcUrl]
2659
2660 proc = None
2661 with Trace("Fetching bundle: %s", " ".join(cmd)):
2662 if verbose:
2663 print("%s: Downloading bundle: %s" % (self.name, srcUrl))
2664 stdout = None if verbose else subprocess.PIPE
2665 stderr = None if verbose else subprocess.STDOUT
2666 try:
2667 proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
2668 except OSError:
2669 return False
2670
2671 (output, _) = proc.communicate()
2672 curlret = proc.returncode
2673
2674 if curlret == 22:
2675 # From curl man page:
2676 # 22: HTTP page not retrieved. The requested url was not found
2677 # or returned another error with the HTTP error code being 400
2678 # or above. This return code only appears if -f, --fail is used.
2679 if verbose:
2680 print(
2681 "%s: Unable to retrieve clone.bundle; ignoring."
2682 % self.name
2683 )
2684 if output:
2685 print("Curl output:\n%s" % output)
2686 return False
2687 elif curlret and not verbose and output:
2688 print("%s" % output, file=sys.stderr)
2689
2690 if os.path.exists(tmpPath):
2691 if curlret == 0 and self._IsValidBundle(tmpPath, quiet):
2692 platform_utils.rename(tmpPath, dstPath)
2693 return True
2694 else:
2695 platform_utils.remove(tmpPath)
2696 return False
2697 else:
2698 return False
2699
2700 def _IsValidBundle(self, path, quiet):
2701 try:
2702 with open(path, "rb") as f:
2703 if f.read(16) == b"# v2 git bundle\n":
2704 return True
2705 else:
2706 if not quiet:
2707 print(
2708 "Invalid clone.bundle file; ignoring.",
2709 file=sys.stderr,
2710 )
2711 return False
2712 except OSError:
2713 return False
2714
2715 def _Checkout(self, rev, quiet=False):
2716 cmd = ["checkout"]
2717 if quiet:
2718 cmd.append("-q")
2719 cmd.append(rev)
2720 cmd.append("--")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002721 if GitCommand(self, cmd).Wait() != 0:
Gavin Makea2e3302023-03-11 06:46:20 +00002722 if self._allrefs:
2723 raise GitError("%s checkout %s " % (self.name, rev))
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002724
Gavin Makea2e3302023-03-11 06:46:20 +00002725 def _CherryPick(self, rev, ffonly=False, record_origin=False):
2726 cmd = ["cherry-pick"]
2727 if ffonly:
2728 cmd.append("--ff")
2729 if record_origin:
2730 cmd.append("-x")
2731 cmd.append(rev)
2732 cmd.append("--")
2733 if GitCommand(self, cmd).Wait() != 0:
2734 if self._allrefs:
2735 raise GitError("%s cherry-pick %s " % (self.name, rev))
Victor Boivie0960b5b2010-11-26 13:42:13 +01002736
Gavin Makea2e3302023-03-11 06:46:20 +00002737 def _LsRemote(self, refs):
2738 cmd = ["ls-remote", self.remote.name, refs]
2739 p = GitCommand(self, cmd, capture_stdout=True)
2740 if p.Wait() == 0:
2741 return p.stdout
2742 return None
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002743
Gavin Makea2e3302023-03-11 06:46:20 +00002744 def _Revert(self, rev):
2745 cmd = ["revert"]
2746 cmd.append("--no-edit")
2747 cmd.append(rev)
2748 cmd.append("--")
2749 if GitCommand(self, cmd).Wait() != 0:
2750 if self._allrefs:
2751 raise GitError("%s revert %s " % (self.name, rev))
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002752
Gavin Makea2e3302023-03-11 06:46:20 +00002753 def _ResetHard(self, rev, quiet=True):
2754 cmd = ["reset", "--hard"]
2755 if quiet:
2756 cmd.append("-q")
2757 cmd.append(rev)
2758 if GitCommand(self, cmd).Wait() != 0:
2759 raise GitError("%s reset --hard %s " % (self.name, rev))
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002760
Gavin Makea2e3302023-03-11 06:46:20 +00002761 def _SyncSubmodules(self, quiet=True):
2762 cmd = ["submodule", "update", "--init", "--recursive"]
2763 if quiet:
2764 cmd.append("-q")
2765 if GitCommand(self, cmd).Wait() != 0:
2766 raise GitError(
2767 "%s submodule update --init --recursive " % self.name
2768 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002769
Gavin Makea2e3302023-03-11 06:46:20 +00002770 def _Rebase(self, upstream, onto=None):
2771 cmd = ["rebase"]
2772 if onto is not None:
2773 cmd.extend(["--onto", onto])
2774 cmd.append(upstream)
2775 if GitCommand(self, cmd).Wait() != 0:
2776 raise GitError("%s rebase %s " % (self.name, upstream))
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002777
Gavin Makea2e3302023-03-11 06:46:20 +00002778 def _FastForward(self, head, ffonly=False):
2779 cmd = ["merge", "--no-stat", head]
2780 if ffonly:
2781 cmd.append("--ff-only")
2782 if GitCommand(self, cmd).Wait() != 0:
2783 raise GitError("%s merge %s " % (self.name, head))
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002784
Gavin Makea2e3302023-03-11 06:46:20 +00002785 def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False):
2786 init_git_dir = not os.path.exists(self.gitdir)
2787 init_obj_dir = not os.path.exists(self.objdir)
2788 try:
2789 # Initialize the bare repository, which contains all of the objects.
2790 if init_obj_dir:
2791 os.makedirs(self.objdir)
2792 self.bare_objdir.init()
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002793
Gavin Makea2e3302023-03-11 06:46:20 +00002794 self._UpdateHooks(quiet=quiet)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002795
Gavin Makea2e3302023-03-11 06:46:20 +00002796 if self.use_git_worktrees:
2797 # Enable per-worktree config file support if possible. This
2798 # is more a nice-to-have feature for users rather than a
2799 # hard requirement.
2800 if git_require((2, 20, 0)):
2801 self.EnableRepositoryExtension("worktreeConfig")
Renaud Paquay788e9622017-01-27 11:41:12 -08002802
Gavin Makea2e3302023-03-11 06:46:20 +00002803 # If we have a separate directory to hold refs, initialize it as
2804 # well.
2805 if self.objdir != self.gitdir:
2806 if init_git_dir:
2807 os.makedirs(self.gitdir)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002808
Gavin Makea2e3302023-03-11 06:46:20 +00002809 if init_obj_dir or init_git_dir:
2810 self._ReferenceGitDir(
2811 self.objdir, self.gitdir, copy_all=True
2812 )
2813 try:
2814 self._CheckDirReference(self.objdir, self.gitdir)
2815 except GitError as e:
2816 if force_sync:
2817 print(
2818 "Retrying clone after deleting %s" % self.gitdir,
2819 file=sys.stderr,
2820 )
2821 try:
2822 platform_utils.rmtree(
2823 platform_utils.realpath(self.gitdir)
2824 )
2825 if self.worktree and os.path.exists(
2826 platform_utils.realpath(self.worktree)
2827 ):
2828 platform_utils.rmtree(
2829 platform_utils.realpath(self.worktree)
2830 )
2831 return self._InitGitDir(
2832 mirror_git=mirror_git,
2833 force_sync=False,
2834 quiet=quiet,
2835 )
2836 except Exception:
2837 raise e
2838 raise e
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002839
Gavin Makea2e3302023-03-11 06:46:20 +00002840 if init_git_dir:
2841 mp = self.manifest.manifestProject
2842 ref_dir = mp.reference or ""
Julien Camperguedd654222014-01-09 16:21:37 +01002843
Gavin Makea2e3302023-03-11 06:46:20 +00002844 def _expanded_ref_dirs():
2845 """Iterate through possible git reference dir paths."""
2846 name = self.name + ".git"
2847 yield mirror_git or os.path.join(ref_dir, name)
2848 for prefix in "", self.remote.name:
2849 yield os.path.join(
2850 ref_dir, ".repo", "project-objects", prefix, name
2851 )
2852 yield os.path.join(
2853 ref_dir, ".repo", "worktrees", prefix, name
2854 )
2855
2856 if ref_dir or mirror_git:
2857 found_ref_dir = None
2858 for path in _expanded_ref_dirs():
2859 if os.path.exists(path):
2860 found_ref_dir = path
2861 break
2862 ref_dir = found_ref_dir
2863
2864 if ref_dir:
2865 if not os.path.isabs(ref_dir):
2866 # The alternate directory is relative to the object
2867 # database.
2868 ref_dir = os.path.relpath(
2869 ref_dir, os.path.join(self.objdir, "objects")
2870 )
2871 _lwrite(
2872 os.path.join(
2873 self.objdir, "objects/info/alternates"
2874 ),
2875 os.path.join(ref_dir, "objects") + "\n",
2876 )
2877
2878 m = self.manifest.manifestProject.config
2879 for key in ["user.name", "user.email"]:
2880 if m.Has(key, include_defaults=False):
2881 self.config.SetString(key, m.GetString(key))
2882 if not self.manifest.EnableGitLfs:
2883 self.config.SetString(
2884 "filter.lfs.smudge", "git-lfs smudge --skip -- %f"
2885 )
2886 self.config.SetString(
2887 "filter.lfs.process", "git-lfs filter-process --skip"
2888 )
2889 self.config.SetBoolean(
2890 "core.bare", True if self.manifest.IsMirror else None
2891 )
2892 except Exception:
2893 if init_obj_dir and os.path.exists(self.objdir):
2894 platform_utils.rmtree(self.objdir)
2895 if init_git_dir and os.path.exists(self.gitdir):
2896 platform_utils.rmtree(self.gitdir)
2897 raise
2898
2899 def _UpdateHooks(self, quiet=False):
2900 if os.path.exists(self.objdir):
2901 self._InitHooks(quiet=quiet)
2902
2903 def _InitHooks(self, quiet=False):
2904 hooks = platform_utils.realpath(os.path.join(self.objdir, "hooks"))
2905 if not os.path.exists(hooks):
2906 os.makedirs(hooks)
2907
2908 # Delete sample hooks. They're noise.
2909 for hook in glob.glob(os.path.join(hooks, "*.sample")):
2910 try:
2911 platform_utils.remove(hook, missing_ok=True)
2912 except PermissionError:
2913 pass
2914
2915 for stock_hook in _ProjectHooks():
2916 name = os.path.basename(stock_hook)
2917
2918 if (
2919 name in ("commit-msg",)
2920 and not self.remote.review
2921 and self is not self.manifest.manifestProject
2922 ):
2923 # Don't install a Gerrit Code Review hook if this
2924 # project does not appear to use it for reviews.
2925 #
2926 # Since the manifest project is one of those, but also
2927 # managed through gerrit, it's excluded.
2928 continue
2929
2930 dst = os.path.join(hooks, name)
2931 if platform_utils.islink(dst):
2932 continue
2933 if os.path.exists(dst):
2934 # If the files are the same, we'll leave it alone. We create
2935 # symlinks below by default but fallback to hardlinks if the OS
2936 # blocks them. So if we're here, it's probably because we made a
2937 # hardlink below.
2938 if not filecmp.cmp(stock_hook, dst, shallow=False):
2939 if not quiet:
2940 _warn(
2941 "%s: Not replacing locally modified %s hook",
2942 self.RelPath(local=False),
2943 name,
2944 )
2945 continue
2946 try:
2947 platform_utils.symlink(
2948 os.path.relpath(stock_hook, os.path.dirname(dst)), dst
2949 )
2950 except OSError as e:
2951 if e.errno == errno.EPERM:
2952 try:
2953 os.link(stock_hook, dst)
2954 except OSError:
2955 raise GitError(self._get_symlink_error_message())
2956 else:
2957 raise
2958
2959 def _InitRemote(self):
2960 if self.remote.url:
2961 remote = self.GetRemote()
2962 remote.url = self.remote.url
2963 remote.pushUrl = self.remote.pushUrl
2964 remote.review = self.remote.review
2965 remote.projectname = self.name
2966
2967 if self.worktree:
2968 remote.ResetFetch(mirror=False)
2969 else:
2970 remote.ResetFetch(mirror=True)
2971 remote.Save()
2972
2973 def _InitMRef(self):
2974 """Initialize the pseudo m/<manifest branch> ref."""
2975 if self.manifest.branch:
2976 if self.use_git_worktrees:
2977 # Set up the m/ space to point to the worktree-specific ref
2978 # space. We'll update the worktree-specific ref space on each
2979 # checkout.
2980 ref = R_M + self.manifest.branch
2981 if not self.bare_ref.symref(ref):
2982 self.bare_git.symbolic_ref(
2983 "-m",
2984 "redirecting to worktree scope",
2985 ref,
2986 R_WORKTREE_M + self.manifest.branch,
2987 )
2988
2989 # We can't update this ref with git worktrees until it exists.
2990 # We'll wait until the initial checkout to set it.
2991 if not os.path.exists(self.worktree):
2992 return
2993
2994 base = R_WORKTREE_M
2995 active_git = self.work_git
2996
2997 self._InitAnyMRef(HEAD, self.bare_git, detach=True)
2998 else:
2999 base = R_M
3000 active_git = self.bare_git
3001
3002 self._InitAnyMRef(base + self.manifest.branch, active_git)
3003
3004 def _InitMirrorHead(self):
3005 self._InitAnyMRef(HEAD, self.bare_git)
3006
3007 def _InitAnyMRef(self, ref, active_git, detach=False):
3008 """Initialize |ref| in |active_git| to the value in the manifest.
3009
3010 This points |ref| to the <project> setting in the manifest.
3011
3012 Args:
3013 ref: The branch to update.
3014 active_git: The git repository to make updates in.
3015 detach: Whether to update target of symbolic refs, or overwrite the
3016 ref directly (and thus make it non-symbolic).
3017 """
3018 cur = self.bare_ref.symref(ref)
3019
3020 if self.revisionId:
3021 if cur != "" or self.bare_ref.get(ref) != self.revisionId:
3022 msg = "manifest set to %s" % self.revisionId
3023 dst = self.revisionId + "^0"
3024 active_git.UpdateRef(ref, dst, message=msg, detach=True)
Julien Camperguedd654222014-01-09 16:21:37 +01003025 else:
Gavin Makea2e3302023-03-11 06:46:20 +00003026 remote = self.GetRemote()
3027 dst = remote.ToLocal(self.revisionExpr)
3028 if cur != dst:
3029 msg = "manifest set to %s" % self.revisionExpr
3030 if detach:
3031 active_git.UpdateRef(ref, dst, message=msg, detach=True)
3032 else:
3033 active_git.symbolic_ref("-m", msg, ref, dst)
Julien Camperguedd654222014-01-09 16:21:37 +01003034
Gavin Makea2e3302023-03-11 06:46:20 +00003035 def _CheckDirReference(self, srcdir, destdir):
3036 # Git worktrees don't use symlinks to share at all.
3037 if self.use_git_worktrees:
3038 return
Julien Camperguedd654222014-01-09 16:21:37 +01003039
Gavin Makea2e3302023-03-11 06:46:20 +00003040 for name in self.shareable_dirs:
3041 # Try to self-heal a bit in simple cases.
3042 dst_path = os.path.join(destdir, name)
3043 src_path = os.path.join(srcdir, name)
Julien Camperguedd654222014-01-09 16:21:37 +01003044
Gavin Makea2e3302023-03-11 06:46:20 +00003045 dst = platform_utils.realpath(dst_path)
3046 if os.path.lexists(dst):
3047 src = platform_utils.realpath(src_path)
3048 # Fail if the links are pointing to the wrong place.
3049 if src != dst:
3050 _error("%s is different in %s vs %s", name, destdir, srcdir)
3051 raise GitError(
3052 "--force-sync not enabled; cannot overwrite a local "
3053 "work tree. If you're comfortable with the "
3054 "possibility of losing the work tree's git metadata,"
3055 " use `repo sync --force-sync {0}` to "
3056 "proceed.".format(self.RelPath(local=False))
3057 )
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003058
Gavin Makea2e3302023-03-11 06:46:20 +00003059 def _ReferenceGitDir(self, gitdir, dotgit, copy_all):
3060 """Update |dotgit| to reference |gitdir|, using symlinks where possible.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003061
Gavin Makea2e3302023-03-11 06:46:20 +00003062 Args:
3063 gitdir: The bare git repository. Must already be initialized.
3064 dotgit: The repository you would like to initialize.
3065 copy_all: If true, copy all remaining files from |gitdir| ->
3066 |dotgit|. This saves you the effort of initializing |dotgit|
3067 yourself.
3068 """
3069 symlink_dirs = self.shareable_dirs[:]
3070 to_symlink = symlink_dirs
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003071
Gavin Makea2e3302023-03-11 06:46:20 +00003072 to_copy = []
3073 if copy_all:
3074 to_copy = platform_utils.listdir(gitdir)
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003075
Gavin Makea2e3302023-03-11 06:46:20 +00003076 dotgit = platform_utils.realpath(dotgit)
3077 for name in set(to_copy).union(to_symlink):
3078 try:
3079 src = platform_utils.realpath(os.path.join(gitdir, name))
3080 dst = os.path.join(dotgit, name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003081
Gavin Makea2e3302023-03-11 06:46:20 +00003082 if os.path.lexists(dst):
3083 continue
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003084
Gavin Makea2e3302023-03-11 06:46:20 +00003085 # If the source dir doesn't exist, create an empty dir.
3086 if name in symlink_dirs and not os.path.lexists(src):
3087 os.makedirs(src)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003088
Gavin Makea2e3302023-03-11 06:46:20 +00003089 if name in to_symlink:
3090 platform_utils.symlink(
3091 os.path.relpath(src, os.path.dirname(dst)), dst
3092 )
3093 elif copy_all and not platform_utils.islink(dst):
3094 if platform_utils.isdir(src):
3095 shutil.copytree(src, dst)
3096 elif os.path.isfile(src):
3097 shutil.copy(src, dst)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003098
Gavin Makea2e3302023-03-11 06:46:20 +00003099 except OSError as e:
3100 if e.errno == errno.EPERM:
3101 raise DownloadError(self._get_symlink_error_message())
3102 else:
3103 raise
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003104
Gavin Makea2e3302023-03-11 06:46:20 +00003105 def _InitGitWorktree(self):
3106 """Init the project using git worktrees."""
3107 self.bare_git.worktree("prune")
3108 self.bare_git.worktree(
3109 "add",
3110 "-ff",
3111 "--checkout",
3112 "--detach",
3113 "--lock",
3114 self.worktree,
3115 self.GetRevisionId(),
3116 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003117
Gavin Makea2e3302023-03-11 06:46:20 +00003118 # Rewrite the internal state files to use relative paths between the
3119 # checkouts & worktrees.
3120 dotgit = os.path.join(self.worktree, ".git")
3121 with open(dotgit, "r") as fp:
3122 # Figure out the checkout->worktree path.
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003123 setting = fp.read()
Gavin Makea2e3302023-03-11 06:46:20 +00003124 assert setting.startswith("gitdir:")
3125 git_worktree_path = setting.split(":", 1)[1].strip()
3126 # Some platforms (e.g. Windows) won't let us update dotgit in situ
3127 # because of file permissions. Delete it and recreate it from scratch
3128 # to avoid.
3129 platform_utils.remove(dotgit)
3130 # Use relative path from checkout->worktree & maintain Unix line endings
3131 # on all OS's to match git behavior.
3132 with open(dotgit, "w", newline="\n") as fp:
3133 print(
3134 "gitdir:",
3135 os.path.relpath(git_worktree_path, self.worktree),
3136 file=fp,
3137 )
3138 # Use relative path from worktree->checkout & maintain Unix line endings
3139 # on all OS's to match git behavior.
3140 with open(
3141 os.path.join(git_worktree_path, "gitdir"), "w", newline="\n"
3142 ) as fp:
3143 print(os.path.relpath(dotgit, git_worktree_path), file=fp)
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003144
Gavin Makea2e3302023-03-11 06:46:20 +00003145 self._InitMRef()
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003146
Gavin Makea2e3302023-03-11 06:46:20 +00003147 def _InitWorkTree(self, force_sync=False, submodules=False):
3148 """Setup the worktree .git path.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003149
Gavin Makea2e3302023-03-11 06:46:20 +00003150 This is the user-visible path like src/foo/.git/.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003151
Gavin Makea2e3302023-03-11 06:46:20 +00003152 With non-git-worktrees, this will be a symlink to the .repo/projects/
3153 path. With git-worktrees, this will be a .git file using "gitdir: ..."
3154 syntax.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003155
Gavin Makea2e3302023-03-11 06:46:20 +00003156 Older checkouts had .git/ directories. If we see that, migrate it.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003157
Gavin Makea2e3302023-03-11 06:46:20 +00003158 This also handles changes in the manifest. Maybe this project was
3159 backed by "foo/bar" on the server, but now it's "new/foo/bar". We have
3160 to update the path we point to under .repo/projects/ to match.
3161 """
3162 dotgit = os.path.join(self.worktree, ".git")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003163
Gavin Makea2e3302023-03-11 06:46:20 +00003164 # If using an old layout style (a directory), migrate it.
3165 if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
3166 self._MigrateOldWorkTreeGitDir(dotgit)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003167
Gavin Makea2e3302023-03-11 06:46:20 +00003168 init_dotgit = not os.path.exists(dotgit)
3169 if self.use_git_worktrees:
3170 if init_dotgit:
3171 self._InitGitWorktree()
3172 self._CopyAndLinkFiles()
3173 else:
3174 if not init_dotgit:
3175 # See if the project has changed.
3176 if platform_utils.realpath(
3177 self.gitdir
3178 ) != platform_utils.realpath(dotgit):
3179 platform_utils.remove(dotgit)
Doug Anderson37282b42011-03-04 11:54:18 -08003180
Gavin Makea2e3302023-03-11 06:46:20 +00003181 if init_dotgit or not os.path.exists(dotgit):
3182 os.makedirs(self.worktree, exist_ok=True)
3183 platform_utils.symlink(
3184 os.path.relpath(self.gitdir, self.worktree), dotgit
3185 )
Doug Anderson37282b42011-03-04 11:54:18 -08003186
Gavin Makea2e3302023-03-11 06:46:20 +00003187 if init_dotgit:
3188 _lwrite(
3189 os.path.join(dotgit, HEAD), "%s\n" % self.GetRevisionId()
3190 )
Doug Anderson37282b42011-03-04 11:54:18 -08003191
Gavin Makea2e3302023-03-11 06:46:20 +00003192 # Finish checking out the worktree.
3193 cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
3194 if GitCommand(self, cmd).Wait() != 0:
3195 raise GitError(
3196 "Cannot initialize work tree for " + self.name
3197 )
Doug Anderson37282b42011-03-04 11:54:18 -08003198
Gavin Makea2e3302023-03-11 06:46:20 +00003199 if submodules:
3200 self._SyncSubmodules(quiet=True)
3201 self._CopyAndLinkFiles()
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003202
Gavin Makea2e3302023-03-11 06:46:20 +00003203 @classmethod
3204 def _MigrateOldWorkTreeGitDir(cls, dotgit):
3205 """Migrate the old worktree .git/ dir style to a symlink.
3206
3207 This logic specifically only uses state from |dotgit| to figure out
3208 where to move content and not |self|. This way if the backing project
3209 also changed places, we only do the .git/ dir to .git symlink migration
3210 here. The path updates will happen independently.
3211 """
3212 # Figure out where in .repo/projects/ it's pointing to.
3213 if not os.path.islink(os.path.join(dotgit, "refs")):
3214 raise GitError(f"{dotgit}: unsupported checkout state")
3215 gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, "refs")))
3216
3217 # Remove known symlink paths that exist in .repo/projects/.
3218 KNOWN_LINKS = {
3219 "config",
3220 "description",
3221 "hooks",
3222 "info",
3223 "logs",
3224 "objects",
3225 "packed-refs",
3226 "refs",
3227 "rr-cache",
3228 "shallow",
3229 "svn",
3230 }
3231 # Paths that we know will be in both, but are safe to clobber in
3232 # .repo/projects/.
3233 SAFE_TO_CLOBBER = {
3234 "COMMIT_EDITMSG",
3235 "FETCH_HEAD",
3236 "HEAD",
3237 "gc.log",
3238 "gitk.cache",
3239 "index",
3240 "ORIG_HEAD",
3241 }
3242
3243 # First see if we'd succeed before starting the migration.
3244 unknown_paths = []
3245 for name in platform_utils.listdir(dotgit):
3246 # Ignore all temporary/backup names. These are common with vim &
3247 # emacs.
3248 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3249 continue
3250
3251 dotgit_path = os.path.join(dotgit, name)
3252 if name in KNOWN_LINKS:
3253 if not platform_utils.islink(dotgit_path):
3254 unknown_paths.append(f"{dotgit_path}: should be a symlink")
3255 else:
3256 gitdir_path = os.path.join(gitdir, name)
3257 if name not in SAFE_TO_CLOBBER and os.path.exists(gitdir_path):
3258 unknown_paths.append(
3259 f"{dotgit_path}: unknown file; please file a bug"
3260 )
3261 if unknown_paths:
3262 raise GitError("Aborting migration: " + "\n".join(unknown_paths))
3263
3264 # Now walk the paths and sync the .git/ to .repo/projects/.
3265 for name in platform_utils.listdir(dotgit):
3266 dotgit_path = os.path.join(dotgit, name)
3267
3268 # Ignore all temporary/backup names. These are common with vim &
3269 # emacs.
3270 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3271 platform_utils.remove(dotgit_path)
3272 elif name in KNOWN_LINKS:
3273 platform_utils.remove(dotgit_path)
3274 else:
3275 gitdir_path = os.path.join(gitdir, name)
3276 platform_utils.remove(gitdir_path, missing_ok=True)
3277 platform_utils.rename(dotgit_path, gitdir_path)
3278
3279 # Now that the dir should be empty, clear it out, and symlink it over.
3280 platform_utils.rmdir(dotgit)
3281 platform_utils.symlink(
3282 os.path.relpath(gitdir, os.path.dirname(dotgit)), dotgit
3283 )
3284
3285 def _get_symlink_error_message(self):
3286 if platform_utils.isWindows():
3287 return (
3288 "Unable to create symbolic link. Please re-run the command as "
3289 "Administrator, or see "
3290 "https://github.com/git-for-windows/git/wiki/Symbolic-Links "
3291 "for other options."
3292 )
3293 return "filesystem must support symlinks"
3294
3295 def _revlist(self, *args, **kw):
3296 a = []
3297 a.extend(args)
3298 a.append("--")
3299 return self.work_git.rev_list(*a, **kw)
3300
3301 @property
3302 def _allrefs(self):
3303 return self.bare_ref.all
3304
3305 def _getLogs(
3306 self, rev1, rev2, oneline=False, color=True, pretty_format=None
3307 ):
3308 """Get logs between two revisions of this project."""
3309 comp = ".."
3310 if rev1:
3311 revs = [rev1]
3312 if rev2:
3313 revs.extend([comp, rev2])
3314 cmd = ["log", "".join(revs)]
3315 out = DiffColoring(self.config)
3316 if out.is_on and color:
3317 cmd.append("--color")
3318 if pretty_format is not None:
3319 cmd.append("--pretty=format:%s" % pretty_format)
3320 if oneline:
3321 cmd.append("--oneline")
3322
3323 try:
3324 log = GitCommand(
3325 self, cmd, capture_stdout=True, capture_stderr=True
3326 )
3327 if log.Wait() == 0:
3328 return log.stdout
3329 except GitError:
3330 # worktree may not exist if groups changed for example. In that
3331 # case, try in gitdir instead.
3332 if not os.path.exists(self.worktree):
3333 return self.bare_git.log(*cmd[1:])
3334 else:
3335 raise
3336 return None
3337
3338 def getAddedAndRemovedLogs(
3339 self, toProject, oneline=False, color=True, pretty_format=None
3340 ):
3341 """Get the list of logs from this revision to given revisionId"""
3342 logs = {}
3343 selfId = self.GetRevisionId(self._allrefs)
3344 toId = toProject.GetRevisionId(toProject._allrefs)
3345
3346 logs["added"] = self._getLogs(
3347 selfId,
3348 toId,
3349 oneline=oneline,
3350 color=color,
3351 pretty_format=pretty_format,
3352 )
3353 logs["removed"] = self._getLogs(
3354 toId,
3355 selfId,
3356 oneline=oneline,
3357 color=color,
3358 pretty_format=pretty_format,
3359 )
3360 return logs
3361
3362 class _GitGetByExec(object):
3363 def __init__(self, project, bare, gitdir):
3364 self._project = project
3365 self._bare = bare
3366 self._gitdir = gitdir
3367
3368 # __getstate__ and __setstate__ are required for pickling because
3369 # __getattr__ exists.
3370 def __getstate__(self):
3371 return (self._project, self._bare, self._gitdir)
3372
3373 def __setstate__(self, state):
3374 self._project, self._bare, self._gitdir = state
3375
3376 def LsOthers(self):
3377 p = GitCommand(
3378 self._project,
3379 ["ls-files", "-z", "--others", "--exclude-standard"],
3380 bare=False,
3381 gitdir=self._gitdir,
3382 capture_stdout=True,
3383 capture_stderr=True,
3384 )
3385 if p.Wait() == 0:
3386 out = p.stdout
3387 if out:
3388 # Backslash is not anomalous.
3389 return out[:-1].split("\0")
3390 return []
3391
3392 def DiffZ(self, name, *args):
3393 cmd = [name]
3394 cmd.append("-z")
3395 cmd.append("--ignore-submodules")
3396 cmd.extend(args)
3397 p = GitCommand(
3398 self._project,
3399 cmd,
3400 gitdir=self._gitdir,
3401 bare=False,
3402 capture_stdout=True,
3403 capture_stderr=True,
3404 )
3405 p.Wait()
3406 r = {}
3407 out = p.stdout
3408 if out:
3409 out = iter(out[:-1].split("\0"))
3410 while out:
3411 try:
3412 info = next(out)
3413 path = next(out)
3414 except StopIteration:
3415 break
3416
3417 class _Info(object):
3418 def __init__(self, path, omode, nmode, oid, nid, state):
3419 self.path = path
3420 self.src_path = None
3421 self.old_mode = omode
3422 self.new_mode = nmode
3423 self.old_id = oid
3424 self.new_id = nid
3425
3426 if len(state) == 1:
3427 self.status = state
3428 self.level = None
3429 else:
3430 self.status = state[:1]
3431 self.level = state[1:]
3432 while self.level.startswith("0"):
3433 self.level = self.level[1:]
3434
3435 info = info[1:].split(" ")
3436 info = _Info(path, *info)
3437 if info.status in ("R", "C"):
3438 info.src_path = info.path
3439 info.path = next(out)
3440 r[info.path] = info
3441 return r
3442
3443 def GetDotgitPath(self, subpath=None):
3444 """Return the full path to the .git dir.
3445
3446 As a convenience, append |subpath| if provided.
3447 """
3448 if self._bare:
3449 dotgit = self._gitdir
3450 else:
3451 dotgit = os.path.join(self._project.worktree, ".git")
3452 if os.path.isfile(dotgit):
3453 # Git worktrees use a "gitdir:" syntax to point to the
3454 # scratch space.
3455 with open(dotgit) as fp:
3456 setting = fp.read()
3457 assert setting.startswith("gitdir:")
3458 gitdir = setting.split(":", 1)[1].strip()
3459 dotgit = os.path.normpath(
3460 os.path.join(self._project.worktree, gitdir)
3461 )
3462
3463 return dotgit if subpath is None else os.path.join(dotgit, subpath)
3464
3465 def GetHead(self):
3466 """Return the ref that HEAD points to."""
3467 path = self.GetDotgitPath(subpath=HEAD)
3468 try:
3469 with open(path) as fd:
3470 line = fd.readline()
3471 except IOError as e:
3472 raise NoManifestException(path, str(e))
3473 try:
3474 line = line.decode()
3475 except AttributeError:
3476 pass
3477 if line.startswith("ref: "):
3478 return line[5:-1]
3479 return line[:-1]
3480
3481 def SetHead(self, ref, message=None):
3482 cmdv = []
3483 if message is not None:
3484 cmdv.extend(["-m", message])
3485 cmdv.append(HEAD)
3486 cmdv.append(ref)
3487 self.symbolic_ref(*cmdv)
3488
3489 def DetachHead(self, new, message=None):
3490 cmdv = ["--no-deref"]
3491 if message is not None:
3492 cmdv.extend(["-m", message])
3493 cmdv.append(HEAD)
3494 cmdv.append(new)
3495 self.update_ref(*cmdv)
3496
3497 def UpdateRef(self, name, new, old=None, message=None, detach=False):
3498 cmdv = []
3499 if message is not None:
3500 cmdv.extend(["-m", message])
3501 if detach:
3502 cmdv.append("--no-deref")
3503 cmdv.append(name)
3504 cmdv.append(new)
3505 if old is not None:
3506 cmdv.append(old)
3507 self.update_ref(*cmdv)
3508
3509 def DeleteRef(self, name, old=None):
3510 if not old:
3511 old = self.rev_parse(name)
3512 self.update_ref("-d", name, old)
3513 self._project.bare_ref.deleted(name)
3514
3515 def rev_list(self, *args, **kw):
3516 if "format" in kw:
3517 cmdv = ["log", "--pretty=format:%s" % kw["format"]]
3518 else:
3519 cmdv = ["rev-list"]
3520 cmdv.extend(args)
3521 p = GitCommand(
3522 self._project,
3523 cmdv,
3524 bare=self._bare,
3525 gitdir=self._gitdir,
3526 capture_stdout=True,
3527 capture_stderr=True,
3528 )
3529 if p.Wait() != 0:
3530 raise GitError(
3531 "%s rev-list %s: %s"
3532 % (self._project.name, str(args), p.stderr)
3533 )
3534 return p.stdout.splitlines()
3535
3536 def __getattr__(self, name):
3537 """Allow arbitrary git commands using pythonic syntax.
3538
3539 This allows you to do things like:
3540 git_obj.rev_parse('HEAD')
3541
3542 Since we don't have a 'rev_parse' method defined, the __getattr__
3543 will run. We'll replace the '_' with a '-' and try to run a git
3544 command. Any other positional arguments will be passed to the git
3545 command, and the following keyword arguments are supported:
3546 config: An optional dict of git config options to be passed with
3547 '-c'.
3548
3549 Args:
3550 name: The name of the git command to call. Any '_' characters
3551 will be replaced with '-'.
3552
3553 Returns:
3554 A callable object that will try to call git with the named
3555 command.
3556 """
3557 name = name.replace("_", "-")
3558
3559 def runner(*args, **kwargs):
3560 cmdv = []
3561 config = kwargs.pop("config", None)
3562 for k in kwargs:
3563 raise TypeError(
3564 "%s() got an unexpected keyword argument %r" % (name, k)
3565 )
3566 if config is not None:
3567 for k, v in config.items():
3568 cmdv.append("-c")
3569 cmdv.append("%s=%s" % (k, v))
3570 cmdv.append(name)
3571 cmdv.extend(args)
3572 p = GitCommand(
3573 self._project,
3574 cmdv,
3575 bare=self._bare,
3576 gitdir=self._gitdir,
3577 capture_stdout=True,
3578 capture_stderr=True,
3579 )
3580 if p.Wait() != 0:
3581 raise GitError(
3582 "%s %s: %s" % (self._project.name, name, p.stderr)
3583 )
3584 r = p.stdout
3585 if r.endswith("\n") and r.index("\n") == len(r) - 1:
3586 return r[:-1]
3587 return r
3588
3589 return runner
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003590
3591
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003592class _PriorSyncFailedError(Exception):
Gavin Makea2e3302023-03-11 06:46:20 +00003593 def __str__(self):
3594 return "prior sync failed; rebase still in progress"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003595
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003596
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003597class _DirtyError(Exception):
Gavin Makea2e3302023-03-11 06:46:20 +00003598 def __str__(self):
3599 return "contains uncommitted changes"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003600
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003601
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003602class _InfoMessage(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003603 def __init__(self, project, text):
3604 self.project = project
3605 self.text = text
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003606
Gavin Makea2e3302023-03-11 06:46:20 +00003607 def Print(self, syncbuf):
3608 syncbuf.out.info(
3609 "%s/: %s", self.project.RelPath(local=False), self.text
3610 )
3611 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003612
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003613
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003614class _Failure(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003615 def __init__(self, project, why):
3616 self.project = project
3617 self.why = why
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003618
Gavin Makea2e3302023-03-11 06:46:20 +00003619 def Print(self, syncbuf):
3620 syncbuf.out.fail(
3621 "error: %s/: %s", self.project.RelPath(local=False), str(self.why)
3622 )
3623 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003624
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003625
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003626class _Later(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003627 def __init__(self, project, action):
3628 self.project = project
3629 self.action = action
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003630
Gavin Makea2e3302023-03-11 06:46:20 +00003631 def Run(self, syncbuf):
3632 out = syncbuf.out
3633 out.project("project %s/", self.project.RelPath(local=False))
3634 out.nl()
3635 try:
3636 self.action()
3637 out.nl()
3638 return True
3639 except GitError:
3640 out.nl()
3641 return False
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003642
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003643
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003644class _SyncColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +00003645 def __init__(self, config):
3646 super().__init__(config, "reposync")
3647 self.project = self.printer("header", attr="bold")
3648 self.info = self.printer("info")
3649 self.fail = self.printer("fail", fg="red")
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003650
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003651
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003652class SyncBuffer(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003653 def __init__(self, config, detach_head=False):
3654 self._messages = []
3655 self._failures = []
3656 self._later_queue1 = []
3657 self._later_queue2 = []
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003658
Gavin Makea2e3302023-03-11 06:46:20 +00003659 self.out = _SyncColoring(config)
3660 self.out.redirect(sys.stderr)
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003661
Gavin Makea2e3302023-03-11 06:46:20 +00003662 self.detach_head = detach_head
3663 self.clean = True
3664 self.recent_clean = True
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003665
Gavin Makea2e3302023-03-11 06:46:20 +00003666 def info(self, project, fmt, *args):
3667 self._messages.append(_InfoMessage(project, fmt % args))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003668
Gavin Makea2e3302023-03-11 06:46:20 +00003669 def fail(self, project, err=None):
3670 self._failures.append(_Failure(project, err))
David Rileye0684ad2017-04-05 00:02:59 -07003671 self._MarkUnclean()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003672
Gavin Makea2e3302023-03-11 06:46:20 +00003673 def later1(self, project, what):
3674 self._later_queue1.append(_Later(project, what))
Mike Frysinger70d861f2019-08-26 15:22:36 -04003675
Gavin Makea2e3302023-03-11 06:46:20 +00003676 def later2(self, project, what):
3677 self._later_queue2.append(_Later(project, what))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003678
Gavin Makea2e3302023-03-11 06:46:20 +00003679 def Finish(self):
3680 self._PrintMessages()
3681 self._RunLater()
3682 self._PrintMessages()
3683 return self.clean
3684
3685 def Recently(self):
3686 recent_clean = self.recent_clean
3687 self.recent_clean = True
3688 return recent_clean
3689
3690 def _MarkUnclean(self):
3691 self.clean = False
3692 self.recent_clean = False
3693
3694 def _RunLater(self):
3695 for q in ["_later_queue1", "_later_queue2"]:
3696 if not self._RunQueue(q):
3697 return
3698
3699 def _RunQueue(self, queue):
3700 for m in getattr(self, queue):
3701 if not m.Run(self):
3702 self._MarkUnclean()
3703 return False
3704 setattr(self, queue, [])
3705 return True
3706
3707 def _PrintMessages(self):
3708 if self._messages or self._failures:
3709 if os.isatty(2):
3710 self.out.write(progress.CSI_ERASE_LINE)
3711 self.out.write("\r")
3712
3713 for m in self._messages:
3714 m.Print(self)
3715 for m in self._failures:
3716 m.Print(self)
3717
3718 self._messages = []
3719 self._failures = []
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003720
3721
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003722class MetaProject(Project):
Gavin Makea2e3302023-03-11 06:46:20 +00003723 """A special project housed under .repo."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003724
Gavin Makea2e3302023-03-11 06:46:20 +00003725 def __init__(self, manifest, name, gitdir, worktree):
3726 Project.__init__(
3727 self,
3728 manifest=manifest,
3729 name=name,
3730 gitdir=gitdir,
3731 objdir=gitdir,
3732 worktree=worktree,
3733 remote=RemoteSpec("origin"),
3734 relpath=".repo/%s" % name,
3735 revisionExpr="refs/heads/master",
3736 revisionId=None,
3737 groups=None,
3738 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003739
Gavin Makea2e3302023-03-11 06:46:20 +00003740 def PreSync(self):
3741 if self.Exists:
3742 cb = self.CurrentBranch
3743 if cb:
3744 base = self.GetBranch(cb).merge
3745 if base:
3746 self.revisionExpr = base
3747 self.revisionId = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003748
Gavin Makea2e3302023-03-11 06:46:20 +00003749 @property
3750 def HasChanges(self):
3751 """Has the remote received new commits not yet checked out?"""
3752 if not self.remote or not self.revisionExpr:
3753 return False
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003754
Gavin Makea2e3302023-03-11 06:46:20 +00003755 all_refs = self.bare_ref.all
3756 revid = self.GetRevisionId(all_refs)
3757 head = self.work_git.GetHead()
3758 if head.startswith(R_HEADS):
3759 try:
3760 head = all_refs[head]
3761 except KeyError:
3762 head = None
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003763
Gavin Makea2e3302023-03-11 06:46:20 +00003764 if revid == head:
3765 return False
3766 elif self._revlist(not_rev(HEAD), revid):
3767 return True
3768 return False
LaMont Jones9b72cf22022-03-29 21:54:22 +00003769
3770
3771class RepoProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003772 """The MetaProject for repo itself."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003773
Gavin Makea2e3302023-03-11 06:46:20 +00003774 @property
3775 def LastFetch(self):
3776 try:
3777 fh = os.path.join(self.gitdir, "FETCH_HEAD")
3778 return os.path.getmtime(fh)
3779 except OSError:
3780 return 0
LaMont Jones9b72cf22022-03-29 21:54:22 +00003781
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +01003782
LaMont Jones9b72cf22022-03-29 21:54:22 +00003783class ManifestProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003784 """The MetaProject for manifests."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003785
Gavin Makea2e3302023-03-11 06:46:20 +00003786 def MetaBranchSwitch(self, submodules=False):
3787 """Prepare for manifest branch switch."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003788
Gavin Makea2e3302023-03-11 06:46:20 +00003789 # detach and delete manifest branch, allowing a new
3790 # branch to take over
3791 syncbuf = SyncBuffer(self.config, detach_head=True)
3792 self.Sync_LocalHalf(syncbuf, submodules=submodules)
3793 syncbuf.Finish()
LaMont Jones9b72cf22022-03-29 21:54:22 +00003794
Gavin Makea2e3302023-03-11 06:46:20 +00003795 return (
3796 GitCommand(
3797 self,
3798 ["update-ref", "-d", "refs/heads/default"],
3799 capture_stdout=True,
3800 capture_stderr=True,
3801 ).Wait()
3802 == 0
3803 )
LaMont Jones9b03f152022-03-29 23:01:18 +00003804
Gavin Makea2e3302023-03-11 06:46:20 +00003805 @property
3806 def standalone_manifest_url(self):
3807 """The URL of the standalone manifest, or None."""
3808 return self.config.GetString("manifest.standalone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003809
Gavin Makea2e3302023-03-11 06:46:20 +00003810 @property
3811 def manifest_groups(self):
3812 """The manifest groups string."""
3813 return self.config.GetString("manifest.groups")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003814
Gavin Makea2e3302023-03-11 06:46:20 +00003815 @property
3816 def reference(self):
3817 """The --reference for this manifest."""
3818 return self.config.GetString("repo.reference")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003819
Gavin Makea2e3302023-03-11 06:46:20 +00003820 @property
3821 def dissociate(self):
3822 """Whether to dissociate."""
3823 return self.config.GetBoolean("repo.dissociate")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003824
Gavin Makea2e3302023-03-11 06:46:20 +00003825 @property
3826 def archive(self):
3827 """Whether we use archive."""
3828 return self.config.GetBoolean("repo.archive")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003829
Gavin Makea2e3302023-03-11 06:46:20 +00003830 @property
3831 def mirror(self):
3832 """Whether we use mirror."""
3833 return self.config.GetBoolean("repo.mirror")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003834
Gavin Makea2e3302023-03-11 06:46:20 +00003835 @property
3836 def use_worktree(self):
3837 """Whether we use worktree."""
3838 return self.config.GetBoolean("repo.worktree")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003839
Gavin Makea2e3302023-03-11 06:46:20 +00003840 @property
3841 def clone_bundle(self):
3842 """Whether we use clone_bundle."""
3843 return self.config.GetBoolean("repo.clonebundle")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003844
Gavin Makea2e3302023-03-11 06:46:20 +00003845 @property
3846 def submodules(self):
3847 """Whether we use submodules."""
3848 return self.config.GetBoolean("repo.submodules")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003849
Gavin Makea2e3302023-03-11 06:46:20 +00003850 @property
3851 def git_lfs(self):
3852 """Whether we use git_lfs."""
3853 return self.config.GetBoolean("repo.git-lfs")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003854
Gavin Makea2e3302023-03-11 06:46:20 +00003855 @property
3856 def use_superproject(self):
3857 """Whether we use superproject."""
3858 return self.config.GetBoolean("repo.superproject")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003859
Gavin Makea2e3302023-03-11 06:46:20 +00003860 @property
3861 def partial_clone(self):
3862 """Whether this is a partial clone."""
3863 return self.config.GetBoolean("repo.partialclone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003864
Gavin Makea2e3302023-03-11 06:46:20 +00003865 @property
3866 def depth(self):
3867 """Partial clone depth."""
3868 return self.config.GetString("repo.depth")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003869
Gavin Makea2e3302023-03-11 06:46:20 +00003870 @property
3871 def clone_filter(self):
3872 """The clone filter."""
3873 return self.config.GetString("repo.clonefilter")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003874
Gavin Makea2e3302023-03-11 06:46:20 +00003875 @property
3876 def partial_clone_exclude(self):
3877 """Partial clone exclude string"""
3878 return self.config.GetString("repo.partialcloneexclude")
LaMont Jones4ada0432022-04-14 15:10:43 +00003879
Gavin Makea2e3302023-03-11 06:46:20 +00003880 @property
3881 def manifest_platform(self):
3882 """The --platform argument from `repo init`."""
3883 return self.config.GetString("manifest.platform")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003884
Gavin Makea2e3302023-03-11 06:46:20 +00003885 @property
3886 def _platform_name(self):
3887 """Return the name of the platform."""
3888 return platform.system().lower()
LaMont Jones9b03f152022-03-29 23:01:18 +00003889
Gavin Makea2e3302023-03-11 06:46:20 +00003890 def SyncWithPossibleInit(
3891 self,
3892 submanifest,
3893 verbose=False,
3894 current_branch_only=False,
3895 tags="",
3896 git_event_log=None,
3897 ):
3898 """Sync a manifestProject, possibly for the first time.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00003899
Gavin Makea2e3302023-03-11 06:46:20 +00003900 Call Sync() with arguments from the most recent `repo init`. If this is
3901 a new sub manifest, then inherit options from the parent's
3902 manifestProject.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00003903
Gavin Makea2e3302023-03-11 06:46:20 +00003904 This is used by subcmds.Sync() to do an initial download of new sub
3905 manifests.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00003906
Gavin Makea2e3302023-03-11 06:46:20 +00003907 Args:
3908 submanifest: an XmlSubmanifest, the submanifest to re-sync.
3909 verbose: a boolean, whether to show all output, rather than only
3910 errors.
3911 current_branch_only: a boolean, whether to only fetch the current
3912 manifest branch from the server.
3913 tags: a boolean, whether to fetch tags.
3914 git_event_log: an EventLog, for git tracing.
3915 """
3916 # TODO(lamontjones): when refactoring sync (and init?) consider how to
3917 # better get the init options that we should use for new submanifests
3918 # that are added when syncing an existing workspace.
3919 git_event_log = git_event_log or EventLog()
LaMont Jonesb90a4222022-04-14 15:00:09 +00003920 spec = submanifest.ToSubmanifestSpec()
Gavin Makea2e3302023-03-11 06:46:20 +00003921 # Use the init options from the existing manifestProject, or the parent
3922 # if it doesn't exist.
3923 #
3924 # Today, we only support changing manifest_groups on the sub-manifest,
3925 # with no supported-for-the-user way to change the other arguments from
3926 # those specified by the outermost manifest.
3927 #
3928 # TODO(lamontjones): determine which of these should come from the
3929 # outermost manifest and which should come from the parent manifest.
3930 mp = self if self.Exists else submanifest.parent.manifestProject
3931 return self.Sync(
LaMont Jones55ee3042022-04-06 17:10:21 +00003932 manifest_url=spec.manifestUrl,
3933 manifest_branch=spec.revision,
Gavin Makea2e3302023-03-11 06:46:20 +00003934 standalone_manifest=mp.standalone_manifest_url,
3935 groups=mp.manifest_groups,
3936 platform=mp.manifest_platform,
3937 mirror=mp.mirror,
3938 dissociate=mp.dissociate,
3939 reference=mp.reference,
3940 worktree=mp.use_worktree,
3941 submodules=mp.submodules,
3942 archive=mp.archive,
3943 partial_clone=mp.partial_clone,
3944 clone_filter=mp.clone_filter,
3945 partial_clone_exclude=mp.partial_clone_exclude,
3946 clone_bundle=mp.clone_bundle,
3947 git_lfs=mp.git_lfs,
3948 use_superproject=mp.use_superproject,
LaMont Jones55ee3042022-04-06 17:10:21 +00003949 verbose=verbose,
3950 current_branch_only=current_branch_only,
3951 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00003952 depth=mp.depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00003953 git_event_log=git_event_log,
3954 manifest_name=spec.manifestName,
Gavin Makea2e3302023-03-11 06:46:20 +00003955 this_manifest_only=True,
LaMont Jones55ee3042022-04-06 17:10:21 +00003956 outer_manifest=False,
3957 )
LaMont Jones409407a2022-04-05 21:21:56 +00003958
Gavin Makea2e3302023-03-11 06:46:20 +00003959 def Sync(
3960 self,
3961 _kwargs_only=(),
3962 manifest_url="",
3963 manifest_branch=None,
3964 standalone_manifest=False,
3965 groups="",
3966 mirror=False,
3967 reference="",
3968 dissociate=False,
3969 worktree=False,
3970 submodules=False,
3971 archive=False,
3972 partial_clone=None,
3973 depth=None,
3974 clone_filter="blob:none",
3975 partial_clone_exclude=None,
3976 clone_bundle=None,
3977 git_lfs=None,
3978 use_superproject=None,
3979 verbose=False,
3980 current_branch_only=False,
3981 git_event_log=None,
3982 platform="",
3983 manifest_name="default.xml",
3984 tags="",
3985 this_manifest_only=False,
3986 outer_manifest=True,
3987 ):
3988 """Sync the manifest and all submanifests.
LaMont Jones409407a2022-04-05 21:21:56 +00003989
Gavin Makea2e3302023-03-11 06:46:20 +00003990 Args:
3991 manifest_url: a string, the URL of the manifest project.
3992 manifest_branch: a string, the manifest branch to use.
3993 standalone_manifest: a boolean, whether to store the manifest as a
3994 static file.
3995 groups: a string, restricts the checkout to projects with the
3996 specified groups.
3997 mirror: a boolean, whether to create a mirror of the remote
3998 repository.
3999 reference: a string, location of a repo instance to use as a
4000 reference.
4001 dissociate: a boolean, whether to dissociate from reference mirrors
4002 after clone.
4003 worktree: a boolean, whether to use git-worktree to manage projects.
4004 submodules: a boolean, whether sync submodules associated with the
4005 manifest project.
4006 archive: a boolean, whether to checkout each project as an archive.
4007 See git-archive.
4008 partial_clone: a boolean, whether to perform a partial clone.
4009 depth: an int, how deep of a shallow clone to create.
4010 clone_filter: a string, filter to use with partial_clone.
4011 partial_clone_exclude : a string, comma-delimeted list of project
4012 names to exclude from partial clone.
4013 clone_bundle: a boolean, whether to enable /clone.bundle on
4014 HTTP/HTTPS.
4015 git_lfs: a boolean, whether to enable git LFS support.
4016 use_superproject: a boolean, whether to use the manifest
4017 superproject to sync projects.
4018 verbose: a boolean, whether to show all output, rather than only
4019 errors.
4020 current_branch_only: a boolean, whether to only fetch the current
4021 manifest branch from the server.
4022 platform: a string, restrict the checkout to projects with the
4023 specified platform group.
4024 git_event_log: an EventLog, for git tracing.
4025 tags: a boolean, whether to fetch tags.
4026 manifest_name: a string, the name of the manifest file to use.
4027 this_manifest_only: a boolean, whether to only operate on the
4028 current sub manifest.
4029 outer_manifest: a boolean, whether to start at the outermost
4030 manifest.
LaMont Jones9b03f152022-03-29 23:01:18 +00004031
Gavin Makea2e3302023-03-11 06:46:20 +00004032 Returns:
4033 a boolean, whether the sync was successful.
4034 """
4035 assert _kwargs_only == (), "Sync only accepts keyword arguments."
LaMont Jones9b03f152022-03-29 23:01:18 +00004036
Gavin Makea2e3302023-03-11 06:46:20 +00004037 groups = groups or self.manifest.GetDefaultGroupsStr(
4038 with_platform=False
4039 )
4040 platform = platform or "auto"
4041 git_event_log = git_event_log or EventLog()
4042 if outer_manifest and self.manifest.is_submanifest:
4043 # In a multi-manifest checkout, use the outer manifest unless we are
4044 # told not to.
4045 return self.client.outer_manifest.manifestProject.Sync(
4046 manifest_url=manifest_url,
4047 manifest_branch=manifest_branch,
4048 standalone_manifest=standalone_manifest,
4049 groups=groups,
4050 platform=platform,
4051 mirror=mirror,
4052 dissociate=dissociate,
4053 reference=reference,
4054 worktree=worktree,
4055 submodules=submodules,
4056 archive=archive,
4057 partial_clone=partial_clone,
4058 clone_filter=clone_filter,
4059 partial_clone_exclude=partial_clone_exclude,
4060 clone_bundle=clone_bundle,
4061 git_lfs=git_lfs,
4062 use_superproject=use_superproject,
4063 verbose=verbose,
4064 current_branch_only=current_branch_only,
4065 tags=tags,
4066 depth=depth,
4067 git_event_log=git_event_log,
4068 manifest_name=manifest_name,
4069 this_manifest_only=this_manifest_only,
4070 outer_manifest=False,
4071 )
LaMont Jones9b03f152022-03-29 23:01:18 +00004072
Gavin Makea2e3302023-03-11 06:46:20 +00004073 # If repo has already been initialized, we take -u with the absence of
4074 # --standalone-manifest to mean "transition to a standard repo set up",
4075 # which necessitates starting fresh.
4076 # If --standalone-manifest is set, we always tear everything down and
4077 # start anew.
4078 if self.Exists:
4079 was_standalone_manifest = self.config.GetString(
4080 "manifest.standalone"
4081 )
4082 if was_standalone_manifest and not manifest_url:
4083 print(
4084 "fatal: repo was initialized with a standlone manifest, "
4085 "cannot be re-initialized without --manifest-url/-u"
4086 )
4087 return False
4088
4089 if standalone_manifest or (
4090 was_standalone_manifest and manifest_url
4091 ):
4092 self.config.ClearCache()
4093 if self.gitdir and os.path.exists(self.gitdir):
4094 platform_utils.rmtree(self.gitdir)
4095 if self.worktree and os.path.exists(self.worktree):
4096 platform_utils.rmtree(self.worktree)
4097
4098 is_new = not self.Exists
4099 if is_new:
4100 if not manifest_url:
4101 print("fatal: manifest url is required.", file=sys.stderr)
4102 return False
4103
4104 if verbose:
4105 print(
4106 "Downloading manifest from %s"
4107 % (GitConfig.ForUser().UrlInsteadOf(manifest_url),),
4108 file=sys.stderr,
4109 )
4110
4111 # The manifest project object doesn't keep track of the path on the
4112 # server where this git is located, so let's save that here.
4113 mirrored_manifest_git = None
4114 if reference:
4115 manifest_git_path = urllib.parse.urlparse(manifest_url).path[1:]
4116 mirrored_manifest_git = os.path.join(
4117 reference, manifest_git_path
4118 )
4119 if not mirrored_manifest_git.endswith(".git"):
4120 mirrored_manifest_git += ".git"
4121 if not os.path.exists(mirrored_manifest_git):
4122 mirrored_manifest_git = os.path.join(
4123 reference, ".repo/manifests.git"
4124 )
4125
4126 self._InitGitDir(mirror_git=mirrored_manifest_git)
4127
4128 # If standalone_manifest is set, mark the project as "standalone" --
4129 # we'll still do much of the manifests.git set up, but will avoid actual
4130 # syncs to a remote.
4131 if standalone_manifest:
4132 self.config.SetString("manifest.standalone", manifest_url)
4133 elif not manifest_url and not manifest_branch:
4134 # If -u is set and --standalone-manifest is not, then we're not in
4135 # standalone mode. Otherwise, use config to infer what we were in
4136 # the last init.
4137 standalone_manifest = bool(
4138 self.config.GetString("manifest.standalone")
4139 )
4140 if not standalone_manifest:
4141 self.config.SetString("manifest.standalone", None)
4142
4143 self._ConfigureDepth(depth)
4144
4145 # Set the remote URL before the remote branch as we might need it below.
4146 if manifest_url:
4147 r = self.GetRemote()
4148 r.url = manifest_url
4149 r.ResetFetch()
4150 r.Save()
4151
4152 if not standalone_manifest:
4153 if manifest_branch:
4154 if manifest_branch == "HEAD":
4155 manifest_branch = self.ResolveRemoteHead()
4156 if manifest_branch is None:
4157 print("fatal: unable to resolve HEAD", file=sys.stderr)
4158 return False
4159 self.revisionExpr = manifest_branch
4160 else:
4161 if is_new:
4162 default_branch = self.ResolveRemoteHead()
4163 if default_branch is None:
4164 # If the remote doesn't have HEAD configured, default to
4165 # master.
4166 default_branch = "refs/heads/master"
4167 self.revisionExpr = default_branch
4168 else:
4169 self.PreSync()
4170
4171 groups = re.split(r"[,\s]+", groups or "")
4172 all_platforms = ["linux", "darwin", "windows"]
4173 platformize = lambda x: "platform-" + x
4174 if platform == "auto":
4175 if not mirror and not self.mirror:
4176 groups.append(platformize(self._platform_name))
4177 elif platform == "all":
4178 groups.extend(map(platformize, all_platforms))
4179 elif platform in all_platforms:
4180 groups.append(platformize(platform))
4181 elif platform != "none":
4182 print("fatal: invalid platform flag", file=sys.stderr)
4183 return False
4184 self.config.SetString("manifest.platform", platform)
4185
4186 groups = [x for x in groups if x]
4187 groupstr = ",".join(groups)
4188 if (
4189 platform == "auto"
4190 and groupstr == self.manifest.GetDefaultGroupsStr()
4191 ):
4192 groupstr = None
4193 self.config.SetString("manifest.groups", groupstr)
4194
4195 if reference:
4196 self.config.SetString("repo.reference", reference)
4197
4198 if dissociate:
4199 self.config.SetBoolean("repo.dissociate", dissociate)
4200
4201 if worktree:
4202 if mirror:
4203 print(
4204 "fatal: --mirror and --worktree are incompatible",
4205 file=sys.stderr,
4206 )
4207 return False
4208 if submodules:
4209 print(
4210 "fatal: --submodules and --worktree are incompatible",
4211 file=sys.stderr,
4212 )
4213 return False
4214 self.config.SetBoolean("repo.worktree", worktree)
4215 if is_new:
4216 self.use_git_worktrees = True
4217 print("warning: --worktree is experimental!", file=sys.stderr)
4218
4219 if archive:
4220 if is_new:
4221 self.config.SetBoolean("repo.archive", archive)
4222 else:
4223 print(
4224 "fatal: --archive is only supported when initializing a "
4225 "new workspace.",
4226 file=sys.stderr,
4227 )
4228 print(
4229 "Either delete the .repo folder in this workspace, or "
4230 "initialize in another location.",
4231 file=sys.stderr,
4232 )
4233 return False
4234
4235 if mirror:
4236 if is_new:
4237 self.config.SetBoolean("repo.mirror", mirror)
4238 else:
4239 print(
4240 "fatal: --mirror is only supported when initializing a new "
4241 "workspace.",
4242 file=sys.stderr,
4243 )
4244 print(
4245 "Either delete the .repo folder in this workspace, or "
4246 "initialize in another location.",
4247 file=sys.stderr,
4248 )
4249 return False
4250
4251 if partial_clone is not None:
4252 if mirror:
4253 print(
4254 "fatal: --mirror and --partial-clone are mutually "
4255 "exclusive",
4256 file=sys.stderr,
4257 )
4258 return False
4259 self.config.SetBoolean("repo.partialclone", partial_clone)
4260 if clone_filter:
4261 self.config.SetString("repo.clonefilter", clone_filter)
4262 elif self.partial_clone:
4263 clone_filter = self.clone_filter
4264 else:
4265 clone_filter = None
4266
4267 if partial_clone_exclude is not None:
4268 self.config.SetString(
4269 "repo.partialcloneexclude", partial_clone_exclude
4270 )
4271
4272 if clone_bundle is None:
4273 clone_bundle = False if partial_clone else True
4274 else:
4275 self.config.SetBoolean("repo.clonebundle", clone_bundle)
4276
4277 if submodules:
4278 self.config.SetBoolean("repo.submodules", submodules)
4279
4280 if git_lfs is not None:
4281 if git_lfs:
4282 git_require((2, 17, 0), fail=True, msg="Git LFS support")
4283
4284 self.config.SetBoolean("repo.git-lfs", git_lfs)
4285 if not is_new:
4286 print(
4287 "warning: Changing --git-lfs settings will only affect new "
4288 "project checkouts.\n"
4289 " Existing projects will require manual updates.\n",
4290 file=sys.stderr,
4291 )
4292
4293 if use_superproject is not None:
4294 self.config.SetBoolean("repo.superproject", use_superproject)
4295
4296 if not standalone_manifest:
4297 success = self.Sync_NetworkHalf(
4298 is_new=is_new,
4299 quiet=not verbose,
4300 verbose=verbose,
4301 clone_bundle=clone_bundle,
4302 current_branch_only=current_branch_only,
4303 tags=tags,
4304 submodules=submodules,
4305 clone_filter=clone_filter,
4306 partial_clone_exclude=self.manifest.PartialCloneExclude,
4307 ).success
4308 if not success:
4309 r = self.GetRemote()
4310 print(
4311 "fatal: cannot obtain manifest %s" % r.url, file=sys.stderr
4312 )
4313
4314 # Better delete the manifest git dir if we created it; otherwise
4315 # next time (when user fixes problems) we won't go through the
4316 # "is_new" logic.
4317 if is_new:
4318 platform_utils.rmtree(self.gitdir)
4319 return False
4320
4321 if manifest_branch:
4322 self.MetaBranchSwitch(submodules=submodules)
4323
4324 syncbuf = SyncBuffer(self.config)
4325 self.Sync_LocalHalf(syncbuf, submodules=submodules)
4326 syncbuf.Finish()
4327
4328 if is_new or self.CurrentBranch is None:
4329 if not self.StartBranch("default"):
4330 print(
4331 "fatal: cannot create default in manifest",
4332 file=sys.stderr,
4333 )
4334 return False
4335
4336 if not manifest_name:
4337 print("fatal: manifest name (-m) is required.", file=sys.stderr)
4338 return False
4339
4340 elif is_new:
4341 # This is a new standalone manifest.
4342 manifest_name = "default.xml"
4343 manifest_data = fetch.fetch_file(manifest_url, verbose=verbose)
4344 dest = os.path.join(self.worktree, manifest_name)
4345 os.makedirs(os.path.dirname(dest), exist_ok=True)
4346 with open(dest, "wb") as f:
4347 f.write(manifest_data)
4348
4349 try:
4350 self.manifest.Link(manifest_name)
4351 except ManifestParseError as e:
4352 print(
4353 "fatal: manifest '%s' not available" % manifest_name,
4354 file=sys.stderr,
4355 )
4356 print("fatal: %s" % str(e), file=sys.stderr)
4357 return False
4358
4359 if not this_manifest_only:
4360 for submanifest in self.manifest.submanifests.values():
4361 spec = submanifest.ToSubmanifestSpec()
4362 submanifest.repo_client.manifestProject.Sync(
4363 manifest_url=spec.manifestUrl,
4364 manifest_branch=spec.revision,
4365 standalone_manifest=standalone_manifest,
4366 groups=self.manifest_groups,
4367 platform=platform,
4368 mirror=mirror,
4369 dissociate=dissociate,
4370 reference=reference,
4371 worktree=worktree,
4372 submodules=submodules,
4373 archive=archive,
4374 partial_clone=partial_clone,
4375 clone_filter=clone_filter,
4376 partial_clone_exclude=partial_clone_exclude,
4377 clone_bundle=clone_bundle,
4378 git_lfs=git_lfs,
4379 use_superproject=use_superproject,
4380 verbose=verbose,
4381 current_branch_only=current_branch_only,
4382 tags=tags,
4383 depth=depth,
4384 git_event_log=git_event_log,
4385 manifest_name=spec.manifestName,
4386 this_manifest_only=False,
4387 outer_manifest=False,
4388 )
4389
4390 # Lastly, if the manifest has a <superproject> then have the
4391 # superproject sync it (if it will be used).
4392 if git_superproject.UseSuperproject(use_superproject, self.manifest):
4393 sync_result = self.manifest.superproject.Sync(git_event_log)
4394 if not sync_result.success:
4395 submanifest = ""
4396 if self.manifest.path_prefix:
4397 submanifest = f"for {self.manifest.path_prefix} "
4398 print(
4399 f"warning: git update of superproject {submanifest}failed, "
4400 "repo sync will not use superproject to fetch source; "
4401 "while this error is not fatal, and you can continue to "
4402 "run repo sync, please run repo init with the "
4403 "--no-use-superproject option to stop seeing this warning",
4404 file=sys.stderr,
4405 )
4406 if sync_result.fatal and use_superproject is not None:
4407 return False
4408
4409 return True
4410
4411 def _ConfigureDepth(self, depth):
4412 """Configure the depth we'll sync down.
4413
4414 Args:
4415 depth: an int, how deep of a partial clone to create.
4416 """
4417 # Opt.depth will be non-None if user actually passed --depth to repo
4418 # init.
4419 if depth is not None:
4420 if depth > 0:
4421 # Positive values will set the depth.
4422 depth = str(depth)
4423 else:
4424 # Negative numbers will clear the depth; passing None to
4425 # SetString will do that.
4426 depth = None
4427
4428 # We store the depth in the main manifest project.
4429 self.config.SetString("repo.depth", depth)