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