blob: 80169daff43accfc81495fd4eda9e5adae5bf042 [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,
Gavin Mak4feff3b2023-05-16 21:31:10 +000041 RefSpec,
Gavin Makea2e3302023-03-11 06:46:20 +000042)
LaMont Jonesff6b1da2022-06-01 21:03:34 +000043import git_superproject
LaMont Jones55ee3042022-04-06 17:10:21 +000044from git_trace2_event_log import EventLog
Remy Bohmer16c13282020-09-10 10:38:04 +020045from error import GitError, UploadError, DownloadError
Ningning Xiac2fbc782016-08-22 14:24:39 -070046from error import CacheApplyError
Mike Frysingere6a202f2019-08-02 15:57:57 -040047from error import ManifestInvalidRevisionError, ManifestInvalidPathError
LaMont Jones409407a2022-04-05 21:21:56 +000048from error import NoManifestException, ManifestParseError
Renaud Paquayd5cec5e2016-11-01 11:24:03 -070049import platform_utils
Mike Frysinger70d861f2019-08-26 15:22:36 -040050import progress
Joanna Wanga6c52f52022-11-03 16:51:19 -040051from repo_trace import Trace
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070052
Mike Frysinger21b7fbe2020-02-26 23:53:36 -050053from 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 -070054
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -070055
LaMont Jones1eddca82022-09-01 15:15:04 +000056class SyncNetworkHalfResult(NamedTuple):
Gavin Makea2e3302023-03-11 06:46:20 +000057 """Sync_NetworkHalf return value."""
58
59 # True if successful.
60 success: bool
61 # Did we query the remote? False when optimized_fetch is True and we have
62 # the commit already present.
63 remote_fetched: bool
LaMont Jones1eddca82022-09-01 15:15:04 +000064
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +010065
George Engelbrecht9bc283e2020-04-02 12:36:09 -060066# Maximum sleep time allowed during retries.
67MAXIMUM_RETRY_SLEEP_SEC = 3600.0
68# +-10% random jitter is added to each Fetches retry sleep duration.
69RETRY_JITTER_PERCENT = 0.1
70
LaMont Jonesfa8d9392022-11-02 22:01:29 +000071# Whether to use alternates. Switching back and forth is *NOT* supported.
Mike Frysinger1d00a7e2021-12-21 00:40:31 -050072# TODO(vapier): Remove knob once behavior is verified.
Gavin Makea2e3302023-03-11 06:46:20 +000073_ALTERNATES = os.environ.get("REPO_USE_ALTERNATES") == "1"
George Engelbrecht9bc283e2020-04-02 12:36:09 -060074
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +010075
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -070076def _lwrite(path, content):
Gavin Makea2e3302023-03-11 06:46:20 +000077 lock = "%s.lock" % path
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -070078
Gavin Makea2e3302023-03-11 06:46:20 +000079 # Maintain Unix line endings on all OS's to match git behavior.
80 with open(lock, "w", newline="\n") as fd:
81 fd.write(content)
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -070082
Gavin Makea2e3302023-03-11 06:46:20 +000083 try:
84 platform_utils.rename(lock, path)
85 except OSError:
86 platform_utils.remove(lock)
87 raise
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -070088
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -070089
Shawn O. Pearce48244782009-04-16 08:25:57 -070090def _error(fmt, *args):
Gavin Makea2e3302023-03-11 06:46:20 +000091 msg = fmt % args
92 print("error: %s" % msg, file=sys.stderr)
Shawn O. Pearce48244782009-04-16 08:25:57 -070093
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -070094
David Pursehousef33929d2015-08-24 14:39:14 +090095def _warn(fmt, *args):
Gavin Makea2e3302023-03-11 06:46:20 +000096 msg = fmt % args
97 print("warn: %s" % msg, file=sys.stderr)
David Pursehousef33929d2015-08-24 14:39:14 +090098
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -070099
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700100def not_rev(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000101 return "^" + r
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700102
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700103
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800104def sq(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000105 return "'" + r.replace("'", "'''") + "'"
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -0800106
David Pursehouse819827a2020-02-12 15:20:19 +0900107
Jonathan Nieder93719792015-03-17 11:29:58 -0700108_project_hook_list = None
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700109
110
Jonathan Nieder93719792015-03-17 11:29:58 -0700111def _ProjectHooks():
Gavin Makea2e3302023-03-11 06:46:20 +0000112 """List the hooks present in the 'hooks' directory.
Jonathan Nieder93719792015-03-17 11:29:58 -0700113
Gavin Makea2e3302023-03-11 06:46:20 +0000114 These hooks are project hooks and are copied to the '.git/hooks' directory
115 of all subprojects.
Jonathan Nieder93719792015-03-17 11:29:58 -0700116
Gavin Makea2e3302023-03-11 06:46:20 +0000117 This function caches the list of hooks (based on the contents of the
118 'repo/hooks' directory) on the first call.
Jonathan Nieder93719792015-03-17 11:29:58 -0700119
Gavin Makea2e3302023-03-11 06:46:20 +0000120 Returns:
121 A list of absolute paths to all of the files in the hooks directory.
122 """
123 global _project_hook_list
124 if _project_hook_list is None:
125 d = platform_utils.realpath(os.path.abspath(os.path.dirname(__file__)))
126 d = os.path.join(d, "hooks")
127 _project_hook_list = [
128 os.path.join(d, x) for x in platform_utils.listdir(d)
129 ]
130 return _project_hook_list
Jonathan Nieder93719792015-03-17 11:29:58 -0700131
132
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700133class DownloadedChange(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000134 _commit_cache = None
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700135
Gavin Makea2e3302023-03-11 06:46:20 +0000136 def __init__(self, project, base, change_id, ps_id, commit):
137 self.project = project
138 self.base = base
139 self.change_id = change_id
140 self.ps_id = ps_id
141 self.commit = commit
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700142
Gavin Makea2e3302023-03-11 06:46:20 +0000143 @property
144 def commits(self):
145 if self._commit_cache is None:
146 self._commit_cache = self.project.bare_git.rev_list(
147 "--abbrev=8",
148 "--abbrev-commit",
149 "--pretty=oneline",
150 "--reverse",
151 "--date-order",
152 not_rev(self.base),
153 self.commit,
154 "--",
155 )
156 return self._commit_cache
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700157
158
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700159class ReviewableBranch(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000160 _commit_cache = None
161 _base_exists = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700162
Gavin Makea2e3302023-03-11 06:46:20 +0000163 def __init__(self, project, branch, base):
164 self.project = project
165 self.branch = branch
166 self.base = base
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700167
Gavin Makea2e3302023-03-11 06:46:20 +0000168 @property
169 def name(self):
170 return self.branch.name
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700171
Gavin Makea2e3302023-03-11 06:46:20 +0000172 @property
173 def commits(self):
174 if self._commit_cache is None:
175 args = (
176 "--abbrev=8",
177 "--abbrev-commit",
178 "--pretty=oneline",
179 "--reverse",
180 "--date-order",
181 not_rev(self.base),
182 R_HEADS + self.name,
183 "--",
184 )
185 try:
186 self._commit_cache = self.project.bare_git.rev_list(*args)
187 except GitError:
188 # We weren't able to probe the commits for this branch. Was it
189 # tracking a branch that no longer exists? If so, return no
190 # commits. Otherwise, rethrow the error as we don't know what's
191 # going on.
192 if self.base_exists:
193 raise
Mike Frysinger6da17752019-09-11 18:43:17 -0400194
Gavin Makea2e3302023-03-11 06:46:20 +0000195 self._commit_cache = []
Mike Frysinger6da17752019-09-11 18:43:17 -0400196
Gavin Makea2e3302023-03-11 06:46:20 +0000197 return self._commit_cache
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700198
Gavin Makea2e3302023-03-11 06:46:20 +0000199 @property
200 def unabbrev_commits(self):
201 r = dict()
202 for commit in self.project.bare_git.rev_list(
203 not_rev(self.base), R_HEADS + self.name, "--"
204 ):
205 r[commit[0:8]] = commit
206 return r
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800207
Gavin Makea2e3302023-03-11 06:46:20 +0000208 @property
209 def date(self):
210 return self.project.bare_git.log(
211 "--pretty=format:%cd", "-n", "1", R_HEADS + self.name, "--"
212 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700213
Gavin Makea2e3302023-03-11 06:46:20 +0000214 @property
215 def base_exists(self):
216 """Whether the branch we're tracking exists.
Mike Frysinger6da17752019-09-11 18:43:17 -0400217
Gavin Makea2e3302023-03-11 06:46:20 +0000218 Normally it should, but sometimes branches we track can get deleted.
219 """
220 if self._base_exists is None:
221 try:
222 self.project.bare_git.rev_parse("--verify", not_rev(self.base))
223 # If we're still here, the base branch exists.
224 self._base_exists = True
225 except GitError:
226 # If we failed to verify, the base branch doesn't exist.
227 self._base_exists = False
Mike Frysinger6da17752019-09-11 18:43:17 -0400228
Gavin Makea2e3302023-03-11 06:46:20 +0000229 return self._base_exists
Mike Frysinger6da17752019-09-11 18:43:17 -0400230
Gavin Makea2e3302023-03-11 06:46:20 +0000231 def UploadForReview(
232 self,
233 people,
234 dryrun=False,
235 auto_topic=False,
236 hashtags=(),
237 labels=(),
238 private=False,
239 notify=None,
240 wip=False,
241 ready=False,
242 dest_branch=None,
243 validate_certs=True,
244 push_options=None,
245 ):
246 self.project.UploadForReview(
247 branch=self.name,
248 people=people,
249 dryrun=dryrun,
250 auto_topic=auto_topic,
251 hashtags=hashtags,
252 labels=labels,
253 private=private,
254 notify=notify,
255 wip=wip,
256 ready=ready,
257 dest_branch=dest_branch,
258 validate_certs=validate_certs,
259 push_options=push_options,
260 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700261
Gavin Makea2e3302023-03-11 06:46:20 +0000262 def GetPublishedRefs(self):
263 refs = {}
264 output = self.project.bare_git.ls_remote(
265 self.branch.remote.SshReviewUrl(self.project.UserEmail),
266 "refs/changes/*",
267 )
268 for line in output.split("\n"):
269 try:
270 (sha, ref) = line.split()
271 refs[sha] = ref
272 except ValueError:
273 pass
Ficus Kirkpatrickbc7ef672009-05-04 12:45:11 -0700274
Gavin Makea2e3302023-03-11 06:46:20 +0000275 return refs
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700276
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700277
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700278class StatusColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000279 def __init__(self, config):
280 super().__init__(config, "status")
281 self.project = self.printer("header", attr="bold")
282 self.branch = self.printer("header", attr="bold")
283 self.nobranch = self.printer("nobranch", fg="red")
284 self.important = self.printer("important", fg="red")
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700285
Gavin Makea2e3302023-03-11 06:46:20 +0000286 self.added = self.printer("added", fg="green")
287 self.changed = self.printer("changed", fg="red")
288 self.untracked = self.printer("untracked", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700289
290
291class DiffColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000292 def __init__(self, config):
293 super().__init__(config, "diff")
294 self.project = self.printer("header", attr="bold")
295 self.fail = self.printer("fail", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700296
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700297
Jack Neus6ea0cae2021-07-20 20:52:33 +0000298class Annotation(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000299 def __init__(self, name, value, keep):
300 self.name = name
301 self.value = value
302 self.keep = keep
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700303
Gavin Makea2e3302023-03-11 06:46:20 +0000304 def __eq__(self, other):
305 if not isinstance(other, Annotation):
306 return False
307 return self.__dict__ == other.__dict__
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700308
Gavin Makea2e3302023-03-11 06:46:20 +0000309 def __lt__(self, other):
310 # This exists just so that lists of Annotation objects can be sorted,
311 # for use in comparisons.
312 if not isinstance(other, Annotation):
313 raise ValueError("comparison is not between two Annotation objects")
314 if self.name == other.name:
315 if self.value == other.value:
316 return self.keep < other.keep
317 return self.value < other.value
318 return self.name < other.name
Jack Neus6ea0cae2021-07-20 20:52:33 +0000319
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700320
Mike Frysingere6a202f2019-08-02 15:57:57 -0400321def _SafeExpandPath(base, subpath, skipfinal=False):
Gavin Makea2e3302023-03-11 06:46:20 +0000322 """Make sure |subpath| is completely safe under |base|.
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700323
Gavin Makea2e3302023-03-11 06:46:20 +0000324 We make sure no intermediate symlinks are traversed, and that the final path
325 is not a special file (e.g. not a socket or fifo).
Mike Frysingere6a202f2019-08-02 15:57:57 -0400326
Gavin Makea2e3302023-03-11 06:46:20 +0000327 NB: We rely on a number of paths already being filtered out while parsing
328 the manifest. See the validation logic in manifest_xml.py for more details.
329 """
330 # Split up the path by its components. We can't use os.path.sep exclusively
331 # as some platforms (like Windows) will convert / to \ and that bypasses all
332 # our constructed logic here. Especially since manifest authors only use
333 # / in their paths.
334 resep = re.compile(r"[/%s]" % re.escape(os.path.sep))
335 components = resep.split(subpath)
336 if skipfinal:
337 # Whether the caller handles the final component itself.
338 finalpart = components.pop()
Mike Frysingere6a202f2019-08-02 15:57:57 -0400339
Gavin Makea2e3302023-03-11 06:46:20 +0000340 path = base
341 for part in components:
342 if part in {".", ".."}:
343 raise ManifestInvalidPathError(
344 '%s: "%s" not allowed in paths' % (subpath, part)
345 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400346
Gavin Makea2e3302023-03-11 06:46:20 +0000347 path = os.path.join(path, part)
348 if platform_utils.islink(path):
349 raise ManifestInvalidPathError(
350 "%s: traversing symlinks not allow" % (path,)
351 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400352
Gavin Makea2e3302023-03-11 06:46:20 +0000353 if os.path.exists(path):
354 if not os.path.isfile(path) and not platform_utils.isdir(path):
355 raise ManifestInvalidPathError(
356 "%s: only regular files & directories allowed" % (path,)
357 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400358
Gavin Makea2e3302023-03-11 06:46:20 +0000359 if skipfinal:
360 path = os.path.join(path, finalpart)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400361
Gavin Makea2e3302023-03-11 06:46:20 +0000362 return path
Mike Frysingere6a202f2019-08-02 15:57:57 -0400363
364
365class _CopyFile(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000366 """Container for <copyfile> manifest element."""
Mike Frysingere6a202f2019-08-02 15:57:57 -0400367
Gavin Makea2e3302023-03-11 06:46:20 +0000368 def __init__(self, git_worktree, src, topdir, dest):
369 """Register a <copyfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400370
Gavin Makea2e3302023-03-11 06:46:20 +0000371 Args:
372 git_worktree: Absolute path to the git project checkout.
373 src: Relative path under |git_worktree| of file to read.
374 topdir: Absolute path to the top of the repo client checkout.
375 dest: Relative path under |topdir| of file to write.
376 """
377 self.git_worktree = git_worktree
378 self.topdir = topdir
379 self.src = src
380 self.dest = dest
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700381
Gavin Makea2e3302023-03-11 06:46:20 +0000382 def _Copy(self):
383 src = _SafeExpandPath(self.git_worktree, self.src)
384 dest = _SafeExpandPath(self.topdir, self.dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400385
Gavin Makea2e3302023-03-11 06:46:20 +0000386 if platform_utils.isdir(src):
387 raise ManifestInvalidPathError(
388 "%s: copying from directory not supported" % (self.src,)
389 )
390 if platform_utils.isdir(dest):
391 raise ManifestInvalidPathError(
392 "%s: copying to directory not allowed" % (self.dest,)
393 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400394
Gavin Makea2e3302023-03-11 06:46:20 +0000395 # Copy file if it does not exist or is out of date.
396 if not os.path.exists(dest) or not filecmp.cmp(src, dest):
397 try:
398 # Remove existing file first, since it might be read-only.
399 if os.path.exists(dest):
400 platform_utils.remove(dest)
401 else:
402 dest_dir = os.path.dirname(dest)
403 if not platform_utils.isdir(dest_dir):
404 os.makedirs(dest_dir)
405 shutil.copy(src, dest)
406 # Make the file read-only.
407 mode = os.stat(dest)[stat.ST_MODE]
408 mode = mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
409 os.chmod(dest, mode)
410 except IOError:
411 _error("Cannot copy file %s to %s", src, dest)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700412
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700413
Anthony King7bdac712014-07-16 12:56:40 +0100414class _LinkFile(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000415 """Container for <linkfile> manifest element."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700416
Gavin Makea2e3302023-03-11 06:46:20 +0000417 def __init__(self, git_worktree, src, topdir, dest):
418 """Register a <linkfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400419
Gavin Makea2e3302023-03-11 06:46:20 +0000420 Args:
421 git_worktree: Absolute path to the git project checkout.
422 src: Target of symlink relative to path under |git_worktree|.
423 topdir: Absolute path to the top of the repo client checkout.
424 dest: Relative path under |topdir| of symlink to create.
425 """
426 self.git_worktree = git_worktree
427 self.topdir = topdir
428 self.src = src
429 self.dest = dest
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500430
Gavin Makea2e3302023-03-11 06:46:20 +0000431 def __linkIt(self, relSrc, absDest):
432 # Link file if it does not exist or is out of date.
433 if not platform_utils.islink(absDest) or (
434 platform_utils.readlink(absDest) != relSrc
435 ):
436 try:
437 # Remove existing file first, since it might be read-only.
438 if os.path.lexists(absDest):
439 platform_utils.remove(absDest)
440 else:
441 dest_dir = os.path.dirname(absDest)
442 if not platform_utils.isdir(dest_dir):
443 os.makedirs(dest_dir)
444 platform_utils.symlink(relSrc, absDest)
445 except IOError:
446 _error("Cannot link file %s to %s", relSrc, absDest)
447
448 def _Link(self):
449 """Link the self.src & self.dest paths.
450
451 Handles wild cards on the src linking all of the files in the source in
452 to the destination directory.
453 """
454 # Some people use src="." to create stable links to projects. Let's
455 # allow that but reject all other uses of "." to keep things simple.
456 if self.src == ".":
457 src = self.git_worktree
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500458 else:
Gavin Makea2e3302023-03-11 06:46:20 +0000459 src = _SafeExpandPath(self.git_worktree, self.src)
Wink Saville4c426ef2015-06-03 08:05:17 -0700460
Gavin Makea2e3302023-03-11 06:46:20 +0000461 if not glob.has_magic(src):
462 # Entity does not contain a wild card so just a simple one to one
463 # link operation.
464 dest = _SafeExpandPath(self.topdir, self.dest, skipfinal=True)
465 # dest & src are absolute paths at this point. Make sure the target
466 # of the symlink is relative in the context of the repo client
467 # checkout.
468 relpath = os.path.relpath(src, os.path.dirname(dest))
469 self.__linkIt(relpath, dest)
470 else:
471 dest = _SafeExpandPath(self.topdir, self.dest)
472 # Entity contains a wild card.
473 if os.path.exists(dest) and not platform_utils.isdir(dest):
474 _error(
475 "Link error: src with wildcard, %s must be a directory",
476 dest,
477 )
478 else:
479 for absSrcFile in glob.glob(src):
480 # Create a releative path from source dir to destination
481 # dir.
482 absSrcDir = os.path.dirname(absSrcFile)
483 relSrcDir = os.path.relpath(absSrcDir, dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400484
Gavin Makea2e3302023-03-11 06:46:20 +0000485 # Get the source file name.
486 srcFile = os.path.basename(absSrcFile)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400487
Gavin Makea2e3302023-03-11 06:46:20 +0000488 # Now form the final full paths to srcFile. They will be
489 # absolute for the desintaiton and relative for the source.
490 absDest = os.path.join(dest, srcFile)
491 relSrc = os.path.join(relSrcDir, srcFile)
492 self.__linkIt(relSrc, absDest)
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500493
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700494
Shawn O. Pearced1f70d92009-05-19 14:58:02 -0700495class RemoteSpec(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000496 def __init__(
497 self,
498 name,
499 url=None,
500 pushUrl=None,
501 review=None,
502 revision=None,
503 orig_name=None,
504 fetchUrl=None,
505 ):
506 self.name = name
507 self.url = url
508 self.pushUrl = pushUrl
509 self.review = review
510 self.revision = revision
511 self.orig_name = orig_name
512 self.fetchUrl = fetchUrl
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700513
Ian Kasprzak0286e312021-02-05 10:06:18 -0800514
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700515class Project(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000516 # These objects can be shared between several working trees.
517 @property
518 def shareable_dirs(self):
519 """Return the shareable directories"""
520 if self.UseAlternates:
521 return ["hooks", "rr-cache"]
522 else:
523 return ["hooks", "objects", "rr-cache"]
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700524
Gavin Makea2e3302023-03-11 06:46:20 +0000525 def __init__(
526 self,
527 manifest,
528 name,
529 remote,
530 gitdir,
531 objdir,
532 worktree,
533 relpath,
534 revisionExpr,
535 revisionId,
536 rebase=True,
537 groups=None,
538 sync_c=False,
539 sync_s=False,
540 sync_tags=True,
541 clone_depth=None,
542 upstream=None,
543 parent=None,
544 use_git_worktrees=False,
545 is_derived=False,
546 dest_branch=None,
547 optimized_fetch=False,
548 retry_fetches=0,
549 old_revision=None,
550 ):
551 """Init a Project object.
Doug Anderson3ba5f952011-04-07 12:51:04 -0700552
553 Args:
Gavin Makea2e3302023-03-11 06:46:20 +0000554 manifest: The XmlManifest object.
555 name: The `name` attribute of manifest.xml's project element.
556 remote: RemoteSpec object specifying its remote's properties.
557 gitdir: Absolute path of git directory.
558 objdir: Absolute path of directory to store git objects.
559 worktree: Absolute path of git working tree.
560 relpath: Relative path of git working tree to repo's top directory.
561 revisionExpr: The `revision` attribute of manifest.xml's project
562 element.
563 revisionId: git commit id for checking out.
564 rebase: The `rebase` attribute of manifest.xml's project element.
565 groups: The `groups` attribute of manifest.xml's project element.
566 sync_c: The `sync-c` attribute of manifest.xml's project element.
567 sync_s: The `sync-s` attribute of manifest.xml's project element.
568 sync_tags: The `sync-tags` attribute of manifest.xml's project
569 element.
570 upstream: The `upstream` attribute of manifest.xml's project
571 element.
572 parent: The parent Project object.
573 use_git_worktrees: Whether to use `git worktree` for this project.
574 is_derived: False if the project was explicitly defined in the
575 manifest; True if the project is a discovered submodule.
576 dest_branch: The branch to which to push changes for review by
577 default.
578 optimized_fetch: If True, when a project is set to a sha1 revision,
579 only fetch from the remote if the sha1 is not present locally.
580 retry_fetches: Retry remote fetches n times upon receiving transient
581 error with exponential backoff and jitter.
582 old_revision: saved git commit id for open GITC projects.
583 """
584 self.client = self.manifest = manifest
585 self.name = name
586 self.remote = remote
587 self.UpdatePaths(relpath, worktree, gitdir, objdir)
588 self.SetRevision(revisionExpr, revisionId=revisionId)
589
590 self.rebase = rebase
591 self.groups = groups
592 self.sync_c = sync_c
593 self.sync_s = sync_s
594 self.sync_tags = sync_tags
595 self.clone_depth = clone_depth
596 self.upstream = upstream
597 self.parent = parent
598 # NB: Do not use this setting in __init__ to change behavior so that the
599 # manifest.git checkout can inspect & change it after instantiating.
600 # See the XmlManifest init code for more info.
601 self.use_git_worktrees = use_git_worktrees
602 self.is_derived = is_derived
603 self.optimized_fetch = optimized_fetch
604 self.retry_fetches = max(0, retry_fetches)
605 self.subprojects = []
606
607 self.snapshots = {}
608 self.copyfiles = []
609 self.linkfiles = []
610 self.annotations = []
611 self.dest_branch = dest_branch
612 self.old_revision = old_revision
613
614 # This will be filled in if a project is later identified to be the
615 # project containing repo hooks.
616 self.enabled_repo_hooks = []
617
618 def RelPath(self, local=True):
619 """Return the path for the project relative to a manifest.
620
621 Args:
622 local: a boolean, if True, the path is relative to the local
623 (sub)manifest. If false, the path is relative to the outermost
624 manifest.
625 """
626 if local:
627 return self.relpath
628 return os.path.join(self.manifest.path_prefix, self.relpath)
629
630 def SetRevision(self, revisionExpr, revisionId=None):
631 """Set revisionId based on revision expression and id"""
632 self.revisionExpr = revisionExpr
633 if revisionId is None and revisionExpr and IsId(revisionExpr):
634 self.revisionId = self.revisionExpr
635 else:
636 self.revisionId = revisionId
637
638 def UpdatePaths(self, relpath, worktree, gitdir, objdir):
639 """Update paths used by this project"""
640 self.gitdir = gitdir.replace("\\", "/")
641 self.objdir = objdir.replace("\\", "/")
642 if worktree:
643 self.worktree = os.path.normpath(worktree).replace("\\", "/")
644 else:
645 self.worktree = None
646 self.relpath = relpath
647
648 self.config = GitConfig.ForRepository(
649 gitdir=self.gitdir, defaults=self.manifest.globalConfig
650 )
651
652 if self.worktree:
653 self.work_git = self._GitGetByExec(
654 self, bare=False, gitdir=self.gitdir
655 )
656 else:
657 self.work_git = None
658 self.bare_git = self._GitGetByExec(self, bare=True, gitdir=self.gitdir)
659 self.bare_ref = GitRefs(self.gitdir)
660 self.bare_objdir = self._GitGetByExec(
661 self, bare=True, gitdir=self.objdir
662 )
663
664 @property
665 def UseAlternates(self):
666 """Whether git alternates are in use.
667
668 This will be removed once migration to alternates is complete.
669 """
670 return _ALTERNATES or self.manifest.is_multimanifest
671
672 @property
673 def Derived(self):
674 return self.is_derived
675
676 @property
677 def Exists(self):
678 return platform_utils.isdir(self.gitdir) and platform_utils.isdir(
679 self.objdir
680 )
681
682 @property
683 def CurrentBranch(self):
684 """Obtain the name of the currently checked out branch.
685
686 The branch name omits the 'refs/heads/' prefix.
687 None is returned if the project is on a detached HEAD, or if the
688 work_git is otheriwse inaccessible (e.g. an incomplete sync).
689 """
690 try:
691 b = self.work_git.GetHead()
692 except NoManifestException:
693 # If the local checkout is in a bad state, don't barf. Let the
694 # callers process this like the head is unreadable.
695 return None
696 if b.startswith(R_HEADS):
697 return b[len(R_HEADS) :]
698 return None
699
700 def IsRebaseInProgress(self):
701 return (
702 os.path.exists(self.work_git.GetDotgitPath("rebase-apply"))
703 or os.path.exists(self.work_git.GetDotgitPath("rebase-merge"))
704 or os.path.exists(os.path.join(self.worktree, ".dotest"))
705 )
706
707 def IsDirty(self, consider_untracked=True):
708 """Is the working directory modified in some way?"""
709 self.work_git.update_index(
710 "-q", "--unmerged", "--ignore-missing", "--refresh"
711 )
712 if self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD):
713 return True
714 if self.work_git.DiffZ("diff-files"):
715 return True
716 if consider_untracked and self.UntrackedFiles():
717 return True
718 return False
719
720 _userident_name = None
721 _userident_email = None
722
723 @property
724 def UserName(self):
725 """Obtain the user's personal name."""
726 if self._userident_name is None:
727 self._LoadUserIdentity()
728 return self._userident_name
729
730 @property
731 def UserEmail(self):
732 """Obtain the user's email address. This is very likely
733 to be their Gerrit login.
734 """
735 if self._userident_email is None:
736 self._LoadUserIdentity()
737 return self._userident_email
738
739 def _LoadUserIdentity(self):
740 u = self.bare_git.var("GIT_COMMITTER_IDENT")
741 m = re.compile("^(.*) <([^>]*)> ").match(u)
742 if m:
743 self._userident_name = m.group(1)
744 self._userident_email = m.group(2)
745 else:
746 self._userident_name = ""
747 self._userident_email = ""
748
749 def GetRemote(self, name=None):
750 """Get the configuration for a single remote.
751
752 Defaults to the current project's remote.
753 """
754 if name is None:
755 name = self.remote.name
756 return self.config.GetRemote(name)
757
758 def GetBranch(self, name):
759 """Get the configuration for a single branch."""
760 return self.config.GetBranch(name)
761
762 def GetBranches(self):
763 """Get all existing local branches."""
764 current = self.CurrentBranch
765 all_refs = self._allrefs
766 heads = {}
767
768 for name, ref_id in all_refs.items():
769 if name.startswith(R_HEADS):
770 name = name[len(R_HEADS) :]
771 b = self.GetBranch(name)
772 b.current = name == current
773 b.published = None
774 b.revision = ref_id
775 heads[name] = b
776
777 for name, ref_id in all_refs.items():
778 if name.startswith(R_PUB):
779 name = name[len(R_PUB) :]
780 b = heads.get(name)
781 if b:
782 b.published = ref_id
783
784 return heads
785
786 def MatchesGroups(self, manifest_groups):
787 """Returns true if the manifest groups specified at init should cause
788 this project to be synced.
789 Prefixing a manifest group with "-" inverts the meaning of a group.
790 All projects are implicitly labelled with "all".
791
792 labels are resolved in order. In the example case of
793 project_groups: "all,group1,group2"
794 manifest_groups: "-group1,group2"
795 the project will be matched.
796
797 The special manifest group "default" will match any project that
798 does not have the special project group "notdefault"
799 """
800 default_groups = self.manifest.default_groups or ["default"]
801 expanded_manifest_groups = manifest_groups or default_groups
802 expanded_project_groups = ["all"] + (self.groups or [])
803 if "notdefault" not in expanded_project_groups:
804 expanded_project_groups += ["default"]
805
806 matched = False
807 for group in expanded_manifest_groups:
808 if group.startswith("-") and group[1:] in expanded_project_groups:
809 matched = False
810 elif group in expanded_project_groups:
811 matched = True
812
813 return matched
814
815 def UncommitedFiles(self, get_all=True):
816 """Returns a list of strings, uncommitted files in the git tree.
817
818 Args:
819 get_all: a boolean, if True - get information about all different
820 uncommitted files. If False - return as soon as any kind of
821 uncommitted files is detected.
822 """
823 details = []
824 self.work_git.update_index(
825 "-q", "--unmerged", "--ignore-missing", "--refresh"
826 )
827 if self.IsRebaseInProgress():
828 details.append("rebase in progress")
829 if not get_all:
830 return details
831
832 changes = self.work_git.DiffZ("diff-index", "--cached", HEAD).keys()
833 if changes:
834 details.extend(changes)
835 if not get_all:
836 return details
837
838 changes = self.work_git.DiffZ("diff-files").keys()
839 if changes:
840 details.extend(changes)
841 if not get_all:
842 return details
843
844 changes = self.UntrackedFiles()
845 if changes:
846 details.extend(changes)
847
848 return details
849
850 def UntrackedFiles(self):
851 """Returns a list of strings, untracked files in the git tree."""
852 return self.work_git.LsOthers()
853
854 def HasChanges(self):
855 """Returns true if there are uncommitted changes."""
856 return bool(self.UncommitedFiles(get_all=False))
857
858 def PrintWorkTreeStatus(self, output_redir=None, quiet=False, local=False):
859 """Prints the status of the repository to stdout.
860
861 Args:
862 output_redir: If specified, redirect the output to this object.
863 quiet: If True then only print the project name. Do not print
864 the modified files, branch name, etc.
865 local: a boolean, if True, the path is relative to the local
866 (sub)manifest. If false, the path is relative to the outermost
867 manifest.
868 """
869 if not platform_utils.isdir(self.worktree):
870 if output_redir is None:
871 output_redir = sys.stdout
872 print(file=output_redir)
873 print("project %s/" % self.RelPath(local), file=output_redir)
874 print(' missing (run "repo sync")', file=output_redir)
875 return
876
877 self.work_git.update_index(
878 "-q", "--unmerged", "--ignore-missing", "--refresh"
879 )
880 rb = self.IsRebaseInProgress()
881 di = self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD)
882 df = self.work_git.DiffZ("diff-files")
883 do = self.work_git.LsOthers()
884 if not rb and not di and not df and not do and not self.CurrentBranch:
885 return "CLEAN"
886
887 out = StatusColoring(self.config)
888 if output_redir is not None:
889 out.redirect(output_redir)
890 out.project("project %-40s", self.RelPath(local) + "/ ")
891
892 if quiet:
893 out.nl()
894 return "DIRTY"
895
896 branch = self.CurrentBranch
897 if branch is None:
898 out.nobranch("(*** NO BRANCH ***)")
899 else:
900 out.branch("branch %s", branch)
901 out.nl()
902
903 if rb:
904 out.important("prior sync failed; rebase still in progress")
905 out.nl()
906
907 paths = list()
908 paths.extend(di.keys())
909 paths.extend(df.keys())
910 paths.extend(do)
911
912 for p in sorted(set(paths)):
913 try:
914 i = di[p]
915 except KeyError:
916 i = None
917
918 try:
919 f = df[p]
920 except KeyError:
921 f = None
922
923 if i:
924 i_status = i.status.upper()
925 else:
926 i_status = "-"
927
928 if f:
929 f_status = f.status.lower()
930 else:
931 f_status = "-"
932
933 if i and i.src_path:
934 line = " %s%s\t%s => %s (%s%%)" % (
935 i_status,
936 f_status,
937 i.src_path,
938 p,
939 i.level,
940 )
941 else:
942 line = " %s%s\t%s" % (i_status, f_status, p)
943
944 if i and not f:
945 out.added("%s", line)
946 elif (i and f) or (not i and f):
947 out.changed("%s", line)
948 elif not i and not f:
949 out.untracked("%s", line)
950 else:
951 out.write("%s", line)
952 out.nl()
953
954 return "DIRTY"
955
956 def PrintWorkTreeDiff(
957 self, absolute_paths=False, output_redir=None, local=False
958 ):
959 """Prints the status of the repository to stdout."""
960 out = DiffColoring(self.config)
961 if output_redir:
962 out.redirect(output_redir)
963 cmd = ["diff"]
964 if out.is_on:
965 cmd.append("--color")
966 cmd.append(HEAD)
967 if absolute_paths:
968 cmd.append("--src-prefix=a/%s/" % self.RelPath(local))
969 cmd.append("--dst-prefix=b/%s/" % self.RelPath(local))
970 cmd.append("--")
971 try:
972 p = GitCommand(self, cmd, capture_stdout=True, capture_stderr=True)
973 p.Wait()
974 except GitError as e:
975 out.nl()
976 out.project("project %s/" % self.RelPath(local))
977 out.nl()
978 out.fail("%s", str(e))
979 out.nl()
980 return False
981 if p.stdout:
982 out.nl()
983 out.project("project %s/" % self.RelPath(local))
984 out.nl()
985 out.write("%s", p.stdout)
986 return p.Wait() == 0
987
988 def WasPublished(self, branch, all_refs=None):
989 """Was the branch published (uploaded) for code review?
990 If so, returns the SHA-1 hash of the last published
991 state for the branch.
992 """
993 key = R_PUB + branch
994 if all_refs is None:
995 try:
996 return self.bare_git.rev_parse(key)
997 except GitError:
998 return None
999 else:
1000 try:
1001 return all_refs[key]
1002 except KeyError:
1003 return None
1004
1005 def CleanPublishedCache(self, all_refs=None):
1006 """Prunes any stale published refs."""
1007 if all_refs is None:
1008 all_refs = self._allrefs
1009 heads = set()
1010 canrm = {}
1011 for name, ref_id in all_refs.items():
1012 if name.startswith(R_HEADS):
1013 heads.add(name)
1014 elif name.startswith(R_PUB):
1015 canrm[name] = ref_id
1016
1017 for name, ref_id in canrm.items():
1018 n = name[len(R_PUB) :]
1019 if R_HEADS + n not in heads:
1020 self.bare_git.DeleteRef(name, ref_id)
1021
1022 def GetUploadableBranches(self, selected_branch=None):
1023 """List any branches which can be uploaded for review."""
1024 heads = {}
1025 pubed = {}
1026
1027 for name, ref_id in self._allrefs.items():
1028 if name.startswith(R_HEADS):
1029 heads[name[len(R_HEADS) :]] = ref_id
1030 elif name.startswith(R_PUB):
1031 pubed[name[len(R_PUB) :]] = ref_id
1032
1033 ready = []
1034 for branch, ref_id in heads.items():
1035 if branch in pubed and pubed[branch] == ref_id:
1036 continue
1037 if selected_branch and branch != selected_branch:
1038 continue
1039
1040 rb = self.GetUploadableBranch(branch)
1041 if rb:
1042 ready.append(rb)
1043 return ready
1044
1045 def GetUploadableBranch(self, branch_name):
1046 """Get a single uploadable branch, or None."""
1047 branch = self.GetBranch(branch_name)
1048 base = branch.LocalMerge
1049 if branch.LocalMerge:
1050 rb = ReviewableBranch(self, branch, base)
1051 if rb.commits:
1052 return rb
1053 return None
1054
1055 def UploadForReview(
1056 self,
1057 branch=None,
1058 people=([], []),
1059 dryrun=False,
1060 auto_topic=False,
1061 hashtags=(),
1062 labels=(),
1063 private=False,
1064 notify=None,
1065 wip=False,
1066 ready=False,
1067 dest_branch=None,
1068 validate_certs=True,
1069 push_options=None,
1070 ):
1071 """Uploads the named branch for code review."""
1072 if branch is None:
1073 branch = self.CurrentBranch
1074 if branch is None:
1075 raise GitError("not currently on a branch")
1076
1077 branch = self.GetBranch(branch)
1078 if not branch.LocalMerge:
1079 raise GitError("branch %s does not track a remote" % branch.name)
1080 if not branch.remote.review:
1081 raise GitError("remote %s has no review url" % branch.remote.name)
1082
1083 # Basic validity check on label syntax.
1084 for label in labels:
1085 if not re.match(r"^.+[+-][0-9]+$", label):
1086 raise UploadError(
1087 f'invalid label syntax "{label}": labels use forms like '
1088 "CodeReview+1 or Verified-1"
1089 )
1090
1091 if dest_branch is None:
1092 dest_branch = self.dest_branch
1093 if dest_branch is None:
1094 dest_branch = branch.merge
1095 if not dest_branch.startswith(R_HEADS):
1096 dest_branch = R_HEADS + dest_branch
1097
1098 if not branch.remote.projectname:
1099 branch.remote.projectname = self.name
1100 branch.remote.Save()
1101
1102 url = branch.remote.ReviewUrl(self.UserEmail, validate_certs)
1103 if url is None:
1104 raise UploadError("review not configured")
1105 cmd = ["push"]
1106 if dryrun:
1107 cmd.append("-n")
1108
1109 if url.startswith("ssh://"):
1110 cmd.append("--receive-pack=gerrit receive-pack")
1111
Aravind Vasudevan99ebf622023-04-04 23:44:37 +00001112 # This stops git from pushing all reachable annotated tags when
1113 # push.followTags is configured. Gerrit does not accept any tags
1114 # pushed to a CL.
1115 if git_require((1, 8, 3)):
1116 cmd.append("--no-follow-tags")
1117
Gavin Makea2e3302023-03-11 06:46:20 +00001118 for push_option in push_options or []:
1119 cmd.append("-o")
1120 cmd.append(push_option)
1121
1122 cmd.append(url)
1123
1124 if dest_branch.startswith(R_HEADS):
1125 dest_branch = dest_branch[len(R_HEADS) :]
1126
1127 ref_spec = "%s:refs/for/%s" % (R_HEADS + branch.name, dest_branch)
1128 opts = []
1129 if auto_topic:
1130 opts += ["topic=" + branch.name]
1131 opts += ["t=%s" % p for p in hashtags]
1132 # NB: No need to encode labels as they've been validated above.
1133 opts += ["l=%s" % p for p in labels]
1134
1135 opts += ["r=%s" % p for p in people[0]]
1136 opts += ["cc=%s" % p for p in people[1]]
1137 if notify:
1138 opts += ["notify=" + notify]
1139 if private:
1140 opts += ["private"]
1141 if wip:
1142 opts += ["wip"]
1143 if ready:
1144 opts += ["ready"]
1145 if opts:
1146 ref_spec = ref_spec + "%" + ",".join(opts)
1147 cmd.append(ref_spec)
1148
1149 if GitCommand(self, cmd, bare=True).Wait() != 0:
1150 raise UploadError("Upload failed")
1151
1152 if not dryrun:
1153 msg = "posted to %s for %s" % (branch.remote.review, dest_branch)
1154 self.bare_git.UpdateRef(
1155 R_PUB + branch.name, R_HEADS + branch.name, message=msg
1156 )
1157
1158 def _ExtractArchive(self, tarpath, path=None):
1159 """Extract the given tar on its current location
1160
1161 Args:
1162 tarpath: The path to the actual tar file
1163
1164 """
1165 try:
1166 with tarfile.open(tarpath, "r") as tar:
1167 tar.extractall(path=path)
1168 return True
1169 except (IOError, tarfile.TarError) as e:
1170 _error("Cannot extract archive %s: %s", tarpath, str(e))
1171 return False
1172
Gavin Mak4feff3b2023-05-16 21:31:10 +00001173 def CachePopulate(self, cache_dir, url):
1174 """Populate cache in the cache_dir.
1175
1176 Args:
1177 cache_dir: Directory to cache git files from Google Storage.
1178 url: Git url of current repository.
1179
1180 Raises:
1181 CacheApplyError if it fails to populate the git cache.
1182 """
1183 cmd = [
1184 "cache",
1185 "populate",
1186 "--ignore_locks",
1187 "-v",
1188 "--cache-dir",
1189 cache_dir,
1190 url,
1191 ]
1192
1193 if GitCommand(self, cmd, cwd=cache_dir).Wait() != 0:
1194 raise CacheApplyError(
1195 "Failed to populate cache. cache_dir: %s "
1196 "url: %s" % (cache_dir, url)
1197 )
1198
1199 def CacheExists(self, cache_dir, url):
1200 """Check the existence of the cache files.
1201
1202 Args:
1203 cache_dir: Directory to cache git files.
1204 url: Git url of current repository.
1205
1206 Raises:
1207 CacheApplyError if the cache files do not exist.
1208 """
1209 cmd = ["cache", "exists", "--quiet", "--cache-dir", cache_dir, url]
1210
1211 exist = GitCommand(self, cmd, cwd=self.gitdir, capture_stdout=True)
1212 if exist.Wait() != 0:
1213 raise CacheApplyError(
1214 "Failed to execute git cache exists cmd. "
1215 "cache_dir: %s url: %s" % (cache_dir, url)
1216 )
1217
1218 if not exist.stdout or not exist.stdout.strip():
1219 raise CacheApplyError(
1220 "Failed to find cache. cache_dir: %s "
1221 "url: %s" % (cache_dir, url)
1222 )
1223 return exist.stdout.strip()
1224
1225 def CacheApply(self, cache_dir):
1226 """Apply git cache files populated from Google Storage buckets.
1227
1228 Args:
1229 cache_dir: Directory to cache git files.
1230
1231 Raises:
1232 CacheApplyError if it fails to apply git caches.
1233 """
1234 remote = self.GetRemote(self.remote.name)
1235
1236 self.CachePopulate(cache_dir, remote.url)
1237
1238 mirror_dir = self.CacheExists(cache_dir, remote.url)
1239
1240 refspec = RefSpec(
1241 True, "refs/heads/*", "refs/remotes/%s/*" % remote.name
1242 )
1243
1244 fetch_cache_cmd = ["fetch", mirror_dir, str(refspec)]
1245 if GitCommand(self, fetch_cache_cmd, self.gitdir).Wait() != 0:
1246 raise CacheApplyError(
1247 "Failed to fetch refs %s from %s" % (mirror_dir, str(refspec))
1248 )
1249
Gavin Makea2e3302023-03-11 06:46:20 +00001250 def Sync_NetworkHalf(
1251 self,
1252 quiet=False,
1253 verbose=False,
1254 output_redir=None,
1255 is_new=None,
1256 current_branch_only=None,
1257 force_sync=False,
1258 clone_bundle=True,
1259 tags=None,
1260 archive=False,
1261 optimized_fetch=False,
1262 retry_fetches=0,
1263 prune=False,
1264 submodules=False,
Gavin Mak4feff3b2023-05-16 21:31:10 +00001265 cache_dir=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001266 ssh_proxy=None,
1267 clone_filter=None,
1268 partial_clone_exclude=set(),
1269 ):
1270 """Perform only the network IO portion of the sync process.
1271 Local working directory/branch state is not affected.
1272 """
1273 if archive and not isinstance(self, MetaProject):
1274 if self.remote.url.startswith(("http://", "https://")):
1275 _error(
1276 "%s: Cannot fetch archives from http/https remotes.",
1277 self.name,
1278 )
1279 return SyncNetworkHalfResult(False, False)
1280
1281 name = self.relpath.replace("\\", "/")
1282 name = name.replace("/", "_")
1283 tarpath = "%s.tar" % name
1284 topdir = self.manifest.topdir
1285
1286 try:
1287 self._FetchArchive(tarpath, cwd=topdir)
1288 except GitError as e:
1289 _error("%s", e)
1290 return SyncNetworkHalfResult(False, False)
1291
1292 # From now on, we only need absolute tarpath.
1293 tarpath = os.path.join(topdir, tarpath)
1294
1295 if not self._ExtractArchive(tarpath, path=topdir):
1296 return SyncNetworkHalfResult(False, True)
1297 try:
1298 platform_utils.remove(tarpath)
1299 except OSError as e:
1300 _warn("Cannot remove archive %s: %s", tarpath, str(e))
1301 self._CopyAndLinkFiles()
1302 return SyncNetworkHalfResult(True, True)
1303
1304 # If the shared object dir already exists, don't try to rebootstrap with
1305 # a clone bundle download. We should have the majority of objects
1306 # already.
1307 if clone_bundle and os.path.exists(self.objdir):
1308 clone_bundle = False
1309
1310 if self.name in partial_clone_exclude:
1311 clone_bundle = True
1312 clone_filter = None
1313
1314 if is_new is None:
1315 is_new = not self.Exists
1316 if is_new:
1317 self._InitGitDir(force_sync=force_sync, quiet=quiet)
1318 else:
1319 self._UpdateHooks(quiet=quiet)
1320 self._InitRemote()
1321
1322 if self.UseAlternates:
1323 # If gitdir/objects is a symlink, migrate it from the old layout.
1324 gitdir_objects = os.path.join(self.gitdir, "objects")
1325 if platform_utils.islink(gitdir_objects):
1326 platform_utils.remove(gitdir_objects, missing_ok=True)
1327 gitdir_alt = os.path.join(self.gitdir, "objects/info/alternates")
1328 if not os.path.exists(gitdir_alt):
1329 os.makedirs(os.path.dirname(gitdir_alt), exist_ok=True)
1330 _lwrite(
1331 gitdir_alt,
1332 os.path.join(
1333 os.path.relpath(self.objdir, gitdir_objects), "objects"
1334 )
1335 + "\n",
1336 )
1337
1338 if is_new:
1339 alt = os.path.join(self.objdir, "objects/info/alternates")
1340 try:
1341 with open(alt) as fd:
1342 # This works for both absolute and relative alternate
1343 # directories.
1344 alt_dir = os.path.join(
1345 self.objdir, "objects", fd.readline().rstrip()
1346 )
1347 except IOError:
1348 alt_dir = None
1349 else:
1350 alt_dir = None
1351
Gavin Mak4feff3b2023-05-16 21:31:10 +00001352 applied_cache = False
1353 # If cache_dir is provided, and it's a new repository without
1354 # alternative_dir, bootstrap this project repo with the git
1355 # cache files.
1356 if cache_dir is not None and is_new and alt_dir is None:
1357 try:
1358 self.CacheApply(cache_dir)
1359 applied_cache = True
1360 is_new = False
1361 except CacheApplyError as e:
1362 _error("Could not apply git cache: %s", e)
1363 _error("Please check if you have the right GS credentials.")
1364 _error("Please check if the cache files exist in GS.")
1365
Gavin Makea2e3302023-03-11 06:46:20 +00001366 if (
1367 clone_bundle
Gavin Mak4feff3b2023-05-16 21:31:10 +00001368 and not applied_cache
Gavin Makea2e3302023-03-11 06:46:20 +00001369 and alt_dir is None
1370 and self._ApplyCloneBundle(
1371 initial=is_new, quiet=quiet, verbose=verbose
1372 )
1373 ):
1374 is_new = False
1375
1376 if current_branch_only is None:
1377 if self.sync_c:
1378 current_branch_only = True
1379 elif not self.manifest._loaded:
1380 # Manifest cannot check defaults until it syncs.
1381 current_branch_only = False
1382 elif self.manifest.default.sync_c:
1383 current_branch_only = True
1384
1385 if tags is None:
1386 tags = self.sync_tags
1387
1388 if self.clone_depth:
1389 depth = self.clone_depth
1390 else:
1391 depth = self.manifest.manifestProject.depth
1392
1393 # See if we can skip the network fetch entirely.
1394 remote_fetched = False
1395 if not (
1396 optimized_fetch
1397 and (
1398 ID_RE.match(self.revisionExpr)
1399 and self._CheckForImmutableRevision()
1400 )
1401 ):
1402 remote_fetched = True
1403 if not self._RemoteFetch(
1404 initial=is_new,
1405 quiet=quiet,
1406 verbose=verbose,
1407 output_redir=output_redir,
1408 alt_dir=alt_dir,
1409 current_branch_only=current_branch_only,
1410 tags=tags,
1411 prune=prune,
1412 depth=depth,
1413 submodules=submodules,
1414 force_sync=force_sync,
1415 ssh_proxy=ssh_proxy,
1416 clone_filter=clone_filter,
1417 retry_fetches=retry_fetches,
1418 ):
1419 return SyncNetworkHalfResult(False, remote_fetched)
1420
1421 mp = self.manifest.manifestProject
1422 dissociate = mp.dissociate
1423 if dissociate:
1424 alternates_file = os.path.join(
1425 self.objdir, "objects/info/alternates"
1426 )
1427 if os.path.exists(alternates_file):
1428 cmd = ["repack", "-a", "-d"]
1429 p = GitCommand(
1430 self,
1431 cmd,
1432 bare=True,
1433 capture_stdout=bool(output_redir),
1434 merge_output=bool(output_redir),
1435 )
1436 if p.stdout and output_redir:
1437 output_redir.write(p.stdout)
1438 if p.Wait() != 0:
1439 return SyncNetworkHalfResult(False, remote_fetched)
1440 platform_utils.remove(alternates_file)
1441
1442 if self.worktree:
1443 self._InitMRef()
1444 else:
1445 self._InitMirrorHead()
1446 platform_utils.remove(
1447 os.path.join(self.gitdir, "FETCH_HEAD"), missing_ok=True
1448 )
1449 return SyncNetworkHalfResult(True, remote_fetched)
1450
1451 def PostRepoUpgrade(self):
1452 self._InitHooks()
1453
1454 def _CopyAndLinkFiles(self):
1455 if self.client.isGitcClient:
1456 return
1457 for copyfile in self.copyfiles:
1458 copyfile._Copy()
1459 for linkfile in self.linkfiles:
1460 linkfile._Link()
1461
1462 def GetCommitRevisionId(self):
1463 """Get revisionId of a commit.
1464
1465 Use this method instead of GetRevisionId to get the id of the commit
1466 rather than the id of the current git object (for example, a tag)
1467
1468 """
1469 if not self.revisionExpr.startswith(R_TAGS):
1470 return self.GetRevisionId(self._allrefs)
1471
1472 try:
1473 return self.bare_git.rev_list(self.revisionExpr, "-1")[0]
1474 except GitError:
1475 raise ManifestInvalidRevisionError(
1476 "revision %s in %s not found" % (self.revisionExpr, self.name)
1477 )
1478
1479 def GetRevisionId(self, all_refs=None):
1480 if self.revisionId:
1481 return self.revisionId
1482
1483 rem = self.GetRemote()
1484 rev = rem.ToLocal(self.revisionExpr)
1485
1486 if all_refs is not None and rev in all_refs:
1487 return all_refs[rev]
1488
1489 try:
1490 return self.bare_git.rev_parse("--verify", "%s^0" % rev)
1491 except GitError:
1492 raise ManifestInvalidRevisionError(
1493 "revision %s in %s not found" % (self.revisionExpr, self.name)
1494 )
1495
1496 def SetRevisionId(self, revisionId):
1497 if self.revisionExpr:
1498 self.upstream = self.revisionExpr
1499
1500 self.revisionId = revisionId
1501
1502 def Sync_LocalHalf(self, syncbuf, force_sync=False, submodules=False):
1503 """Perform only the local IO portion of the sync process.
1504
1505 Network access is not required.
1506 """
1507 if not os.path.exists(self.gitdir):
1508 syncbuf.fail(
1509 self,
1510 "Cannot checkout %s due to missing network sync; Run "
1511 "`repo sync -n %s` first." % (self.name, self.name),
1512 )
1513 return
1514
1515 self._InitWorkTree(force_sync=force_sync, submodules=submodules)
1516 all_refs = self.bare_ref.all
1517 self.CleanPublishedCache(all_refs)
1518 revid = self.GetRevisionId(all_refs)
1519
1520 # Special case the root of the repo client checkout. Make sure it
1521 # doesn't contain files being checked out to dirs we don't allow.
1522 if self.relpath == ".":
1523 PROTECTED_PATHS = {".repo"}
1524 paths = set(
1525 self.work_git.ls_tree("-z", "--name-only", "--", revid).split(
1526 "\0"
1527 )
1528 )
1529 bad_paths = paths & PROTECTED_PATHS
1530 if bad_paths:
1531 syncbuf.fail(
1532 self,
1533 "Refusing to checkout project that writes to protected "
1534 "paths: %s" % (", ".join(bad_paths),),
1535 )
1536 return
1537
1538 def _doff():
1539 self._FastForward(revid)
1540 self._CopyAndLinkFiles()
1541
1542 def _dosubmodules():
1543 self._SyncSubmodules(quiet=True)
1544
1545 head = self.work_git.GetHead()
1546 if head.startswith(R_HEADS):
1547 branch = head[len(R_HEADS) :]
1548 try:
1549 head = all_refs[head]
1550 except KeyError:
1551 head = None
1552 else:
1553 branch = None
1554
1555 if branch is None or syncbuf.detach_head:
1556 # Currently on a detached HEAD. The user is assumed to
1557 # not have any local modifications worth worrying about.
1558 if self.IsRebaseInProgress():
1559 syncbuf.fail(self, _PriorSyncFailedError())
1560 return
1561
1562 if head == revid:
1563 # No changes; don't do anything further.
1564 # Except if the head needs to be detached.
1565 if not syncbuf.detach_head:
1566 # The copy/linkfile config may have changed.
1567 self._CopyAndLinkFiles()
1568 return
1569 else:
1570 lost = self._revlist(not_rev(revid), HEAD)
1571 if lost:
1572 syncbuf.info(self, "discarding %d commits", len(lost))
1573
1574 try:
1575 self._Checkout(revid, quiet=True)
1576 if submodules:
1577 self._SyncSubmodules(quiet=True)
1578 except GitError as e:
1579 syncbuf.fail(self, e)
1580 return
1581 self._CopyAndLinkFiles()
1582 return
1583
1584 if head == revid:
1585 # No changes; don't do anything further.
1586 #
1587 # The copy/linkfile config may have changed.
1588 self._CopyAndLinkFiles()
1589 return
1590
1591 branch = self.GetBranch(branch)
1592
1593 if not branch.LocalMerge:
1594 # The current branch has no tracking configuration.
1595 # Jump off it to a detached HEAD.
1596 syncbuf.info(
1597 self, "leaving %s; does not track upstream", branch.name
1598 )
1599 try:
1600 self._Checkout(revid, quiet=True)
1601 if submodules:
1602 self._SyncSubmodules(quiet=True)
1603 except GitError as e:
1604 syncbuf.fail(self, e)
1605 return
1606 self._CopyAndLinkFiles()
1607 return
1608
1609 upstream_gain = self._revlist(not_rev(HEAD), revid)
1610
1611 # See if we can perform a fast forward merge. This can happen if our
1612 # branch isn't in the exact same state as we last published.
1613 try:
1614 self.work_git.merge_base("--is-ancestor", HEAD, revid)
1615 # Skip the published logic.
1616 pub = False
1617 except GitError:
1618 pub = self.WasPublished(branch.name, all_refs)
1619
1620 if pub:
1621 not_merged = self._revlist(not_rev(revid), pub)
1622 if not_merged:
1623 if upstream_gain:
1624 # The user has published this branch and some of those
1625 # commits are not yet merged upstream. We do not want
1626 # to rewrite the published commits so we punt.
1627 syncbuf.fail(
1628 self,
1629 "branch %s is published (but not merged) and is now "
1630 "%d commits behind" % (branch.name, len(upstream_gain)),
1631 )
1632 return
1633 elif pub == head:
1634 # All published commits are merged, and thus we are a
1635 # strict subset. We can fast-forward safely.
1636 syncbuf.later1(self, _doff)
1637 if submodules:
1638 syncbuf.later1(self, _dosubmodules)
1639 return
1640
1641 # Examine the local commits not in the remote. Find the
1642 # last one attributed to this user, if any.
1643 local_changes = self._revlist(not_rev(revid), HEAD, format="%H %ce")
1644 last_mine = None
1645 cnt_mine = 0
1646 for commit in local_changes:
1647 commit_id, committer_email = commit.split(" ", 1)
1648 if committer_email == self.UserEmail:
1649 last_mine = commit_id
1650 cnt_mine += 1
1651
1652 if not upstream_gain and cnt_mine == len(local_changes):
1653 # The copy/linkfile config may have changed.
1654 self._CopyAndLinkFiles()
1655 return
1656
1657 if self.IsDirty(consider_untracked=False):
1658 syncbuf.fail(self, _DirtyError())
1659 return
1660
1661 # If the upstream switched on us, warn the user.
1662 if branch.merge != self.revisionExpr:
1663 if branch.merge and self.revisionExpr:
1664 syncbuf.info(
1665 self,
1666 "manifest switched %s...%s",
1667 branch.merge,
1668 self.revisionExpr,
1669 )
1670 elif branch.merge:
1671 syncbuf.info(self, "manifest no longer tracks %s", branch.merge)
1672
1673 if cnt_mine < len(local_changes):
1674 # Upstream rebased. Not everything in HEAD was created by this user.
1675 syncbuf.info(
1676 self,
1677 "discarding %d commits removed from upstream",
1678 len(local_changes) - cnt_mine,
1679 )
1680
1681 branch.remote = self.GetRemote()
1682 if not ID_RE.match(self.revisionExpr):
1683 # In case of manifest sync the revisionExpr might be a SHA1.
1684 branch.merge = self.revisionExpr
1685 if not branch.merge.startswith("refs/"):
1686 branch.merge = R_HEADS + branch.merge
1687 branch.Save()
1688
1689 if cnt_mine > 0 and self.rebase:
1690
1691 def _docopyandlink():
1692 self._CopyAndLinkFiles()
1693
1694 def _dorebase():
1695 self._Rebase(upstream="%s^1" % last_mine, onto=revid)
1696
1697 syncbuf.later2(self, _dorebase)
1698 if submodules:
1699 syncbuf.later2(self, _dosubmodules)
1700 syncbuf.later2(self, _docopyandlink)
1701 elif local_changes:
1702 try:
1703 self._ResetHard(revid)
1704 if submodules:
1705 self._SyncSubmodules(quiet=True)
1706 self._CopyAndLinkFiles()
1707 except GitError as e:
1708 syncbuf.fail(self, e)
1709 return
1710 else:
1711 syncbuf.later1(self, _doff)
1712 if submodules:
1713 syncbuf.later1(self, _dosubmodules)
1714
1715 def AddCopyFile(self, src, dest, topdir):
1716 """Mark |src| for copying to |dest| (relative to |topdir|).
1717
1718 No filesystem changes occur here. Actual copying happens later on.
1719
1720 Paths should have basic validation run on them before being queued.
1721 Further checking will be handled when the actual copy happens.
1722 """
1723 self.copyfiles.append(_CopyFile(self.worktree, src, topdir, dest))
1724
1725 def AddLinkFile(self, src, dest, topdir):
1726 """Mark |dest| to create a symlink (relative to |topdir|) pointing to
1727 |src|.
1728
1729 No filesystem changes occur here. Actual linking happens later on.
1730
1731 Paths should have basic validation run on them before being queued.
1732 Further checking will be handled when the actual link happens.
1733 """
1734 self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest))
1735
1736 def AddAnnotation(self, name, value, keep):
1737 self.annotations.append(Annotation(name, value, keep))
1738
1739 def DownloadPatchSet(self, change_id, patch_id):
1740 """Download a single patch set of a single change to FETCH_HEAD."""
1741 remote = self.GetRemote()
1742
1743 cmd = ["fetch", remote.name]
1744 cmd.append(
1745 "refs/changes/%2.2d/%d/%d" % (change_id % 100, change_id, patch_id)
1746 )
1747 if GitCommand(self, cmd, bare=True).Wait() != 0:
1748 return None
1749 return DownloadedChange(
1750 self,
1751 self.GetRevisionId(),
1752 change_id,
1753 patch_id,
1754 self.bare_git.rev_parse("FETCH_HEAD"),
1755 )
1756
1757 def DeleteWorktree(self, quiet=False, force=False):
1758 """Delete the source checkout and any other housekeeping tasks.
1759
1760 This currently leaves behind the internal .repo/ cache state. This
1761 helps when switching branches or manifest changes get reverted as we
1762 don't have to redownload all the git objects. But we should do some GC
1763 at some point.
1764
1765 Args:
1766 quiet: Whether to hide normal messages.
1767 force: Always delete tree even if dirty.
Doug Anderson3ba5f952011-04-07 12:51:04 -07001768
1769 Returns:
Gavin Makea2e3302023-03-11 06:46:20 +00001770 True if the worktree was completely cleaned out.
1771 """
1772 if self.IsDirty():
1773 if force:
1774 print(
1775 "warning: %s: Removing dirty project: uncommitted changes "
1776 "lost." % (self.RelPath(local=False),),
1777 file=sys.stderr,
1778 )
1779 else:
1780 print(
1781 "error: %s: Cannot remove project: uncommitted changes are "
1782 "present.\n" % (self.RelPath(local=False),),
1783 file=sys.stderr,
1784 )
1785 return False
Wink Saville02d79452009-04-10 13:01:24 -07001786
Gavin Makea2e3302023-03-11 06:46:20 +00001787 if not quiet:
1788 print(
1789 "%s: Deleting obsolete checkout." % (self.RelPath(local=False),)
1790 )
Wink Saville02d79452009-04-10 13:01:24 -07001791
Gavin Makea2e3302023-03-11 06:46:20 +00001792 # Unlock and delink from the main worktree. We don't use git's worktree
1793 # remove because it will recursively delete projects -- we handle that
1794 # ourselves below. https://crbug.com/git/48
1795 if self.use_git_worktrees:
1796 needle = platform_utils.realpath(self.gitdir)
1797 # Find the git worktree commondir under .repo/worktrees/.
1798 output = self.bare_git.worktree("list", "--porcelain").splitlines()[
1799 0
1800 ]
1801 assert output.startswith("worktree "), output
1802 commondir = output[9:]
1803 # Walk each of the git worktrees to see where they point.
1804 configs = os.path.join(commondir, "worktrees")
1805 for name in os.listdir(configs):
1806 gitdir = os.path.join(configs, name, "gitdir")
1807 with open(gitdir) as fp:
1808 relpath = fp.read().strip()
1809 # Resolve the checkout path and see if it matches this project.
1810 fullpath = platform_utils.realpath(
1811 os.path.join(configs, name, relpath)
1812 )
1813 if fullpath == needle:
1814 platform_utils.rmtree(os.path.join(configs, name))
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001815
Gavin Makea2e3302023-03-11 06:46:20 +00001816 # Delete the .git directory first, so we're less likely to have a
1817 # partially working git repository around. There shouldn't be any git
1818 # projects here, so rmtree works.
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001819
Gavin Makea2e3302023-03-11 06:46:20 +00001820 # Try to remove plain files first in case of git worktrees. If this
1821 # fails for any reason, we'll fall back to rmtree, and that'll display
1822 # errors if it can't remove things either.
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001823 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001824 platform_utils.remove(self.gitdir)
1825 except OSError:
1826 pass
1827 try:
1828 platform_utils.rmtree(self.gitdir)
1829 except OSError as e:
1830 if e.errno != errno.ENOENT:
1831 print("error: %s: %s" % (self.gitdir, e), file=sys.stderr)
1832 print(
1833 "error: %s: Failed to delete obsolete checkout; remove "
1834 "manually, then run `repo sync -l`."
1835 % (self.RelPath(local=False),),
1836 file=sys.stderr,
1837 )
1838 return False
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001839
Gavin Makea2e3302023-03-11 06:46:20 +00001840 # Delete everything under the worktree, except for directories that
1841 # contain another git project.
1842 dirs_to_remove = []
1843 failed = False
1844 for root, dirs, files in platform_utils.walk(self.worktree):
1845 for f in files:
1846 path = os.path.join(root, f)
1847 try:
1848 platform_utils.remove(path)
1849 except OSError as e:
1850 if e.errno != errno.ENOENT:
1851 print(
1852 "error: %s: Failed to remove: %s" % (path, e),
1853 file=sys.stderr,
1854 )
1855 failed = True
1856 dirs[:] = [
1857 d
1858 for d in dirs
1859 if not os.path.lexists(os.path.join(root, d, ".git"))
1860 ]
1861 dirs_to_remove += [
1862 os.path.join(root, d)
1863 for d in dirs
1864 if os.path.join(root, d) not in dirs_to_remove
1865 ]
1866 for d in reversed(dirs_to_remove):
1867 if platform_utils.islink(d):
1868 try:
1869 platform_utils.remove(d)
1870 except OSError as e:
1871 if e.errno != errno.ENOENT:
1872 print(
1873 "error: %s: Failed to remove: %s" % (d, e),
1874 file=sys.stderr,
1875 )
1876 failed = True
1877 elif not platform_utils.listdir(d):
1878 try:
1879 platform_utils.rmdir(d)
1880 except OSError as e:
1881 if e.errno != errno.ENOENT:
1882 print(
1883 "error: %s: Failed to remove: %s" % (d, e),
1884 file=sys.stderr,
1885 )
1886 failed = True
1887 if failed:
1888 print(
1889 "error: %s: Failed to delete obsolete checkout."
1890 % (self.RelPath(local=False),),
1891 file=sys.stderr,
1892 )
1893 print(
1894 " Remove manually, then run `repo sync -l`.",
1895 file=sys.stderr,
1896 )
1897 return False
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07001898
Gavin Makea2e3302023-03-11 06:46:20 +00001899 # Try deleting parent dirs if they are empty.
1900 path = self.worktree
1901 while path != self.manifest.topdir:
1902 try:
1903 platform_utils.rmdir(path)
1904 except OSError as e:
1905 if e.errno != errno.ENOENT:
1906 break
1907 path = os.path.dirname(path)
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001908
Gavin Makea2e3302023-03-11 06:46:20 +00001909 return True
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001910
Gavin Makea2e3302023-03-11 06:46:20 +00001911 def StartBranch(self, name, branch_merge="", revision=None):
1912 """Create a new branch off the manifest's revision."""
1913 if not branch_merge:
1914 branch_merge = self.revisionExpr
1915 head = self.work_git.GetHead()
1916 if head == (R_HEADS + name):
1917 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001918
David Pursehouse8a68ff92012-09-24 12:15:13 +09001919 all_refs = self.bare_ref.all
Gavin Makea2e3302023-03-11 06:46:20 +00001920 if R_HEADS + name in all_refs:
1921 return GitCommand(self, ["checkout", "-q", name, "--"]).Wait() == 0
Shawn O. Pearce88443382010-10-08 10:02:09 +02001922
Gavin Makea2e3302023-03-11 06:46:20 +00001923 branch = self.GetBranch(name)
1924 branch.remote = self.GetRemote()
1925 branch.merge = branch_merge
1926 if not branch.merge.startswith("refs/") and not ID_RE.match(
1927 branch_merge
1928 ):
1929 branch.merge = R_HEADS + branch_merge
Shawn O. Pearce88443382010-10-08 10:02:09 +02001930
Gavin Makea2e3302023-03-11 06:46:20 +00001931 if revision is None:
1932 revid = self.GetRevisionId(all_refs)
Shawn O. Pearce88443382010-10-08 10:02:09 +02001933 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001934 revid = self.work_git.rev_parse(revision)
Brian Harring14a66742012-09-28 20:21:57 -07001935
Gavin Makea2e3302023-03-11 06:46:20 +00001936 if head.startswith(R_HEADS):
Kevin Degiabaa7f32014-11-12 11:27:45 -07001937 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001938 head = all_refs[head]
1939 except KeyError:
1940 head = None
1941 if revid and head and revid == head:
1942 ref = R_HEADS + name
1943 self.work_git.update_ref(ref, revid)
1944 self.work_git.symbolic_ref(HEAD, ref)
1945 branch.Save()
1946 return True
Kevin Degib1a07b82015-07-27 13:33:43 -06001947
Gavin Makea2e3302023-03-11 06:46:20 +00001948 if (
1949 GitCommand(
1950 self, ["checkout", "-q", "-b", branch.name, revid]
1951 ).Wait()
1952 == 0
1953 ):
1954 branch.Save()
1955 return True
1956 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06001957
Gavin Makea2e3302023-03-11 06:46:20 +00001958 def CheckoutBranch(self, name):
1959 """Checkout a local topic branch.
Shawn O. Pearce2816d4f2009-03-03 17:53:18 -08001960
Gavin Makea2e3302023-03-11 06:46:20 +00001961 Args:
1962 name: The name of the branch to checkout.
Shawn O. Pearce88443382010-10-08 10:02:09 +02001963
Gavin Makea2e3302023-03-11 06:46:20 +00001964 Returns:
1965 True if the checkout succeeded; False if it didn't; None if the
1966 branch didn't exist.
1967 """
1968 rev = R_HEADS + name
1969 head = self.work_git.GetHead()
1970 if head == rev:
1971 # Already on the branch.
1972 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001973
Gavin Makea2e3302023-03-11 06:46:20 +00001974 all_refs = self.bare_ref.all
1975 try:
1976 revid = all_refs[rev]
1977 except KeyError:
1978 # Branch does not exist in this project.
1979 return None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001980
Gavin Makea2e3302023-03-11 06:46:20 +00001981 if head.startswith(R_HEADS):
1982 try:
1983 head = all_refs[head]
1984 except KeyError:
1985 head = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001986
Gavin Makea2e3302023-03-11 06:46:20 +00001987 if head == revid:
1988 # Same revision; just update HEAD to point to the new
1989 # target branch, but otherwise take no other action.
1990 _lwrite(
1991 self.work_git.GetDotgitPath(subpath=HEAD),
1992 "ref: %s%s\n" % (R_HEADS, name),
1993 )
1994 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05001995
Gavin Makea2e3302023-03-11 06:46:20 +00001996 return (
1997 GitCommand(
1998 self,
1999 ["checkout", name, "--"],
2000 capture_stdout=True,
2001 capture_stderr=True,
2002 ).Wait()
2003 == 0
2004 )
Mike Frysinger98bb7652021-12-20 21:15:59 -05002005
Gavin Makea2e3302023-03-11 06:46:20 +00002006 def AbandonBranch(self, name):
2007 """Destroy a local topic branch.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002008
Gavin Makea2e3302023-03-11 06:46:20 +00002009 Args:
2010 name: The name of the branch to abandon.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002011
Gavin Makea2e3302023-03-11 06:46:20 +00002012 Returns:
2013 True if the abandon succeeded; False if it didn't; None if the
2014 branch didn't exist.
2015 """
2016 rev = R_HEADS + name
2017 all_refs = self.bare_ref.all
2018 if rev not in all_refs:
2019 # Doesn't exist
2020 return None
2021
2022 head = self.work_git.GetHead()
2023 if head == rev:
2024 # We can't destroy the branch while we are sitting
2025 # on it. Switch to a detached HEAD.
2026 head = all_refs[head]
2027
2028 revid = self.GetRevisionId(all_refs)
2029 if head == revid:
2030 _lwrite(
2031 self.work_git.GetDotgitPath(subpath=HEAD), "%s\n" % revid
2032 )
2033 else:
2034 self._Checkout(revid, quiet=True)
2035
2036 return (
2037 GitCommand(
2038 self,
2039 ["branch", "-D", name],
2040 capture_stdout=True,
2041 capture_stderr=True,
2042 ).Wait()
2043 == 0
2044 )
2045
2046 def PruneHeads(self):
2047 """Prune any topic branches already merged into upstream."""
2048 cb = self.CurrentBranch
2049 kill = []
2050 left = self._allrefs
2051 for name in left.keys():
2052 if name.startswith(R_HEADS):
2053 name = name[len(R_HEADS) :]
2054 if cb is None or name != cb:
2055 kill.append(name)
2056
2057 # Minor optimization: If there's nothing to prune, then don't try to
2058 # read any project state.
2059 if not kill and not cb:
2060 return []
2061
2062 rev = self.GetRevisionId(left)
2063 if (
2064 cb is not None
2065 and not self._revlist(HEAD + "..." + rev)
2066 and not self.IsDirty(consider_untracked=False)
2067 ):
2068 self.work_git.DetachHead(HEAD)
2069 kill.append(cb)
2070
2071 if kill:
2072 old = self.bare_git.GetHead()
2073
2074 try:
2075 self.bare_git.DetachHead(rev)
2076
2077 b = ["branch", "-d"]
2078 b.extend(kill)
2079 b = GitCommand(
2080 self, b, bare=True, capture_stdout=True, capture_stderr=True
2081 )
2082 b.Wait()
2083 finally:
2084 if ID_RE.match(old):
2085 self.bare_git.DetachHead(old)
2086 else:
2087 self.bare_git.SetHead(old)
2088 left = self._allrefs
2089
2090 for branch in kill:
2091 if (R_HEADS + branch) not in left:
2092 self.CleanPublishedCache()
2093 break
2094
2095 if cb and cb not in kill:
2096 kill.append(cb)
2097 kill.sort()
2098
2099 kept = []
2100 for branch in kill:
2101 if R_HEADS + branch in left:
2102 branch = self.GetBranch(branch)
2103 base = branch.LocalMerge
2104 if not base:
2105 base = rev
2106 kept.append(ReviewableBranch(self, branch, base))
2107 return kept
2108
2109 def GetRegisteredSubprojects(self):
2110 result = []
2111
2112 def rec(subprojects):
2113 if not subprojects:
2114 return
2115 result.extend(subprojects)
2116 for p in subprojects:
2117 rec(p.subprojects)
2118
2119 rec(self.subprojects)
2120 return result
2121
2122 def _GetSubmodules(self):
2123 # Unfortunately we cannot call `git submodule status --recursive` here
2124 # because the working tree might not exist yet, and it cannot be used
2125 # without a working tree in its current implementation.
2126
2127 def get_submodules(gitdir, rev):
2128 # Parse .gitmodules for submodule sub_paths and sub_urls.
2129 sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
2130 if not sub_paths:
2131 return []
2132 # Run `git ls-tree` to read SHAs of submodule object, which happen
2133 # to be revision of submodule repository.
2134 sub_revs = git_ls_tree(gitdir, rev, sub_paths)
2135 submodules = []
2136 for sub_path, sub_url in zip(sub_paths, sub_urls):
2137 try:
2138 sub_rev = sub_revs[sub_path]
2139 except KeyError:
2140 # Ignore non-exist submodules.
2141 continue
2142 submodules.append((sub_rev, sub_path, sub_url))
2143 return submodules
2144
2145 re_path = re.compile(r"^submodule\.(.+)\.path=(.*)$")
2146 re_url = re.compile(r"^submodule\.(.+)\.url=(.*)$")
2147
2148 def parse_gitmodules(gitdir, rev):
2149 cmd = ["cat-file", "blob", "%s:.gitmodules" % rev]
2150 try:
2151 p = GitCommand(
2152 None,
2153 cmd,
2154 capture_stdout=True,
2155 capture_stderr=True,
2156 bare=True,
2157 gitdir=gitdir,
2158 )
2159 except GitError:
2160 return [], []
2161 if p.Wait() != 0:
2162 return [], []
2163
2164 gitmodules_lines = []
2165 fd, temp_gitmodules_path = tempfile.mkstemp()
2166 try:
2167 os.write(fd, p.stdout.encode("utf-8"))
2168 os.close(fd)
2169 cmd = ["config", "--file", temp_gitmodules_path, "--list"]
2170 p = GitCommand(
2171 None,
2172 cmd,
2173 capture_stdout=True,
2174 capture_stderr=True,
2175 bare=True,
2176 gitdir=gitdir,
2177 )
2178 if p.Wait() != 0:
2179 return [], []
2180 gitmodules_lines = p.stdout.split("\n")
2181 except GitError:
2182 return [], []
2183 finally:
2184 platform_utils.remove(temp_gitmodules_path)
2185
2186 names = set()
2187 paths = {}
2188 urls = {}
2189 for line in gitmodules_lines:
2190 if not line:
2191 continue
2192 m = re_path.match(line)
2193 if m:
2194 names.add(m.group(1))
2195 paths[m.group(1)] = m.group(2)
2196 continue
2197 m = re_url.match(line)
2198 if m:
2199 names.add(m.group(1))
2200 urls[m.group(1)] = m.group(2)
2201 continue
2202 names = sorted(names)
2203 return (
2204 [paths.get(name, "") for name in names],
2205 [urls.get(name, "") for name in names],
2206 )
2207
2208 def git_ls_tree(gitdir, rev, paths):
2209 cmd = ["ls-tree", rev, "--"]
2210 cmd.extend(paths)
2211 try:
2212 p = GitCommand(
2213 None,
2214 cmd,
2215 capture_stdout=True,
2216 capture_stderr=True,
2217 bare=True,
2218 gitdir=gitdir,
2219 )
2220 except GitError:
2221 return []
2222 if p.Wait() != 0:
2223 return []
2224 objects = {}
2225 for line in p.stdout.split("\n"):
2226 if not line.strip():
2227 continue
2228 object_rev, object_path = line.split()[2:4]
2229 objects[object_path] = object_rev
2230 return objects
2231
2232 try:
2233 rev = self.GetRevisionId()
2234 except GitError:
2235 return []
2236 return get_submodules(self.gitdir, rev)
2237
2238 def GetDerivedSubprojects(self):
2239 result = []
2240 if not self.Exists:
2241 # If git repo does not exist yet, querying its submodules will
2242 # mess up its states; so return here.
2243 return result
2244 for rev, path, url in self._GetSubmodules():
2245 name = self.manifest.GetSubprojectName(self, path)
2246 (
2247 relpath,
2248 worktree,
2249 gitdir,
2250 objdir,
2251 ) = self.manifest.GetSubprojectPaths(self, name, path)
2252 project = self.manifest.paths.get(relpath)
2253 if project:
2254 result.extend(project.GetDerivedSubprojects())
2255 continue
2256
2257 if url.startswith(".."):
2258 url = urllib.parse.urljoin("%s/" % self.remote.url, url)
2259 remote = RemoteSpec(
2260 self.remote.name,
2261 url=url,
2262 pushUrl=self.remote.pushUrl,
2263 review=self.remote.review,
2264 revision=self.remote.revision,
2265 )
2266 subproject = Project(
2267 manifest=self.manifest,
2268 name=name,
2269 remote=remote,
2270 gitdir=gitdir,
2271 objdir=objdir,
2272 worktree=worktree,
2273 relpath=relpath,
2274 revisionExpr=rev,
2275 revisionId=rev,
2276 rebase=self.rebase,
2277 groups=self.groups,
2278 sync_c=self.sync_c,
2279 sync_s=self.sync_s,
2280 sync_tags=self.sync_tags,
2281 parent=self,
2282 is_derived=True,
2283 )
2284 result.append(subproject)
2285 result.extend(subproject.GetDerivedSubprojects())
2286 return result
2287
2288 def EnableRepositoryExtension(self, key, value="true", version=1):
2289 """Enable git repository extension |key| with |value|.
2290
2291 Args:
2292 key: The extension to enabled. Omit the "extensions." prefix.
2293 value: The value to use for the extension.
2294 version: The minimum git repository version needed.
2295 """
2296 # Make sure the git repo version is new enough already.
2297 found_version = self.config.GetInt("core.repositoryFormatVersion")
2298 if found_version is None:
2299 found_version = 0
2300 if found_version < version:
2301 self.config.SetString("core.repositoryFormatVersion", str(version))
2302
2303 # Enable the extension!
2304 self.config.SetString("extensions.%s" % (key,), value)
2305
2306 def ResolveRemoteHead(self, name=None):
2307 """Find out what the default branch (HEAD) points to.
2308
2309 Normally this points to refs/heads/master, but projects are moving to
2310 main. Support whatever the server uses rather than hardcoding "master"
2311 ourselves.
2312 """
2313 if name is None:
2314 name = self.remote.name
2315
2316 # The output will look like (NB: tabs are separators):
2317 # ref: refs/heads/master HEAD
2318 # 5f6803b100bb3cd0f534e96e88c91373e8ed1c44 HEAD
2319 output = self.bare_git.ls_remote(
2320 "-q", "--symref", "--exit-code", name, "HEAD"
2321 )
2322
2323 for line in output.splitlines():
2324 lhs, rhs = line.split("\t", 1)
2325 if rhs == "HEAD" and lhs.startswith("ref:"):
2326 return lhs[4:].strip()
2327
2328 return None
2329
2330 def _CheckForImmutableRevision(self):
2331 try:
2332 # if revision (sha or tag) is not present then following function
2333 # throws an error.
2334 self.bare_git.rev_list(
2335 "-1", "--missing=allow-any", "%s^0" % self.revisionExpr, "--"
2336 )
2337 if self.upstream:
2338 rev = self.GetRemote().ToLocal(self.upstream)
2339 self.bare_git.rev_list(
2340 "-1", "--missing=allow-any", "%s^0" % rev, "--"
2341 )
2342 self.bare_git.merge_base(
2343 "--is-ancestor", self.revisionExpr, rev
2344 )
2345 return True
2346 except GitError:
2347 # There is no such persistent revision. We have to fetch it.
2348 return False
2349
2350 def _FetchArchive(self, tarpath, cwd=None):
2351 cmd = ["archive", "-v", "-o", tarpath]
2352 cmd.append("--remote=%s" % self.remote.url)
2353 cmd.append("--prefix=%s/" % self.RelPath(local=False))
2354 cmd.append(self.revisionExpr)
2355
2356 command = GitCommand(
2357 self, cmd, cwd=cwd, capture_stdout=True, capture_stderr=True
2358 )
2359
2360 if command.Wait() != 0:
2361 raise GitError("git archive %s: %s" % (self.name, command.stderr))
2362
2363 def _RemoteFetch(
2364 self,
2365 name=None,
2366 current_branch_only=False,
2367 initial=False,
2368 quiet=False,
2369 verbose=False,
2370 output_redir=None,
2371 alt_dir=None,
2372 tags=True,
2373 prune=False,
2374 depth=None,
2375 submodules=False,
2376 ssh_proxy=None,
2377 force_sync=False,
2378 clone_filter=None,
2379 retry_fetches=2,
2380 retry_sleep_initial_sec=4.0,
2381 retry_exp_factor=2.0,
2382 ):
2383 is_sha1 = False
2384 tag_name = None
2385 # The depth should not be used when fetching to a mirror because
2386 # it will result in a shallow repository that cannot be cloned or
2387 # fetched from.
2388 # The repo project should also never be synced with partial depth.
2389 if self.manifest.IsMirror or self.relpath == ".repo/repo":
2390 depth = None
2391
2392 if depth:
2393 current_branch_only = True
2394
2395 if ID_RE.match(self.revisionExpr) is not None:
2396 is_sha1 = True
2397
2398 if current_branch_only:
2399 if self.revisionExpr.startswith(R_TAGS):
2400 # This is a tag and its commit id should never change.
2401 tag_name = self.revisionExpr[len(R_TAGS) :]
2402 elif self.upstream and self.upstream.startswith(R_TAGS):
2403 # This is a tag and its commit id should never change.
2404 tag_name = self.upstream[len(R_TAGS) :]
2405
2406 if is_sha1 or tag_name is not None:
2407 if self._CheckForImmutableRevision():
2408 if verbose:
2409 print(
2410 "Skipped fetching project %s (already have "
2411 "persistent ref)" % self.name
2412 )
2413 return True
2414 if is_sha1 and not depth:
2415 # When syncing a specific commit and --depth is not set:
2416 # * if upstream is explicitly specified and is not a sha1, fetch
2417 # only upstream as users expect only upstream to be fetch.
2418 # Note: The commit might not be in upstream in which case the
2419 # sync will fail.
2420 # * otherwise, fetch all branches to make sure we end up with
2421 # the specific commit.
2422 if self.upstream:
2423 current_branch_only = not ID_RE.match(self.upstream)
2424 else:
2425 current_branch_only = False
2426
2427 if not name:
2428 name = self.remote.name
2429
2430 remote = self.GetRemote(name)
2431 if not remote.PreConnectFetch(ssh_proxy):
2432 ssh_proxy = None
2433
2434 if initial:
2435 if alt_dir and "objects" == os.path.basename(alt_dir):
2436 ref_dir = os.path.dirname(alt_dir)
2437 packed_refs = os.path.join(self.gitdir, "packed-refs")
2438
2439 all_refs = self.bare_ref.all
2440 ids = set(all_refs.values())
2441 tmp = set()
2442
2443 for r, ref_id in GitRefs(ref_dir).all.items():
2444 if r not in all_refs:
2445 if r.startswith(R_TAGS) or remote.WritesTo(r):
2446 all_refs[r] = ref_id
2447 ids.add(ref_id)
2448 continue
2449
2450 if ref_id in ids:
2451 continue
2452
2453 r = "refs/_alt/%s" % ref_id
2454 all_refs[r] = ref_id
2455 ids.add(ref_id)
2456 tmp.add(r)
2457
2458 tmp_packed_lines = []
2459 old_packed_lines = []
2460
2461 for r in sorted(all_refs):
2462 line = "%s %s\n" % (all_refs[r], r)
2463 tmp_packed_lines.append(line)
2464 if r not in tmp:
2465 old_packed_lines.append(line)
2466
2467 tmp_packed = "".join(tmp_packed_lines)
2468 old_packed = "".join(old_packed_lines)
2469 _lwrite(packed_refs, tmp_packed)
2470 else:
2471 alt_dir = None
2472
2473 cmd = ["fetch"]
2474
2475 if clone_filter:
2476 git_require((2, 19, 0), fail=True, msg="partial clones")
2477 cmd.append("--filter=%s" % clone_filter)
2478 self.EnableRepositoryExtension("partialclone", self.remote.name)
2479
2480 if depth:
2481 cmd.append("--depth=%s" % depth)
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002482 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002483 # If this repo has shallow objects, then we don't know which refs
2484 # have shallow objects or not. Tell git to unshallow all fetched
2485 # refs. Don't do this with projects that don't have shallow
2486 # objects, since it is less efficient.
2487 if os.path.exists(os.path.join(self.gitdir, "shallow")):
2488 cmd.append("--depth=2147483647")
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002489
Gavin Makea2e3302023-03-11 06:46:20 +00002490 if not verbose:
2491 cmd.append("--quiet")
2492 if not quiet and sys.stdout.isatty():
2493 cmd.append("--progress")
2494 if not self.worktree:
2495 cmd.append("--update-head-ok")
2496 cmd.append(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002497
Gavin Makea2e3302023-03-11 06:46:20 +00002498 if force_sync:
2499 cmd.append("--force")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002500
Gavin Makea2e3302023-03-11 06:46:20 +00002501 if prune:
2502 cmd.append("--prune")
Mike Frysinger29626b42021-05-01 09:37:13 -04002503
Gavin Makea2e3302023-03-11 06:46:20 +00002504 # Always pass something for --recurse-submodules, git with GIT_DIR
2505 # behaves incorrectly when not given `--recurse-submodules=no`.
2506 # (b/218891912)
2507 cmd.append(
2508 f'--recurse-submodules={"on-demand" if submodules else "no"}'
2509 )
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002510
Gavin Makea2e3302023-03-11 06:46:20 +00002511 spec = []
2512 if not current_branch_only:
2513 # Fetch whole repo.
2514 spec.append(
2515 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2516 )
2517 elif tag_name is not None:
2518 spec.append("tag")
2519 spec.append(tag_name)
Remy Böhmer1469c282020-12-15 18:49:02 +01002520
Gavin Makea2e3302023-03-11 06:46:20 +00002521 if self.manifest.IsMirror and not current_branch_only:
2522 branch = None
Remy Böhmer1469c282020-12-15 18:49:02 +01002523 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002524 branch = self.revisionExpr
2525 if (
2526 not self.manifest.IsMirror
2527 and is_sha1
2528 and depth
2529 and git_require((1, 8, 3))
2530 ):
2531 # Shallow checkout of a specific commit, fetch from that commit and
2532 # not the heads only as the commit might be deeper in the history.
2533 spec.append(branch)
2534 if self.upstream:
2535 spec.append(self.upstream)
David James8d201162013-10-11 17:03:19 -07002536 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002537 if is_sha1:
2538 branch = self.upstream
2539 if branch is not None and branch.strip():
2540 if not branch.startswith("refs/"):
2541 branch = R_HEADS + branch
2542 spec.append(str(("+%s:" % branch) + remote.ToLocal(branch)))
David James8d201162013-10-11 17:03:19 -07002543
Gavin Makea2e3302023-03-11 06:46:20 +00002544 # If mirroring repo and we cannot deduce the tag or branch to fetch,
2545 # fetch whole repo.
2546 if self.manifest.IsMirror and not spec:
2547 spec.append(
2548 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2549 )
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002550
Gavin Makea2e3302023-03-11 06:46:20 +00002551 # If using depth then we should not get all the tags since they may
2552 # be outside of the depth.
2553 if not tags or depth:
2554 cmd.append("--no-tags")
2555 else:
2556 cmd.append("--tags")
2557 spec.append(str(("+refs/tags/*:") + remote.ToLocal("refs/tags/*")))
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002558
Gavin Makea2e3302023-03-11 06:46:20 +00002559 cmd.extend(spec)
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002560
Gavin Makea2e3302023-03-11 06:46:20 +00002561 # At least one retry minimum due to git remote prune.
2562 retry_fetches = max(retry_fetches, 2)
2563 retry_cur_sleep = retry_sleep_initial_sec
2564 ok = prune_tried = False
2565 for try_n in range(retry_fetches):
2566 gitcmd = GitCommand(
2567 self,
2568 cmd,
2569 bare=True,
2570 objdir=os.path.join(self.objdir, "objects"),
2571 ssh_proxy=ssh_proxy,
2572 merge_output=True,
2573 capture_stdout=quiet or bool(output_redir),
2574 )
2575 if gitcmd.stdout and not quiet and output_redir:
2576 output_redir.write(gitcmd.stdout)
2577 ret = gitcmd.Wait()
2578 if ret == 0:
2579 ok = True
2580 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002581
Gavin Makea2e3302023-03-11 06:46:20 +00002582 # Retry later due to HTTP 429 Too Many Requests.
2583 elif (
2584 gitcmd.stdout
2585 and "error:" in gitcmd.stdout
2586 and "HTTP 429" in gitcmd.stdout
2587 ):
2588 # Fallthru to sleep+retry logic at the bottom.
2589 pass
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002590
Gavin Makea2e3302023-03-11 06:46:20 +00002591 # Try to prune remote branches once in case there are conflicts.
2592 # For example, if the remote had refs/heads/upstream, but deleted
2593 # that and now has refs/heads/upstream/foo.
2594 elif (
2595 gitcmd.stdout
2596 and "error:" in gitcmd.stdout
2597 and "git remote prune" in gitcmd.stdout
2598 and not prune_tried
2599 ):
2600 prune_tried = True
2601 prunecmd = GitCommand(
2602 self,
2603 ["remote", "prune", name],
2604 bare=True,
2605 ssh_proxy=ssh_proxy,
2606 )
2607 ret = prunecmd.Wait()
2608 if ret:
2609 break
2610 print(
2611 "retrying fetch after pruning remote branches",
2612 file=output_redir,
2613 )
2614 # Continue right away so we don't sleep as we shouldn't need to.
2615 continue
2616 elif current_branch_only and is_sha1 and ret == 128:
2617 # Exit code 128 means "couldn't find the ref you asked for"; if
2618 # we're in sha1 mode, we just tried sync'ing from the upstream
2619 # field; it doesn't exist, thus abort the optimization attempt
2620 # and do a full sync.
2621 break
2622 elif ret < 0:
2623 # Git died with a signal, exit immediately.
2624 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002625
Gavin Makea2e3302023-03-11 06:46:20 +00002626 # Figure out how long to sleep before the next attempt, if there is
2627 # one.
2628 if not verbose and gitcmd.stdout:
2629 print(
2630 "\n%s:\n%s" % (self.name, gitcmd.stdout),
2631 end="",
2632 file=output_redir,
2633 )
2634 if try_n < retry_fetches - 1:
2635 print(
2636 "%s: sleeping %s seconds before retrying"
2637 % (self.name, retry_cur_sleep),
2638 file=output_redir,
2639 )
2640 time.sleep(retry_cur_sleep)
2641 retry_cur_sleep = min(
2642 retry_exp_factor * retry_cur_sleep, MAXIMUM_RETRY_SLEEP_SEC
2643 )
2644 retry_cur_sleep *= 1 - random.uniform(
2645 -RETRY_JITTER_PERCENT, RETRY_JITTER_PERCENT
2646 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002647
Gavin Makea2e3302023-03-11 06:46:20 +00002648 if initial:
2649 if alt_dir:
2650 if old_packed != "":
2651 _lwrite(packed_refs, old_packed)
2652 else:
2653 platform_utils.remove(packed_refs)
2654 self.bare_git.pack_refs("--all", "--prune")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002655
Gavin Makea2e3302023-03-11 06:46:20 +00002656 if is_sha1 and current_branch_only:
2657 # We just synced the upstream given branch; verify we
2658 # got what we wanted, else trigger a second run of all
2659 # refs.
2660 if not self._CheckForImmutableRevision():
2661 # Sync the current branch only with depth set to None.
2662 # We always pass depth=None down to avoid infinite recursion.
2663 return self._RemoteFetch(
2664 name=name,
2665 quiet=quiet,
2666 verbose=verbose,
2667 output_redir=output_redir,
2668 current_branch_only=current_branch_only and depth,
2669 initial=False,
2670 alt_dir=alt_dir,
2671 depth=None,
2672 ssh_proxy=ssh_proxy,
2673 clone_filter=clone_filter,
2674 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002675
Gavin Makea2e3302023-03-11 06:46:20 +00002676 return ok
Mike Frysingerf4545122019-11-11 04:34:16 -05002677
Gavin Makea2e3302023-03-11 06:46:20 +00002678 def _ApplyCloneBundle(self, initial=False, quiet=False, verbose=False):
2679 if initial and (
2680 self.manifest.manifestProject.depth or self.clone_depth
2681 ):
2682 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002683
Gavin Makea2e3302023-03-11 06:46:20 +00002684 remote = self.GetRemote()
2685 bundle_url = remote.url + "/clone.bundle"
2686 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
2687 if GetSchemeFromUrl(bundle_url) not in (
2688 "http",
2689 "https",
2690 "persistent-http",
2691 "persistent-https",
2692 ):
2693 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06002694
Gavin Makea2e3302023-03-11 06:46:20 +00002695 bundle_dst = os.path.join(self.gitdir, "clone.bundle")
2696 bundle_tmp = os.path.join(self.gitdir, "clone.bundle.tmp")
2697
2698 exist_dst = os.path.exists(bundle_dst)
2699 exist_tmp = os.path.exists(bundle_tmp)
2700
2701 if not initial and not exist_dst and not exist_tmp:
2702 return False
2703
2704 if not exist_dst:
2705 exist_dst = self._FetchBundle(
2706 bundle_url, bundle_tmp, bundle_dst, quiet, verbose
2707 )
2708 if not exist_dst:
2709 return False
2710
2711 cmd = ["fetch"]
2712 if not verbose:
2713 cmd.append("--quiet")
2714 if not quiet and sys.stdout.isatty():
2715 cmd.append("--progress")
2716 if not self.worktree:
2717 cmd.append("--update-head-ok")
2718 cmd.append(bundle_dst)
2719 for f in remote.fetch:
2720 cmd.append(str(f))
2721 cmd.append("+refs/tags/*:refs/tags/*")
2722
2723 ok = (
2724 GitCommand(
2725 self,
2726 cmd,
2727 bare=True,
2728 objdir=os.path.join(self.objdir, "objects"),
2729 ).Wait()
2730 == 0
2731 )
2732 platform_utils.remove(bundle_dst, missing_ok=True)
2733 platform_utils.remove(bundle_tmp, missing_ok=True)
2734 return ok
2735
2736 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
2737 platform_utils.remove(dstPath, missing_ok=True)
2738
2739 cmd = ["curl", "--fail", "--output", tmpPath, "--netrc", "--location"]
2740 if quiet:
2741 cmd += ["--silent", "--show-error"]
2742 if os.path.exists(tmpPath):
2743 size = os.stat(tmpPath).st_size
2744 if size >= 1024:
2745 cmd += ["--continue-at", "%d" % (size,)]
2746 else:
2747 platform_utils.remove(tmpPath)
2748 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
2749 if cookiefile:
2750 cmd += ["--cookie", cookiefile]
2751 if proxy:
2752 cmd += ["--proxy", proxy]
2753 elif "http_proxy" in os.environ and "darwin" == sys.platform:
2754 cmd += ["--proxy", os.environ["http_proxy"]]
2755 if srcUrl.startswith("persistent-https"):
2756 srcUrl = "http" + srcUrl[len("persistent-https") :]
2757 elif srcUrl.startswith("persistent-http"):
2758 srcUrl = "http" + srcUrl[len("persistent-http") :]
2759 cmd += [srcUrl]
2760
2761 proc = None
2762 with Trace("Fetching bundle: %s", " ".join(cmd)):
2763 if verbose:
2764 print("%s: Downloading bundle: %s" % (self.name, srcUrl))
2765 stdout = None if verbose else subprocess.PIPE
2766 stderr = None if verbose else subprocess.STDOUT
2767 try:
2768 proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
2769 except OSError:
2770 return False
2771
2772 (output, _) = proc.communicate()
2773 curlret = proc.returncode
2774
2775 if curlret == 22:
2776 # From curl man page:
2777 # 22: HTTP page not retrieved. The requested url was not found
2778 # or returned another error with the HTTP error code being 400
2779 # or above. This return code only appears if -f, --fail is used.
2780 if verbose:
2781 print(
2782 "%s: Unable to retrieve clone.bundle; ignoring."
2783 % self.name
2784 )
2785 if output:
2786 print("Curl output:\n%s" % output)
2787 return False
2788 elif curlret and not verbose and output:
2789 print("%s" % output, file=sys.stderr)
2790
2791 if os.path.exists(tmpPath):
2792 if curlret == 0 and self._IsValidBundle(tmpPath, quiet):
2793 platform_utils.rename(tmpPath, dstPath)
2794 return True
2795 else:
2796 platform_utils.remove(tmpPath)
2797 return False
2798 else:
2799 return False
2800
2801 def _IsValidBundle(self, path, quiet):
2802 try:
2803 with open(path, "rb") as f:
2804 if f.read(16) == b"# v2 git bundle\n":
2805 return True
2806 else:
2807 if not quiet:
2808 print(
2809 "Invalid clone.bundle file; ignoring.",
2810 file=sys.stderr,
2811 )
2812 return False
2813 except OSError:
2814 return False
2815
2816 def _Checkout(self, rev, quiet=False):
2817 cmd = ["checkout"]
2818 if quiet:
2819 cmd.append("-q")
2820 cmd.append(rev)
2821 cmd.append("--")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002822 if GitCommand(self, cmd).Wait() != 0:
Gavin Makea2e3302023-03-11 06:46:20 +00002823 if self._allrefs:
2824 raise GitError("%s checkout %s " % (self.name, rev))
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002825
Gavin Makea2e3302023-03-11 06:46:20 +00002826 def _CherryPick(self, rev, ffonly=False, record_origin=False):
2827 cmd = ["cherry-pick"]
2828 if ffonly:
2829 cmd.append("--ff")
2830 if record_origin:
2831 cmd.append("-x")
2832 cmd.append(rev)
2833 cmd.append("--")
2834 if GitCommand(self, cmd).Wait() != 0:
2835 if self._allrefs:
2836 raise GitError("%s cherry-pick %s " % (self.name, rev))
Victor Boivie0960b5b2010-11-26 13:42:13 +01002837
Gavin Makea2e3302023-03-11 06:46:20 +00002838 def _LsRemote(self, refs):
2839 cmd = ["ls-remote", self.remote.name, refs]
2840 p = GitCommand(self, cmd, capture_stdout=True)
2841 if p.Wait() == 0:
2842 return p.stdout
2843 return None
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002844
Gavin Makea2e3302023-03-11 06:46:20 +00002845 def _Revert(self, rev):
2846 cmd = ["revert"]
2847 cmd.append("--no-edit")
2848 cmd.append(rev)
2849 cmd.append("--")
2850 if GitCommand(self, cmd).Wait() != 0:
2851 if self._allrefs:
2852 raise GitError("%s revert %s " % (self.name, rev))
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002853
Gavin Makea2e3302023-03-11 06:46:20 +00002854 def _ResetHard(self, rev, quiet=True):
2855 cmd = ["reset", "--hard"]
2856 if quiet:
2857 cmd.append("-q")
2858 cmd.append(rev)
2859 if GitCommand(self, cmd).Wait() != 0:
2860 raise GitError("%s reset --hard %s " % (self.name, rev))
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002861
Gavin Makea2e3302023-03-11 06:46:20 +00002862 def _SyncSubmodules(self, quiet=True):
2863 cmd = ["submodule", "update", "--init", "--recursive"]
2864 if quiet:
2865 cmd.append("-q")
2866 if GitCommand(self, cmd).Wait() != 0:
2867 raise GitError(
2868 "%s submodule update --init --recursive " % self.name
2869 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002870
Gavin Makea2e3302023-03-11 06:46:20 +00002871 def _Rebase(self, upstream, onto=None):
2872 cmd = ["rebase"]
2873 if onto is not None:
2874 cmd.extend(["--onto", onto])
2875 cmd.append(upstream)
2876 if GitCommand(self, cmd).Wait() != 0:
2877 raise GitError("%s rebase %s " % (self.name, upstream))
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002878
Gavin Makea2e3302023-03-11 06:46:20 +00002879 def _FastForward(self, head, ffonly=False):
2880 cmd = ["merge", "--no-stat", head]
2881 if ffonly:
2882 cmd.append("--ff-only")
2883 if GitCommand(self, cmd).Wait() != 0:
2884 raise GitError("%s merge %s " % (self.name, head))
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002885
Gavin Makea2e3302023-03-11 06:46:20 +00002886 def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False):
2887 init_git_dir = not os.path.exists(self.gitdir)
2888 init_obj_dir = not os.path.exists(self.objdir)
2889 try:
2890 # Initialize the bare repository, which contains all of the objects.
2891 if init_obj_dir:
2892 os.makedirs(self.objdir)
2893 self.bare_objdir.init()
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002894
Gavin Makea2e3302023-03-11 06:46:20 +00002895 self._UpdateHooks(quiet=quiet)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002896
Gavin Makea2e3302023-03-11 06:46:20 +00002897 if self.use_git_worktrees:
2898 # Enable per-worktree config file support if possible. This
2899 # is more a nice-to-have feature for users rather than a
2900 # hard requirement.
2901 if git_require((2, 20, 0)):
2902 self.EnableRepositoryExtension("worktreeConfig")
Renaud Paquay788e9622017-01-27 11:41:12 -08002903
Gavin Makea2e3302023-03-11 06:46:20 +00002904 # If we have a separate directory to hold refs, initialize it as
2905 # well.
2906 if self.objdir != self.gitdir:
2907 if init_git_dir:
2908 os.makedirs(self.gitdir)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002909
Gavin Makea2e3302023-03-11 06:46:20 +00002910 if init_obj_dir or init_git_dir:
2911 self._ReferenceGitDir(
2912 self.objdir, self.gitdir, copy_all=True
2913 )
2914 try:
2915 self._CheckDirReference(self.objdir, self.gitdir)
2916 except GitError as e:
2917 if force_sync:
2918 print(
2919 "Retrying clone after deleting %s" % self.gitdir,
2920 file=sys.stderr,
2921 )
2922 try:
2923 platform_utils.rmtree(
2924 platform_utils.realpath(self.gitdir)
2925 )
2926 if self.worktree and os.path.exists(
2927 platform_utils.realpath(self.worktree)
2928 ):
2929 platform_utils.rmtree(
2930 platform_utils.realpath(self.worktree)
2931 )
2932 return self._InitGitDir(
2933 mirror_git=mirror_git,
2934 force_sync=False,
2935 quiet=quiet,
2936 )
2937 except Exception:
2938 raise e
2939 raise e
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002940
Gavin Makea2e3302023-03-11 06:46:20 +00002941 if init_git_dir:
2942 mp = self.manifest.manifestProject
2943 ref_dir = mp.reference or ""
Julien Camperguedd654222014-01-09 16:21:37 +01002944
Gavin Makea2e3302023-03-11 06:46:20 +00002945 def _expanded_ref_dirs():
2946 """Iterate through possible git reference dir paths."""
2947 name = self.name + ".git"
2948 yield mirror_git or os.path.join(ref_dir, name)
2949 for prefix in "", self.remote.name:
2950 yield os.path.join(
2951 ref_dir, ".repo", "project-objects", prefix, name
2952 )
2953 yield os.path.join(
2954 ref_dir, ".repo", "worktrees", prefix, name
2955 )
2956
2957 if ref_dir or mirror_git:
2958 found_ref_dir = None
2959 for path in _expanded_ref_dirs():
2960 if os.path.exists(path):
2961 found_ref_dir = path
2962 break
2963 ref_dir = found_ref_dir
2964
2965 if ref_dir:
2966 if not os.path.isabs(ref_dir):
2967 # The alternate directory is relative to the object
2968 # database.
2969 ref_dir = os.path.relpath(
2970 ref_dir, os.path.join(self.objdir, "objects")
2971 )
2972 _lwrite(
2973 os.path.join(
2974 self.objdir, "objects/info/alternates"
2975 ),
2976 os.path.join(ref_dir, "objects") + "\n",
2977 )
2978
2979 m = self.manifest.manifestProject.config
2980 for key in ["user.name", "user.email"]:
2981 if m.Has(key, include_defaults=False):
2982 self.config.SetString(key, m.GetString(key))
2983 if not self.manifest.EnableGitLfs:
2984 self.config.SetString(
2985 "filter.lfs.smudge", "git-lfs smudge --skip -- %f"
2986 )
2987 self.config.SetString(
2988 "filter.lfs.process", "git-lfs filter-process --skip"
2989 )
2990 self.config.SetBoolean(
2991 "core.bare", True if self.manifest.IsMirror else None
2992 )
2993 except Exception:
2994 if init_obj_dir and os.path.exists(self.objdir):
2995 platform_utils.rmtree(self.objdir)
2996 if init_git_dir and os.path.exists(self.gitdir):
2997 platform_utils.rmtree(self.gitdir)
2998 raise
2999
3000 def _UpdateHooks(self, quiet=False):
3001 if os.path.exists(self.objdir):
3002 self._InitHooks(quiet=quiet)
3003
3004 def _InitHooks(self, quiet=False):
3005 hooks = platform_utils.realpath(os.path.join(self.objdir, "hooks"))
3006 if not os.path.exists(hooks):
3007 os.makedirs(hooks)
3008
3009 # Delete sample hooks. They're noise.
3010 for hook in glob.glob(os.path.join(hooks, "*.sample")):
3011 try:
3012 platform_utils.remove(hook, missing_ok=True)
3013 except PermissionError:
3014 pass
3015
3016 for stock_hook in _ProjectHooks():
3017 name = os.path.basename(stock_hook)
3018
3019 if (
3020 name in ("commit-msg",)
3021 and not self.remote.review
3022 and self is not self.manifest.manifestProject
3023 ):
3024 # Don't install a Gerrit Code Review hook if this
3025 # project does not appear to use it for reviews.
3026 #
3027 # Since the manifest project is one of those, but also
3028 # managed through gerrit, it's excluded.
3029 continue
3030
3031 dst = os.path.join(hooks, name)
3032 if platform_utils.islink(dst):
3033 continue
3034 if os.path.exists(dst):
3035 # If the files are the same, we'll leave it alone. We create
3036 # symlinks below by default but fallback to hardlinks if the OS
3037 # blocks them. So if we're here, it's probably because we made a
3038 # hardlink below.
3039 if not filecmp.cmp(stock_hook, dst, shallow=False):
3040 if not quiet:
3041 _warn(
3042 "%s: Not replacing locally modified %s hook",
3043 self.RelPath(local=False),
3044 name,
3045 )
3046 continue
3047 try:
3048 platform_utils.symlink(
3049 os.path.relpath(stock_hook, os.path.dirname(dst)), dst
3050 )
3051 except OSError as e:
3052 if e.errno == errno.EPERM:
3053 try:
3054 os.link(stock_hook, dst)
3055 except OSError:
3056 raise GitError(self._get_symlink_error_message())
3057 else:
3058 raise
3059
3060 def _InitRemote(self):
3061 if self.remote.url:
3062 remote = self.GetRemote()
3063 remote.url = self.remote.url
3064 remote.pushUrl = self.remote.pushUrl
3065 remote.review = self.remote.review
3066 remote.projectname = self.name
3067
3068 if self.worktree:
3069 remote.ResetFetch(mirror=False)
3070 else:
3071 remote.ResetFetch(mirror=True)
3072 remote.Save()
3073
3074 def _InitMRef(self):
3075 """Initialize the pseudo m/<manifest branch> ref."""
3076 if self.manifest.branch:
3077 if self.use_git_worktrees:
3078 # Set up the m/ space to point to the worktree-specific ref
3079 # space. We'll update the worktree-specific ref space on each
3080 # checkout.
3081 ref = R_M + self.manifest.branch
3082 if not self.bare_ref.symref(ref):
3083 self.bare_git.symbolic_ref(
3084 "-m",
3085 "redirecting to worktree scope",
3086 ref,
3087 R_WORKTREE_M + self.manifest.branch,
3088 )
3089
3090 # We can't update this ref with git worktrees until it exists.
3091 # We'll wait until the initial checkout to set it.
3092 if not os.path.exists(self.worktree):
3093 return
3094
3095 base = R_WORKTREE_M
3096 active_git = self.work_git
3097
3098 self._InitAnyMRef(HEAD, self.bare_git, detach=True)
3099 else:
3100 base = R_M
3101 active_git = self.bare_git
3102
3103 self._InitAnyMRef(base + self.manifest.branch, active_git)
3104
3105 def _InitMirrorHead(self):
3106 self._InitAnyMRef(HEAD, self.bare_git)
3107
3108 def _InitAnyMRef(self, ref, active_git, detach=False):
3109 """Initialize |ref| in |active_git| to the value in the manifest.
3110
3111 This points |ref| to the <project> setting in the manifest.
3112
3113 Args:
3114 ref: The branch to update.
3115 active_git: The git repository to make updates in.
3116 detach: Whether to update target of symbolic refs, or overwrite the
3117 ref directly (and thus make it non-symbolic).
3118 """
3119 cur = self.bare_ref.symref(ref)
3120
3121 if self.revisionId:
3122 if cur != "" or self.bare_ref.get(ref) != self.revisionId:
3123 msg = "manifest set to %s" % self.revisionId
3124 dst = self.revisionId + "^0"
3125 active_git.UpdateRef(ref, dst, message=msg, detach=True)
Julien Camperguedd654222014-01-09 16:21:37 +01003126 else:
Gavin Makea2e3302023-03-11 06:46:20 +00003127 remote = self.GetRemote()
3128 dst = remote.ToLocal(self.revisionExpr)
3129 if cur != dst:
3130 msg = "manifest set to %s" % self.revisionExpr
3131 if detach:
3132 active_git.UpdateRef(ref, dst, message=msg, detach=True)
3133 else:
3134 active_git.symbolic_ref("-m", msg, ref, dst)
Julien Camperguedd654222014-01-09 16:21:37 +01003135
Gavin Makea2e3302023-03-11 06:46:20 +00003136 def _CheckDirReference(self, srcdir, destdir):
3137 # Git worktrees don't use symlinks to share at all.
3138 if self.use_git_worktrees:
3139 return
Julien Camperguedd654222014-01-09 16:21:37 +01003140
Gavin Makea2e3302023-03-11 06:46:20 +00003141 for name in self.shareable_dirs:
3142 # Try to self-heal a bit in simple cases.
3143 dst_path = os.path.join(destdir, name)
3144 src_path = os.path.join(srcdir, name)
Julien Camperguedd654222014-01-09 16:21:37 +01003145
Gavin Makea2e3302023-03-11 06:46:20 +00003146 dst = platform_utils.realpath(dst_path)
3147 if os.path.lexists(dst):
3148 src = platform_utils.realpath(src_path)
3149 # Fail if the links are pointing to the wrong place.
3150 if src != dst:
3151 _error("%s is different in %s vs %s", name, destdir, srcdir)
3152 raise GitError(
3153 "--force-sync not enabled; cannot overwrite a local "
3154 "work tree. If you're comfortable with the "
3155 "possibility of losing the work tree's git metadata,"
3156 " use `repo sync --force-sync {0}` to "
3157 "proceed.".format(self.RelPath(local=False))
3158 )
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003159
Gavin Makea2e3302023-03-11 06:46:20 +00003160 def _ReferenceGitDir(self, gitdir, dotgit, copy_all):
3161 """Update |dotgit| to reference |gitdir|, using symlinks where possible.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003162
Gavin Makea2e3302023-03-11 06:46:20 +00003163 Args:
3164 gitdir: The bare git repository. Must already be initialized.
3165 dotgit: The repository you would like to initialize.
3166 copy_all: If true, copy all remaining files from |gitdir| ->
3167 |dotgit|. This saves you the effort of initializing |dotgit|
3168 yourself.
3169 """
3170 symlink_dirs = self.shareable_dirs[:]
3171 to_symlink = symlink_dirs
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003172
Gavin Makea2e3302023-03-11 06:46:20 +00003173 to_copy = []
3174 if copy_all:
3175 to_copy = platform_utils.listdir(gitdir)
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003176
Gavin Makea2e3302023-03-11 06:46:20 +00003177 dotgit = platform_utils.realpath(dotgit)
3178 for name in set(to_copy).union(to_symlink):
3179 try:
3180 src = platform_utils.realpath(os.path.join(gitdir, name))
3181 dst = os.path.join(dotgit, name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003182
Gavin Makea2e3302023-03-11 06:46:20 +00003183 if os.path.lexists(dst):
3184 continue
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003185
Gavin Makea2e3302023-03-11 06:46:20 +00003186 # If the source dir doesn't exist, create an empty dir.
3187 if name in symlink_dirs and not os.path.lexists(src):
3188 os.makedirs(src)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003189
Gavin Makea2e3302023-03-11 06:46:20 +00003190 if name in to_symlink:
3191 platform_utils.symlink(
3192 os.path.relpath(src, os.path.dirname(dst)), dst
3193 )
3194 elif copy_all and not platform_utils.islink(dst):
3195 if platform_utils.isdir(src):
3196 shutil.copytree(src, dst)
3197 elif os.path.isfile(src):
3198 shutil.copy(src, dst)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003199
Gavin Makea2e3302023-03-11 06:46:20 +00003200 except OSError as e:
3201 if e.errno == errno.EPERM:
3202 raise DownloadError(self._get_symlink_error_message())
3203 else:
3204 raise
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003205
Gavin Makea2e3302023-03-11 06:46:20 +00003206 def _InitGitWorktree(self):
3207 """Init the project using git worktrees."""
3208 self.bare_git.worktree("prune")
3209 self.bare_git.worktree(
3210 "add",
3211 "-ff",
3212 "--checkout",
3213 "--detach",
3214 "--lock",
3215 self.worktree,
3216 self.GetRevisionId(),
3217 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003218
Gavin Makea2e3302023-03-11 06:46:20 +00003219 # Rewrite the internal state files to use relative paths between the
3220 # checkouts & worktrees.
3221 dotgit = os.path.join(self.worktree, ".git")
3222 with open(dotgit, "r") as fp:
3223 # Figure out the checkout->worktree path.
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003224 setting = fp.read()
Gavin Makea2e3302023-03-11 06:46:20 +00003225 assert setting.startswith("gitdir:")
3226 git_worktree_path = setting.split(":", 1)[1].strip()
3227 # Some platforms (e.g. Windows) won't let us update dotgit in situ
3228 # because of file permissions. Delete it and recreate it from scratch
3229 # to avoid.
3230 platform_utils.remove(dotgit)
3231 # Use relative path from checkout->worktree & maintain Unix line endings
3232 # on all OS's to match git behavior.
3233 with open(dotgit, "w", newline="\n") as fp:
3234 print(
3235 "gitdir:",
3236 os.path.relpath(git_worktree_path, self.worktree),
3237 file=fp,
3238 )
3239 # Use relative path from worktree->checkout & maintain Unix line endings
3240 # on all OS's to match git behavior.
3241 with open(
3242 os.path.join(git_worktree_path, "gitdir"), "w", newline="\n"
3243 ) as fp:
3244 print(os.path.relpath(dotgit, git_worktree_path), file=fp)
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003245
Gavin Makea2e3302023-03-11 06:46:20 +00003246 self._InitMRef()
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003247
Gavin Makea2e3302023-03-11 06:46:20 +00003248 def _InitWorkTree(self, force_sync=False, submodules=False):
3249 """Setup the worktree .git path.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003250
Gavin Makea2e3302023-03-11 06:46:20 +00003251 This is the user-visible path like src/foo/.git/.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003252
Gavin Makea2e3302023-03-11 06:46:20 +00003253 With non-git-worktrees, this will be a symlink to the .repo/projects/
3254 path. With git-worktrees, this will be a .git file using "gitdir: ..."
3255 syntax.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003256
Gavin Makea2e3302023-03-11 06:46:20 +00003257 Older checkouts had .git/ directories. If we see that, migrate it.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003258
Gavin Makea2e3302023-03-11 06:46:20 +00003259 This also handles changes in the manifest. Maybe this project was
3260 backed by "foo/bar" on the server, but now it's "new/foo/bar". We have
3261 to update the path we point to under .repo/projects/ to match.
3262 """
3263 dotgit = os.path.join(self.worktree, ".git")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003264
Gavin Makea2e3302023-03-11 06:46:20 +00003265 # If using an old layout style (a directory), migrate it.
3266 if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
3267 self._MigrateOldWorkTreeGitDir(dotgit)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003268
Gavin Makea2e3302023-03-11 06:46:20 +00003269 init_dotgit = not os.path.exists(dotgit)
3270 if self.use_git_worktrees:
3271 if init_dotgit:
3272 self._InitGitWorktree()
3273 self._CopyAndLinkFiles()
3274 else:
3275 if not init_dotgit:
3276 # See if the project has changed.
3277 if platform_utils.realpath(
3278 self.gitdir
3279 ) != platform_utils.realpath(dotgit):
3280 platform_utils.remove(dotgit)
Doug Anderson37282b42011-03-04 11:54:18 -08003281
Gavin Makea2e3302023-03-11 06:46:20 +00003282 if init_dotgit or not os.path.exists(dotgit):
3283 os.makedirs(self.worktree, exist_ok=True)
3284 platform_utils.symlink(
3285 os.path.relpath(self.gitdir, self.worktree), dotgit
3286 )
Doug Anderson37282b42011-03-04 11:54:18 -08003287
Gavin Makea2e3302023-03-11 06:46:20 +00003288 if init_dotgit:
3289 _lwrite(
3290 os.path.join(dotgit, HEAD), "%s\n" % self.GetRevisionId()
3291 )
Doug Anderson37282b42011-03-04 11:54:18 -08003292
Gavin Makea2e3302023-03-11 06:46:20 +00003293 # Finish checking out the worktree.
3294 cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
3295 if GitCommand(self, cmd).Wait() != 0:
3296 raise GitError(
3297 "Cannot initialize work tree for " + self.name
3298 )
Doug Anderson37282b42011-03-04 11:54:18 -08003299
Gavin Makea2e3302023-03-11 06:46:20 +00003300 if submodules:
3301 self._SyncSubmodules(quiet=True)
3302 self._CopyAndLinkFiles()
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003303
Gavin Makea2e3302023-03-11 06:46:20 +00003304 @classmethod
3305 def _MigrateOldWorkTreeGitDir(cls, dotgit):
3306 """Migrate the old worktree .git/ dir style to a symlink.
3307
3308 This logic specifically only uses state from |dotgit| to figure out
3309 where to move content and not |self|. This way if the backing project
3310 also changed places, we only do the .git/ dir to .git symlink migration
3311 here. The path updates will happen independently.
3312 """
3313 # Figure out where in .repo/projects/ it's pointing to.
3314 if not os.path.islink(os.path.join(dotgit, "refs")):
3315 raise GitError(f"{dotgit}: unsupported checkout state")
3316 gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, "refs")))
3317
3318 # Remove known symlink paths that exist in .repo/projects/.
3319 KNOWN_LINKS = {
3320 "config",
3321 "description",
3322 "hooks",
3323 "info",
3324 "logs",
3325 "objects",
3326 "packed-refs",
3327 "refs",
3328 "rr-cache",
3329 "shallow",
3330 "svn",
3331 }
3332 # Paths that we know will be in both, but are safe to clobber in
3333 # .repo/projects/.
3334 SAFE_TO_CLOBBER = {
3335 "COMMIT_EDITMSG",
3336 "FETCH_HEAD",
3337 "HEAD",
3338 "gc.log",
3339 "gitk.cache",
3340 "index",
3341 "ORIG_HEAD",
3342 }
3343
3344 # First see if we'd succeed before starting the migration.
3345 unknown_paths = []
3346 for name in platform_utils.listdir(dotgit):
3347 # Ignore all temporary/backup names. These are common with vim &
3348 # emacs.
3349 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3350 continue
3351
3352 dotgit_path = os.path.join(dotgit, name)
3353 if name in KNOWN_LINKS:
3354 if not platform_utils.islink(dotgit_path):
3355 unknown_paths.append(f"{dotgit_path}: should be a symlink")
3356 else:
3357 gitdir_path = os.path.join(gitdir, name)
3358 if name not in SAFE_TO_CLOBBER and os.path.exists(gitdir_path):
3359 unknown_paths.append(
3360 f"{dotgit_path}: unknown file; please file a bug"
3361 )
3362 if unknown_paths:
3363 raise GitError("Aborting migration: " + "\n".join(unknown_paths))
3364
3365 # Now walk the paths and sync the .git/ to .repo/projects/.
3366 for name in platform_utils.listdir(dotgit):
3367 dotgit_path = os.path.join(dotgit, name)
3368
3369 # Ignore all temporary/backup names. These are common with vim &
3370 # emacs.
3371 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3372 platform_utils.remove(dotgit_path)
3373 elif name in KNOWN_LINKS:
3374 platform_utils.remove(dotgit_path)
3375 else:
3376 gitdir_path = os.path.join(gitdir, name)
3377 platform_utils.remove(gitdir_path, missing_ok=True)
3378 platform_utils.rename(dotgit_path, gitdir_path)
3379
3380 # Now that the dir should be empty, clear it out, and symlink it over.
3381 platform_utils.rmdir(dotgit)
3382 platform_utils.symlink(
3383 os.path.relpath(gitdir, os.path.dirname(dotgit)), dotgit
3384 )
3385
3386 def _get_symlink_error_message(self):
3387 if platform_utils.isWindows():
3388 return (
3389 "Unable to create symbolic link. Please re-run the command as "
3390 "Administrator, or see "
3391 "https://github.com/git-for-windows/git/wiki/Symbolic-Links "
3392 "for other options."
3393 )
3394 return "filesystem must support symlinks"
3395
3396 def _revlist(self, *args, **kw):
3397 a = []
3398 a.extend(args)
3399 a.append("--")
3400 return self.work_git.rev_list(*a, **kw)
3401
3402 @property
3403 def _allrefs(self):
3404 return self.bare_ref.all
3405
3406 def _getLogs(
3407 self, rev1, rev2, oneline=False, color=True, pretty_format=None
3408 ):
3409 """Get logs between two revisions of this project."""
3410 comp = ".."
3411 if rev1:
3412 revs = [rev1]
3413 if rev2:
3414 revs.extend([comp, rev2])
3415 cmd = ["log", "".join(revs)]
3416 out = DiffColoring(self.config)
3417 if out.is_on and color:
3418 cmd.append("--color")
3419 if pretty_format is not None:
3420 cmd.append("--pretty=format:%s" % pretty_format)
3421 if oneline:
3422 cmd.append("--oneline")
3423
3424 try:
3425 log = GitCommand(
3426 self, cmd, capture_stdout=True, capture_stderr=True
3427 )
3428 if log.Wait() == 0:
3429 return log.stdout
3430 except GitError:
3431 # worktree may not exist if groups changed for example. In that
3432 # case, try in gitdir instead.
3433 if not os.path.exists(self.worktree):
3434 return self.bare_git.log(*cmd[1:])
3435 else:
3436 raise
3437 return None
3438
3439 def getAddedAndRemovedLogs(
3440 self, toProject, oneline=False, color=True, pretty_format=None
3441 ):
3442 """Get the list of logs from this revision to given revisionId"""
3443 logs = {}
3444 selfId = self.GetRevisionId(self._allrefs)
3445 toId = toProject.GetRevisionId(toProject._allrefs)
3446
3447 logs["added"] = self._getLogs(
3448 selfId,
3449 toId,
3450 oneline=oneline,
3451 color=color,
3452 pretty_format=pretty_format,
3453 )
3454 logs["removed"] = self._getLogs(
3455 toId,
3456 selfId,
3457 oneline=oneline,
3458 color=color,
3459 pretty_format=pretty_format,
3460 )
3461 return logs
3462
3463 class _GitGetByExec(object):
3464 def __init__(self, project, bare, gitdir):
3465 self._project = project
3466 self._bare = bare
3467 self._gitdir = gitdir
3468
3469 # __getstate__ and __setstate__ are required for pickling because
3470 # __getattr__ exists.
3471 def __getstate__(self):
3472 return (self._project, self._bare, self._gitdir)
3473
3474 def __setstate__(self, state):
3475 self._project, self._bare, self._gitdir = state
3476
3477 def LsOthers(self):
3478 p = GitCommand(
3479 self._project,
3480 ["ls-files", "-z", "--others", "--exclude-standard"],
3481 bare=False,
3482 gitdir=self._gitdir,
3483 capture_stdout=True,
3484 capture_stderr=True,
3485 )
3486 if p.Wait() == 0:
3487 out = p.stdout
3488 if out:
3489 # Backslash is not anomalous.
3490 return out[:-1].split("\0")
3491 return []
3492
3493 def DiffZ(self, name, *args):
3494 cmd = [name]
3495 cmd.append("-z")
3496 cmd.append("--ignore-submodules")
3497 cmd.extend(args)
3498 p = GitCommand(
3499 self._project,
3500 cmd,
3501 gitdir=self._gitdir,
3502 bare=False,
3503 capture_stdout=True,
3504 capture_stderr=True,
3505 )
3506 p.Wait()
3507 r = {}
3508 out = p.stdout
3509 if out:
3510 out = iter(out[:-1].split("\0"))
3511 while out:
3512 try:
3513 info = next(out)
3514 path = next(out)
3515 except StopIteration:
3516 break
3517
3518 class _Info(object):
3519 def __init__(self, path, omode, nmode, oid, nid, state):
3520 self.path = path
3521 self.src_path = None
3522 self.old_mode = omode
3523 self.new_mode = nmode
3524 self.old_id = oid
3525 self.new_id = nid
3526
3527 if len(state) == 1:
3528 self.status = state
3529 self.level = None
3530 else:
3531 self.status = state[:1]
3532 self.level = state[1:]
3533 while self.level.startswith("0"):
3534 self.level = self.level[1:]
3535
3536 info = info[1:].split(" ")
3537 info = _Info(path, *info)
3538 if info.status in ("R", "C"):
3539 info.src_path = info.path
3540 info.path = next(out)
3541 r[info.path] = info
3542 return r
3543
3544 def GetDotgitPath(self, subpath=None):
3545 """Return the full path to the .git dir.
3546
3547 As a convenience, append |subpath| if provided.
3548 """
3549 if self._bare:
3550 dotgit = self._gitdir
3551 else:
3552 dotgit = os.path.join(self._project.worktree, ".git")
3553 if os.path.isfile(dotgit):
3554 # Git worktrees use a "gitdir:" syntax to point to the
3555 # scratch space.
3556 with open(dotgit) as fp:
3557 setting = fp.read()
3558 assert setting.startswith("gitdir:")
3559 gitdir = setting.split(":", 1)[1].strip()
3560 dotgit = os.path.normpath(
3561 os.path.join(self._project.worktree, gitdir)
3562 )
3563
3564 return dotgit if subpath is None else os.path.join(dotgit, subpath)
3565
3566 def GetHead(self):
3567 """Return the ref that HEAD points to."""
3568 path = self.GetDotgitPath(subpath=HEAD)
3569 try:
3570 with open(path) as fd:
3571 line = fd.readline()
3572 except IOError as e:
3573 raise NoManifestException(path, str(e))
3574 try:
3575 line = line.decode()
3576 except AttributeError:
3577 pass
3578 if line.startswith("ref: "):
3579 return line[5:-1]
3580 return line[:-1]
3581
3582 def SetHead(self, ref, message=None):
3583 cmdv = []
3584 if message is not None:
3585 cmdv.extend(["-m", message])
3586 cmdv.append(HEAD)
3587 cmdv.append(ref)
3588 self.symbolic_ref(*cmdv)
3589
3590 def DetachHead(self, new, message=None):
3591 cmdv = ["--no-deref"]
3592 if message is not None:
3593 cmdv.extend(["-m", message])
3594 cmdv.append(HEAD)
3595 cmdv.append(new)
3596 self.update_ref(*cmdv)
3597
3598 def UpdateRef(self, name, new, old=None, message=None, detach=False):
3599 cmdv = []
3600 if message is not None:
3601 cmdv.extend(["-m", message])
3602 if detach:
3603 cmdv.append("--no-deref")
3604 cmdv.append(name)
3605 cmdv.append(new)
3606 if old is not None:
3607 cmdv.append(old)
3608 self.update_ref(*cmdv)
3609
3610 def DeleteRef(self, name, old=None):
3611 if not old:
3612 old = self.rev_parse(name)
3613 self.update_ref("-d", name, old)
3614 self._project.bare_ref.deleted(name)
3615
3616 def rev_list(self, *args, **kw):
3617 if "format" in kw:
3618 cmdv = ["log", "--pretty=format:%s" % kw["format"]]
3619 else:
3620 cmdv = ["rev-list"]
3621 cmdv.extend(args)
3622 p = GitCommand(
3623 self._project,
3624 cmdv,
3625 bare=self._bare,
3626 gitdir=self._gitdir,
3627 capture_stdout=True,
3628 capture_stderr=True,
3629 )
3630 if p.Wait() != 0:
3631 raise GitError(
3632 "%s rev-list %s: %s"
3633 % (self._project.name, str(args), p.stderr)
3634 )
3635 return p.stdout.splitlines()
3636
3637 def __getattr__(self, name):
3638 """Allow arbitrary git commands using pythonic syntax.
3639
3640 This allows you to do things like:
3641 git_obj.rev_parse('HEAD')
3642
3643 Since we don't have a 'rev_parse' method defined, the __getattr__
3644 will run. We'll replace the '_' with a '-' and try to run a git
3645 command. Any other positional arguments will be passed to the git
3646 command, and the following keyword arguments are supported:
3647 config: An optional dict of git config options to be passed with
3648 '-c'.
3649
3650 Args:
3651 name: The name of the git command to call. Any '_' characters
3652 will be replaced with '-'.
3653
3654 Returns:
3655 A callable object that will try to call git with the named
3656 command.
3657 """
3658 name = name.replace("_", "-")
3659
3660 def runner(*args, **kwargs):
3661 cmdv = []
3662 config = kwargs.pop("config", None)
3663 for k in kwargs:
3664 raise TypeError(
3665 "%s() got an unexpected keyword argument %r" % (name, k)
3666 )
3667 if config is not None:
3668 for k, v in config.items():
3669 cmdv.append("-c")
3670 cmdv.append("%s=%s" % (k, v))
3671 cmdv.append(name)
3672 cmdv.extend(args)
3673 p = GitCommand(
3674 self._project,
3675 cmdv,
3676 bare=self._bare,
3677 gitdir=self._gitdir,
3678 capture_stdout=True,
3679 capture_stderr=True,
3680 )
3681 if p.Wait() != 0:
3682 raise GitError(
3683 "%s %s: %s" % (self._project.name, name, p.stderr)
3684 )
3685 r = p.stdout
3686 if r.endswith("\n") and r.index("\n") == len(r) - 1:
3687 return r[:-1]
3688 return r
3689
3690 return runner
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003691
3692
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003693class _PriorSyncFailedError(Exception):
Gavin Makea2e3302023-03-11 06:46:20 +00003694 def __str__(self):
3695 return "prior sync failed; rebase still in progress"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003696
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003697
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003698class _DirtyError(Exception):
Gavin Makea2e3302023-03-11 06:46:20 +00003699 def __str__(self):
3700 return "contains uncommitted changes"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003701
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003702
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003703class _InfoMessage(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003704 def __init__(self, project, text):
3705 self.project = project
3706 self.text = text
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003707
Gavin Makea2e3302023-03-11 06:46:20 +00003708 def Print(self, syncbuf):
3709 syncbuf.out.info(
3710 "%s/: %s", self.project.RelPath(local=False), self.text
3711 )
3712 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003713
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003714
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003715class _Failure(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003716 def __init__(self, project, why):
3717 self.project = project
3718 self.why = why
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003719
Gavin Makea2e3302023-03-11 06:46:20 +00003720 def Print(self, syncbuf):
3721 syncbuf.out.fail(
3722 "error: %s/: %s", self.project.RelPath(local=False), str(self.why)
3723 )
3724 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003725
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003726
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003727class _Later(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003728 def __init__(self, project, action):
3729 self.project = project
3730 self.action = action
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003731
Gavin Makea2e3302023-03-11 06:46:20 +00003732 def Run(self, syncbuf):
3733 out = syncbuf.out
3734 out.project("project %s/", self.project.RelPath(local=False))
3735 out.nl()
3736 try:
3737 self.action()
3738 out.nl()
3739 return True
3740 except GitError:
3741 out.nl()
3742 return False
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003743
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003744
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003745class _SyncColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +00003746 def __init__(self, config):
3747 super().__init__(config, "reposync")
3748 self.project = self.printer("header", attr="bold")
3749 self.info = self.printer("info")
3750 self.fail = self.printer("fail", fg="red")
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003751
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003752
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003753class SyncBuffer(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003754 def __init__(self, config, detach_head=False):
3755 self._messages = []
3756 self._failures = []
3757 self._later_queue1 = []
3758 self._later_queue2 = []
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003759
Gavin Makea2e3302023-03-11 06:46:20 +00003760 self.out = _SyncColoring(config)
3761 self.out.redirect(sys.stderr)
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003762
Gavin Makea2e3302023-03-11 06:46:20 +00003763 self.detach_head = detach_head
3764 self.clean = True
3765 self.recent_clean = True
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003766
Gavin Makea2e3302023-03-11 06:46:20 +00003767 def info(self, project, fmt, *args):
3768 self._messages.append(_InfoMessage(project, fmt % args))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003769
Gavin Makea2e3302023-03-11 06:46:20 +00003770 def fail(self, project, err=None):
3771 self._failures.append(_Failure(project, err))
David Rileye0684ad2017-04-05 00:02:59 -07003772 self._MarkUnclean()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003773
Gavin Makea2e3302023-03-11 06:46:20 +00003774 def later1(self, project, what):
3775 self._later_queue1.append(_Later(project, what))
Mike Frysinger70d861f2019-08-26 15:22:36 -04003776
Gavin Makea2e3302023-03-11 06:46:20 +00003777 def later2(self, project, what):
3778 self._later_queue2.append(_Later(project, what))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003779
Gavin Makea2e3302023-03-11 06:46:20 +00003780 def Finish(self):
3781 self._PrintMessages()
3782 self._RunLater()
3783 self._PrintMessages()
3784 return self.clean
3785
3786 def Recently(self):
3787 recent_clean = self.recent_clean
3788 self.recent_clean = True
3789 return recent_clean
3790
3791 def _MarkUnclean(self):
3792 self.clean = False
3793 self.recent_clean = False
3794
3795 def _RunLater(self):
3796 for q in ["_later_queue1", "_later_queue2"]:
3797 if not self._RunQueue(q):
3798 return
3799
3800 def _RunQueue(self, queue):
3801 for m in getattr(self, queue):
3802 if not m.Run(self):
3803 self._MarkUnclean()
3804 return False
3805 setattr(self, queue, [])
3806 return True
3807
3808 def _PrintMessages(self):
3809 if self._messages or self._failures:
3810 if os.isatty(2):
3811 self.out.write(progress.CSI_ERASE_LINE)
3812 self.out.write("\r")
3813
3814 for m in self._messages:
3815 m.Print(self)
3816 for m in self._failures:
3817 m.Print(self)
3818
3819 self._messages = []
3820 self._failures = []
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003821
3822
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003823class MetaProject(Project):
Gavin Makea2e3302023-03-11 06:46:20 +00003824 """A special project housed under .repo."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003825
Gavin Makea2e3302023-03-11 06:46:20 +00003826 def __init__(self, manifest, name, gitdir, worktree):
3827 Project.__init__(
3828 self,
3829 manifest=manifest,
3830 name=name,
3831 gitdir=gitdir,
3832 objdir=gitdir,
3833 worktree=worktree,
3834 remote=RemoteSpec("origin"),
3835 relpath=".repo/%s" % name,
3836 revisionExpr="refs/heads/master",
3837 revisionId=None,
3838 groups=None,
3839 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003840
Gavin Makea2e3302023-03-11 06:46:20 +00003841 def PreSync(self):
3842 if self.Exists:
3843 cb = self.CurrentBranch
3844 if cb:
3845 base = self.GetBranch(cb).merge
3846 if base:
3847 self.revisionExpr = base
3848 self.revisionId = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003849
Gavin Makea2e3302023-03-11 06:46:20 +00003850 @property
3851 def HasChanges(self):
3852 """Has the remote received new commits not yet checked out?"""
3853 if not self.remote or not self.revisionExpr:
3854 return False
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003855
Gavin Makea2e3302023-03-11 06:46:20 +00003856 all_refs = self.bare_ref.all
3857 revid = self.GetRevisionId(all_refs)
3858 head = self.work_git.GetHead()
3859 if head.startswith(R_HEADS):
3860 try:
3861 head = all_refs[head]
3862 except KeyError:
3863 head = None
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003864
Gavin Makea2e3302023-03-11 06:46:20 +00003865 if revid == head:
3866 return False
3867 elif self._revlist(not_rev(HEAD), revid):
3868 return True
3869 return False
LaMont Jones9b72cf22022-03-29 21:54:22 +00003870
3871
3872class RepoProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003873 """The MetaProject for repo itself."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003874
Gavin Makea2e3302023-03-11 06:46:20 +00003875 @property
3876 def LastFetch(self):
3877 try:
3878 fh = os.path.join(self.gitdir, "FETCH_HEAD")
3879 return os.path.getmtime(fh)
3880 except OSError:
3881 return 0
LaMont Jones9b72cf22022-03-29 21:54:22 +00003882
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +01003883
LaMont Jones9b72cf22022-03-29 21:54:22 +00003884class ManifestProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003885 """The MetaProject for manifests."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003886
Gavin Makea2e3302023-03-11 06:46:20 +00003887 def MetaBranchSwitch(self, submodules=False):
3888 """Prepare for manifest branch switch."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003889
Gavin Makea2e3302023-03-11 06:46:20 +00003890 # detach and delete manifest branch, allowing a new
3891 # branch to take over
3892 syncbuf = SyncBuffer(self.config, detach_head=True)
3893 self.Sync_LocalHalf(syncbuf, submodules=submodules)
3894 syncbuf.Finish()
LaMont Jones9b72cf22022-03-29 21:54:22 +00003895
Gavin Makea2e3302023-03-11 06:46:20 +00003896 return (
3897 GitCommand(
3898 self,
3899 ["update-ref", "-d", "refs/heads/default"],
3900 capture_stdout=True,
3901 capture_stderr=True,
3902 ).Wait()
3903 == 0
3904 )
LaMont Jones9b03f152022-03-29 23:01:18 +00003905
Gavin Makea2e3302023-03-11 06:46:20 +00003906 @property
3907 def standalone_manifest_url(self):
3908 """The URL of the standalone manifest, or None."""
3909 return self.config.GetString("manifest.standalone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003910
Gavin Makea2e3302023-03-11 06:46:20 +00003911 @property
3912 def manifest_groups(self):
3913 """The manifest groups string."""
3914 return self.config.GetString("manifest.groups")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003915
Gavin Makea2e3302023-03-11 06:46:20 +00003916 @property
3917 def reference(self):
3918 """The --reference for this manifest."""
3919 return self.config.GetString("repo.reference")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003920
Gavin Makea2e3302023-03-11 06:46:20 +00003921 @property
3922 def dissociate(self):
3923 """Whether to dissociate."""
3924 return self.config.GetBoolean("repo.dissociate")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003925
Gavin Makea2e3302023-03-11 06:46:20 +00003926 @property
3927 def archive(self):
3928 """Whether we use archive."""
3929 return self.config.GetBoolean("repo.archive")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003930
Gavin Makea2e3302023-03-11 06:46:20 +00003931 @property
3932 def mirror(self):
3933 """Whether we use mirror."""
3934 return self.config.GetBoolean("repo.mirror")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003935
Gavin Makea2e3302023-03-11 06:46:20 +00003936 @property
3937 def use_worktree(self):
3938 """Whether we use worktree."""
3939 return self.config.GetBoolean("repo.worktree")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003940
Gavin Makea2e3302023-03-11 06:46:20 +00003941 @property
3942 def clone_bundle(self):
3943 """Whether we use clone_bundle."""
3944 return self.config.GetBoolean("repo.clonebundle")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003945
Gavin Makea2e3302023-03-11 06:46:20 +00003946 @property
3947 def submodules(self):
3948 """Whether we use submodules."""
3949 return self.config.GetBoolean("repo.submodules")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003950
Gavin Makea2e3302023-03-11 06:46:20 +00003951 @property
3952 def git_lfs(self):
3953 """Whether we use git_lfs."""
3954 return self.config.GetBoolean("repo.git-lfs")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003955
Gavin Makea2e3302023-03-11 06:46:20 +00003956 @property
3957 def use_superproject(self):
3958 """Whether we use superproject."""
3959 return self.config.GetBoolean("repo.superproject")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003960
Gavin Makea2e3302023-03-11 06:46:20 +00003961 @property
3962 def partial_clone(self):
3963 """Whether this is a partial clone."""
3964 return self.config.GetBoolean("repo.partialclone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003965
Gavin Makea2e3302023-03-11 06:46:20 +00003966 @property
3967 def depth(self):
3968 """Partial clone depth."""
3969 return self.config.GetString("repo.depth")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003970
Gavin Makea2e3302023-03-11 06:46:20 +00003971 @property
3972 def clone_filter(self):
3973 """The clone filter."""
3974 return self.config.GetString("repo.clonefilter")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003975
Gavin Makea2e3302023-03-11 06:46:20 +00003976 @property
3977 def partial_clone_exclude(self):
3978 """Partial clone exclude string"""
3979 return self.config.GetString("repo.partialcloneexclude")
LaMont Jones4ada0432022-04-14 15:10:43 +00003980
Gavin Makea2e3302023-03-11 06:46:20 +00003981 @property
3982 def manifest_platform(self):
3983 """The --platform argument from `repo init`."""
3984 return self.config.GetString("manifest.platform")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003985
Gavin Makea2e3302023-03-11 06:46:20 +00003986 @property
3987 def _platform_name(self):
3988 """Return the name of the platform."""
3989 return platform.system().lower()
LaMont Jones9b03f152022-03-29 23:01:18 +00003990
Gavin Makea2e3302023-03-11 06:46:20 +00003991 def SyncWithPossibleInit(
3992 self,
3993 submanifest,
3994 verbose=False,
3995 current_branch_only=False,
3996 tags="",
3997 git_event_log=None,
3998 ):
3999 """Sync a manifestProject, possibly for the first time.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004000
Gavin Makea2e3302023-03-11 06:46:20 +00004001 Call Sync() with arguments from the most recent `repo init`. If this is
4002 a new sub manifest, then inherit options from the parent's
4003 manifestProject.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004004
Gavin Makea2e3302023-03-11 06:46:20 +00004005 This is used by subcmds.Sync() to do an initial download of new sub
4006 manifests.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004007
Gavin Makea2e3302023-03-11 06:46:20 +00004008 Args:
4009 submanifest: an XmlSubmanifest, the submanifest to re-sync.
4010 verbose: a boolean, whether to show all output, rather than only
4011 errors.
4012 current_branch_only: a boolean, whether to only fetch the current
4013 manifest branch from the server.
4014 tags: a boolean, whether to fetch tags.
4015 git_event_log: an EventLog, for git tracing.
4016 """
4017 # TODO(lamontjones): when refactoring sync (and init?) consider how to
4018 # better get the init options that we should use for new submanifests
4019 # that are added when syncing an existing workspace.
4020 git_event_log = git_event_log or EventLog()
LaMont Jonesb90a4222022-04-14 15:00:09 +00004021 spec = submanifest.ToSubmanifestSpec()
Gavin Makea2e3302023-03-11 06:46:20 +00004022 # Use the init options from the existing manifestProject, or the parent
4023 # if it doesn't exist.
4024 #
4025 # Today, we only support changing manifest_groups on the sub-manifest,
4026 # with no supported-for-the-user way to change the other arguments from
4027 # those specified by the outermost manifest.
4028 #
4029 # TODO(lamontjones): determine which of these should come from the
4030 # outermost manifest and which should come from the parent manifest.
4031 mp = self if self.Exists else submanifest.parent.manifestProject
4032 return self.Sync(
LaMont Jones55ee3042022-04-06 17:10:21 +00004033 manifest_url=spec.manifestUrl,
4034 manifest_branch=spec.revision,
Gavin Makea2e3302023-03-11 06:46:20 +00004035 standalone_manifest=mp.standalone_manifest_url,
4036 groups=mp.manifest_groups,
4037 platform=mp.manifest_platform,
4038 mirror=mp.mirror,
4039 dissociate=mp.dissociate,
4040 reference=mp.reference,
4041 worktree=mp.use_worktree,
4042 submodules=mp.submodules,
4043 archive=mp.archive,
4044 partial_clone=mp.partial_clone,
4045 clone_filter=mp.clone_filter,
4046 partial_clone_exclude=mp.partial_clone_exclude,
4047 clone_bundle=mp.clone_bundle,
4048 git_lfs=mp.git_lfs,
4049 use_superproject=mp.use_superproject,
LaMont Jones55ee3042022-04-06 17:10:21 +00004050 verbose=verbose,
4051 current_branch_only=current_branch_only,
4052 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00004053 depth=mp.depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004054 git_event_log=git_event_log,
4055 manifest_name=spec.manifestName,
Gavin Makea2e3302023-03-11 06:46:20 +00004056 this_manifest_only=True,
LaMont Jones55ee3042022-04-06 17:10:21 +00004057 outer_manifest=False,
4058 )
LaMont Jones409407a2022-04-05 21:21:56 +00004059
Gavin Makea2e3302023-03-11 06:46:20 +00004060 def Sync(
4061 self,
4062 _kwargs_only=(),
4063 manifest_url="",
4064 manifest_branch=None,
4065 standalone_manifest=False,
4066 groups="",
4067 mirror=False,
4068 reference="",
4069 dissociate=False,
4070 worktree=False,
4071 submodules=False,
4072 archive=False,
4073 partial_clone=None,
4074 depth=None,
4075 clone_filter="blob:none",
4076 partial_clone_exclude=None,
4077 clone_bundle=None,
4078 git_lfs=None,
4079 use_superproject=None,
4080 verbose=False,
4081 current_branch_only=False,
4082 git_event_log=None,
4083 platform="",
4084 manifest_name="default.xml",
4085 tags="",
4086 this_manifest_only=False,
4087 outer_manifest=True,
4088 ):
4089 """Sync the manifest and all submanifests.
LaMont Jones409407a2022-04-05 21:21:56 +00004090
Gavin Makea2e3302023-03-11 06:46:20 +00004091 Args:
4092 manifest_url: a string, the URL of the manifest project.
4093 manifest_branch: a string, the manifest branch to use.
4094 standalone_manifest: a boolean, whether to store the manifest as a
4095 static file.
4096 groups: a string, restricts the checkout to projects with the
4097 specified groups.
4098 mirror: a boolean, whether to create a mirror of the remote
4099 repository.
4100 reference: a string, location of a repo instance to use as a
4101 reference.
4102 dissociate: a boolean, whether to dissociate from reference mirrors
4103 after clone.
4104 worktree: a boolean, whether to use git-worktree to manage projects.
4105 submodules: a boolean, whether sync submodules associated with the
4106 manifest project.
4107 archive: a boolean, whether to checkout each project as an archive.
4108 See git-archive.
4109 partial_clone: a boolean, whether to perform a partial clone.
4110 depth: an int, how deep of a shallow clone to create.
4111 clone_filter: a string, filter to use with partial_clone.
4112 partial_clone_exclude : a string, comma-delimeted list of project
4113 names to exclude from partial clone.
4114 clone_bundle: a boolean, whether to enable /clone.bundle on
4115 HTTP/HTTPS.
4116 git_lfs: a boolean, whether to enable git LFS support.
4117 use_superproject: a boolean, whether to use the manifest
4118 superproject to sync projects.
4119 verbose: a boolean, whether to show all output, rather than only
4120 errors.
4121 current_branch_only: a boolean, whether to only fetch the current
4122 manifest branch from the server.
4123 platform: a string, restrict the checkout to projects with the
4124 specified platform group.
4125 git_event_log: an EventLog, for git tracing.
4126 tags: a boolean, whether to fetch tags.
4127 manifest_name: a string, the name of the manifest file to use.
4128 this_manifest_only: a boolean, whether to only operate on the
4129 current sub manifest.
4130 outer_manifest: a boolean, whether to start at the outermost
4131 manifest.
LaMont Jones9b03f152022-03-29 23:01:18 +00004132
Gavin Makea2e3302023-03-11 06:46:20 +00004133 Returns:
4134 a boolean, whether the sync was successful.
4135 """
4136 assert _kwargs_only == (), "Sync only accepts keyword arguments."
LaMont Jones9b03f152022-03-29 23:01:18 +00004137
Gavin Makea2e3302023-03-11 06:46:20 +00004138 groups = groups or self.manifest.GetDefaultGroupsStr(
4139 with_platform=False
4140 )
4141 platform = platform or "auto"
4142 git_event_log = git_event_log or EventLog()
4143 if outer_manifest and self.manifest.is_submanifest:
4144 # In a multi-manifest checkout, use the outer manifest unless we are
4145 # told not to.
4146 return self.client.outer_manifest.manifestProject.Sync(
4147 manifest_url=manifest_url,
4148 manifest_branch=manifest_branch,
4149 standalone_manifest=standalone_manifest,
4150 groups=groups,
4151 platform=platform,
4152 mirror=mirror,
4153 dissociate=dissociate,
4154 reference=reference,
4155 worktree=worktree,
4156 submodules=submodules,
4157 archive=archive,
4158 partial_clone=partial_clone,
4159 clone_filter=clone_filter,
4160 partial_clone_exclude=partial_clone_exclude,
4161 clone_bundle=clone_bundle,
4162 git_lfs=git_lfs,
4163 use_superproject=use_superproject,
4164 verbose=verbose,
4165 current_branch_only=current_branch_only,
4166 tags=tags,
4167 depth=depth,
4168 git_event_log=git_event_log,
4169 manifest_name=manifest_name,
4170 this_manifest_only=this_manifest_only,
4171 outer_manifest=False,
4172 )
LaMont Jones9b03f152022-03-29 23:01:18 +00004173
Gavin Makea2e3302023-03-11 06:46:20 +00004174 # If repo has already been initialized, we take -u with the absence of
4175 # --standalone-manifest to mean "transition to a standard repo set up",
4176 # which necessitates starting fresh.
4177 # If --standalone-manifest is set, we always tear everything down and
4178 # start anew.
4179 if self.Exists:
4180 was_standalone_manifest = self.config.GetString(
4181 "manifest.standalone"
4182 )
4183 if was_standalone_manifest and not manifest_url:
4184 print(
4185 "fatal: repo was initialized with a standlone manifest, "
4186 "cannot be re-initialized without --manifest-url/-u"
4187 )
4188 return False
4189
4190 if standalone_manifest or (
4191 was_standalone_manifest and manifest_url
4192 ):
4193 self.config.ClearCache()
4194 if self.gitdir and os.path.exists(self.gitdir):
4195 platform_utils.rmtree(self.gitdir)
4196 if self.worktree and os.path.exists(self.worktree):
4197 platform_utils.rmtree(self.worktree)
4198
4199 is_new = not self.Exists
4200 if is_new:
4201 if not manifest_url:
4202 print("fatal: manifest url is required.", file=sys.stderr)
4203 return False
4204
4205 if verbose:
4206 print(
4207 "Downloading manifest from %s"
4208 % (GitConfig.ForUser().UrlInsteadOf(manifest_url),),
4209 file=sys.stderr,
4210 )
4211
4212 # The manifest project object doesn't keep track of the path on the
4213 # server where this git is located, so let's save that here.
4214 mirrored_manifest_git = None
4215 if reference:
4216 manifest_git_path = urllib.parse.urlparse(manifest_url).path[1:]
4217 mirrored_manifest_git = os.path.join(
4218 reference, manifest_git_path
4219 )
4220 if not mirrored_manifest_git.endswith(".git"):
4221 mirrored_manifest_git += ".git"
4222 if not os.path.exists(mirrored_manifest_git):
4223 mirrored_manifest_git = os.path.join(
4224 reference, ".repo/manifests.git"
4225 )
4226
4227 self._InitGitDir(mirror_git=mirrored_manifest_git)
4228
4229 # If standalone_manifest is set, mark the project as "standalone" --
4230 # we'll still do much of the manifests.git set up, but will avoid actual
4231 # syncs to a remote.
4232 if standalone_manifest:
4233 self.config.SetString("manifest.standalone", manifest_url)
4234 elif not manifest_url and not manifest_branch:
4235 # If -u is set and --standalone-manifest is not, then we're not in
4236 # standalone mode. Otherwise, use config to infer what we were in
4237 # the last init.
4238 standalone_manifest = bool(
4239 self.config.GetString("manifest.standalone")
4240 )
4241 if not standalone_manifest:
4242 self.config.SetString("manifest.standalone", None)
4243
4244 self._ConfigureDepth(depth)
4245
4246 # Set the remote URL before the remote branch as we might need it below.
4247 if manifest_url:
4248 r = self.GetRemote()
4249 r.url = manifest_url
4250 r.ResetFetch()
4251 r.Save()
4252
4253 if not standalone_manifest:
4254 if manifest_branch:
4255 if manifest_branch == "HEAD":
4256 manifest_branch = self.ResolveRemoteHead()
4257 if manifest_branch is None:
4258 print("fatal: unable to resolve HEAD", file=sys.stderr)
4259 return False
4260 self.revisionExpr = manifest_branch
4261 else:
4262 if is_new:
4263 default_branch = self.ResolveRemoteHead()
4264 if default_branch is None:
4265 # If the remote doesn't have HEAD configured, default to
4266 # master.
4267 default_branch = "refs/heads/master"
4268 self.revisionExpr = default_branch
4269 else:
4270 self.PreSync()
4271
4272 groups = re.split(r"[,\s]+", groups or "")
4273 all_platforms = ["linux", "darwin", "windows"]
4274 platformize = lambda x: "platform-" + x
4275 if platform == "auto":
4276 if not mirror and not self.mirror:
4277 groups.append(platformize(self._platform_name))
4278 elif platform == "all":
4279 groups.extend(map(platformize, all_platforms))
4280 elif platform in all_platforms:
4281 groups.append(platformize(platform))
4282 elif platform != "none":
4283 print("fatal: invalid platform flag", file=sys.stderr)
4284 return False
4285 self.config.SetString("manifest.platform", platform)
4286
4287 groups = [x for x in groups if x]
4288 groupstr = ",".join(groups)
4289 if (
4290 platform == "auto"
4291 and groupstr == self.manifest.GetDefaultGroupsStr()
4292 ):
4293 groupstr = None
4294 self.config.SetString("manifest.groups", groupstr)
4295
4296 if reference:
4297 self.config.SetString("repo.reference", reference)
4298
4299 if dissociate:
4300 self.config.SetBoolean("repo.dissociate", dissociate)
4301
4302 if worktree:
4303 if mirror:
4304 print(
4305 "fatal: --mirror and --worktree are incompatible",
4306 file=sys.stderr,
4307 )
4308 return False
4309 if submodules:
4310 print(
4311 "fatal: --submodules and --worktree are incompatible",
4312 file=sys.stderr,
4313 )
4314 return False
4315 self.config.SetBoolean("repo.worktree", worktree)
4316 if is_new:
4317 self.use_git_worktrees = True
4318 print("warning: --worktree is experimental!", file=sys.stderr)
4319
4320 if archive:
4321 if is_new:
4322 self.config.SetBoolean("repo.archive", archive)
4323 else:
4324 print(
4325 "fatal: --archive is only supported when initializing a "
4326 "new workspace.",
4327 file=sys.stderr,
4328 )
4329 print(
4330 "Either delete the .repo folder in this workspace, or "
4331 "initialize in another location.",
4332 file=sys.stderr,
4333 )
4334 return False
4335
4336 if mirror:
4337 if is_new:
4338 self.config.SetBoolean("repo.mirror", mirror)
4339 else:
4340 print(
4341 "fatal: --mirror is only supported when initializing a new "
4342 "workspace.",
4343 file=sys.stderr,
4344 )
4345 print(
4346 "Either delete the .repo folder in this workspace, or "
4347 "initialize in another location.",
4348 file=sys.stderr,
4349 )
4350 return False
4351
4352 if partial_clone is not None:
4353 if mirror:
4354 print(
4355 "fatal: --mirror and --partial-clone are mutually "
4356 "exclusive",
4357 file=sys.stderr,
4358 )
4359 return False
4360 self.config.SetBoolean("repo.partialclone", partial_clone)
4361 if clone_filter:
4362 self.config.SetString("repo.clonefilter", clone_filter)
4363 elif self.partial_clone:
4364 clone_filter = self.clone_filter
4365 else:
4366 clone_filter = None
4367
4368 if partial_clone_exclude is not None:
4369 self.config.SetString(
4370 "repo.partialcloneexclude", partial_clone_exclude
4371 )
4372
4373 if clone_bundle is None:
4374 clone_bundle = False if partial_clone else True
4375 else:
4376 self.config.SetBoolean("repo.clonebundle", clone_bundle)
4377
4378 if submodules:
4379 self.config.SetBoolean("repo.submodules", submodules)
4380
4381 if git_lfs is not None:
4382 if git_lfs:
4383 git_require((2, 17, 0), fail=True, msg="Git LFS support")
4384
4385 self.config.SetBoolean("repo.git-lfs", git_lfs)
4386 if not is_new:
4387 print(
4388 "warning: Changing --git-lfs settings will only affect new "
4389 "project checkouts.\n"
4390 " Existing projects will require manual updates.\n",
4391 file=sys.stderr,
4392 )
4393
4394 if use_superproject is not None:
4395 self.config.SetBoolean("repo.superproject", use_superproject)
4396
4397 if not standalone_manifest:
4398 success = self.Sync_NetworkHalf(
4399 is_new=is_new,
4400 quiet=not verbose,
4401 verbose=verbose,
4402 clone_bundle=clone_bundle,
4403 current_branch_only=current_branch_only,
4404 tags=tags,
4405 submodules=submodules,
4406 clone_filter=clone_filter,
4407 partial_clone_exclude=self.manifest.PartialCloneExclude,
4408 ).success
4409 if not success:
4410 r = self.GetRemote()
4411 print(
4412 "fatal: cannot obtain manifest %s" % r.url, file=sys.stderr
4413 )
4414
4415 # Better delete the manifest git dir if we created it; otherwise
4416 # next time (when user fixes problems) we won't go through the
4417 # "is_new" logic.
4418 if is_new:
4419 platform_utils.rmtree(self.gitdir)
4420 return False
4421
4422 if manifest_branch:
4423 self.MetaBranchSwitch(submodules=submodules)
4424
4425 syncbuf = SyncBuffer(self.config)
4426 self.Sync_LocalHalf(syncbuf, submodules=submodules)
4427 syncbuf.Finish()
4428
4429 if is_new or self.CurrentBranch is None:
4430 if not self.StartBranch("default"):
4431 print(
4432 "fatal: cannot create default in manifest",
4433 file=sys.stderr,
4434 )
4435 return False
4436
4437 if not manifest_name:
4438 print("fatal: manifest name (-m) is required.", file=sys.stderr)
4439 return False
4440
4441 elif is_new:
4442 # This is a new standalone manifest.
4443 manifest_name = "default.xml"
4444 manifest_data = fetch.fetch_file(manifest_url, verbose=verbose)
4445 dest = os.path.join(self.worktree, manifest_name)
4446 os.makedirs(os.path.dirname(dest), exist_ok=True)
4447 with open(dest, "wb") as f:
4448 f.write(manifest_data)
4449
4450 try:
4451 self.manifest.Link(manifest_name)
4452 except ManifestParseError as e:
4453 print(
4454 "fatal: manifest '%s' not available" % manifest_name,
4455 file=sys.stderr,
4456 )
4457 print("fatal: %s" % str(e), file=sys.stderr)
4458 return False
4459
4460 if not this_manifest_only:
4461 for submanifest in self.manifest.submanifests.values():
4462 spec = submanifest.ToSubmanifestSpec()
4463 submanifest.repo_client.manifestProject.Sync(
4464 manifest_url=spec.manifestUrl,
4465 manifest_branch=spec.revision,
4466 standalone_manifest=standalone_manifest,
4467 groups=self.manifest_groups,
4468 platform=platform,
4469 mirror=mirror,
4470 dissociate=dissociate,
4471 reference=reference,
4472 worktree=worktree,
4473 submodules=submodules,
4474 archive=archive,
4475 partial_clone=partial_clone,
4476 clone_filter=clone_filter,
4477 partial_clone_exclude=partial_clone_exclude,
4478 clone_bundle=clone_bundle,
4479 git_lfs=git_lfs,
4480 use_superproject=use_superproject,
4481 verbose=verbose,
4482 current_branch_only=current_branch_only,
4483 tags=tags,
4484 depth=depth,
4485 git_event_log=git_event_log,
4486 manifest_name=spec.manifestName,
4487 this_manifest_only=False,
4488 outer_manifest=False,
4489 )
4490
4491 # Lastly, if the manifest has a <superproject> then have the
4492 # superproject sync it (if it will be used).
4493 if git_superproject.UseSuperproject(use_superproject, self.manifest):
4494 sync_result = self.manifest.superproject.Sync(git_event_log)
4495 if not sync_result.success:
4496 submanifest = ""
4497 if self.manifest.path_prefix:
4498 submanifest = f"for {self.manifest.path_prefix} "
4499 print(
4500 f"warning: git update of superproject {submanifest}failed, "
4501 "repo sync will not use superproject to fetch source; "
4502 "while this error is not fatal, and you can continue to "
4503 "run repo sync, please run repo init with the "
4504 "--no-use-superproject option to stop seeing this warning",
4505 file=sys.stderr,
4506 )
4507 if sync_result.fatal and use_superproject is not None:
4508 return False
4509
4510 return True
4511
4512 def _ConfigureDepth(self, depth):
4513 """Configure the depth we'll sync down.
4514
4515 Args:
4516 depth: an int, how deep of a partial clone to create.
4517 """
4518 # Opt.depth will be non-None if user actually passed --depth to repo
4519 # init.
4520 if depth is not None:
4521 if depth > 0:
4522 # Positive values will set the depth.
4523 depth = str(depth)
4524 else:
4525 # Negative numbers will clear the depth; passing None to
4526 # SetString will do that.
4527 depth = None
4528
4529 # We store the depth in the main manifest project.
4530 self.config.SetString("repo.depth", depth)