blob: 58bb2744c65137f23eda2a9c424966c4e6ef2ada [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(),
Jason Chang17833322023-05-23 13:06:55 -07001269 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001270 ):
1271 """Perform only the network IO portion of the sync process.
1272 Local working directory/branch state is not affected.
1273 """
1274 if archive and not isinstance(self, MetaProject):
1275 if self.remote.url.startswith(("http://", "https://")):
1276 _error(
1277 "%s: Cannot fetch archives from http/https remotes.",
1278 self.name,
1279 )
1280 return SyncNetworkHalfResult(False, False)
1281
1282 name = self.relpath.replace("\\", "/")
1283 name = name.replace("/", "_")
1284 tarpath = "%s.tar" % name
1285 topdir = self.manifest.topdir
1286
1287 try:
1288 self._FetchArchive(tarpath, cwd=topdir)
1289 except GitError as e:
1290 _error("%s", e)
1291 return SyncNetworkHalfResult(False, False)
1292
1293 # From now on, we only need absolute tarpath.
1294 tarpath = os.path.join(topdir, tarpath)
1295
1296 if not self._ExtractArchive(tarpath, path=topdir):
1297 return SyncNetworkHalfResult(False, True)
1298 try:
1299 platform_utils.remove(tarpath)
1300 except OSError as e:
1301 _warn("Cannot remove archive %s: %s", tarpath, str(e))
1302 self._CopyAndLinkFiles()
1303 return SyncNetworkHalfResult(True, True)
1304
1305 # If the shared object dir already exists, don't try to rebootstrap with
1306 # a clone bundle download. We should have the majority of objects
1307 # already.
1308 if clone_bundle and os.path.exists(self.objdir):
1309 clone_bundle = False
1310
1311 if self.name in partial_clone_exclude:
1312 clone_bundle = True
1313 clone_filter = None
1314
1315 if is_new is None:
1316 is_new = not self.Exists
1317 if is_new:
1318 self._InitGitDir(force_sync=force_sync, quiet=quiet)
1319 else:
1320 self._UpdateHooks(quiet=quiet)
1321 self._InitRemote()
1322
1323 if self.UseAlternates:
1324 # If gitdir/objects is a symlink, migrate it from the old layout.
1325 gitdir_objects = os.path.join(self.gitdir, "objects")
1326 if platform_utils.islink(gitdir_objects):
1327 platform_utils.remove(gitdir_objects, missing_ok=True)
1328 gitdir_alt = os.path.join(self.gitdir, "objects/info/alternates")
1329 if not os.path.exists(gitdir_alt):
1330 os.makedirs(os.path.dirname(gitdir_alt), exist_ok=True)
1331 _lwrite(
1332 gitdir_alt,
1333 os.path.join(
1334 os.path.relpath(self.objdir, gitdir_objects), "objects"
1335 )
1336 + "\n",
1337 )
1338
1339 if is_new:
1340 alt = os.path.join(self.objdir, "objects/info/alternates")
1341 try:
1342 with open(alt) as fd:
1343 # This works for both absolute and relative alternate
1344 # directories.
1345 alt_dir = os.path.join(
1346 self.objdir, "objects", fd.readline().rstrip()
1347 )
1348 except IOError:
1349 alt_dir = None
1350 else:
1351 alt_dir = None
1352
Gavin Mak4feff3b2023-05-16 21:31:10 +00001353 applied_cache = False
1354 # If cache_dir is provided, and it's a new repository without
1355 # alternative_dir, bootstrap this project repo with the git
1356 # cache files.
1357 if cache_dir is not None and is_new and alt_dir is None:
1358 try:
1359 self.CacheApply(cache_dir)
1360 applied_cache = True
1361 is_new = False
1362 except CacheApplyError as e:
1363 _error("Could not apply git cache: %s", e)
1364 _error("Please check if you have the right GS credentials.")
1365 _error("Please check if the cache files exist in GS.")
1366
Gavin Makea2e3302023-03-11 06:46:20 +00001367 if (
1368 clone_bundle
Gavin Mak4feff3b2023-05-16 21:31:10 +00001369 and not applied_cache
Gavin Makea2e3302023-03-11 06:46:20 +00001370 and alt_dir is None
1371 and self._ApplyCloneBundle(
1372 initial=is_new, quiet=quiet, verbose=verbose
1373 )
1374 ):
1375 is_new = False
1376
1377 if current_branch_only is None:
1378 if self.sync_c:
1379 current_branch_only = True
1380 elif not self.manifest._loaded:
1381 # Manifest cannot check defaults until it syncs.
1382 current_branch_only = False
1383 elif self.manifest.default.sync_c:
1384 current_branch_only = True
1385
1386 if tags is None:
1387 tags = self.sync_tags
1388
1389 if self.clone_depth:
1390 depth = self.clone_depth
1391 else:
1392 depth = self.manifest.manifestProject.depth
1393
Jason Chang17833322023-05-23 13:06:55 -07001394 if depth and clone_filter_for_depth:
1395 depth = None
1396 clone_filter = clone_filter_for_depth
1397
Gavin Makea2e3302023-03-11 06:46:20 +00001398 # See if we can skip the network fetch entirely.
1399 remote_fetched = False
1400 if not (
1401 optimized_fetch
1402 and (
1403 ID_RE.match(self.revisionExpr)
1404 and self._CheckForImmutableRevision()
1405 )
1406 ):
1407 remote_fetched = True
1408 if not self._RemoteFetch(
1409 initial=is_new,
1410 quiet=quiet,
1411 verbose=verbose,
1412 output_redir=output_redir,
1413 alt_dir=alt_dir,
1414 current_branch_only=current_branch_only,
1415 tags=tags,
1416 prune=prune,
1417 depth=depth,
1418 submodules=submodules,
1419 force_sync=force_sync,
1420 ssh_proxy=ssh_proxy,
1421 clone_filter=clone_filter,
1422 retry_fetches=retry_fetches,
1423 ):
1424 return SyncNetworkHalfResult(False, remote_fetched)
1425
1426 mp = self.manifest.manifestProject
1427 dissociate = mp.dissociate
1428 if dissociate:
1429 alternates_file = os.path.join(
1430 self.objdir, "objects/info/alternates"
1431 )
1432 if os.path.exists(alternates_file):
1433 cmd = ["repack", "-a", "-d"]
1434 p = GitCommand(
1435 self,
1436 cmd,
1437 bare=True,
1438 capture_stdout=bool(output_redir),
1439 merge_output=bool(output_redir),
1440 )
1441 if p.stdout and output_redir:
1442 output_redir.write(p.stdout)
1443 if p.Wait() != 0:
1444 return SyncNetworkHalfResult(False, remote_fetched)
1445 platform_utils.remove(alternates_file)
1446
1447 if self.worktree:
1448 self._InitMRef()
1449 else:
1450 self._InitMirrorHead()
1451 platform_utils.remove(
1452 os.path.join(self.gitdir, "FETCH_HEAD"), missing_ok=True
1453 )
1454 return SyncNetworkHalfResult(True, remote_fetched)
1455
1456 def PostRepoUpgrade(self):
1457 self._InitHooks()
1458
1459 def _CopyAndLinkFiles(self):
1460 if self.client.isGitcClient:
1461 return
1462 for copyfile in self.copyfiles:
1463 copyfile._Copy()
1464 for linkfile in self.linkfiles:
1465 linkfile._Link()
1466
1467 def GetCommitRevisionId(self):
1468 """Get revisionId of a commit.
1469
1470 Use this method instead of GetRevisionId to get the id of the commit
1471 rather than the id of the current git object (for example, a tag)
1472
1473 """
1474 if not self.revisionExpr.startswith(R_TAGS):
1475 return self.GetRevisionId(self._allrefs)
1476
1477 try:
1478 return self.bare_git.rev_list(self.revisionExpr, "-1")[0]
1479 except GitError:
1480 raise ManifestInvalidRevisionError(
1481 "revision %s in %s not found" % (self.revisionExpr, self.name)
1482 )
1483
1484 def GetRevisionId(self, all_refs=None):
1485 if self.revisionId:
1486 return self.revisionId
1487
1488 rem = self.GetRemote()
1489 rev = rem.ToLocal(self.revisionExpr)
1490
1491 if all_refs is not None and rev in all_refs:
1492 return all_refs[rev]
1493
1494 try:
1495 return self.bare_git.rev_parse("--verify", "%s^0" % rev)
1496 except GitError:
1497 raise ManifestInvalidRevisionError(
1498 "revision %s in %s not found" % (self.revisionExpr, self.name)
1499 )
1500
1501 def SetRevisionId(self, revisionId):
1502 if self.revisionExpr:
1503 self.upstream = self.revisionExpr
1504
1505 self.revisionId = revisionId
1506
1507 def Sync_LocalHalf(self, syncbuf, force_sync=False, submodules=False):
1508 """Perform only the local IO portion of the sync process.
1509
1510 Network access is not required.
1511 """
1512 if not os.path.exists(self.gitdir):
1513 syncbuf.fail(
1514 self,
1515 "Cannot checkout %s due to missing network sync; Run "
1516 "`repo sync -n %s` first." % (self.name, self.name),
1517 )
1518 return
1519
1520 self._InitWorkTree(force_sync=force_sync, submodules=submodules)
1521 all_refs = self.bare_ref.all
1522 self.CleanPublishedCache(all_refs)
1523 revid = self.GetRevisionId(all_refs)
1524
1525 # Special case the root of the repo client checkout. Make sure it
1526 # doesn't contain files being checked out to dirs we don't allow.
1527 if self.relpath == ".":
1528 PROTECTED_PATHS = {".repo"}
1529 paths = set(
1530 self.work_git.ls_tree("-z", "--name-only", "--", revid).split(
1531 "\0"
1532 )
1533 )
1534 bad_paths = paths & PROTECTED_PATHS
1535 if bad_paths:
1536 syncbuf.fail(
1537 self,
1538 "Refusing to checkout project that writes to protected "
1539 "paths: %s" % (", ".join(bad_paths),),
1540 )
1541 return
1542
1543 def _doff():
1544 self._FastForward(revid)
1545 self._CopyAndLinkFiles()
1546
1547 def _dosubmodules():
1548 self._SyncSubmodules(quiet=True)
1549
1550 head = self.work_git.GetHead()
1551 if head.startswith(R_HEADS):
1552 branch = head[len(R_HEADS) :]
1553 try:
1554 head = all_refs[head]
1555 except KeyError:
1556 head = None
1557 else:
1558 branch = None
1559
1560 if branch is None or syncbuf.detach_head:
1561 # Currently on a detached HEAD. The user is assumed to
1562 # not have any local modifications worth worrying about.
1563 if self.IsRebaseInProgress():
1564 syncbuf.fail(self, _PriorSyncFailedError())
1565 return
1566
1567 if head == revid:
1568 # No changes; don't do anything further.
1569 # Except if the head needs to be detached.
1570 if not syncbuf.detach_head:
1571 # The copy/linkfile config may have changed.
1572 self._CopyAndLinkFiles()
1573 return
1574 else:
1575 lost = self._revlist(not_rev(revid), HEAD)
1576 if lost:
1577 syncbuf.info(self, "discarding %d commits", len(lost))
1578
1579 try:
1580 self._Checkout(revid, quiet=True)
1581 if submodules:
1582 self._SyncSubmodules(quiet=True)
1583 except GitError as e:
1584 syncbuf.fail(self, e)
1585 return
1586 self._CopyAndLinkFiles()
1587 return
1588
1589 if head == revid:
1590 # No changes; don't do anything further.
1591 #
1592 # The copy/linkfile config may have changed.
1593 self._CopyAndLinkFiles()
1594 return
1595
1596 branch = self.GetBranch(branch)
1597
1598 if not branch.LocalMerge:
1599 # The current branch has no tracking configuration.
1600 # Jump off it to a detached HEAD.
1601 syncbuf.info(
1602 self, "leaving %s; does not track upstream", branch.name
1603 )
1604 try:
1605 self._Checkout(revid, quiet=True)
1606 if submodules:
1607 self._SyncSubmodules(quiet=True)
1608 except GitError as e:
1609 syncbuf.fail(self, e)
1610 return
1611 self._CopyAndLinkFiles()
1612 return
1613
1614 upstream_gain = self._revlist(not_rev(HEAD), revid)
1615
1616 # See if we can perform a fast forward merge. This can happen if our
1617 # branch isn't in the exact same state as we last published.
1618 try:
1619 self.work_git.merge_base("--is-ancestor", HEAD, revid)
1620 # Skip the published logic.
1621 pub = False
1622 except GitError:
1623 pub = self.WasPublished(branch.name, all_refs)
1624
1625 if pub:
1626 not_merged = self._revlist(not_rev(revid), pub)
1627 if not_merged:
1628 if upstream_gain:
1629 # The user has published this branch and some of those
1630 # commits are not yet merged upstream. We do not want
1631 # to rewrite the published commits so we punt.
1632 syncbuf.fail(
1633 self,
1634 "branch %s is published (but not merged) and is now "
1635 "%d commits behind" % (branch.name, len(upstream_gain)),
1636 )
1637 return
1638 elif pub == head:
1639 # All published commits are merged, and thus we are a
1640 # strict subset. We can fast-forward safely.
1641 syncbuf.later1(self, _doff)
1642 if submodules:
1643 syncbuf.later1(self, _dosubmodules)
1644 return
1645
1646 # Examine the local commits not in the remote. Find the
1647 # last one attributed to this user, if any.
1648 local_changes = self._revlist(not_rev(revid), HEAD, format="%H %ce")
1649 last_mine = None
1650 cnt_mine = 0
1651 for commit in local_changes:
1652 commit_id, committer_email = commit.split(" ", 1)
1653 if committer_email == self.UserEmail:
1654 last_mine = commit_id
1655 cnt_mine += 1
1656
1657 if not upstream_gain and cnt_mine == len(local_changes):
1658 # The copy/linkfile config may have changed.
1659 self._CopyAndLinkFiles()
1660 return
1661
1662 if self.IsDirty(consider_untracked=False):
1663 syncbuf.fail(self, _DirtyError())
1664 return
1665
1666 # If the upstream switched on us, warn the user.
1667 if branch.merge != self.revisionExpr:
1668 if branch.merge and self.revisionExpr:
1669 syncbuf.info(
1670 self,
1671 "manifest switched %s...%s",
1672 branch.merge,
1673 self.revisionExpr,
1674 )
1675 elif branch.merge:
1676 syncbuf.info(self, "manifest no longer tracks %s", branch.merge)
1677
1678 if cnt_mine < len(local_changes):
1679 # Upstream rebased. Not everything in HEAD was created by this user.
1680 syncbuf.info(
1681 self,
1682 "discarding %d commits removed from upstream",
1683 len(local_changes) - cnt_mine,
1684 )
1685
1686 branch.remote = self.GetRemote()
1687 if not ID_RE.match(self.revisionExpr):
1688 # In case of manifest sync the revisionExpr might be a SHA1.
1689 branch.merge = self.revisionExpr
1690 if not branch.merge.startswith("refs/"):
1691 branch.merge = R_HEADS + branch.merge
1692 branch.Save()
1693
1694 if cnt_mine > 0 and self.rebase:
1695
1696 def _docopyandlink():
1697 self._CopyAndLinkFiles()
1698
1699 def _dorebase():
1700 self._Rebase(upstream="%s^1" % last_mine, onto=revid)
1701
1702 syncbuf.later2(self, _dorebase)
1703 if submodules:
1704 syncbuf.later2(self, _dosubmodules)
1705 syncbuf.later2(self, _docopyandlink)
1706 elif local_changes:
1707 try:
1708 self._ResetHard(revid)
1709 if submodules:
1710 self._SyncSubmodules(quiet=True)
1711 self._CopyAndLinkFiles()
1712 except GitError as e:
1713 syncbuf.fail(self, e)
1714 return
1715 else:
1716 syncbuf.later1(self, _doff)
1717 if submodules:
1718 syncbuf.later1(self, _dosubmodules)
1719
1720 def AddCopyFile(self, src, dest, topdir):
1721 """Mark |src| for copying to |dest| (relative to |topdir|).
1722
1723 No filesystem changes occur here. Actual copying happens later on.
1724
1725 Paths should have basic validation run on them before being queued.
1726 Further checking will be handled when the actual copy happens.
1727 """
1728 self.copyfiles.append(_CopyFile(self.worktree, src, topdir, dest))
1729
1730 def AddLinkFile(self, src, dest, topdir):
1731 """Mark |dest| to create a symlink (relative to |topdir|) pointing to
1732 |src|.
1733
1734 No filesystem changes occur here. Actual linking happens later on.
1735
1736 Paths should have basic validation run on them before being queued.
1737 Further checking will be handled when the actual link happens.
1738 """
1739 self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest))
1740
1741 def AddAnnotation(self, name, value, keep):
1742 self.annotations.append(Annotation(name, value, keep))
1743
1744 def DownloadPatchSet(self, change_id, patch_id):
1745 """Download a single patch set of a single change to FETCH_HEAD."""
1746 remote = self.GetRemote()
1747
1748 cmd = ["fetch", remote.name]
1749 cmd.append(
1750 "refs/changes/%2.2d/%d/%d" % (change_id % 100, change_id, patch_id)
1751 )
1752 if GitCommand(self, cmd, bare=True).Wait() != 0:
1753 return None
1754 return DownloadedChange(
1755 self,
1756 self.GetRevisionId(),
1757 change_id,
1758 patch_id,
1759 self.bare_git.rev_parse("FETCH_HEAD"),
1760 )
1761
1762 def DeleteWorktree(self, quiet=False, force=False):
1763 """Delete the source checkout and any other housekeeping tasks.
1764
1765 This currently leaves behind the internal .repo/ cache state. This
1766 helps when switching branches or manifest changes get reverted as we
1767 don't have to redownload all the git objects. But we should do some GC
1768 at some point.
1769
1770 Args:
1771 quiet: Whether to hide normal messages.
1772 force: Always delete tree even if dirty.
Doug Anderson3ba5f952011-04-07 12:51:04 -07001773
1774 Returns:
Gavin Makea2e3302023-03-11 06:46:20 +00001775 True if the worktree was completely cleaned out.
1776 """
1777 if self.IsDirty():
1778 if force:
1779 print(
1780 "warning: %s: Removing dirty project: uncommitted changes "
1781 "lost." % (self.RelPath(local=False),),
1782 file=sys.stderr,
1783 )
1784 else:
1785 print(
1786 "error: %s: Cannot remove project: uncommitted changes are "
1787 "present.\n" % (self.RelPath(local=False),),
1788 file=sys.stderr,
1789 )
1790 return False
Wink Saville02d79452009-04-10 13:01:24 -07001791
Gavin Makea2e3302023-03-11 06:46:20 +00001792 if not quiet:
1793 print(
1794 "%s: Deleting obsolete checkout." % (self.RelPath(local=False),)
1795 )
Wink Saville02d79452009-04-10 13:01:24 -07001796
Gavin Makea2e3302023-03-11 06:46:20 +00001797 # Unlock and delink from the main worktree. We don't use git's worktree
1798 # remove because it will recursively delete projects -- we handle that
1799 # ourselves below. https://crbug.com/git/48
1800 if self.use_git_worktrees:
1801 needle = platform_utils.realpath(self.gitdir)
1802 # Find the git worktree commondir under .repo/worktrees/.
1803 output = self.bare_git.worktree("list", "--porcelain").splitlines()[
1804 0
1805 ]
1806 assert output.startswith("worktree "), output
1807 commondir = output[9:]
1808 # Walk each of the git worktrees to see where they point.
1809 configs = os.path.join(commondir, "worktrees")
1810 for name in os.listdir(configs):
1811 gitdir = os.path.join(configs, name, "gitdir")
1812 with open(gitdir) as fp:
1813 relpath = fp.read().strip()
1814 # Resolve the checkout path and see if it matches this project.
1815 fullpath = platform_utils.realpath(
1816 os.path.join(configs, name, relpath)
1817 )
1818 if fullpath == needle:
1819 platform_utils.rmtree(os.path.join(configs, name))
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001820
Gavin Makea2e3302023-03-11 06:46:20 +00001821 # Delete the .git directory first, so we're less likely to have a
1822 # partially working git repository around. There shouldn't be any git
1823 # projects here, so rmtree works.
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001824
Gavin Makea2e3302023-03-11 06:46:20 +00001825 # Try to remove plain files first in case of git worktrees. If this
1826 # fails for any reason, we'll fall back to rmtree, and that'll display
1827 # errors if it can't remove things either.
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001828 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001829 platform_utils.remove(self.gitdir)
1830 except OSError:
1831 pass
1832 try:
1833 platform_utils.rmtree(self.gitdir)
1834 except OSError as e:
1835 if e.errno != errno.ENOENT:
1836 print("error: %s: %s" % (self.gitdir, e), file=sys.stderr)
1837 print(
1838 "error: %s: Failed to delete obsolete checkout; remove "
1839 "manually, then run `repo sync -l`."
1840 % (self.RelPath(local=False),),
1841 file=sys.stderr,
1842 )
1843 return False
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001844
Gavin Makea2e3302023-03-11 06:46:20 +00001845 # Delete everything under the worktree, except for directories that
1846 # contain another git project.
1847 dirs_to_remove = []
1848 failed = False
1849 for root, dirs, files in platform_utils.walk(self.worktree):
1850 for f in files:
1851 path = os.path.join(root, f)
1852 try:
1853 platform_utils.remove(path)
1854 except OSError as e:
1855 if e.errno != errno.ENOENT:
1856 print(
1857 "error: %s: Failed to remove: %s" % (path, e),
1858 file=sys.stderr,
1859 )
1860 failed = True
1861 dirs[:] = [
1862 d
1863 for d in dirs
1864 if not os.path.lexists(os.path.join(root, d, ".git"))
1865 ]
1866 dirs_to_remove += [
1867 os.path.join(root, d)
1868 for d in dirs
1869 if os.path.join(root, d) not in dirs_to_remove
1870 ]
1871 for d in reversed(dirs_to_remove):
1872 if platform_utils.islink(d):
1873 try:
1874 platform_utils.remove(d)
1875 except OSError as e:
1876 if e.errno != errno.ENOENT:
1877 print(
1878 "error: %s: Failed to remove: %s" % (d, e),
1879 file=sys.stderr,
1880 )
1881 failed = True
1882 elif not platform_utils.listdir(d):
1883 try:
1884 platform_utils.rmdir(d)
1885 except OSError as e:
1886 if e.errno != errno.ENOENT:
1887 print(
1888 "error: %s: Failed to remove: %s" % (d, e),
1889 file=sys.stderr,
1890 )
1891 failed = True
1892 if failed:
1893 print(
1894 "error: %s: Failed to delete obsolete checkout."
1895 % (self.RelPath(local=False),),
1896 file=sys.stderr,
1897 )
1898 print(
1899 " Remove manually, then run `repo sync -l`.",
1900 file=sys.stderr,
1901 )
1902 return False
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07001903
Gavin Makea2e3302023-03-11 06:46:20 +00001904 # Try deleting parent dirs if they are empty.
1905 path = self.worktree
1906 while path != self.manifest.topdir:
1907 try:
1908 platform_utils.rmdir(path)
1909 except OSError as e:
1910 if e.errno != errno.ENOENT:
1911 break
1912 path = os.path.dirname(path)
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001913
Gavin Makea2e3302023-03-11 06:46:20 +00001914 return True
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001915
Gavin Makea2e3302023-03-11 06:46:20 +00001916 def StartBranch(self, name, branch_merge="", revision=None):
1917 """Create a new branch off the manifest's revision."""
1918 if not branch_merge:
1919 branch_merge = self.revisionExpr
1920 head = self.work_git.GetHead()
1921 if head == (R_HEADS + name):
1922 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001923
David Pursehouse8a68ff92012-09-24 12:15:13 +09001924 all_refs = self.bare_ref.all
Gavin Makea2e3302023-03-11 06:46:20 +00001925 if R_HEADS + name in all_refs:
1926 return GitCommand(self, ["checkout", "-q", name, "--"]).Wait() == 0
Shawn O. Pearce88443382010-10-08 10:02:09 +02001927
Gavin Makea2e3302023-03-11 06:46:20 +00001928 branch = self.GetBranch(name)
1929 branch.remote = self.GetRemote()
1930 branch.merge = branch_merge
1931 if not branch.merge.startswith("refs/") and not ID_RE.match(
1932 branch_merge
1933 ):
1934 branch.merge = R_HEADS + branch_merge
Shawn O. Pearce88443382010-10-08 10:02:09 +02001935
Gavin Makea2e3302023-03-11 06:46:20 +00001936 if revision is None:
1937 revid = self.GetRevisionId(all_refs)
Shawn O. Pearce88443382010-10-08 10:02:09 +02001938 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001939 revid = self.work_git.rev_parse(revision)
Brian Harring14a66742012-09-28 20:21:57 -07001940
Gavin Makea2e3302023-03-11 06:46:20 +00001941 if head.startswith(R_HEADS):
Kevin Degiabaa7f32014-11-12 11:27:45 -07001942 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001943 head = all_refs[head]
1944 except KeyError:
1945 head = None
1946 if revid and head and revid == head:
1947 ref = R_HEADS + name
1948 self.work_git.update_ref(ref, revid)
1949 self.work_git.symbolic_ref(HEAD, ref)
1950 branch.Save()
1951 return True
Kevin Degib1a07b82015-07-27 13:33:43 -06001952
Gavin Makea2e3302023-03-11 06:46:20 +00001953 if (
1954 GitCommand(
1955 self, ["checkout", "-q", "-b", branch.name, revid]
1956 ).Wait()
1957 == 0
1958 ):
1959 branch.Save()
1960 return True
1961 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06001962
Gavin Makea2e3302023-03-11 06:46:20 +00001963 def CheckoutBranch(self, name):
1964 """Checkout a local topic branch.
Shawn O. Pearce2816d4f2009-03-03 17:53:18 -08001965
Gavin Makea2e3302023-03-11 06:46:20 +00001966 Args:
1967 name: The name of the branch to checkout.
Shawn O. Pearce88443382010-10-08 10:02:09 +02001968
Gavin Makea2e3302023-03-11 06:46:20 +00001969 Returns:
1970 True if the checkout succeeded; False if it didn't; None if the
1971 branch didn't exist.
1972 """
1973 rev = R_HEADS + name
1974 head = self.work_git.GetHead()
1975 if head == rev:
1976 # Already on the branch.
1977 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001978
Gavin Makea2e3302023-03-11 06:46:20 +00001979 all_refs = self.bare_ref.all
1980 try:
1981 revid = all_refs[rev]
1982 except KeyError:
1983 # Branch does not exist in this project.
1984 return None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001985
Gavin Makea2e3302023-03-11 06:46:20 +00001986 if head.startswith(R_HEADS):
1987 try:
1988 head = all_refs[head]
1989 except KeyError:
1990 head = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001991
Gavin Makea2e3302023-03-11 06:46:20 +00001992 if head == revid:
1993 # Same revision; just update HEAD to point to the new
1994 # target branch, but otherwise take no other action.
1995 _lwrite(
1996 self.work_git.GetDotgitPath(subpath=HEAD),
1997 "ref: %s%s\n" % (R_HEADS, name),
1998 )
1999 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05002000
Gavin Makea2e3302023-03-11 06:46:20 +00002001 return (
2002 GitCommand(
2003 self,
2004 ["checkout", name, "--"],
2005 capture_stdout=True,
2006 capture_stderr=True,
2007 ).Wait()
2008 == 0
2009 )
Mike Frysinger98bb7652021-12-20 21:15:59 -05002010
Gavin Makea2e3302023-03-11 06:46:20 +00002011 def AbandonBranch(self, name):
2012 """Destroy a local topic branch.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002013
Gavin Makea2e3302023-03-11 06:46:20 +00002014 Args:
2015 name: The name of the branch to abandon.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002016
Gavin Makea2e3302023-03-11 06:46:20 +00002017 Returns:
2018 True if the abandon succeeded; False if it didn't; None if the
2019 branch didn't exist.
2020 """
2021 rev = R_HEADS + name
2022 all_refs = self.bare_ref.all
2023 if rev not in all_refs:
2024 # Doesn't exist
2025 return None
2026
2027 head = self.work_git.GetHead()
2028 if head == rev:
2029 # We can't destroy the branch while we are sitting
2030 # on it. Switch to a detached HEAD.
2031 head = all_refs[head]
2032
2033 revid = self.GetRevisionId(all_refs)
2034 if head == revid:
2035 _lwrite(
2036 self.work_git.GetDotgitPath(subpath=HEAD), "%s\n" % revid
2037 )
2038 else:
2039 self._Checkout(revid, quiet=True)
2040
2041 return (
2042 GitCommand(
2043 self,
2044 ["branch", "-D", name],
2045 capture_stdout=True,
2046 capture_stderr=True,
2047 ).Wait()
2048 == 0
2049 )
2050
2051 def PruneHeads(self):
2052 """Prune any topic branches already merged into upstream."""
2053 cb = self.CurrentBranch
2054 kill = []
2055 left = self._allrefs
2056 for name in left.keys():
2057 if name.startswith(R_HEADS):
2058 name = name[len(R_HEADS) :]
2059 if cb is None or name != cb:
2060 kill.append(name)
2061
2062 # Minor optimization: If there's nothing to prune, then don't try to
2063 # read any project state.
2064 if not kill and not cb:
2065 return []
2066
2067 rev = self.GetRevisionId(left)
2068 if (
2069 cb is not None
2070 and not self._revlist(HEAD + "..." + rev)
2071 and not self.IsDirty(consider_untracked=False)
2072 ):
2073 self.work_git.DetachHead(HEAD)
2074 kill.append(cb)
2075
2076 if kill:
2077 old = self.bare_git.GetHead()
2078
2079 try:
2080 self.bare_git.DetachHead(rev)
2081
2082 b = ["branch", "-d"]
2083 b.extend(kill)
2084 b = GitCommand(
2085 self, b, bare=True, capture_stdout=True, capture_stderr=True
2086 )
2087 b.Wait()
2088 finally:
2089 if ID_RE.match(old):
2090 self.bare_git.DetachHead(old)
2091 else:
2092 self.bare_git.SetHead(old)
2093 left = self._allrefs
2094
2095 for branch in kill:
2096 if (R_HEADS + branch) not in left:
2097 self.CleanPublishedCache()
2098 break
2099
2100 if cb and cb not in kill:
2101 kill.append(cb)
2102 kill.sort()
2103
2104 kept = []
2105 for branch in kill:
2106 if R_HEADS + branch in left:
2107 branch = self.GetBranch(branch)
2108 base = branch.LocalMerge
2109 if not base:
2110 base = rev
2111 kept.append(ReviewableBranch(self, branch, base))
2112 return kept
2113
2114 def GetRegisteredSubprojects(self):
2115 result = []
2116
2117 def rec(subprojects):
2118 if not subprojects:
2119 return
2120 result.extend(subprojects)
2121 for p in subprojects:
2122 rec(p.subprojects)
2123
2124 rec(self.subprojects)
2125 return result
2126
2127 def _GetSubmodules(self):
2128 # Unfortunately we cannot call `git submodule status --recursive` here
2129 # because the working tree might not exist yet, and it cannot be used
2130 # without a working tree in its current implementation.
2131
2132 def get_submodules(gitdir, rev):
2133 # Parse .gitmodules for submodule sub_paths and sub_urls.
2134 sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
2135 if not sub_paths:
2136 return []
2137 # Run `git ls-tree` to read SHAs of submodule object, which happen
2138 # to be revision of submodule repository.
2139 sub_revs = git_ls_tree(gitdir, rev, sub_paths)
2140 submodules = []
2141 for sub_path, sub_url in zip(sub_paths, sub_urls):
2142 try:
2143 sub_rev = sub_revs[sub_path]
2144 except KeyError:
2145 # Ignore non-exist submodules.
2146 continue
2147 submodules.append((sub_rev, sub_path, sub_url))
2148 return submodules
2149
2150 re_path = re.compile(r"^submodule\.(.+)\.path=(.*)$")
2151 re_url = re.compile(r"^submodule\.(.+)\.url=(.*)$")
2152
2153 def parse_gitmodules(gitdir, rev):
2154 cmd = ["cat-file", "blob", "%s:.gitmodules" % rev]
2155 try:
2156 p = GitCommand(
2157 None,
2158 cmd,
2159 capture_stdout=True,
2160 capture_stderr=True,
2161 bare=True,
2162 gitdir=gitdir,
2163 )
2164 except GitError:
2165 return [], []
2166 if p.Wait() != 0:
2167 return [], []
2168
2169 gitmodules_lines = []
2170 fd, temp_gitmodules_path = tempfile.mkstemp()
2171 try:
2172 os.write(fd, p.stdout.encode("utf-8"))
2173 os.close(fd)
2174 cmd = ["config", "--file", temp_gitmodules_path, "--list"]
2175 p = GitCommand(
2176 None,
2177 cmd,
2178 capture_stdout=True,
2179 capture_stderr=True,
2180 bare=True,
2181 gitdir=gitdir,
2182 )
2183 if p.Wait() != 0:
2184 return [], []
2185 gitmodules_lines = p.stdout.split("\n")
2186 except GitError:
2187 return [], []
2188 finally:
2189 platform_utils.remove(temp_gitmodules_path)
2190
2191 names = set()
2192 paths = {}
2193 urls = {}
2194 for line in gitmodules_lines:
2195 if not line:
2196 continue
2197 m = re_path.match(line)
2198 if m:
2199 names.add(m.group(1))
2200 paths[m.group(1)] = m.group(2)
2201 continue
2202 m = re_url.match(line)
2203 if m:
2204 names.add(m.group(1))
2205 urls[m.group(1)] = m.group(2)
2206 continue
2207 names = sorted(names)
2208 return (
2209 [paths.get(name, "") for name in names],
2210 [urls.get(name, "") for name in names],
2211 )
2212
2213 def git_ls_tree(gitdir, rev, paths):
2214 cmd = ["ls-tree", rev, "--"]
2215 cmd.extend(paths)
2216 try:
2217 p = GitCommand(
2218 None,
2219 cmd,
2220 capture_stdout=True,
2221 capture_stderr=True,
2222 bare=True,
2223 gitdir=gitdir,
2224 )
2225 except GitError:
2226 return []
2227 if p.Wait() != 0:
2228 return []
2229 objects = {}
2230 for line in p.stdout.split("\n"):
2231 if not line.strip():
2232 continue
2233 object_rev, object_path = line.split()[2:4]
2234 objects[object_path] = object_rev
2235 return objects
2236
2237 try:
2238 rev = self.GetRevisionId()
2239 except GitError:
2240 return []
2241 return get_submodules(self.gitdir, rev)
2242
2243 def GetDerivedSubprojects(self):
2244 result = []
2245 if not self.Exists:
2246 # If git repo does not exist yet, querying its submodules will
2247 # mess up its states; so return here.
2248 return result
2249 for rev, path, url in self._GetSubmodules():
2250 name = self.manifest.GetSubprojectName(self, path)
2251 (
2252 relpath,
2253 worktree,
2254 gitdir,
2255 objdir,
2256 ) = self.manifest.GetSubprojectPaths(self, name, path)
2257 project = self.manifest.paths.get(relpath)
2258 if project:
2259 result.extend(project.GetDerivedSubprojects())
2260 continue
2261
2262 if url.startswith(".."):
2263 url = urllib.parse.urljoin("%s/" % self.remote.url, url)
2264 remote = RemoteSpec(
2265 self.remote.name,
2266 url=url,
2267 pushUrl=self.remote.pushUrl,
2268 review=self.remote.review,
2269 revision=self.remote.revision,
2270 )
2271 subproject = Project(
2272 manifest=self.manifest,
2273 name=name,
2274 remote=remote,
2275 gitdir=gitdir,
2276 objdir=objdir,
2277 worktree=worktree,
2278 relpath=relpath,
2279 revisionExpr=rev,
2280 revisionId=rev,
2281 rebase=self.rebase,
2282 groups=self.groups,
2283 sync_c=self.sync_c,
2284 sync_s=self.sync_s,
2285 sync_tags=self.sync_tags,
2286 parent=self,
2287 is_derived=True,
2288 )
2289 result.append(subproject)
2290 result.extend(subproject.GetDerivedSubprojects())
2291 return result
2292
2293 def EnableRepositoryExtension(self, key, value="true", version=1):
2294 """Enable git repository extension |key| with |value|.
2295
2296 Args:
2297 key: The extension to enabled. Omit the "extensions." prefix.
2298 value: The value to use for the extension.
2299 version: The minimum git repository version needed.
2300 """
2301 # Make sure the git repo version is new enough already.
2302 found_version = self.config.GetInt("core.repositoryFormatVersion")
2303 if found_version is None:
2304 found_version = 0
2305 if found_version < version:
2306 self.config.SetString("core.repositoryFormatVersion", str(version))
2307
2308 # Enable the extension!
2309 self.config.SetString("extensions.%s" % (key,), value)
2310
2311 def ResolveRemoteHead(self, name=None):
2312 """Find out what the default branch (HEAD) points to.
2313
2314 Normally this points to refs/heads/master, but projects are moving to
2315 main. Support whatever the server uses rather than hardcoding "master"
2316 ourselves.
2317 """
2318 if name is None:
2319 name = self.remote.name
2320
2321 # The output will look like (NB: tabs are separators):
2322 # ref: refs/heads/master HEAD
2323 # 5f6803b100bb3cd0f534e96e88c91373e8ed1c44 HEAD
2324 output = self.bare_git.ls_remote(
2325 "-q", "--symref", "--exit-code", name, "HEAD"
2326 )
2327
2328 for line in output.splitlines():
2329 lhs, rhs = line.split("\t", 1)
2330 if rhs == "HEAD" and lhs.startswith("ref:"):
2331 return lhs[4:].strip()
2332
2333 return None
2334
2335 def _CheckForImmutableRevision(self):
2336 try:
2337 # if revision (sha or tag) is not present then following function
2338 # throws an error.
2339 self.bare_git.rev_list(
2340 "-1", "--missing=allow-any", "%s^0" % self.revisionExpr, "--"
2341 )
2342 if self.upstream:
2343 rev = self.GetRemote().ToLocal(self.upstream)
2344 self.bare_git.rev_list(
2345 "-1", "--missing=allow-any", "%s^0" % rev, "--"
2346 )
2347 self.bare_git.merge_base(
2348 "--is-ancestor", self.revisionExpr, rev
2349 )
2350 return True
2351 except GitError:
2352 # There is no such persistent revision. We have to fetch it.
2353 return False
2354
2355 def _FetchArchive(self, tarpath, cwd=None):
2356 cmd = ["archive", "-v", "-o", tarpath]
2357 cmd.append("--remote=%s" % self.remote.url)
2358 cmd.append("--prefix=%s/" % self.RelPath(local=False))
2359 cmd.append(self.revisionExpr)
2360
2361 command = GitCommand(
2362 self, cmd, cwd=cwd, capture_stdout=True, capture_stderr=True
2363 )
2364
2365 if command.Wait() != 0:
2366 raise GitError("git archive %s: %s" % (self.name, command.stderr))
2367
2368 def _RemoteFetch(
2369 self,
2370 name=None,
2371 current_branch_only=False,
2372 initial=False,
2373 quiet=False,
2374 verbose=False,
2375 output_redir=None,
2376 alt_dir=None,
2377 tags=True,
2378 prune=False,
2379 depth=None,
2380 submodules=False,
2381 ssh_proxy=None,
2382 force_sync=False,
2383 clone_filter=None,
2384 retry_fetches=2,
2385 retry_sleep_initial_sec=4.0,
2386 retry_exp_factor=2.0,
2387 ):
2388 is_sha1 = False
2389 tag_name = None
2390 # The depth should not be used when fetching to a mirror because
2391 # it will result in a shallow repository that cannot be cloned or
2392 # fetched from.
2393 # The repo project should also never be synced with partial depth.
2394 if self.manifest.IsMirror or self.relpath == ".repo/repo":
2395 depth = None
2396
2397 if depth:
2398 current_branch_only = True
2399
2400 if ID_RE.match(self.revisionExpr) is not None:
2401 is_sha1 = True
2402
2403 if current_branch_only:
2404 if self.revisionExpr.startswith(R_TAGS):
2405 # This is a tag and its commit id should never change.
2406 tag_name = self.revisionExpr[len(R_TAGS) :]
2407 elif self.upstream and self.upstream.startswith(R_TAGS):
2408 # This is a tag and its commit id should never change.
2409 tag_name = self.upstream[len(R_TAGS) :]
2410
2411 if is_sha1 or tag_name is not None:
2412 if self._CheckForImmutableRevision():
2413 if verbose:
2414 print(
2415 "Skipped fetching project %s (already have "
2416 "persistent ref)" % self.name
2417 )
2418 return True
2419 if is_sha1 and not depth:
2420 # When syncing a specific commit and --depth is not set:
2421 # * if upstream is explicitly specified and is not a sha1, fetch
2422 # only upstream as users expect only upstream to be fetch.
2423 # Note: The commit might not be in upstream in which case the
2424 # sync will fail.
2425 # * otherwise, fetch all branches to make sure we end up with
2426 # the specific commit.
2427 if self.upstream:
2428 current_branch_only = not ID_RE.match(self.upstream)
2429 else:
2430 current_branch_only = False
2431
2432 if not name:
2433 name = self.remote.name
2434
2435 remote = self.GetRemote(name)
2436 if not remote.PreConnectFetch(ssh_proxy):
2437 ssh_proxy = None
2438
2439 if initial:
2440 if alt_dir and "objects" == os.path.basename(alt_dir):
2441 ref_dir = os.path.dirname(alt_dir)
2442 packed_refs = os.path.join(self.gitdir, "packed-refs")
2443
2444 all_refs = self.bare_ref.all
2445 ids = set(all_refs.values())
2446 tmp = set()
2447
2448 for r, ref_id in GitRefs(ref_dir).all.items():
2449 if r not in all_refs:
2450 if r.startswith(R_TAGS) or remote.WritesTo(r):
2451 all_refs[r] = ref_id
2452 ids.add(ref_id)
2453 continue
2454
2455 if ref_id in ids:
2456 continue
2457
2458 r = "refs/_alt/%s" % ref_id
2459 all_refs[r] = ref_id
2460 ids.add(ref_id)
2461 tmp.add(r)
2462
2463 tmp_packed_lines = []
2464 old_packed_lines = []
2465
2466 for r in sorted(all_refs):
2467 line = "%s %s\n" % (all_refs[r], r)
2468 tmp_packed_lines.append(line)
2469 if r not in tmp:
2470 old_packed_lines.append(line)
2471
2472 tmp_packed = "".join(tmp_packed_lines)
2473 old_packed = "".join(old_packed_lines)
2474 _lwrite(packed_refs, tmp_packed)
2475 else:
2476 alt_dir = None
2477
2478 cmd = ["fetch"]
2479
2480 if clone_filter:
2481 git_require((2, 19, 0), fail=True, msg="partial clones")
2482 cmd.append("--filter=%s" % clone_filter)
2483 self.EnableRepositoryExtension("partialclone", self.remote.name)
2484
2485 if depth:
2486 cmd.append("--depth=%s" % depth)
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002487 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002488 # If this repo has shallow objects, then we don't know which refs
2489 # have shallow objects or not. Tell git to unshallow all fetched
2490 # refs. Don't do this with projects that don't have shallow
2491 # objects, since it is less efficient.
2492 if os.path.exists(os.path.join(self.gitdir, "shallow")):
2493 cmd.append("--depth=2147483647")
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002494
Gavin Makea2e3302023-03-11 06:46:20 +00002495 if not verbose:
2496 cmd.append("--quiet")
2497 if not quiet and sys.stdout.isatty():
2498 cmd.append("--progress")
2499 if not self.worktree:
2500 cmd.append("--update-head-ok")
2501 cmd.append(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002502
Gavin Makea2e3302023-03-11 06:46:20 +00002503 if force_sync:
2504 cmd.append("--force")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002505
Gavin Makea2e3302023-03-11 06:46:20 +00002506 if prune:
2507 cmd.append("--prune")
Mike Frysinger29626b42021-05-01 09:37:13 -04002508
Gavin Makea2e3302023-03-11 06:46:20 +00002509 # Always pass something for --recurse-submodules, git with GIT_DIR
2510 # behaves incorrectly when not given `--recurse-submodules=no`.
2511 # (b/218891912)
2512 cmd.append(
2513 f'--recurse-submodules={"on-demand" if submodules else "no"}'
2514 )
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002515
Gavin Makea2e3302023-03-11 06:46:20 +00002516 spec = []
2517 if not current_branch_only:
2518 # Fetch whole repo.
2519 spec.append(
2520 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2521 )
2522 elif tag_name is not None:
2523 spec.append("tag")
2524 spec.append(tag_name)
Remy Böhmer1469c282020-12-15 18:49:02 +01002525
Gavin Makea2e3302023-03-11 06:46:20 +00002526 if self.manifest.IsMirror and not current_branch_only:
2527 branch = None
Remy Böhmer1469c282020-12-15 18:49:02 +01002528 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002529 branch = self.revisionExpr
2530 if (
2531 not self.manifest.IsMirror
2532 and is_sha1
2533 and depth
2534 and git_require((1, 8, 3))
2535 ):
2536 # Shallow checkout of a specific commit, fetch from that commit and
2537 # not the heads only as the commit might be deeper in the history.
2538 spec.append(branch)
2539 if self.upstream:
2540 spec.append(self.upstream)
David James8d201162013-10-11 17:03:19 -07002541 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002542 if is_sha1:
2543 branch = self.upstream
2544 if branch is not None and branch.strip():
2545 if not branch.startswith("refs/"):
2546 branch = R_HEADS + branch
2547 spec.append(str(("+%s:" % branch) + remote.ToLocal(branch)))
David James8d201162013-10-11 17:03:19 -07002548
Gavin Makea2e3302023-03-11 06:46:20 +00002549 # If mirroring repo and we cannot deduce the tag or branch to fetch,
2550 # fetch whole repo.
2551 if self.manifest.IsMirror and not spec:
2552 spec.append(
2553 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2554 )
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002555
Gavin Makea2e3302023-03-11 06:46:20 +00002556 # If using depth then we should not get all the tags since they may
2557 # be outside of the depth.
2558 if not tags or depth:
2559 cmd.append("--no-tags")
2560 else:
2561 cmd.append("--tags")
2562 spec.append(str(("+refs/tags/*:") + remote.ToLocal("refs/tags/*")))
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002563
Gavin Makea2e3302023-03-11 06:46:20 +00002564 cmd.extend(spec)
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002565
Gavin Makea2e3302023-03-11 06:46:20 +00002566 # At least one retry minimum due to git remote prune.
2567 retry_fetches = max(retry_fetches, 2)
2568 retry_cur_sleep = retry_sleep_initial_sec
2569 ok = prune_tried = False
2570 for try_n in range(retry_fetches):
2571 gitcmd = GitCommand(
2572 self,
2573 cmd,
2574 bare=True,
2575 objdir=os.path.join(self.objdir, "objects"),
2576 ssh_proxy=ssh_proxy,
2577 merge_output=True,
2578 capture_stdout=quiet or bool(output_redir),
2579 )
2580 if gitcmd.stdout and not quiet and output_redir:
2581 output_redir.write(gitcmd.stdout)
2582 ret = gitcmd.Wait()
2583 if ret == 0:
2584 ok = True
2585 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002586
Gavin Makea2e3302023-03-11 06:46:20 +00002587 # Retry later due to HTTP 429 Too Many Requests.
2588 elif (
2589 gitcmd.stdout
2590 and "error:" in gitcmd.stdout
2591 and "HTTP 429" in gitcmd.stdout
2592 ):
2593 # Fallthru to sleep+retry logic at the bottom.
2594 pass
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002595
Gavin Makea2e3302023-03-11 06:46:20 +00002596 # Try to prune remote branches once in case there are conflicts.
2597 # For example, if the remote had refs/heads/upstream, but deleted
2598 # that and now has refs/heads/upstream/foo.
2599 elif (
2600 gitcmd.stdout
2601 and "error:" in gitcmd.stdout
2602 and "git remote prune" in gitcmd.stdout
2603 and not prune_tried
2604 ):
2605 prune_tried = True
2606 prunecmd = GitCommand(
2607 self,
2608 ["remote", "prune", name],
2609 bare=True,
2610 ssh_proxy=ssh_proxy,
2611 )
2612 ret = prunecmd.Wait()
2613 if ret:
2614 break
2615 print(
2616 "retrying fetch after pruning remote branches",
2617 file=output_redir,
2618 )
2619 # Continue right away so we don't sleep as we shouldn't need to.
2620 continue
2621 elif current_branch_only and is_sha1 and ret == 128:
2622 # Exit code 128 means "couldn't find the ref you asked for"; if
2623 # we're in sha1 mode, we just tried sync'ing from the upstream
2624 # field; it doesn't exist, thus abort the optimization attempt
2625 # and do a full sync.
2626 break
2627 elif ret < 0:
2628 # Git died with a signal, exit immediately.
2629 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002630
Gavin Makea2e3302023-03-11 06:46:20 +00002631 # Figure out how long to sleep before the next attempt, if there is
2632 # one.
2633 if not verbose and gitcmd.stdout:
2634 print(
2635 "\n%s:\n%s" % (self.name, gitcmd.stdout),
2636 end="",
2637 file=output_redir,
2638 )
2639 if try_n < retry_fetches - 1:
2640 print(
2641 "%s: sleeping %s seconds before retrying"
2642 % (self.name, retry_cur_sleep),
2643 file=output_redir,
2644 )
2645 time.sleep(retry_cur_sleep)
2646 retry_cur_sleep = min(
2647 retry_exp_factor * retry_cur_sleep, MAXIMUM_RETRY_SLEEP_SEC
2648 )
2649 retry_cur_sleep *= 1 - random.uniform(
2650 -RETRY_JITTER_PERCENT, RETRY_JITTER_PERCENT
2651 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002652
Gavin Makea2e3302023-03-11 06:46:20 +00002653 if initial:
2654 if alt_dir:
2655 if old_packed != "":
2656 _lwrite(packed_refs, old_packed)
2657 else:
2658 platform_utils.remove(packed_refs)
2659 self.bare_git.pack_refs("--all", "--prune")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002660
Gavin Makea2e3302023-03-11 06:46:20 +00002661 if is_sha1 and current_branch_only:
2662 # We just synced the upstream given branch; verify we
2663 # got what we wanted, else trigger a second run of all
2664 # refs.
2665 if not self._CheckForImmutableRevision():
2666 # Sync the current branch only with depth set to None.
2667 # We always pass depth=None down to avoid infinite recursion.
2668 return self._RemoteFetch(
2669 name=name,
2670 quiet=quiet,
2671 verbose=verbose,
2672 output_redir=output_redir,
2673 current_branch_only=current_branch_only and depth,
2674 initial=False,
2675 alt_dir=alt_dir,
Nasser Grainawiaafed292023-05-24 12:51:03 -06002676 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00002677 depth=None,
2678 ssh_proxy=ssh_proxy,
2679 clone_filter=clone_filter,
2680 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002681
Gavin Makea2e3302023-03-11 06:46:20 +00002682 return ok
Mike Frysingerf4545122019-11-11 04:34:16 -05002683
Gavin Makea2e3302023-03-11 06:46:20 +00002684 def _ApplyCloneBundle(self, initial=False, quiet=False, verbose=False):
2685 if initial and (
2686 self.manifest.manifestProject.depth or self.clone_depth
2687 ):
2688 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002689
Gavin Makea2e3302023-03-11 06:46:20 +00002690 remote = self.GetRemote()
2691 bundle_url = remote.url + "/clone.bundle"
2692 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
2693 if GetSchemeFromUrl(bundle_url) not in (
2694 "http",
2695 "https",
2696 "persistent-http",
2697 "persistent-https",
2698 ):
2699 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06002700
Gavin Makea2e3302023-03-11 06:46:20 +00002701 bundle_dst = os.path.join(self.gitdir, "clone.bundle")
2702 bundle_tmp = os.path.join(self.gitdir, "clone.bundle.tmp")
2703
2704 exist_dst = os.path.exists(bundle_dst)
2705 exist_tmp = os.path.exists(bundle_tmp)
2706
2707 if not initial and not exist_dst and not exist_tmp:
2708 return False
2709
2710 if not exist_dst:
2711 exist_dst = self._FetchBundle(
2712 bundle_url, bundle_tmp, bundle_dst, quiet, verbose
2713 )
2714 if not exist_dst:
2715 return False
2716
2717 cmd = ["fetch"]
2718 if not verbose:
2719 cmd.append("--quiet")
2720 if not quiet and sys.stdout.isatty():
2721 cmd.append("--progress")
2722 if not self.worktree:
2723 cmd.append("--update-head-ok")
2724 cmd.append(bundle_dst)
2725 for f in remote.fetch:
2726 cmd.append(str(f))
2727 cmd.append("+refs/tags/*:refs/tags/*")
2728
2729 ok = (
2730 GitCommand(
2731 self,
2732 cmd,
2733 bare=True,
2734 objdir=os.path.join(self.objdir, "objects"),
2735 ).Wait()
2736 == 0
2737 )
2738 platform_utils.remove(bundle_dst, missing_ok=True)
2739 platform_utils.remove(bundle_tmp, missing_ok=True)
2740 return ok
2741
2742 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
2743 platform_utils.remove(dstPath, missing_ok=True)
2744
2745 cmd = ["curl", "--fail", "--output", tmpPath, "--netrc", "--location"]
2746 if quiet:
2747 cmd += ["--silent", "--show-error"]
2748 if os.path.exists(tmpPath):
2749 size = os.stat(tmpPath).st_size
2750 if size >= 1024:
2751 cmd += ["--continue-at", "%d" % (size,)]
2752 else:
2753 platform_utils.remove(tmpPath)
2754 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
2755 if cookiefile:
2756 cmd += ["--cookie", cookiefile]
2757 if proxy:
2758 cmd += ["--proxy", proxy]
2759 elif "http_proxy" in os.environ and "darwin" == sys.platform:
2760 cmd += ["--proxy", os.environ["http_proxy"]]
2761 if srcUrl.startswith("persistent-https"):
2762 srcUrl = "http" + srcUrl[len("persistent-https") :]
2763 elif srcUrl.startswith("persistent-http"):
2764 srcUrl = "http" + srcUrl[len("persistent-http") :]
2765 cmd += [srcUrl]
2766
2767 proc = None
2768 with Trace("Fetching bundle: %s", " ".join(cmd)):
2769 if verbose:
2770 print("%s: Downloading bundle: %s" % (self.name, srcUrl))
2771 stdout = None if verbose else subprocess.PIPE
2772 stderr = None if verbose else subprocess.STDOUT
2773 try:
2774 proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
2775 except OSError:
2776 return False
2777
2778 (output, _) = proc.communicate()
2779 curlret = proc.returncode
2780
2781 if curlret == 22:
2782 # From curl man page:
2783 # 22: HTTP page not retrieved. The requested url was not found
2784 # or returned another error with the HTTP error code being 400
2785 # or above. This return code only appears if -f, --fail is used.
2786 if verbose:
2787 print(
2788 "%s: Unable to retrieve clone.bundle; ignoring."
2789 % self.name
2790 )
2791 if output:
2792 print("Curl output:\n%s" % output)
2793 return False
2794 elif curlret and not verbose and output:
2795 print("%s" % output, file=sys.stderr)
2796
2797 if os.path.exists(tmpPath):
2798 if curlret == 0 and self._IsValidBundle(tmpPath, quiet):
2799 platform_utils.rename(tmpPath, dstPath)
2800 return True
2801 else:
2802 platform_utils.remove(tmpPath)
2803 return False
2804 else:
2805 return False
2806
2807 def _IsValidBundle(self, path, quiet):
2808 try:
2809 with open(path, "rb") as f:
2810 if f.read(16) == b"# v2 git bundle\n":
2811 return True
2812 else:
2813 if not quiet:
2814 print(
2815 "Invalid clone.bundle file; ignoring.",
2816 file=sys.stderr,
2817 )
2818 return False
2819 except OSError:
2820 return False
2821
2822 def _Checkout(self, rev, quiet=False):
2823 cmd = ["checkout"]
2824 if quiet:
2825 cmd.append("-q")
2826 cmd.append(rev)
2827 cmd.append("--")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002828 if GitCommand(self, cmd).Wait() != 0:
Gavin Makea2e3302023-03-11 06:46:20 +00002829 if self._allrefs:
2830 raise GitError("%s checkout %s " % (self.name, rev))
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002831
Gavin Makea2e3302023-03-11 06:46:20 +00002832 def _CherryPick(self, rev, ffonly=False, record_origin=False):
2833 cmd = ["cherry-pick"]
2834 if ffonly:
2835 cmd.append("--ff")
2836 if record_origin:
2837 cmd.append("-x")
2838 cmd.append(rev)
2839 cmd.append("--")
2840 if GitCommand(self, cmd).Wait() != 0:
2841 if self._allrefs:
2842 raise GitError("%s cherry-pick %s " % (self.name, rev))
Victor Boivie0960b5b2010-11-26 13:42:13 +01002843
Gavin Makea2e3302023-03-11 06:46:20 +00002844 def _LsRemote(self, refs):
2845 cmd = ["ls-remote", self.remote.name, refs]
2846 p = GitCommand(self, cmd, capture_stdout=True)
2847 if p.Wait() == 0:
2848 return p.stdout
2849 return None
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002850
Gavin Makea2e3302023-03-11 06:46:20 +00002851 def _Revert(self, rev):
2852 cmd = ["revert"]
2853 cmd.append("--no-edit")
2854 cmd.append(rev)
2855 cmd.append("--")
2856 if GitCommand(self, cmd).Wait() != 0:
2857 if self._allrefs:
2858 raise GitError("%s revert %s " % (self.name, rev))
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002859
Gavin Makea2e3302023-03-11 06:46:20 +00002860 def _ResetHard(self, rev, quiet=True):
2861 cmd = ["reset", "--hard"]
2862 if quiet:
2863 cmd.append("-q")
2864 cmd.append(rev)
2865 if GitCommand(self, cmd).Wait() != 0:
2866 raise GitError("%s reset --hard %s " % (self.name, rev))
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002867
Gavin Makea2e3302023-03-11 06:46:20 +00002868 def _SyncSubmodules(self, quiet=True):
2869 cmd = ["submodule", "update", "--init", "--recursive"]
2870 if quiet:
2871 cmd.append("-q")
2872 if GitCommand(self, cmd).Wait() != 0:
2873 raise GitError(
2874 "%s submodule update --init --recursive " % self.name
2875 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002876
Gavin Makea2e3302023-03-11 06:46:20 +00002877 def _Rebase(self, upstream, onto=None):
2878 cmd = ["rebase"]
2879 if onto is not None:
2880 cmd.extend(["--onto", onto])
2881 cmd.append(upstream)
2882 if GitCommand(self, cmd).Wait() != 0:
2883 raise GitError("%s rebase %s " % (self.name, upstream))
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002884
Gavin Makea2e3302023-03-11 06:46:20 +00002885 def _FastForward(self, head, ffonly=False):
2886 cmd = ["merge", "--no-stat", head]
2887 if ffonly:
2888 cmd.append("--ff-only")
2889 if GitCommand(self, cmd).Wait() != 0:
2890 raise GitError("%s merge %s " % (self.name, head))
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002891
Gavin Makea2e3302023-03-11 06:46:20 +00002892 def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False):
2893 init_git_dir = not os.path.exists(self.gitdir)
2894 init_obj_dir = not os.path.exists(self.objdir)
2895 try:
2896 # Initialize the bare repository, which contains all of the objects.
2897 if init_obj_dir:
2898 os.makedirs(self.objdir)
2899 self.bare_objdir.init()
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002900
Gavin Makea2e3302023-03-11 06:46:20 +00002901 self._UpdateHooks(quiet=quiet)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002902
Gavin Makea2e3302023-03-11 06:46:20 +00002903 if self.use_git_worktrees:
2904 # Enable per-worktree config file support if possible. This
2905 # is more a nice-to-have feature for users rather than a
2906 # hard requirement.
2907 if git_require((2, 20, 0)):
2908 self.EnableRepositoryExtension("worktreeConfig")
Renaud Paquay788e9622017-01-27 11:41:12 -08002909
Gavin Makea2e3302023-03-11 06:46:20 +00002910 # If we have a separate directory to hold refs, initialize it as
2911 # well.
2912 if self.objdir != self.gitdir:
2913 if init_git_dir:
2914 os.makedirs(self.gitdir)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002915
Gavin Makea2e3302023-03-11 06:46:20 +00002916 if init_obj_dir or init_git_dir:
2917 self._ReferenceGitDir(
2918 self.objdir, self.gitdir, copy_all=True
2919 )
2920 try:
2921 self._CheckDirReference(self.objdir, self.gitdir)
2922 except GitError as e:
2923 if force_sync:
2924 print(
2925 "Retrying clone after deleting %s" % self.gitdir,
2926 file=sys.stderr,
2927 )
2928 try:
2929 platform_utils.rmtree(
2930 platform_utils.realpath(self.gitdir)
2931 )
2932 if self.worktree and os.path.exists(
2933 platform_utils.realpath(self.worktree)
2934 ):
2935 platform_utils.rmtree(
2936 platform_utils.realpath(self.worktree)
2937 )
2938 return self._InitGitDir(
2939 mirror_git=mirror_git,
2940 force_sync=False,
2941 quiet=quiet,
2942 )
2943 except Exception:
2944 raise e
2945 raise e
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002946
Gavin Makea2e3302023-03-11 06:46:20 +00002947 if init_git_dir:
2948 mp = self.manifest.manifestProject
2949 ref_dir = mp.reference or ""
Julien Camperguedd654222014-01-09 16:21:37 +01002950
Gavin Makea2e3302023-03-11 06:46:20 +00002951 def _expanded_ref_dirs():
2952 """Iterate through possible git reference dir paths."""
2953 name = self.name + ".git"
2954 yield mirror_git or os.path.join(ref_dir, name)
2955 for prefix in "", self.remote.name:
2956 yield os.path.join(
2957 ref_dir, ".repo", "project-objects", prefix, name
2958 )
2959 yield os.path.join(
2960 ref_dir, ".repo", "worktrees", prefix, name
2961 )
2962
2963 if ref_dir or mirror_git:
2964 found_ref_dir = None
2965 for path in _expanded_ref_dirs():
2966 if os.path.exists(path):
2967 found_ref_dir = path
2968 break
2969 ref_dir = found_ref_dir
2970
2971 if ref_dir:
2972 if not os.path.isabs(ref_dir):
2973 # The alternate directory is relative to the object
2974 # database.
2975 ref_dir = os.path.relpath(
2976 ref_dir, os.path.join(self.objdir, "objects")
2977 )
2978 _lwrite(
2979 os.path.join(
2980 self.objdir, "objects/info/alternates"
2981 ),
2982 os.path.join(ref_dir, "objects") + "\n",
2983 )
2984
2985 m = self.manifest.manifestProject.config
2986 for key in ["user.name", "user.email"]:
2987 if m.Has(key, include_defaults=False):
2988 self.config.SetString(key, m.GetString(key))
2989 if not self.manifest.EnableGitLfs:
2990 self.config.SetString(
2991 "filter.lfs.smudge", "git-lfs smudge --skip -- %f"
2992 )
2993 self.config.SetString(
2994 "filter.lfs.process", "git-lfs filter-process --skip"
2995 )
2996 self.config.SetBoolean(
2997 "core.bare", True if self.manifest.IsMirror else None
2998 )
2999 except Exception:
3000 if init_obj_dir and os.path.exists(self.objdir):
3001 platform_utils.rmtree(self.objdir)
3002 if init_git_dir and os.path.exists(self.gitdir):
3003 platform_utils.rmtree(self.gitdir)
3004 raise
3005
3006 def _UpdateHooks(self, quiet=False):
3007 if os.path.exists(self.objdir):
3008 self._InitHooks(quiet=quiet)
3009
3010 def _InitHooks(self, quiet=False):
3011 hooks = platform_utils.realpath(os.path.join(self.objdir, "hooks"))
3012 if not os.path.exists(hooks):
3013 os.makedirs(hooks)
3014
3015 # Delete sample hooks. They're noise.
3016 for hook in glob.glob(os.path.join(hooks, "*.sample")):
3017 try:
3018 platform_utils.remove(hook, missing_ok=True)
3019 except PermissionError:
3020 pass
3021
3022 for stock_hook in _ProjectHooks():
3023 name = os.path.basename(stock_hook)
3024
3025 if (
3026 name in ("commit-msg",)
3027 and not self.remote.review
3028 and self is not self.manifest.manifestProject
3029 ):
3030 # Don't install a Gerrit Code Review hook if this
3031 # project does not appear to use it for reviews.
3032 #
3033 # Since the manifest project is one of those, but also
3034 # managed through gerrit, it's excluded.
3035 continue
3036
3037 dst = os.path.join(hooks, name)
3038 if platform_utils.islink(dst):
3039 continue
3040 if os.path.exists(dst):
3041 # If the files are the same, we'll leave it alone. We create
3042 # symlinks below by default but fallback to hardlinks if the OS
3043 # blocks them. So if we're here, it's probably because we made a
3044 # hardlink below.
3045 if not filecmp.cmp(stock_hook, dst, shallow=False):
3046 if not quiet:
3047 _warn(
3048 "%s: Not replacing locally modified %s hook",
3049 self.RelPath(local=False),
3050 name,
3051 )
3052 continue
3053 try:
3054 platform_utils.symlink(
3055 os.path.relpath(stock_hook, os.path.dirname(dst)), dst
3056 )
3057 except OSError as e:
3058 if e.errno == errno.EPERM:
3059 try:
3060 os.link(stock_hook, dst)
3061 except OSError:
3062 raise GitError(self._get_symlink_error_message())
3063 else:
3064 raise
3065
3066 def _InitRemote(self):
3067 if self.remote.url:
3068 remote = self.GetRemote()
3069 remote.url = self.remote.url
3070 remote.pushUrl = self.remote.pushUrl
3071 remote.review = self.remote.review
3072 remote.projectname = self.name
3073
3074 if self.worktree:
3075 remote.ResetFetch(mirror=False)
3076 else:
3077 remote.ResetFetch(mirror=True)
3078 remote.Save()
3079
3080 def _InitMRef(self):
3081 """Initialize the pseudo m/<manifest branch> ref."""
3082 if self.manifest.branch:
3083 if self.use_git_worktrees:
3084 # Set up the m/ space to point to the worktree-specific ref
3085 # space. We'll update the worktree-specific ref space on each
3086 # checkout.
3087 ref = R_M + self.manifest.branch
3088 if not self.bare_ref.symref(ref):
3089 self.bare_git.symbolic_ref(
3090 "-m",
3091 "redirecting to worktree scope",
3092 ref,
3093 R_WORKTREE_M + self.manifest.branch,
3094 )
3095
3096 # We can't update this ref with git worktrees until it exists.
3097 # We'll wait until the initial checkout to set it.
3098 if not os.path.exists(self.worktree):
3099 return
3100
3101 base = R_WORKTREE_M
3102 active_git = self.work_git
3103
3104 self._InitAnyMRef(HEAD, self.bare_git, detach=True)
3105 else:
3106 base = R_M
3107 active_git = self.bare_git
3108
3109 self._InitAnyMRef(base + self.manifest.branch, active_git)
3110
3111 def _InitMirrorHead(self):
3112 self._InitAnyMRef(HEAD, self.bare_git)
3113
3114 def _InitAnyMRef(self, ref, active_git, detach=False):
3115 """Initialize |ref| in |active_git| to the value in the manifest.
3116
3117 This points |ref| to the <project> setting in the manifest.
3118
3119 Args:
3120 ref: The branch to update.
3121 active_git: The git repository to make updates in.
3122 detach: Whether to update target of symbolic refs, or overwrite the
3123 ref directly (and thus make it non-symbolic).
3124 """
3125 cur = self.bare_ref.symref(ref)
3126
3127 if self.revisionId:
3128 if cur != "" or self.bare_ref.get(ref) != self.revisionId:
3129 msg = "manifest set to %s" % self.revisionId
3130 dst = self.revisionId + "^0"
3131 active_git.UpdateRef(ref, dst, message=msg, detach=True)
Julien Camperguedd654222014-01-09 16:21:37 +01003132 else:
Gavin Makea2e3302023-03-11 06:46:20 +00003133 remote = self.GetRemote()
3134 dst = remote.ToLocal(self.revisionExpr)
3135 if cur != dst:
3136 msg = "manifest set to %s" % self.revisionExpr
3137 if detach:
3138 active_git.UpdateRef(ref, dst, message=msg, detach=True)
3139 else:
3140 active_git.symbolic_ref("-m", msg, ref, dst)
Julien Camperguedd654222014-01-09 16:21:37 +01003141
Gavin Makea2e3302023-03-11 06:46:20 +00003142 def _CheckDirReference(self, srcdir, destdir):
3143 # Git worktrees don't use symlinks to share at all.
3144 if self.use_git_worktrees:
3145 return
Julien Camperguedd654222014-01-09 16:21:37 +01003146
Gavin Makea2e3302023-03-11 06:46:20 +00003147 for name in self.shareable_dirs:
3148 # Try to self-heal a bit in simple cases.
3149 dst_path = os.path.join(destdir, name)
3150 src_path = os.path.join(srcdir, name)
Julien Camperguedd654222014-01-09 16:21:37 +01003151
Gavin Makea2e3302023-03-11 06:46:20 +00003152 dst = platform_utils.realpath(dst_path)
3153 if os.path.lexists(dst):
3154 src = platform_utils.realpath(src_path)
3155 # Fail if the links are pointing to the wrong place.
3156 if src != dst:
3157 _error("%s is different in %s vs %s", name, destdir, srcdir)
3158 raise GitError(
3159 "--force-sync not enabled; cannot overwrite a local "
3160 "work tree. If you're comfortable with the "
3161 "possibility of losing the work tree's git metadata,"
3162 " use `repo sync --force-sync {0}` to "
3163 "proceed.".format(self.RelPath(local=False))
3164 )
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003165
Gavin Makea2e3302023-03-11 06:46:20 +00003166 def _ReferenceGitDir(self, gitdir, dotgit, copy_all):
3167 """Update |dotgit| to reference |gitdir|, using symlinks where possible.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003168
Gavin Makea2e3302023-03-11 06:46:20 +00003169 Args:
3170 gitdir: The bare git repository. Must already be initialized.
3171 dotgit: The repository you would like to initialize.
3172 copy_all: If true, copy all remaining files from |gitdir| ->
3173 |dotgit|. This saves you the effort of initializing |dotgit|
3174 yourself.
3175 """
3176 symlink_dirs = self.shareable_dirs[:]
3177 to_symlink = symlink_dirs
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003178
Gavin Makea2e3302023-03-11 06:46:20 +00003179 to_copy = []
3180 if copy_all:
3181 to_copy = platform_utils.listdir(gitdir)
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003182
Gavin Makea2e3302023-03-11 06:46:20 +00003183 dotgit = platform_utils.realpath(dotgit)
3184 for name in set(to_copy).union(to_symlink):
3185 try:
3186 src = platform_utils.realpath(os.path.join(gitdir, name))
3187 dst = os.path.join(dotgit, name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003188
Gavin Makea2e3302023-03-11 06:46:20 +00003189 if os.path.lexists(dst):
3190 continue
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003191
Gavin Makea2e3302023-03-11 06:46:20 +00003192 # If the source dir doesn't exist, create an empty dir.
3193 if name in symlink_dirs and not os.path.lexists(src):
3194 os.makedirs(src)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003195
Gavin Makea2e3302023-03-11 06:46:20 +00003196 if name in to_symlink:
3197 platform_utils.symlink(
3198 os.path.relpath(src, os.path.dirname(dst)), dst
3199 )
3200 elif copy_all and not platform_utils.islink(dst):
3201 if platform_utils.isdir(src):
3202 shutil.copytree(src, dst)
3203 elif os.path.isfile(src):
3204 shutil.copy(src, dst)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003205
Gavin Makea2e3302023-03-11 06:46:20 +00003206 except OSError as e:
3207 if e.errno == errno.EPERM:
3208 raise DownloadError(self._get_symlink_error_message())
3209 else:
3210 raise
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003211
Gavin Makea2e3302023-03-11 06:46:20 +00003212 def _InitGitWorktree(self):
3213 """Init the project using git worktrees."""
3214 self.bare_git.worktree("prune")
3215 self.bare_git.worktree(
3216 "add",
3217 "-ff",
3218 "--checkout",
3219 "--detach",
3220 "--lock",
3221 self.worktree,
3222 self.GetRevisionId(),
3223 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003224
Gavin Makea2e3302023-03-11 06:46:20 +00003225 # Rewrite the internal state files to use relative paths between the
3226 # checkouts & worktrees.
3227 dotgit = os.path.join(self.worktree, ".git")
3228 with open(dotgit, "r") as fp:
3229 # Figure out the checkout->worktree path.
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003230 setting = fp.read()
Gavin Makea2e3302023-03-11 06:46:20 +00003231 assert setting.startswith("gitdir:")
3232 git_worktree_path = setting.split(":", 1)[1].strip()
3233 # Some platforms (e.g. Windows) won't let us update dotgit in situ
3234 # because of file permissions. Delete it and recreate it from scratch
3235 # to avoid.
3236 platform_utils.remove(dotgit)
3237 # Use relative path from checkout->worktree & maintain Unix line endings
3238 # on all OS's to match git behavior.
3239 with open(dotgit, "w", newline="\n") as fp:
3240 print(
3241 "gitdir:",
3242 os.path.relpath(git_worktree_path, self.worktree),
3243 file=fp,
3244 )
3245 # Use relative path from worktree->checkout & maintain Unix line endings
3246 # on all OS's to match git behavior.
3247 with open(
3248 os.path.join(git_worktree_path, "gitdir"), "w", newline="\n"
3249 ) as fp:
3250 print(os.path.relpath(dotgit, git_worktree_path), file=fp)
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003251
Gavin Makea2e3302023-03-11 06:46:20 +00003252 self._InitMRef()
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003253
Gavin Makea2e3302023-03-11 06:46:20 +00003254 def _InitWorkTree(self, force_sync=False, submodules=False):
3255 """Setup the worktree .git path.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003256
Gavin Makea2e3302023-03-11 06:46:20 +00003257 This is the user-visible path like src/foo/.git/.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003258
Gavin Makea2e3302023-03-11 06:46:20 +00003259 With non-git-worktrees, this will be a symlink to the .repo/projects/
3260 path. With git-worktrees, this will be a .git file using "gitdir: ..."
3261 syntax.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003262
Gavin Makea2e3302023-03-11 06:46:20 +00003263 Older checkouts had .git/ directories. If we see that, migrate it.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003264
Gavin Makea2e3302023-03-11 06:46:20 +00003265 This also handles changes in the manifest. Maybe this project was
3266 backed by "foo/bar" on the server, but now it's "new/foo/bar". We have
3267 to update the path we point to under .repo/projects/ to match.
3268 """
3269 dotgit = os.path.join(self.worktree, ".git")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003270
Gavin Makea2e3302023-03-11 06:46:20 +00003271 # If using an old layout style (a directory), migrate it.
3272 if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
3273 self._MigrateOldWorkTreeGitDir(dotgit)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003274
Gavin Makea2e3302023-03-11 06:46:20 +00003275 init_dotgit = not os.path.exists(dotgit)
3276 if self.use_git_worktrees:
3277 if init_dotgit:
3278 self._InitGitWorktree()
3279 self._CopyAndLinkFiles()
3280 else:
3281 if not init_dotgit:
3282 # See if the project has changed.
3283 if platform_utils.realpath(
3284 self.gitdir
3285 ) != platform_utils.realpath(dotgit):
3286 platform_utils.remove(dotgit)
Doug Anderson37282b42011-03-04 11:54:18 -08003287
Gavin Makea2e3302023-03-11 06:46:20 +00003288 if init_dotgit or not os.path.exists(dotgit):
3289 os.makedirs(self.worktree, exist_ok=True)
3290 platform_utils.symlink(
3291 os.path.relpath(self.gitdir, self.worktree), dotgit
3292 )
Doug Anderson37282b42011-03-04 11:54:18 -08003293
Gavin Makea2e3302023-03-11 06:46:20 +00003294 if init_dotgit:
3295 _lwrite(
3296 os.path.join(dotgit, HEAD), "%s\n" % self.GetRevisionId()
3297 )
Doug Anderson37282b42011-03-04 11:54:18 -08003298
Gavin Makea2e3302023-03-11 06:46:20 +00003299 # Finish checking out the worktree.
3300 cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
3301 if GitCommand(self, cmd).Wait() != 0:
3302 raise GitError(
3303 "Cannot initialize work tree for " + self.name
3304 )
Doug Anderson37282b42011-03-04 11:54:18 -08003305
Gavin Makea2e3302023-03-11 06:46:20 +00003306 if submodules:
3307 self._SyncSubmodules(quiet=True)
3308 self._CopyAndLinkFiles()
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003309
Gavin Makea2e3302023-03-11 06:46:20 +00003310 @classmethod
3311 def _MigrateOldWorkTreeGitDir(cls, dotgit):
3312 """Migrate the old worktree .git/ dir style to a symlink.
3313
3314 This logic specifically only uses state from |dotgit| to figure out
3315 where to move content and not |self|. This way if the backing project
3316 also changed places, we only do the .git/ dir to .git symlink migration
3317 here. The path updates will happen independently.
3318 """
3319 # Figure out where in .repo/projects/ it's pointing to.
3320 if not os.path.islink(os.path.join(dotgit, "refs")):
3321 raise GitError(f"{dotgit}: unsupported checkout state")
3322 gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, "refs")))
3323
3324 # Remove known symlink paths that exist in .repo/projects/.
3325 KNOWN_LINKS = {
3326 "config",
3327 "description",
3328 "hooks",
3329 "info",
3330 "logs",
3331 "objects",
3332 "packed-refs",
3333 "refs",
3334 "rr-cache",
3335 "shallow",
3336 "svn",
3337 }
3338 # Paths that we know will be in both, but are safe to clobber in
3339 # .repo/projects/.
3340 SAFE_TO_CLOBBER = {
3341 "COMMIT_EDITMSG",
3342 "FETCH_HEAD",
3343 "HEAD",
3344 "gc.log",
3345 "gitk.cache",
3346 "index",
3347 "ORIG_HEAD",
3348 }
3349
3350 # First see if we'd succeed before starting the migration.
3351 unknown_paths = []
3352 for name in platform_utils.listdir(dotgit):
3353 # Ignore all temporary/backup names. These are common with vim &
3354 # emacs.
3355 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3356 continue
3357
3358 dotgit_path = os.path.join(dotgit, name)
3359 if name in KNOWN_LINKS:
3360 if not platform_utils.islink(dotgit_path):
3361 unknown_paths.append(f"{dotgit_path}: should be a symlink")
3362 else:
3363 gitdir_path = os.path.join(gitdir, name)
3364 if name not in SAFE_TO_CLOBBER and os.path.exists(gitdir_path):
3365 unknown_paths.append(
3366 f"{dotgit_path}: unknown file; please file a bug"
3367 )
3368 if unknown_paths:
3369 raise GitError("Aborting migration: " + "\n".join(unknown_paths))
3370
3371 # Now walk the paths and sync the .git/ to .repo/projects/.
3372 for name in platform_utils.listdir(dotgit):
3373 dotgit_path = os.path.join(dotgit, name)
3374
3375 # Ignore all temporary/backup names. These are common with vim &
3376 # emacs.
3377 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3378 platform_utils.remove(dotgit_path)
3379 elif name in KNOWN_LINKS:
3380 platform_utils.remove(dotgit_path)
3381 else:
3382 gitdir_path = os.path.join(gitdir, name)
3383 platform_utils.remove(gitdir_path, missing_ok=True)
3384 platform_utils.rename(dotgit_path, gitdir_path)
3385
3386 # Now that the dir should be empty, clear it out, and symlink it over.
3387 platform_utils.rmdir(dotgit)
3388 platform_utils.symlink(
3389 os.path.relpath(gitdir, os.path.dirname(dotgit)), dotgit
3390 )
3391
3392 def _get_symlink_error_message(self):
3393 if platform_utils.isWindows():
3394 return (
3395 "Unable to create symbolic link. Please re-run the command as "
3396 "Administrator, or see "
3397 "https://github.com/git-for-windows/git/wiki/Symbolic-Links "
3398 "for other options."
3399 )
3400 return "filesystem must support symlinks"
3401
3402 def _revlist(self, *args, **kw):
3403 a = []
3404 a.extend(args)
3405 a.append("--")
3406 return self.work_git.rev_list(*a, **kw)
3407
3408 @property
3409 def _allrefs(self):
3410 return self.bare_ref.all
3411
3412 def _getLogs(
3413 self, rev1, rev2, oneline=False, color=True, pretty_format=None
3414 ):
3415 """Get logs between two revisions of this project."""
3416 comp = ".."
3417 if rev1:
3418 revs = [rev1]
3419 if rev2:
3420 revs.extend([comp, rev2])
3421 cmd = ["log", "".join(revs)]
3422 out = DiffColoring(self.config)
3423 if out.is_on and color:
3424 cmd.append("--color")
3425 if pretty_format is not None:
3426 cmd.append("--pretty=format:%s" % pretty_format)
3427 if oneline:
3428 cmd.append("--oneline")
3429
3430 try:
3431 log = GitCommand(
3432 self, cmd, capture_stdout=True, capture_stderr=True
3433 )
3434 if log.Wait() == 0:
3435 return log.stdout
3436 except GitError:
3437 # worktree may not exist if groups changed for example. In that
3438 # case, try in gitdir instead.
3439 if not os.path.exists(self.worktree):
3440 return self.bare_git.log(*cmd[1:])
3441 else:
3442 raise
3443 return None
3444
3445 def getAddedAndRemovedLogs(
3446 self, toProject, oneline=False, color=True, pretty_format=None
3447 ):
3448 """Get the list of logs from this revision to given revisionId"""
3449 logs = {}
3450 selfId = self.GetRevisionId(self._allrefs)
3451 toId = toProject.GetRevisionId(toProject._allrefs)
3452
3453 logs["added"] = self._getLogs(
3454 selfId,
3455 toId,
3456 oneline=oneline,
3457 color=color,
3458 pretty_format=pretty_format,
3459 )
3460 logs["removed"] = self._getLogs(
3461 toId,
3462 selfId,
3463 oneline=oneline,
3464 color=color,
3465 pretty_format=pretty_format,
3466 )
3467 return logs
3468
3469 class _GitGetByExec(object):
3470 def __init__(self, project, bare, gitdir):
3471 self._project = project
3472 self._bare = bare
3473 self._gitdir = gitdir
3474
3475 # __getstate__ and __setstate__ are required for pickling because
3476 # __getattr__ exists.
3477 def __getstate__(self):
3478 return (self._project, self._bare, self._gitdir)
3479
3480 def __setstate__(self, state):
3481 self._project, self._bare, self._gitdir = state
3482
3483 def LsOthers(self):
3484 p = GitCommand(
3485 self._project,
3486 ["ls-files", "-z", "--others", "--exclude-standard"],
3487 bare=False,
3488 gitdir=self._gitdir,
3489 capture_stdout=True,
3490 capture_stderr=True,
3491 )
3492 if p.Wait() == 0:
3493 out = p.stdout
3494 if out:
3495 # Backslash is not anomalous.
3496 return out[:-1].split("\0")
3497 return []
3498
3499 def DiffZ(self, name, *args):
3500 cmd = [name]
3501 cmd.append("-z")
3502 cmd.append("--ignore-submodules")
3503 cmd.extend(args)
3504 p = GitCommand(
3505 self._project,
3506 cmd,
3507 gitdir=self._gitdir,
3508 bare=False,
3509 capture_stdout=True,
3510 capture_stderr=True,
3511 )
3512 p.Wait()
3513 r = {}
3514 out = p.stdout
3515 if out:
3516 out = iter(out[:-1].split("\0"))
3517 while out:
3518 try:
3519 info = next(out)
3520 path = next(out)
3521 except StopIteration:
3522 break
3523
3524 class _Info(object):
3525 def __init__(self, path, omode, nmode, oid, nid, state):
3526 self.path = path
3527 self.src_path = None
3528 self.old_mode = omode
3529 self.new_mode = nmode
3530 self.old_id = oid
3531 self.new_id = nid
3532
3533 if len(state) == 1:
3534 self.status = state
3535 self.level = None
3536 else:
3537 self.status = state[:1]
3538 self.level = state[1:]
3539 while self.level.startswith("0"):
3540 self.level = self.level[1:]
3541
3542 info = info[1:].split(" ")
3543 info = _Info(path, *info)
3544 if info.status in ("R", "C"):
3545 info.src_path = info.path
3546 info.path = next(out)
3547 r[info.path] = info
3548 return r
3549
3550 def GetDotgitPath(self, subpath=None):
3551 """Return the full path to the .git dir.
3552
3553 As a convenience, append |subpath| if provided.
3554 """
3555 if self._bare:
3556 dotgit = self._gitdir
3557 else:
3558 dotgit = os.path.join(self._project.worktree, ".git")
3559 if os.path.isfile(dotgit):
3560 # Git worktrees use a "gitdir:" syntax to point to the
3561 # scratch space.
3562 with open(dotgit) as fp:
3563 setting = fp.read()
3564 assert setting.startswith("gitdir:")
3565 gitdir = setting.split(":", 1)[1].strip()
3566 dotgit = os.path.normpath(
3567 os.path.join(self._project.worktree, gitdir)
3568 )
3569
3570 return dotgit if subpath is None else os.path.join(dotgit, subpath)
3571
3572 def GetHead(self):
3573 """Return the ref that HEAD points to."""
3574 path = self.GetDotgitPath(subpath=HEAD)
3575 try:
3576 with open(path) as fd:
3577 line = fd.readline()
3578 except IOError as e:
3579 raise NoManifestException(path, str(e))
3580 try:
3581 line = line.decode()
3582 except AttributeError:
3583 pass
3584 if line.startswith("ref: "):
3585 return line[5:-1]
3586 return line[:-1]
3587
3588 def SetHead(self, ref, message=None):
3589 cmdv = []
3590 if message is not None:
3591 cmdv.extend(["-m", message])
3592 cmdv.append(HEAD)
3593 cmdv.append(ref)
3594 self.symbolic_ref(*cmdv)
3595
3596 def DetachHead(self, new, message=None):
3597 cmdv = ["--no-deref"]
3598 if message is not None:
3599 cmdv.extend(["-m", message])
3600 cmdv.append(HEAD)
3601 cmdv.append(new)
3602 self.update_ref(*cmdv)
3603
3604 def UpdateRef(self, name, new, old=None, message=None, detach=False):
3605 cmdv = []
3606 if message is not None:
3607 cmdv.extend(["-m", message])
3608 if detach:
3609 cmdv.append("--no-deref")
3610 cmdv.append(name)
3611 cmdv.append(new)
3612 if old is not None:
3613 cmdv.append(old)
3614 self.update_ref(*cmdv)
3615
3616 def DeleteRef(self, name, old=None):
3617 if not old:
3618 old = self.rev_parse(name)
3619 self.update_ref("-d", name, old)
3620 self._project.bare_ref.deleted(name)
3621
3622 def rev_list(self, *args, **kw):
3623 if "format" in kw:
3624 cmdv = ["log", "--pretty=format:%s" % kw["format"]]
3625 else:
3626 cmdv = ["rev-list"]
3627 cmdv.extend(args)
3628 p = GitCommand(
3629 self._project,
3630 cmdv,
3631 bare=self._bare,
3632 gitdir=self._gitdir,
3633 capture_stdout=True,
3634 capture_stderr=True,
3635 )
3636 if p.Wait() != 0:
3637 raise GitError(
3638 "%s rev-list %s: %s"
3639 % (self._project.name, str(args), p.stderr)
3640 )
3641 return p.stdout.splitlines()
3642
3643 def __getattr__(self, name):
3644 """Allow arbitrary git commands using pythonic syntax.
3645
3646 This allows you to do things like:
3647 git_obj.rev_parse('HEAD')
3648
3649 Since we don't have a 'rev_parse' method defined, the __getattr__
3650 will run. We'll replace the '_' with a '-' and try to run a git
3651 command. Any other positional arguments will be passed to the git
3652 command, and the following keyword arguments are supported:
3653 config: An optional dict of git config options to be passed with
3654 '-c'.
3655
3656 Args:
3657 name: The name of the git command to call. Any '_' characters
3658 will be replaced with '-'.
3659
3660 Returns:
3661 A callable object that will try to call git with the named
3662 command.
3663 """
3664 name = name.replace("_", "-")
3665
3666 def runner(*args, **kwargs):
3667 cmdv = []
3668 config = kwargs.pop("config", None)
3669 for k in kwargs:
3670 raise TypeError(
3671 "%s() got an unexpected keyword argument %r" % (name, k)
3672 )
3673 if config is not None:
3674 for k, v in config.items():
3675 cmdv.append("-c")
3676 cmdv.append("%s=%s" % (k, v))
3677 cmdv.append(name)
3678 cmdv.extend(args)
3679 p = GitCommand(
3680 self._project,
3681 cmdv,
3682 bare=self._bare,
3683 gitdir=self._gitdir,
3684 capture_stdout=True,
3685 capture_stderr=True,
3686 )
3687 if p.Wait() != 0:
3688 raise GitError(
3689 "%s %s: %s" % (self._project.name, name, p.stderr)
3690 )
3691 r = p.stdout
3692 if r.endswith("\n") and r.index("\n") == len(r) - 1:
3693 return r[:-1]
3694 return r
3695
3696 return runner
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003697
3698
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003699class _PriorSyncFailedError(Exception):
Gavin Makea2e3302023-03-11 06:46:20 +00003700 def __str__(self):
3701 return "prior sync failed; rebase still in progress"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003702
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003703
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003704class _DirtyError(Exception):
Gavin Makea2e3302023-03-11 06:46:20 +00003705 def __str__(self):
3706 return "contains uncommitted changes"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003707
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003708
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003709class _InfoMessage(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003710 def __init__(self, project, text):
3711 self.project = project
3712 self.text = text
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003713
Gavin Makea2e3302023-03-11 06:46:20 +00003714 def Print(self, syncbuf):
3715 syncbuf.out.info(
3716 "%s/: %s", self.project.RelPath(local=False), self.text
3717 )
3718 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003719
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003720
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003721class _Failure(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003722 def __init__(self, project, why):
3723 self.project = project
3724 self.why = why
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003725
Gavin Makea2e3302023-03-11 06:46:20 +00003726 def Print(self, syncbuf):
3727 syncbuf.out.fail(
3728 "error: %s/: %s", self.project.RelPath(local=False), str(self.why)
3729 )
3730 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003731
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003732
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003733class _Later(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003734 def __init__(self, project, action):
3735 self.project = project
3736 self.action = action
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003737
Gavin Makea2e3302023-03-11 06:46:20 +00003738 def Run(self, syncbuf):
3739 out = syncbuf.out
3740 out.project("project %s/", self.project.RelPath(local=False))
3741 out.nl()
3742 try:
3743 self.action()
3744 out.nl()
3745 return True
3746 except GitError:
3747 out.nl()
3748 return False
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003749
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003750
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003751class _SyncColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +00003752 def __init__(self, config):
3753 super().__init__(config, "reposync")
3754 self.project = self.printer("header", attr="bold")
3755 self.info = self.printer("info")
3756 self.fail = self.printer("fail", fg="red")
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003757
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003758
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003759class SyncBuffer(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003760 def __init__(self, config, detach_head=False):
3761 self._messages = []
3762 self._failures = []
3763 self._later_queue1 = []
3764 self._later_queue2 = []
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003765
Gavin Makea2e3302023-03-11 06:46:20 +00003766 self.out = _SyncColoring(config)
3767 self.out.redirect(sys.stderr)
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003768
Gavin Makea2e3302023-03-11 06:46:20 +00003769 self.detach_head = detach_head
3770 self.clean = True
3771 self.recent_clean = True
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003772
Gavin Makea2e3302023-03-11 06:46:20 +00003773 def info(self, project, fmt, *args):
3774 self._messages.append(_InfoMessage(project, fmt % args))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003775
Gavin Makea2e3302023-03-11 06:46:20 +00003776 def fail(self, project, err=None):
3777 self._failures.append(_Failure(project, err))
David Rileye0684ad2017-04-05 00:02:59 -07003778 self._MarkUnclean()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003779
Gavin Makea2e3302023-03-11 06:46:20 +00003780 def later1(self, project, what):
3781 self._later_queue1.append(_Later(project, what))
Mike Frysinger70d861f2019-08-26 15:22:36 -04003782
Gavin Makea2e3302023-03-11 06:46:20 +00003783 def later2(self, project, what):
3784 self._later_queue2.append(_Later(project, what))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003785
Gavin Makea2e3302023-03-11 06:46:20 +00003786 def Finish(self):
3787 self._PrintMessages()
3788 self._RunLater()
3789 self._PrintMessages()
3790 return self.clean
3791
3792 def Recently(self):
3793 recent_clean = self.recent_clean
3794 self.recent_clean = True
3795 return recent_clean
3796
3797 def _MarkUnclean(self):
3798 self.clean = False
3799 self.recent_clean = False
3800
3801 def _RunLater(self):
3802 for q in ["_later_queue1", "_later_queue2"]:
3803 if not self._RunQueue(q):
3804 return
3805
3806 def _RunQueue(self, queue):
3807 for m in getattr(self, queue):
3808 if not m.Run(self):
3809 self._MarkUnclean()
3810 return False
3811 setattr(self, queue, [])
3812 return True
3813
3814 def _PrintMessages(self):
3815 if self._messages or self._failures:
3816 if os.isatty(2):
3817 self.out.write(progress.CSI_ERASE_LINE)
3818 self.out.write("\r")
3819
3820 for m in self._messages:
3821 m.Print(self)
3822 for m in self._failures:
3823 m.Print(self)
3824
3825 self._messages = []
3826 self._failures = []
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003827
3828
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003829class MetaProject(Project):
Gavin Makea2e3302023-03-11 06:46:20 +00003830 """A special project housed under .repo."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003831
Gavin Makea2e3302023-03-11 06:46:20 +00003832 def __init__(self, manifest, name, gitdir, worktree):
3833 Project.__init__(
3834 self,
3835 manifest=manifest,
3836 name=name,
3837 gitdir=gitdir,
3838 objdir=gitdir,
3839 worktree=worktree,
3840 remote=RemoteSpec("origin"),
3841 relpath=".repo/%s" % name,
3842 revisionExpr="refs/heads/master",
3843 revisionId=None,
3844 groups=None,
3845 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003846
Gavin Makea2e3302023-03-11 06:46:20 +00003847 def PreSync(self):
3848 if self.Exists:
3849 cb = self.CurrentBranch
3850 if cb:
3851 base = self.GetBranch(cb).merge
3852 if base:
3853 self.revisionExpr = base
3854 self.revisionId = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003855
Gavin Makea2e3302023-03-11 06:46:20 +00003856 @property
3857 def HasChanges(self):
3858 """Has the remote received new commits not yet checked out?"""
3859 if not self.remote or not self.revisionExpr:
3860 return False
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003861
Gavin Makea2e3302023-03-11 06:46:20 +00003862 all_refs = self.bare_ref.all
3863 revid = self.GetRevisionId(all_refs)
3864 head = self.work_git.GetHead()
3865 if head.startswith(R_HEADS):
3866 try:
3867 head = all_refs[head]
3868 except KeyError:
3869 head = None
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003870
Gavin Makea2e3302023-03-11 06:46:20 +00003871 if revid == head:
3872 return False
3873 elif self._revlist(not_rev(HEAD), revid):
3874 return True
3875 return False
LaMont Jones9b72cf22022-03-29 21:54:22 +00003876
3877
3878class RepoProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003879 """The MetaProject for repo itself."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003880
Gavin Makea2e3302023-03-11 06:46:20 +00003881 @property
3882 def LastFetch(self):
3883 try:
3884 fh = os.path.join(self.gitdir, "FETCH_HEAD")
3885 return os.path.getmtime(fh)
3886 except OSError:
3887 return 0
LaMont Jones9b72cf22022-03-29 21:54:22 +00003888
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +01003889
LaMont Jones9b72cf22022-03-29 21:54:22 +00003890class ManifestProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003891 """The MetaProject for manifests."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003892
Gavin Makea2e3302023-03-11 06:46:20 +00003893 def MetaBranchSwitch(self, submodules=False):
3894 """Prepare for manifest branch switch."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003895
Gavin Makea2e3302023-03-11 06:46:20 +00003896 # detach and delete manifest branch, allowing a new
3897 # branch to take over
3898 syncbuf = SyncBuffer(self.config, detach_head=True)
3899 self.Sync_LocalHalf(syncbuf, submodules=submodules)
3900 syncbuf.Finish()
LaMont Jones9b72cf22022-03-29 21:54:22 +00003901
Gavin Makea2e3302023-03-11 06:46:20 +00003902 return (
3903 GitCommand(
3904 self,
3905 ["update-ref", "-d", "refs/heads/default"],
3906 capture_stdout=True,
3907 capture_stderr=True,
3908 ).Wait()
3909 == 0
3910 )
LaMont Jones9b03f152022-03-29 23:01:18 +00003911
Gavin Makea2e3302023-03-11 06:46:20 +00003912 @property
3913 def standalone_manifest_url(self):
3914 """The URL of the standalone manifest, or None."""
3915 return self.config.GetString("manifest.standalone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003916
Gavin Makea2e3302023-03-11 06:46:20 +00003917 @property
3918 def manifest_groups(self):
3919 """The manifest groups string."""
3920 return self.config.GetString("manifest.groups")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003921
Gavin Makea2e3302023-03-11 06:46:20 +00003922 @property
3923 def reference(self):
3924 """The --reference for this manifest."""
3925 return self.config.GetString("repo.reference")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003926
Gavin Makea2e3302023-03-11 06:46:20 +00003927 @property
3928 def dissociate(self):
3929 """Whether to dissociate."""
3930 return self.config.GetBoolean("repo.dissociate")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003931
Gavin Makea2e3302023-03-11 06:46:20 +00003932 @property
3933 def archive(self):
3934 """Whether we use archive."""
3935 return self.config.GetBoolean("repo.archive")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003936
Gavin Makea2e3302023-03-11 06:46:20 +00003937 @property
3938 def mirror(self):
3939 """Whether we use mirror."""
3940 return self.config.GetBoolean("repo.mirror")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003941
Gavin Makea2e3302023-03-11 06:46:20 +00003942 @property
3943 def use_worktree(self):
3944 """Whether we use worktree."""
3945 return self.config.GetBoolean("repo.worktree")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003946
Gavin Makea2e3302023-03-11 06:46:20 +00003947 @property
3948 def clone_bundle(self):
3949 """Whether we use clone_bundle."""
3950 return self.config.GetBoolean("repo.clonebundle")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003951
Gavin Makea2e3302023-03-11 06:46:20 +00003952 @property
3953 def submodules(self):
3954 """Whether we use submodules."""
3955 return self.config.GetBoolean("repo.submodules")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003956
Gavin Makea2e3302023-03-11 06:46:20 +00003957 @property
3958 def git_lfs(self):
3959 """Whether we use git_lfs."""
3960 return self.config.GetBoolean("repo.git-lfs")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003961
Gavin Makea2e3302023-03-11 06:46:20 +00003962 @property
3963 def use_superproject(self):
3964 """Whether we use superproject."""
3965 return self.config.GetBoolean("repo.superproject")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003966
Gavin Makea2e3302023-03-11 06:46:20 +00003967 @property
3968 def partial_clone(self):
3969 """Whether this is a partial clone."""
3970 return self.config.GetBoolean("repo.partialclone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003971
Gavin Makea2e3302023-03-11 06:46:20 +00003972 @property
3973 def depth(self):
3974 """Partial clone depth."""
3975 return self.config.GetString("repo.depth")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003976
Gavin Makea2e3302023-03-11 06:46:20 +00003977 @property
3978 def clone_filter(self):
3979 """The clone filter."""
3980 return self.config.GetString("repo.clonefilter")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003981
Gavin Makea2e3302023-03-11 06:46:20 +00003982 @property
3983 def partial_clone_exclude(self):
3984 """Partial clone exclude string"""
3985 return self.config.GetString("repo.partialcloneexclude")
LaMont Jones4ada0432022-04-14 15:10:43 +00003986
Gavin Makea2e3302023-03-11 06:46:20 +00003987 @property
Jason Chang17833322023-05-23 13:06:55 -07003988 def clone_filter_for_depth(self):
3989 """Replace shallow clone with partial clone."""
3990 return self.config.GetString("repo.clonefilterfordepth")
3991
3992 @property
Gavin Makea2e3302023-03-11 06:46:20 +00003993 def manifest_platform(self):
3994 """The --platform argument from `repo init`."""
3995 return self.config.GetString("manifest.platform")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003996
Gavin Makea2e3302023-03-11 06:46:20 +00003997 @property
3998 def _platform_name(self):
3999 """Return the name of the platform."""
4000 return platform.system().lower()
LaMont Jones9b03f152022-03-29 23:01:18 +00004001
Gavin Makea2e3302023-03-11 06:46:20 +00004002 def SyncWithPossibleInit(
4003 self,
4004 submanifest,
4005 verbose=False,
4006 current_branch_only=False,
4007 tags="",
4008 git_event_log=None,
4009 ):
4010 """Sync a manifestProject, possibly for the first time.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004011
Gavin Makea2e3302023-03-11 06:46:20 +00004012 Call Sync() with arguments from the most recent `repo init`. If this is
4013 a new sub manifest, then inherit options from the parent's
4014 manifestProject.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004015
Gavin Makea2e3302023-03-11 06:46:20 +00004016 This is used by subcmds.Sync() to do an initial download of new sub
4017 manifests.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004018
Gavin Makea2e3302023-03-11 06:46:20 +00004019 Args:
4020 submanifest: an XmlSubmanifest, the submanifest to re-sync.
4021 verbose: a boolean, whether to show all output, rather than only
4022 errors.
4023 current_branch_only: a boolean, whether to only fetch the current
4024 manifest branch from the server.
4025 tags: a boolean, whether to fetch tags.
4026 git_event_log: an EventLog, for git tracing.
4027 """
4028 # TODO(lamontjones): when refactoring sync (and init?) consider how to
4029 # better get the init options that we should use for new submanifests
4030 # that are added when syncing an existing workspace.
4031 git_event_log = git_event_log or EventLog()
LaMont Jonesb90a4222022-04-14 15:00:09 +00004032 spec = submanifest.ToSubmanifestSpec()
Gavin Makea2e3302023-03-11 06:46:20 +00004033 # Use the init options from the existing manifestProject, or the parent
4034 # if it doesn't exist.
4035 #
4036 # Today, we only support changing manifest_groups on the sub-manifest,
4037 # with no supported-for-the-user way to change the other arguments from
4038 # those specified by the outermost manifest.
4039 #
4040 # TODO(lamontjones): determine which of these should come from the
4041 # outermost manifest and which should come from the parent manifest.
4042 mp = self if self.Exists else submanifest.parent.manifestProject
4043 return self.Sync(
LaMont Jones55ee3042022-04-06 17:10:21 +00004044 manifest_url=spec.manifestUrl,
4045 manifest_branch=spec.revision,
Gavin Makea2e3302023-03-11 06:46:20 +00004046 standalone_manifest=mp.standalone_manifest_url,
4047 groups=mp.manifest_groups,
4048 platform=mp.manifest_platform,
4049 mirror=mp.mirror,
4050 dissociate=mp.dissociate,
4051 reference=mp.reference,
4052 worktree=mp.use_worktree,
4053 submodules=mp.submodules,
4054 archive=mp.archive,
4055 partial_clone=mp.partial_clone,
4056 clone_filter=mp.clone_filter,
4057 partial_clone_exclude=mp.partial_clone_exclude,
4058 clone_bundle=mp.clone_bundle,
4059 git_lfs=mp.git_lfs,
4060 use_superproject=mp.use_superproject,
LaMont Jones55ee3042022-04-06 17:10:21 +00004061 verbose=verbose,
4062 current_branch_only=current_branch_only,
4063 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00004064 depth=mp.depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004065 git_event_log=git_event_log,
4066 manifest_name=spec.manifestName,
Gavin Makea2e3302023-03-11 06:46:20 +00004067 this_manifest_only=True,
LaMont Jones55ee3042022-04-06 17:10:21 +00004068 outer_manifest=False,
Jason Chang17833322023-05-23 13:06:55 -07004069 clone_filter_for_depth=mp.clone_filter_for_depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004070 )
LaMont Jones409407a2022-04-05 21:21:56 +00004071
Gavin Makea2e3302023-03-11 06:46:20 +00004072 def Sync(
4073 self,
4074 _kwargs_only=(),
4075 manifest_url="",
4076 manifest_branch=None,
4077 standalone_manifest=False,
4078 groups="",
4079 mirror=False,
4080 reference="",
4081 dissociate=False,
4082 worktree=False,
4083 submodules=False,
4084 archive=False,
4085 partial_clone=None,
4086 depth=None,
4087 clone_filter="blob:none",
4088 partial_clone_exclude=None,
4089 clone_bundle=None,
4090 git_lfs=None,
4091 use_superproject=None,
4092 verbose=False,
4093 current_branch_only=False,
4094 git_event_log=None,
4095 platform="",
4096 manifest_name="default.xml",
4097 tags="",
4098 this_manifest_only=False,
4099 outer_manifest=True,
Jason Chang17833322023-05-23 13:06:55 -07004100 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00004101 ):
4102 """Sync the manifest and all submanifests.
LaMont Jones409407a2022-04-05 21:21:56 +00004103
Gavin Makea2e3302023-03-11 06:46:20 +00004104 Args:
4105 manifest_url: a string, the URL of the manifest project.
4106 manifest_branch: a string, the manifest branch to use.
4107 standalone_manifest: a boolean, whether to store the manifest as a
4108 static file.
4109 groups: a string, restricts the checkout to projects with the
4110 specified groups.
4111 mirror: a boolean, whether to create a mirror of the remote
4112 repository.
4113 reference: a string, location of a repo instance to use as a
4114 reference.
4115 dissociate: a boolean, whether to dissociate from reference mirrors
4116 after clone.
4117 worktree: a boolean, whether to use git-worktree to manage projects.
4118 submodules: a boolean, whether sync submodules associated with the
4119 manifest project.
4120 archive: a boolean, whether to checkout each project as an archive.
4121 See git-archive.
4122 partial_clone: a boolean, whether to perform a partial clone.
4123 depth: an int, how deep of a shallow clone to create.
4124 clone_filter: a string, filter to use with partial_clone.
4125 partial_clone_exclude : a string, comma-delimeted list of project
4126 names to exclude from partial clone.
4127 clone_bundle: a boolean, whether to enable /clone.bundle on
4128 HTTP/HTTPS.
4129 git_lfs: a boolean, whether to enable git LFS support.
4130 use_superproject: a boolean, whether to use the manifest
4131 superproject to sync projects.
4132 verbose: a boolean, whether to show all output, rather than only
4133 errors.
4134 current_branch_only: a boolean, whether to only fetch the current
4135 manifest branch from the server.
4136 platform: a string, restrict the checkout to projects with the
4137 specified platform group.
4138 git_event_log: an EventLog, for git tracing.
4139 tags: a boolean, whether to fetch tags.
4140 manifest_name: a string, the name of the manifest file to use.
4141 this_manifest_only: a boolean, whether to only operate on the
4142 current sub manifest.
4143 outer_manifest: a boolean, whether to start at the outermost
4144 manifest.
Jason Chang17833322023-05-23 13:06:55 -07004145 clone_filter_for_depth: a string, when specified replaces shallow
4146 clones with partial.
LaMont Jones9b03f152022-03-29 23:01:18 +00004147
Gavin Makea2e3302023-03-11 06:46:20 +00004148 Returns:
4149 a boolean, whether the sync was successful.
4150 """
4151 assert _kwargs_only == (), "Sync only accepts keyword arguments."
LaMont Jones9b03f152022-03-29 23:01:18 +00004152
Gavin Makea2e3302023-03-11 06:46:20 +00004153 groups = groups or self.manifest.GetDefaultGroupsStr(
4154 with_platform=False
4155 )
4156 platform = platform or "auto"
4157 git_event_log = git_event_log or EventLog()
4158 if outer_manifest and self.manifest.is_submanifest:
4159 # In a multi-manifest checkout, use the outer manifest unless we are
4160 # told not to.
4161 return self.client.outer_manifest.manifestProject.Sync(
4162 manifest_url=manifest_url,
4163 manifest_branch=manifest_branch,
4164 standalone_manifest=standalone_manifest,
4165 groups=groups,
4166 platform=platform,
4167 mirror=mirror,
4168 dissociate=dissociate,
4169 reference=reference,
4170 worktree=worktree,
4171 submodules=submodules,
4172 archive=archive,
4173 partial_clone=partial_clone,
4174 clone_filter=clone_filter,
4175 partial_clone_exclude=partial_clone_exclude,
4176 clone_bundle=clone_bundle,
4177 git_lfs=git_lfs,
4178 use_superproject=use_superproject,
4179 verbose=verbose,
4180 current_branch_only=current_branch_only,
4181 tags=tags,
4182 depth=depth,
4183 git_event_log=git_event_log,
4184 manifest_name=manifest_name,
4185 this_manifest_only=this_manifest_only,
4186 outer_manifest=False,
4187 )
LaMont Jones9b03f152022-03-29 23:01:18 +00004188
Gavin Makea2e3302023-03-11 06:46:20 +00004189 # If repo has already been initialized, we take -u with the absence of
4190 # --standalone-manifest to mean "transition to a standard repo set up",
4191 # which necessitates starting fresh.
4192 # If --standalone-manifest is set, we always tear everything down and
4193 # start anew.
4194 if self.Exists:
4195 was_standalone_manifest = self.config.GetString(
4196 "manifest.standalone"
4197 )
4198 if was_standalone_manifest and not manifest_url:
4199 print(
4200 "fatal: repo was initialized with a standlone manifest, "
4201 "cannot be re-initialized without --manifest-url/-u"
4202 )
4203 return False
4204
4205 if standalone_manifest or (
4206 was_standalone_manifest and manifest_url
4207 ):
4208 self.config.ClearCache()
4209 if self.gitdir and os.path.exists(self.gitdir):
4210 platform_utils.rmtree(self.gitdir)
4211 if self.worktree and os.path.exists(self.worktree):
4212 platform_utils.rmtree(self.worktree)
4213
4214 is_new = not self.Exists
4215 if is_new:
4216 if not manifest_url:
4217 print("fatal: manifest url is required.", file=sys.stderr)
4218 return False
4219
4220 if verbose:
4221 print(
4222 "Downloading manifest from %s"
4223 % (GitConfig.ForUser().UrlInsteadOf(manifest_url),),
4224 file=sys.stderr,
4225 )
4226
4227 # The manifest project object doesn't keep track of the path on the
4228 # server where this git is located, so let's save that here.
4229 mirrored_manifest_git = None
4230 if reference:
4231 manifest_git_path = urllib.parse.urlparse(manifest_url).path[1:]
4232 mirrored_manifest_git = os.path.join(
4233 reference, manifest_git_path
4234 )
4235 if not mirrored_manifest_git.endswith(".git"):
4236 mirrored_manifest_git += ".git"
4237 if not os.path.exists(mirrored_manifest_git):
4238 mirrored_manifest_git = os.path.join(
4239 reference, ".repo/manifests.git"
4240 )
4241
4242 self._InitGitDir(mirror_git=mirrored_manifest_git)
4243
4244 # If standalone_manifest is set, mark the project as "standalone" --
4245 # we'll still do much of the manifests.git set up, but will avoid actual
4246 # syncs to a remote.
4247 if standalone_manifest:
4248 self.config.SetString("manifest.standalone", manifest_url)
4249 elif not manifest_url and not manifest_branch:
4250 # If -u is set and --standalone-manifest is not, then we're not in
4251 # standalone mode. Otherwise, use config to infer what we were in
4252 # the last init.
4253 standalone_manifest = bool(
4254 self.config.GetString("manifest.standalone")
4255 )
4256 if not standalone_manifest:
4257 self.config.SetString("manifest.standalone", None)
4258
4259 self._ConfigureDepth(depth)
4260
4261 # Set the remote URL before the remote branch as we might need it below.
4262 if manifest_url:
4263 r = self.GetRemote()
4264 r.url = manifest_url
4265 r.ResetFetch()
4266 r.Save()
4267
4268 if not standalone_manifest:
4269 if manifest_branch:
4270 if manifest_branch == "HEAD":
4271 manifest_branch = self.ResolveRemoteHead()
4272 if manifest_branch is None:
4273 print("fatal: unable to resolve HEAD", file=sys.stderr)
4274 return False
4275 self.revisionExpr = manifest_branch
4276 else:
4277 if is_new:
4278 default_branch = self.ResolveRemoteHead()
4279 if default_branch is None:
4280 # If the remote doesn't have HEAD configured, default to
4281 # master.
4282 default_branch = "refs/heads/master"
4283 self.revisionExpr = default_branch
4284 else:
4285 self.PreSync()
4286
4287 groups = re.split(r"[,\s]+", groups or "")
4288 all_platforms = ["linux", "darwin", "windows"]
4289 platformize = lambda x: "platform-" + x
4290 if platform == "auto":
4291 if not mirror and not self.mirror:
4292 groups.append(platformize(self._platform_name))
4293 elif platform == "all":
4294 groups.extend(map(platformize, all_platforms))
4295 elif platform in all_platforms:
4296 groups.append(platformize(platform))
4297 elif platform != "none":
4298 print("fatal: invalid platform flag", file=sys.stderr)
4299 return False
4300 self.config.SetString("manifest.platform", platform)
4301
4302 groups = [x for x in groups if x]
4303 groupstr = ",".join(groups)
4304 if (
4305 platform == "auto"
4306 and groupstr == self.manifest.GetDefaultGroupsStr()
4307 ):
4308 groupstr = None
4309 self.config.SetString("manifest.groups", groupstr)
4310
4311 if reference:
4312 self.config.SetString("repo.reference", reference)
4313
4314 if dissociate:
4315 self.config.SetBoolean("repo.dissociate", dissociate)
4316
4317 if worktree:
4318 if mirror:
4319 print(
4320 "fatal: --mirror and --worktree are incompatible",
4321 file=sys.stderr,
4322 )
4323 return False
4324 if submodules:
4325 print(
4326 "fatal: --submodules and --worktree are incompatible",
4327 file=sys.stderr,
4328 )
4329 return False
4330 self.config.SetBoolean("repo.worktree", worktree)
4331 if is_new:
4332 self.use_git_worktrees = True
4333 print("warning: --worktree is experimental!", file=sys.stderr)
4334
4335 if archive:
4336 if is_new:
4337 self.config.SetBoolean("repo.archive", archive)
4338 else:
4339 print(
4340 "fatal: --archive is only supported when initializing a "
4341 "new workspace.",
4342 file=sys.stderr,
4343 )
4344 print(
4345 "Either delete the .repo folder in this workspace, or "
4346 "initialize in another location.",
4347 file=sys.stderr,
4348 )
4349 return False
4350
4351 if mirror:
4352 if is_new:
4353 self.config.SetBoolean("repo.mirror", mirror)
4354 else:
4355 print(
4356 "fatal: --mirror is only supported when initializing a new "
4357 "workspace.",
4358 file=sys.stderr,
4359 )
4360 print(
4361 "Either delete the .repo folder in this workspace, or "
4362 "initialize in another location.",
4363 file=sys.stderr,
4364 )
4365 return False
4366
4367 if partial_clone is not None:
4368 if mirror:
4369 print(
4370 "fatal: --mirror and --partial-clone are mutually "
4371 "exclusive",
4372 file=sys.stderr,
4373 )
4374 return False
4375 self.config.SetBoolean("repo.partialclone", partial_clone)
4376 if clone_filter:
4377 self.config.SetString("repo.clonefilter", clone_filter)
4378 elif self.partial_clone:
4379 clone_filter = self.clone_filter
4380 else:
4381 clone_filter = None
4382
4383 if partial_clone_exclude is not None:
4384 self.config.SetString(
4385 "repo.partialcloneexclude", partial_clone_exclude
4386 )
4387
4388 if clone_bundle is None:
4389 clone_bundle = False if partial_clone else True
4390 else:
4391 self.config.SetBoolean("repo.clonebundle", clone_bundle)
4392
4393 if submodules:
4394 self.config.SetBoolean("repo.submodules", submodules)
4395
4396 if git_lfs is not None:
4397 if git_lfs:
4398 git_require((2, 17, 0), fail=True, msg="Git LFS support")
4399
4400 self.config.SetBoolean("repo.git-lfs", git_lfs)
4401 if not is_new:
4402 print(
4403 "warning: Changing --git-lfs settings will only affect new "
4404 "project checkouts.\n"
4405 " Existing projects will require manual updates.\n",
4406 file=sys.stderr,
4407 )
4408
Jason Chang17833322023-05-23 13:06:55 -07004409 if clone_filter_for_depth is not None:
4410 self.ConfigureCloneFilterForDepth(clone_filter_for_depth)
4411
Gavin Makea2e3302023-03-11 06:46:20 +00004412 if use_superproject is not None:
4413 self.config.SetBoolean("repo.superproject", use_superproject)
4414
4415 if not standalone_manifest:
4416 success = self.Sync_NetworkHalf(
4417 is_new=is_new,
4418 quiet=not verbose,
4419 verbose=verbose,
4420 clone_bundle=clone_bundle,
4421 current_branch_only=current_branch_only,
4422 tags=tags,
4423 submodules=submodules,
4424 clone_filter=clone_filter,
4425 partial_clone_exclude=self.manifest.PartialCloneExclude,
Jason Chang17833322023-05-23 13:06:55 -07004426 clone_filter_for_depth=self.manifest.CloneFilterForDepth,
Gavin Makea2e3302023-03-11 06:46:20 +00004427 ).success
4428 if not success:
4429 r = self.GetRemote()
4430 print(
4431 "fatal: cannot obtain manifest %s" % r.url, file=sys.stderr
4432 )
4433
4434 # Better delete the manifest git dir if we created it; otherwise
4435 # next time (when user fixes problems) we won't go through the
4436 # "is_new" logic.
4437 if is_new:
4438 platform_utils.rmtree(self.gitdir)
4439 return False
4440
4441 if manifest_branch:
4442 self.MetaBranchSwitch(submodules=submodules)
4443
4444 syncbuf = SyncBuffer(self.config)
4445 self.Sync_LocalHalf(syncbuf, submodules=submodules)
4446 syncbuf.Finish()
4447
4448 if is_new or self.CurrentBranch is None:
4449 if not self.StartBranch("default"):
4450 print(
4451 "fatal: cannot create default in manifest",
4452 file=sys.stderr,
4453 )
4454 return False
4455
4456 if not manifest_name:
4457 print("fatal: manifest name (-m) is required.", file=sys.stderr)
4458 return False
4459
4460 elif is_new:
4461 # This is a new standalone manifest.
4462 manifest_name = "default.xml"
4463 manifest_data = fetch.fetch_file(manifest_url, verbose=verbose)
4464 dest = os.path.join(self.worktree, manifest_name)
4465 os.makedirs(os.path.dirname(dest), exist_ok=True)
4466 with open(dest, "wb") as f:
4467 f.write(manifest_data)
4468
4469 try:
4470 self.manifest.Link(manifest_name)
4471 except ManifestParseError as e:
4472 print(
4473 "fatal: manifest '%s' not available" % manifest_name,
4474 file=sys.stderr,
4475 )
4476 print("fatal: %s" % str(e), file=sys.stderr)
4477 return False
4478
4479 if not this_manifest_only:
4480 for submanifest in self.manifest.submanifests.values():
4481 spec = submanifest.ToSubmanifestSpec()
4482 submanifest.repo_client.manifestProject.Sync(
4483 manifest_url=spec.manifestUrl,
4484 manifest_branch=spec.revision,
4485 standalone_manifest=standalone_manifest,
4486 groups=self.manifest_groups,
4487 platform=platform,
4488 mirror=mirror,
4489 dissociate=dissociate,
4490 reference=reference,
4491 worktree=worktree,
4492 submodules=submodules,
4493 archive=archive,
4494 partial_clone=partial_clone,
4495 clone_filter=clone_filter,
4496 partial_clone_exclude=partial_clone_exclude,
4497 clone_bundle=clone_bundle,
4498 git_lfs=git_lfs,
4499 use_superproject=use_superproject,
4500 verbose=verbose,
4501 current_branch_only=current_branch_only,
4502 tags=tags,
4503 depth=depth,
4504 git_event_log=git_event_log,
4505 manifest_name=spec.manifestName,
4506 this_manifest_only=False,
4507 outer_manifest=False,
4508 )
4509
4510 # Lastly, if the manifest has a <superproject> then have the
4511 # superproject sync it (if it will be used).
4512 if git_superproject.UseSuperproject(use_superproject, self.manifest):
4513 sync_result = self.manifest.superproject.Sync(git_event_log)
4514 if not sync_result.success:
4515 submanifest = ""
4516 if self.manifest.path_prefix:
4517 submanifest = f"for {self.manifest.path_prefix} "
4518 print(
4519 f"warning: git update of superproject {submanifest}failed, "
4520 "repo sync will not use superproject to fetch source; "
4521 "while this error is not fatal, and you can continue to "
4522 "run repo sync, please run repo init with the "
4523 "--no-use-superproject option to stop seeing this warning",
4524 file=sys.stderr,
4525 )
4526 if sync_result.fatal and use_superproject is not None:
4527 return False
4528
4529 return True
4530
Jason Chang17833322023-05-23 13:06:55 -07004531 def ConfigureCloneFilterForDepth(self, clone_filter_for_depth):
4532 """Configure clone filter to replace shallow clones.
4533
4534 Args:
4535 clone_filter_for_depth: a string or None, e.g. 'blob:none' will
4536 disable shallow clones and replace with partial clone. None will
4537 enable shallow clones.
4538 """
4539 self.config.SetString(
4540 "repo.clonefilterfordepth", clone_filter_for_depth
4541 )
4542
Gavin Makea2e3302023-03-11 06:46:20 +00004543 def _ConfigureDepth(self, depth):
4544 """Configure the depth we'll sync down.
4545
4546 Args:
4547 depth: an int, how deep of a partial clone to create.
4548 """
4549 # Opt.depth will be non-None if user actually passed --depth to repo
4550 # init.
4551 if depth is not None:
4552 if depth > 0:
4553 # Positive values will set the depth.
4554 depth = str(depth)
4555 else:
4556 # Negative numbers will clear the depth; passing None to
4557 # SetString will do that.
4558 depth = None
4559
4560 # We store the depth in the main manifest project.
4561 self.config.SetString("repo.depth", depth)