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