blob: 84c668675f666dd375d0124242ae62cff1519fe8 [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
Mike Frysinger64477332023-08-21 21:20:32 -040029from typing import List, 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
Mike Frysinger64477332023-08-21 21:20:32 -040033from error import DownloadError
34from error import GitError
35from error import ManifestInvalidPathError
36from error import ManifestInvalidRevisionError
37from error import ManifestParseError
38from error import NoManifestException
39from error import RepoError
40from error import UploadError
LaMont Jones0de4fc32022-04-21 17:18:35 +000041import fetch
Mike Frysinger64477332023-08-21 21:20:32 -040042from git_command import git_require
43from git_command import GitCommand
44from git_config import GetSchemeFromUrl
45from git_config import GetUrlCookieFile
46from git_config import GitConfig
Mike Frysinger64477332023-08-21 21:20:32 -040047from git_config import IsId
48from git_refs import GitRefs
49from git_refs import HEAD
50from git_refs import R_HEADS
51from git_refs import R_M
52from git_refs import R_PUB
53from git_refs import R_TAGS
54from git_refs import R_WORKTREE_M
LaMont Jonesff6b1da2022-06-01 21:03:34 +000055import git_superproject
LaMont Jones55ee3042022-04-06 17:10:21 +000056from git_trace2_event_log import EventLog
Renaud Paquayd5cec5e2016-11-01 11:24:03 -070057import platform_utils
Mike Frysinger70d861f2019-08-26 15:22:36 -040058import progress
Joanna Wanga6c52f52022-11-03 16:51:19 -040059from repo_trace import Trace
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070060
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -070061
LaMont Jones1eddca82022-09-01 15:15:04 +000062class SyncNetworkHalfResult(NamedTuple):
Gavin Makea2e3302023-03-11 06:46:20 +000063 """Sync_NetworkHalf return value."""
64
Gavin Makea2e3302023-03-11 06:46:20 +000065 # Did we query the remote? False when optimized_fetch is True and we have
66 # the commit already present.
67 remote_fetched: bool
Jason Chang32b59562023-07-14 16:45:35 -070068 # Error from SyncNetworkHalf
69 error: Exception = None
70
71 @property
72 def success(self) -> bool:
73 return not self.error
74
75
76class SyncNetworkHalfError(RepoError):
77 """Failure trying to sync."""
78
79
80class DeleteWorktreeError(RepoError):
81 """Failure to delete worktree."""
82
83 def __init__(
84 self, *args, aggregate_errors: List[Exception] = None, **kwargs
85 ) -> None:
86 super().__init__(*args, **kwargs)
87 self.aggregate_errors = aggregate_errors or []
88
89
90class DeleteDirtyWorktreeError(DeleteWorktreeError):
91 """Failure to delete worktree due to uncommitted changes."""
LaMont Jones1eddca82022-09-01 15:15:04 +000092
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +010093
George Engelbrecht9bc283e2020-04-02 12:36:09 -060094# Maximum sleep time allowed during retries.
95MAXIMUM_RETRY_SLEEP_SEC = 3600.0
96# +-10% random jitter is added to each Fetches retry sleep duration.
97RETRY_JITTER_PERCENT = 0.1
98
LaMont Jonesfa8d9392022-11-02 22:01:29 +000099# Whether to use alternates. Switching back and forth is *NOT* supported.
Mike Frysinger1d00a7e2021-12-21 00:40:31 -0500100# TODO(vapier): Remove knob once behavior is verified.
Gavin Makea2e3302023-03-11 06:46:20 +0000101_ALTERNATES = os.environ.get("REPO_USE_ALTERNATES") == "1"
George Engelbrecht9bc283e2020-04-02 12:36:09 -0600102
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +0100103
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700104def _lwrite(path, content):
Gavin Makea2e3302023-03-11 06:46:20 +0000105 lock = "%s.lock" % path
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700106
Gavin Makea2e3302023-03-11 06:46:20 +0000107 # Maintain Unix line endings on all OS's to match git behavior.
108 with open(lock, "w", newline="\n") as fd:
109 fd.write(content)
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700110
Gavin Makea2e3302023-03-11 06:46:20 +0000111 try:
112 platform_utils.rename(lock, path)
113 except OSError:
114 platform_utils.remove(lock)
115 raise
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700116
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700117
Shawn O. Pearce48244782009-04-16 08:25:57 -0700118def _error(fmt, *args):
Gavin Makea2e3302023-03-11 06:46:20 +0000119 msg = fmt % args
120 print("error: %s" % msg, file=sys.stderr)
Shawn O. Pearce48244782009-04-16 08:25:57 -0700121
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700122
David Pursehousef33929d2015-08-24 14:39:14 +0900123def _warn(fmt, *args):
Gavin Makea2e3302023-03-11 06:46:20 +0000124 msg = fmt % args
125 print("warn: %s" % msg, file=sys.stderr)
David Pursehousef33929d2015-08-24 14:39:14 +0900126
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700127
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700128def not_rev(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000129 return "^" + r
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700130
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700131
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800132def sq(r):
Gavin Makea2e3302023-03-11 06:46:20 +0000133 return "'" + r.replace("'", "'''") + "'"
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -0800134
David Pursehouse819827a2020-02-12 15:20:19 +0900135
Jonathan Nieder93719792015-03-17 11:29:58 -0700136_project_hook_list = None
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700137
138
Jonathan Nieder93719792015-03-17 11:29:58 -0700139def _ProjectHooks():
Gavin Makea2e3302023-03-11 06:46:20 +0000140 """List the hooks present in the 'hooks' directory.
Jonathan Nieder93719792015-03-17 11:29:58 -0700141
Gavin Makea2e3302023-03-11 06:46:20 +0000142 These hooks are project hooks and are copied to the '.git/hooks' directory
143 of all subprojects.
Jonathan Nieder93719792015-03-17 11:29:58 -0700144
Gavin Makea2e3302023-03-11 06:46:20 +0000145 This function caches the list of hooks (based on the contents of the
146 'repo/hooks' directory) on the first call.
Jonathan Nieder93719792015-03-17 11:29:58 -0700147
Gavin Makea2e3302023-03-11 06:46:20 +0000148 Returns:
149 A list of absolute paths to all of the files in the hooks directory.
150 """
151 global _project_hook_list
152 if _project_hook_list is None:
153 d = platform_utils.realpath(os.path.abspath(os.path.dirname(__file__)))
154 d = os.path.join(d, "hooks")
155 _project_hook_list = [
156 os.path.join(d, x) for x in platform_utils.listdir(d)
157 ]
158 return _project_hook_list
Jonathan Nieder93719792015-03-17 11:29:58 -0700159
160
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700161class DownloadedChange(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000162 _commit_cache = None
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700163
Gavin Makea2e3302023-03-11 06:46:20 +0000164 def __init__(self, project, base, change_id, ps_id, commit):
165 self.project = project
166 self.base = base
167 self.change_id = change_id
168 self.ps_id = ps_id
169 self.commit = commit
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700170
Gavin Makea2e3302023-03-11 06:46:20 +0000171 @property
172 def commits(self):
173 if self._commit_cache is None:
174 self._commit_cache = self.project.bare_git.rev_list(
175 "--abbrev=8",
176 "--abbrev-commit",
177 "--pretty=oneline",
178 "--reverse",
179 "--date-order",
180 not_rev(self.base),
181 self.commit,
182 "--",
183 )
184 return self._commit_cache
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700185
186
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700187class ReviewableBranch(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000188 _commit_cache = None
189 _base_exists = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700190
Gavin Makea2e3302023-03-11 06:46:20 +0000191 def __init__(self, project, branch, base):
192 self.project = project
193 self.branch = branch
194 self.base = base
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700195
Gavin Makea2e3302023-03-11 06:46:20 +0000196 @property
197 def name(self):
198 return self.branch.name
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700199
Gavin Makea2e3302023-03-11 06:46:20 +0000200 @property
201 def commits(self):
202 if self._commit_cache is None:
203 args = (
204 "--abbrev=8",
205 "--abbrev-commit",
206 "--pretty=oneline",
207 "--reverse",
208 "--date-order",
209 not_rev(self.base),
210 R_HEADS + self.name,
211 "--",
212 )
213 try:
214 self._commit_cache = self.project.bare_git.rev_list(*args)
215 except GitError:
216 # We weren't able to probe the commits for this branch. Was it
217 # tracking a branch that no longer exists? If so, return no
218 # commits. Otherwise, rethrow the error as we don't know what's
219 # going on.
220 if self.base_exists:
221 raise
Mike Frysinger6da17752019-09-11 18:43:17 -0400222
Gavin Makea2e3302023-03-11 06:46:20 +0000223 self._commit_cache = []
Mike Frysinger6da17752019-09-11 18:43:17 -0400224
Gavin Makea2e3302023-03-11 06:46:20 +0000225 return self._commit_cache
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700226
Gavin Makea2e3302023-03-11 06:46:20 +0000227 @property
228 def unabbrev_commits(self):
229 r = dict()
230 for commit in self.project.bare_git.rev_list(
231 not_rev(self.base), R_HEADS + self.name, "--"
232 ):
233 r[commit[0:8]] = commit
234 return r
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800235
Gavin Makea2e3302023-03-11 06:46:20 +0000236 @property
237 def date(self):
238 return self.project.bare_git.log(
239 "--pretty=format:%cd", "-n", "1", R_HEADS + self.name, "--"
240 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700241
Gavin Makea2e3302023-03-11 06:46:20 +0000242 @property
243 def base_exists(self):
244 """Whether the branch we're tracking exists.
Mike Frysinger6da17752019-09-11 18:43:17 -0400245
Gavin Makea2e3302023-03-11 06:46:20 +0000246 Normally it should, but sometimes branches we track can get deleted.
247 """
248 if self._base_exists is None:
249 try:
250 self.project.bare_git.rev_parse("--verify", not_rev(self.base))
251 # If we're still here, the base branch exists.
252 self._base_exists = True
253 except GitError:
254 # If we failed to verify, the base branch doesn't exist.
255 self._base_exists = False
Mike Frysinger6da17752019-09-11 18:43:17 -0400256
Gavin Makea2e3302023-03-11 06:46:20 +0000257 return self._base_exists
Mike Frysinger6da17752019-09-11 18:43:17 -0400258
Gavin Makea2e3302023-03-11 06:46:20 +0000259 def UploadForReview(
260 self,
261 people,
262 dryrun=False,
263 auto_topic=False,
264 hashtags=(),
265 labels=(),
266 private=False,
267 notify=None,
268 wip=False,
269 ready=False,
270 dest_branch=None,
271 validate_certs=True,
272 push_options=None,
273 ):
274 self.project.UploadForReview(
275 branch=self.name,
276 people=people,
277 dryrun=dryrun,
278 auto_topic=auto_topic,
279 hashtags=hashtags,
280 labels=labels,
281 private=private,
282 notify=notify,
283 wip=wip,
284 ready=ready,
285 dest_branch=dest_branch,
286 validate_certs=validate_certs,
287 push_options=push_options,
288 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700289
Gavin Makea2e3302023-03-11 06:46:20 +0000290 def GetPublishedRefs(self):
291 refs = {}
292 output = self.project.bare_git.ls_remote(
293 self.branch.remote.SshReviewUrl(self.project.UserEmail),
294 "refs/changes/*",
295 )
296 for line in output.split("\n"):
297 try:
298 (sha, ref) = line.split()
299 refs[sha] = ref
300 except ValueError:
301 pass
Ficus Kirkpatrickbc7ef672009-05-04 12:45:11 -0700302
Gavin Makea2e3302023-03-11 06:46:20 +0000303 return refs
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700304
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700305
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700306class StatusColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000307 def __init__(self, config):
308 super().__init__(config, "status")
309 self.project = self.printer("header", attr="bold")
310 self.branch = self.printer("header", attr="bold")
311 self.nobranch = self.printer("nobranch", fg="red")
312 self.important = self.printer("important", fg="red")
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700313
Gavin Makea2e3302023-03-11 06:46:20 +0000314 self.added = self.printer("added", fg="green")
315 self.changed = self.printer("changed", fg="red")
316 self.untracked = self.printer("untracked", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700317
318
319class DiffColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +0000320 def __init__(self, config):
321 super().__init__(config, "diff")
322 self.project = self.printer("header", attr="bold")
323 self.fail = self.printer("fail", fg="red")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700324
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700325
Jack Neus6ea0cae2021-07-20 20:52:33 +0000326class Annotation(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000327 def __init__(self, name, value, keep):
328 self.name = name
329 self.value = value
330 self.keep = keep
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700331
Gavin Makea2e3302023-03-11 06:46:20 +0000332 def __eq__(self, other):
333 if not isinstance(other, Annotation):
334 return False
335 return self.__dict__ == other.__dict__
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700336
Gavin Makea2e3302023-03-11 06:46:20 +0000337 def __lt__(self, other):
338 # This exists just so that lists of Annotation objects can be sorted,
339 # for use in comparisons.
340 if not isinstance(other, Annotation):
341 raise ValueError("comparison is not between two Annotation objects")
342 if self.name == other.name:
343 if self.value == other.value:
344 return self.keep < other.keep
345 return self.value < other.value
346 return self.name < other.name
Jack Neus6ea0cae2021-07-20 20:52:33 +0000347
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700348
Mike Frysingere6a202f2019-08-02 15:57:57 -0400349def _SafeExpandPath(base, subpath, skipfinal=False):
Gavin Makea2e3302023-03-11 06:46:20 +0000350 """Make sure |subpath| is completely safe under |base|.
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700351
Gavin Makea2e3302023-03-11 06:46:20 +0000352 We make sure no intermediate symlinks are traversed, and that the final path
353 is not a special file (e.g. not a socket or fifo).
Mike Frysingere6a202f2019-08-02 15:57:57 -0400354
Gavin Makea2e3302023-03-11 06:46:20 +0000355 NB: We rely on a number of paths already being filtered out while parsing
356 the manifest. See the validation logic in manifest_xml.py for more details.
357 """
358 # Split up the path by its components. We can't use os.path.sep exclusively
359 # as some platforms (like Windows) will convert / to \ and that bypasses all
360 # our constructed logic here. Especially since manifest authors only use
361 # / in their paths.
362 resep = re.compile(r"[/%s]" % re.escape(os.path.sep))
363 components = resep.split(subpath)
364 if skipfinal:
365 # Whether the caller handles the final component itself.
366 finalpart = components.pop()
Mike Frysingere6a202f2019-08-02 15:57:57 -0400367
Gavin Makea2e3302023-03-11 06:46:20 +0000368 path = base
369 for part in components:
370 if part in {".", ".."}:
371 raise ManifestInvalidPathError(
372 '%s: "%s" not allowed in paths' % (subpath, part)
373 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400374
Gavin Makea2e3302023-03-11 06:46:20 +0000375 path = os.path.join(path, part)
376 if platform_utils.islink(path):
377 raise ManifestInvalidPathError(
378 "%s: traversing symlinks not allow" % (path,)
379 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400380
Gavin Makea2e3302023-03-11 06:46:20 +0000381 if os.path.exists(path):
382 if not os.path.isfile(path) and not platform_utils.isdir(path):
383 raise ManifestInvalidPathError(
384 "%s: only regular files & directories allowed" % (path,)
385 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400386
Gavin Makea2e3302023-03-11 06:46:20 +0000387 if skipfinal:
388 path = os.path.join(path, finalpart)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400389
Gavin Makea2e3302023-03-11 06:46:20 +0000390 return path
Mike Frysingere6a202f2019-08-02 15:57:57 -0400391
392
393class _CopyFile(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000394 """Container for <copyfile> manifest element."""
Mike Frysingere6a202f2019-08-02 15:57:57 -0400395
Gavin Makea2e3302023-03-11 06:46:20 +0000396 def __init__(self, git_worktree, src, topdir, dest):
397 """Register a <copyfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400398
Gavin Makea2e3302023-03-11 06:46:20 +0000399 Args:
400 git_worktree: Absolute path to the git project checkout.
401 src: Relative path under |git_worktree| of file to read.
402 topdir: Absolute path to the top of the repo client checkout.
403 dest: Relative path under |topdir| of file to write.
404 """
405 self.git_worktree = git_worktree
406 self.topdir = topdir
407 self.src = src
408 self.dest = dest
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700409
Gavin Makea2e3302023-03-11 06:46:20 +0000410 def _Copy(self):
411 src = _SafeExpandPath(self.git_worktree, self.src)
412 dest = _SafeExpandPath(self.topdir, self.dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400413
Gavin Makea2e3302023-03-11 06:46:20 +0000414 if platform_utils.isdir(src):
415 raise ManifestInvalidPathError(
416 "%s: copying from directory not supported" % (self.src,)
417 )
418 if platform_utils.isdir(dest):
419 raise ManifestInvalidPathError(
420 "%s: copying to directory not allowed" % (self.dest,)
421 )
Mike Frysingere6a202f2019-08-02 15:57:57 -0400422
Gavin Makea2e3302023-03-11 06:46:20 +0000423 # Copy file if it does not exist or is out of date.
424 if not os.path.exists(dest) or not filecmp.cmp(src, dest):
425 try:
426 # Remove existing file first, since it might be read-only.
427 if os.path.exists(dest):
428 platform_utils.remove(dest)
429 else:
430 dest_dir = os.path.dirname(dest)
431 if not platform_utils.isdir(dest_dir):
432 os.makedirs(dest_dir)
433 shutil.copy(src, dest)
434 # Make the file read-only.
435 mode = os.stat(dest)[stat.ST_MODE]
436 mode = mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
437 os.chmod(dest, mode)
438 except IOError:
439 _error("Cannot copy file %s to %s", src, dest)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700440
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700441
Anthony King7bdac712014-07-16 12:56:40 +0100442class _LinkFile(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000443 """Container for <linkfile> manifest element."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700444
Gavin Makea2e3302023-03-11 06:46:20 +0000445 def __init__(self, git_worktree, src, topdir, dest):
446 """Register a <linkfile> request.
Mike Frysingere6a202f2019-08-02 15:57:57 -0400447
Gavin Makea2e3302023-03-11 06:46:20 +0000448 Args:
449 git_worktree: Absolute path to the git project checkout.
450 src: Target of symlink relative to path under |git_worktree|.
451 topdir: Absolute path to the top of the repo client checkout.
452 dest: Relative path under |topdir| of symlink to create.
453 """
454 self.git_worktree = git_worktree
455 self.topdir = topdir
456 self.src = src
457 self.dest = dest
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500458
Gavin Makea2e3302023-03-11 06:46:20 +0000459 def __linkIt(self, relSrc, absDest):
460 # Link file if it does not exist or is out of date.
461 if not platform_utils.islink(absDest) or (
462 platform_utils.readlink(absDest) != relSrc
463 ):
464 try:
465 # Remove existing file first, since it might be read-only.
466 if os.path.lexists(absDest):
467 platform_utils.remove(absDest)
468 else:
469 dest_dir = os.path.dirname(absDest)
470 if not platform_utils.isdir(dest_dir):
471 os.makedirs(dest_dir)
472 platform_utils.symlink(relSrc, absDest)
473 except IOError:
474 _error("Cannot link file %s to %s", relSrc, absDest)
475
476 def _Link(self):
477 """Link the self.src & self.dest paths.
478
479 Handles wild cards on the src linking all of the files in the source in
480 to the destination directory.
481 """
482 # Some people use src="." to create stable links to projects. Let's
483 # allow that but reject all other uses of "." to keep things simple.
484 if self.src == ".":
485 src = self.git_worktree
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500486 else:
Gavin Makea2e3302023-03-11 06:46:20 +0000487 src = _SafeExpandPath(self.git_worktree, self.src)
Wink Saville4c426ef2015-06-03 08:05:17 -0700488
Gavin Makea2e3302023-03-11 06:46:20 +0000489 if not glob.has_magic(src):
490 # Entity does not contain a wild card so just a simple one to one
491 # link operation.
492 dest = _SafeExpandPath(self.topdir, self.dest, skipfinal=True)
493 # dest & src are absolute paths at this point. Make sure the target
494 # of the symlink is relative in the context of the repo client
495 # checkout.
496 relpath = os.path.relpath(src, os.path.dirname(dest))
497 self.__linkIt(relpath, dest)
498 else:
499 dest = _SafeExpandPath(self.topdir, self.dest)
500 # Entity contains a wild card.
501 if os.path.exists(dest) and not platform_utils.isdir(dest):
502 _error(
503 "Link error: src with wildcard, %s must be a directory",
504 dest,
505 )
506 else:
507 for absSrcFile in glob.glob(src):
508 # Create a releative path from source dir to destination
509 # dir.
510 absSrcDir = os.path.dirname(absSrcFile)
511 relSrcDir = os.path.relpath(absSrcDir, dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400512
Gavin Makea2e3302023-03-11 06:46:20 +0000513 # Get the source file name.
514 srcFile = os.path.basename(absSrcFile)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400515
Gavin Makea2e3302023-03-11 06:46:20 +0000516 # Now form the final full paths to srcFile. They will be
517 # absolute for the desintaiton and relative for the source.
518 absDest = os.path.join(dest, srcFile)
519 relSrc = os.path.join(relSrcDir, srcFile)
520 self.__linkIt(relSrc, absDest)
Jeff Hamiltone0df2322014-04-21 17:10:59 -0500521
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700522
Shawn O. Pearced1f70d92009-05-19 14:58:02 -0700523class RemoteSpec(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000524 def __init__(
525 self,
526 name,
527 url=None,
528 pushUrl=None,
529 review=None,
530 revision=None,
531 orig_name=None,
532 fetchUrl=None,
533 ):
534 self.name = name
535 self.url = url
536 self.pushUrl = pushUrl
537 self.review = review
538 self.revision = revision
539 self.orig_name = orig_name
540 self.fetchUrl = fetchUrl
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700541
Ian Kasprzak0286e312021-02-05 10:06:18 -0800542
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700543class Project(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000544 # These objects can be shared between several working trees.
545 @property
546 def shareable_dirs(self):
547 """Return the shareable directories"""
548 if self.UseAlternates:
549 return ["hooks", "rr-cache"]
550 else:
551 return ["hooks", "objects", "rr-cache"]
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -0700552
Gavin Makea2e3302023-03-11 06:46:20 +0000553 def __init__(
554 self,
555 manifest,
556 name,
557 remote,
558 gitdir,
559 objdir,
560 worktree,
561 relpath,
562 revisionExpr,
563 revisionId,
564 rebase=True,
565 groups=None,
566 sync_c=False,
567 sync_s=False,
568 sync_tags=True,
569 clone_depth=None,
570 upstream=None,
571 parent=None,
572 use_git_worktrees=False,
573 is_derived=False,
574 dest_branch=None,
575 optimized_fetch=False,
576 retry_fetches=0,
577 old_revision=None,
578 ):
579 """Init a Project object.
Doug Anderson3ba5f952011-04-07 12:51:04 -0700580
581 Args:
Gavin Makea2e3302023-03-11 06:46:20 +0000582 manifest: The XmlManifest object.
583 name: The `name` attribute of manifest.xml's project element.
584 remote: RemoteSpec object specifying its remote's properties.
585 gitdir: Absolute path of git directory.
586 objdir: Absolute path of directory to store git objects.
587 worktree: Absolute path of git working tree.
588 relpath: Relative path of git working tree to repo's top directory.
589 revisionExpr: The `revision` attribute of manifest.xml's project
590 element.
591 revisionId: git commit id for checking out.
592 rebase: The `rebase` attribute of manifest.xml's project element.
593 groups: The `groups` attribute of manifest.xml's project element.
594 sync_c: The `sync-c` attribute of manifest.xml's project element.
595 sync_s: The `sync-s` attribute of manifest.xml's project element.
596 sync_tags: The `sync-tags` attribute of manifest.xml's project
597 element.
598 upstream: The `upstream` attribute of manifest.xml's project
599 element.
600 parent: The parent Project object.
601 use_git_worktrees: Whether to use `git worktree` for this project.
602 is_derived: False if the project was explicitly defined in the
603 manifest; True if the project is a discovered submodule.
604 dest_branch: The branch to which to push changes for review by
605 default.
606 optimized_fetch: If True, when a project is set to a sha1 revision,
607 only fetch from the remote if the sha1 is not present locally.
608 retry_fetches: Retry remote fetches n times upon receiving transient
609 error with exponential backoff and jitter.
610 old_revision: saved git commit id for open GITC projects.
611 """
612 self.client = self.manifest = manifest
613 self.name = name
614 self.remote = remote
615 self.UpdatePaths(relpath, worktree, gitdir, objdir)
616 self.SetRevision(revisionExpr, revisionId=revisionId)
617
618 self.rebase = rebase
619 self.groups = groups
620 self.sync_c = sync_c
621 self.sync_s = sync_s
622 self.sync_tags = sync_tags
623 self.clone_depth = clone_depth
624 self.upstream = upstream
625 self.parent = parent
626 # NB: Do not use this setting in __init__ to change behavior so that the
627 # manifest.git checkout can inspect & change it after instantiating.
628 # See the XmlManifest init code for more info.
629 self.use_git_worktrees = use_git_worktrees
630 self.is_derived = is_derived
631 self.optimized_fetch = optimized_fetch
632 self.retry_fetches = max(0, retry_fetches)
633 self.subprojects = []
634
635 self.snapshots = {}
636 self.copyfiles = []
637 self.linkfiles = []
638 self.annotations = []
639 self.dest_branch = dest_branch
640 self.old_revision = old_revision
641
642 # This will be filled in if a project is later identified to be the
643 # project containing repo hooks.
644 self.enabled_repo_hooks = []
645
646 def RelPath(self, local=True):
647 """Return the path for the project relative to a manifest.
648
649 Args:
650 local: a boolean, if True, the path is relative to the local
651 (sub)manifest. If false, the path is relative to the outermost
652 manifest.
653 """
654 if local:
655 return self.relpath
656 return os.path.join(self.manifest.path_prefix, self.relpath)
657
658 def SetRevision(self, revisionExpr, revisionId=None):
659 """Set revisionId based on revision expression and id"""
660 self.revisionExpr = revisionExpr
661 if revisionId is None and revisionExpr and IsId(revisionExpr):
662 self.revisionId = self.revisionExpr
663 else:
664 self.revisionId = revisionId
665
666 def UpdatePaths(self, relpath, worktree, gitdir, objdir):
667 """Update paths used by this project"""
668 self.gitdir = gitdir.replace("\\", "/")
669 self.objdir = objdir.replace("\\", "/")
670 if worktree:
671 self.worktree = os.path.normpath(worktree).replace("\\", "/")
672 else:
673 self.worktree = None
674 self.relpath = relpath
675
676 self.config = GitConfig.ForRepository(
677 gitdir=self.gitdir, defaults=self.manifest.globalConfig
678 )
679
680 if self.worktree:
681 self.work_git = self._GitGetByExec(
682 self, bare=False, gitdir=self.gitdir
683 )
684 else:
685 self.work_git = None
686 self.bare_git = self._GitGetByExec(self, bare=True, gitdir=self.gitdir)
687 self.bare_ref = GitRefs(self.gitdir)
688 self.bare_objdir = self._GitGetByExec(
689 self, bare=True, gitdir=self.objdir
690 )
691
692 @property
693 def UseAlternates(self):
694 """Whether git alternates are in use.
695
696 This will be removed once migration to alternates is complete.
697 """
698 return _ALTERNATES or self.manifest.is_multimanifest
699
700 @property
701 def Derived(self):
702 return self.is_derived
703
704 @property
705 def Exists(self):
706 return platform_utils.isdir(self.gitdir) and platform_utils.isdir(
707 self.objdir
708 )
709
710 @property
711 def CurrentBranch(self):
712 """Obtain the name of the currently checked out branch.
713
714 The branch name omits the 'refs/heads/' prefix.
715 None is returned if the project is on a detached HEAD, or if the
716 work_git is otheriwse inaccessible (e.g. an incomplete sync).
717 """
718 try:
719 b = self.work_git.GetHead()
720 except NoManifestException:
721 # If the local checkout is in a bad state, don't barf. Let the
722 # callers process this like the head is unreadable.
723 return None
724 if b.startswith(R_HEADS):
725 return b[len(R_HEADS) :]
726 return None
727
728 def IsRebaseInProgress(self):
729 return (
730 os.path.exists(self.work_git.GetDotgitPath("rebase-apply"))
731 or os.path.exists(self.work_git.GetDotgitPath("rebase-merge"))
732 or os.path.exists(os.path.join(self.worktree, ".dotest"))
733 )
734
735 def IsDirty(self, consider_untracked=True):
736 """Is the working directory modified in some way?"""
737 self.work_git.update_index(
738 "-q", "--unmerged", "--ignore-missing", "--refresh"
739 )
740 if self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD):
741 return True
742 if self.work_git.DiffZ("diff-files"):
743 return True
744 if consider_untracked and self.UntrackedFiles():
745 return True
746 return False
747
748 _userident_name = None
749 _userident_email = None
750
751 @property
752 def UserName(self):
753 """Obtain the user's personal name."""
754 if self._userident_name is None:
755 self._LoadUserIdentity()
756 return self._userident_name
757
758 @property
759 def UserEmail(self):
760 """Obtain the user's email address. This is very likely
761 to be their Gerrit login.
762 """
763 if self._userident_email is None:
764 self._LoadUserIdentity()
765 return self._userident_email
766
767 def _LoadUserIdentity(self):
768 u = self.bare_git.var("GIT_COMMITTER_IDENT")
769 m = re.compile("^(.*) <([^>]*)> ").match(u)
770 if m:
771 self._userident_name = m.group(1)
772 self._userident_email = m.group(2)
773 else:
774 self._userident_name = ""
775 self._userident_email = ""
776
777 def GetRemote(self, name=None):
778 """Get the configuration for a single remote.
779
780 Defaults to the current project's remote.
781 """
782 if name is None:
783 name = self.remote.name
784 return self.config.GetRemote(name)
785
786 def GetBranch(self, name):
787 """Get the configuration for a single branch."""
788 return self.config.GetBranch(name)
789
790 def GetBranches(self):
791 """Get all existing local branches."""
792 current = self.CurrentBranch
793 all_refs = self._allrefs
794 heads = {}
795
796 for name, ref_id in all_refs.items():
797 if name.startswith(R_HEADS):
798 name = name[len(R_HEADS) :]
799 b = self.GetBranch(name)
800 b.current = name == current
801 b.published = None
802 b.revision = ref_id
803 heads[name] = b
804
805 for name, ref_id in all_refs.items():
806 if name.startswith(R_PUB):
807 name = name[len(R_PUB) :]
808 b = heads.get(name)
809 if b:
810 b.published = ref_id
811
812 return heads
813
814 def MatchesGroups(self, manifest_groups):
815 """Returns true if the manifest groups specified at init should cause
816 this project to be synced.
817 Prefixing a manifest group with "-" inverts the meaning of a group.
818 All projects are implicitly labelled with "all".
819
820 labels are resolved in order. In the example case of
821 project_groups: "all,group1,group2"
822 manifest_groups: "-group1,group2"
823 the project will be matched.
824
825 The special manifest group "default" will match any project that
826 does not have the special project group "notdefault"
827 """
828 default_groups = self.manifest.default_groups or ["default"]
829 expanded_manifest_groups = manifest_groups or default_groups
830 expanded_project_groups = ["all"] + (self.groups or [])
831 if "notdefault" not in expanded_project_groups:
832 expanded_project_groups += ["default"]
833
834 matched = False
835 for group in expanded_manifest_groups:
836 if group.startswith("-") and group[1:] in expanded_project_groups:
837 matched = False
838 elif group in expanded_project_groups:
839 matched = True
840
841 return matched
842
843 def UncommitedFiles(self, get_all=True):
844 """Returns a list of strings, uncommitted files in the git tree.
845
846 Args:
847 get_all: a boolean, if True - get information about all different
848 uncommitted files. If False - return as soon as any kind of
849 uncommitted files is detected.
850 """
851 details = []
852 self.work_git.update_index(
853 "-q", "--unmerged", "--ignore-missing", "--refresh"
854 )
855 if self.IsRebaseInProgress():
856 details.append("rebase in progress")
857 if not get_all:
858 return details
859
860 changes = self.work_git.DiffZ("diff-index", "--cached", HEAD).keys()
861 if changes:
862 details.extend(changes)
863 if not get_all:
864 return details
865
866 changes = self.work_git.DiffZ("diff-files").keys()
867 if changes:
868 details.extend(changes)
869 if not get_all:
870 return details
871
872 changes = self.UntrackedFiles()
873 if changes:
874 details.extend(changes)
875
876 return details
877
878 def UntrackedFiles(self):
879 """Returns a list of strings, untracked files in the git tree."""
880 return self.work_git.LsOthers()
881
882 def HasChanges(self):
883 """Returns true if there are uncommitted changes."""
884 return bool(self.UncommitedFiles(get_all=False))
885
886 def PrintWorkTreeStatus(self, output_redir=None, quiet=False, local=False):
887 """Prints the status of the repository to stdout.
888
889 Args:
890 output_redir: If specified, redirect the output to this object.
891 quiet: If True then only print the project name. Do not print
892 the modified files, branch name, etc.
893 local: a boolean, if True, the path is relative to the local
894 (sub)manifest. If false, the path is relative to the outermost
895 manifest.
896 """
897 if not platform_utils.isdir(self.worktree):
898 if output_redir is None:
899 output_redir = sys.stdout
900 print(file=output_redir)
901 print("project %s/" % self.RelPath(local), file=output_redir)
902 print(' missing (run "repo sync")', file=output_redir)
903 return
904
905 self.work_git.update_index(
906 "-q", "--unmerged", "--ignore-missing", "--refresh"
907 )
908 rb = self.IsRebaseInProgress()
909 di = self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD)
910 df = self.work_git.DiffZ("diff-files")
911 do = self.work_git.LsOthers()
912 if not rb and not di and not df and not do and not self.CurrentBranch:
913 return "CLEAN"
914
915 out = StatusColoring(self.config)
916 if output_redir is not None:
917 out.redirect(output_redir)
918 out.project("project %-40s", self.RelPath(local) + "/ ")
919
920 if quiet:
921 out.nl()
922 return "DIRTY"
923
924 branch = self.CurrentBranch
925 if branch is None:
926 out.nobranch("(*** NO BRANCH ***)")
927 else:
928 out.branch("branch %s", branch)
929 out.nl()
930
931 if rb:
932 out.important("prior sync failed; rebase still in progress")
933 out.nl()
934
935 paths = list()
936 paths.extend(di.keys())
937 paths.extend(df.keys())
938 paths.extend(do)
939
940 for p in sorted(set(paths)):
941 try:
942 i = di[p]
943 except KeyError:
944 i = None
945
946 try:
947 f = df[p]
948 except KeyError:
949 f = None
950
951 if i:
952 i_status = i.status.upper()
953 else:
954 i_status = "-"
955
956 if f:
957 f_status = f.status.lower()
958 else:
959 f_status = "-"
960
961 if i and i.src_path:
962 line = " %s%s\t%s => %s (%s%%)" % (
963 i_status,
964 f_status,
965 i.src_path,
966 p,
967 i.level,
968 )
969 else:
970 line = " %s%s\t%s" % (i_status, f_status, p)
971
972 if i and not f:
973 out.added("%s", line)
974 elif (i and f) or (not i and f):
975 out.changed("%s", line)
976 elif not i and not f:
977 out.untracked("%s", line)
978 else:
979 out.write("%s", line)
980 out.nl()
981
982 return "DIRTY"
983
984 def PrintWorkTreeDiff(
985 self, absolute_paths=False, output_redir=None, local=False
986 ):
987 """Prints the status of the repository to stdout."""
988 out = DiffColoring(self.config)
989 if output_redir:
990 out.redirect(output_redir)
991 cmd = ["diff"]
992 if out.is_on:
993 cmd.append("--color")
994 cmd.append(HEAD)
995 if absolute_paths:
996 cmd.append("--src-prefix=a/%s/" % self.RelPath(local))
997 cmd.append("--dst-prefix=b/%s/" % self.RelPath(local))
998 cmd.append("--")
999 try:
1000 p = GitCommand(self, cmd, capture_stdout=True, capture_stderr=True)
1001 p.Wait()
1002 except GitError as e:
1003 out.nl()
1004 out.project("project %s/" % self.RelPath(local))
1005 out.nl()
1006 out.fail("%s", str(e))
1007 out.nl()
1008 return False
1009 if p.stdout:
1010 out.nl()
1011 out.project("project %s/" % self.RelPath(local))
1012 out.nl()
1013 out.write("%s", p.stdout)
1014 return p.Wait() == 0
1015
1016 def WasPublished(self, branch, all_refs=None):
1017 """Was the branch published (uploaded) for code review?
1018 If so, returns the SHA-1 hash of the last published
1019 state for the branch.
1020 """
1021 key = R_PUB + branch
1022 if all_refs is None:
1023 try:
1024 return self.bare_git.rev_parse(key)
1025 except GitError:
1026 return None
1027 else:
1028 try:
1029 return all_refs[key]
1030 except KeyError:
1031 return None
1032
1033 def CleanPublishedCache(self, all_refs=None):
1034 """Prunes any stale published refs."""
1035 if all_refs is None:
1036 all_refs = self._allrefs
1037 heads = set()
1038 canrm = {}
1039 for name, ref_id in all_refs.items():
1040 if name.startswith(R_HEADS):
1041 heads.add(name)
1042 elif name.startswith(R_PUB):
1043 canrm[name] = ref_id
1044
1045 for name, ref_id in canrm.items():
1046 n = name[len(R_PUB) :]
1047 if R_HEADS + n not in heads:
1048 self.bare_git.DeleteRef(name, ref_id)
1049
1050 def GetUploadableBranches(self, selected_branch=None):
1051 """List any branches which can be uploaded for review."""
1052 heads = {}
1053 pubed = {}
1054
1055 for name, ref_id in self._allrefs.items():
1056 if name.startswith(R_HEADS):
1057 heads[name[len(R_HEADS) :]] = ref_id
1058 elif name.startswith(R_PUB):
1059 pubed[name[len(R_PUB) :]] = ref_id
1060
1061 ready = []
1062 for branch, ref_id in heads.items():
1063 if branch in pubed and pubed[branch] == ref_id:
1064 continue
1065 if selected_branch and branch != selected_branch:
1066 continue
1067
1068 rb = self.GetUploadableBranch(branch)
1069 if rb:
1070 ready.append(rb)
1071 return ready
1072
1073 def GetUploadableBranch(self, branch_name):
1074 """Get a single uploadable branch, or None."""
1075 branch = self.GetBranch(branch_name)
1076 base = branch.LocalMerge
1077 if branch.LocalMerge:
1078 rb = ReviewableBranch(self, branch, base)
1079 if rb.commits:
1080 return rb
1081 return None
1082
1083 def UploadForReview(
1084 self,
1085 branch=None,
1086 people=([], []),
1087 dryrun=False,
1088 auto_topic=False,
1089 hashtags=(),
1090 labels=(),
1091 private=False,
1092 notify=None,
1093 wip=False,
1094 ready=False,
1095 dest_branch=None,
1096 validate_certs=True,
1097 push_options=None,
1098 ):
1099 """Uploads the named branch for code review."""
1100 if branch is None:
1101 branch = self.CurrentBranch
1102 if branch is None:
Jason Chang32b59562023-07-14 16:45:35 -07001103 raise GitError("not currently on a branch", project=self.name)
Gavin Makea2e3302023-03-11 06:46:20 +00001104
1105 branch = self.GetBranch(branch)
1106 if not branch.LocalMerge:
Jason Chang32b59562023-07-14 16:45:35 -07001107 raise GitError(
1108 "branch %s does not track a remote" % branch.name,
1109 project=self.name,
1110 )
Gavin Makea2e3302023-03-11 06:46:20 +00001111 if not branch.remote.review:
Jason Chang32b59562023-07-14 16:45:35 -07001112 raise GitError(
1113 "remote %s has no review url" % branch.remote.name,
1114 project=self.name,
1115 )
Gavin Makea2e3302023-03-11 06:46:20 +00001116
1117 # Basic validity check on label syntax.
1118 for label in labels:
1119 if not re.match(r"^.+[+-][0-9]+$", label):
1120 raise UploadError(
1121 f'invalid label syntax "{label}": labels use forms like '
Jason Chang5a3a5f72023-08-17 11:36:41 -07001122 "CodeReview+1 or Verified-1",
1123 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00001124 )
1125
1126 if dest_branch is None:
1127 dest_branch = self.dest_branch
1128 if dest_branch is None:
1129 dest_branch = branch.merge
1130 if not dest_branch.startswith(R_HEADS):
1131 dest_branch = R_HEADS + dest_branch
1132
1133 if not branch.remote.projectname:
1134 branch.remote.projectname = self.name
1135 branch.remote.Save()
1136
1137 url = branch.remote.ReviewUrl(self.UserEmail, validate_certs)
1138 if url is None:
Jason Chang5a3a5f72023-08-17 11:36:41 -07001139 raise UploadError("review not configured", project=self.name)
Gavin Makea2e3302023-03-11 06:46:20 +00001140 cmd = ["push"]
1141 if dryrun:
1142 cmd.append("-n")
1143
1144 if url.startswith("ssh://"):
1145 cmd.append("--receive-pack=gerrit receive-pack")
1146
Aravind Vasudevan99ebf622023-04-04 23:44:37 +00001147 # This stops git from pushing all reachable annotated tags when
1148 # push.followTags is configured. Gerrit does not accept any tags
1149 # pushed to a CL.
1150 if git_require((1, 8, 3)):
1151 cmd.append("--no-follow-tags")
1152
Gavin Makea2e3302023-03-11 06:46:20 +00001153 for push_option in push_options or []:
1154 cmd.append("-o")
1155 cmd.append(push_option)
1156
1157 cmd.append(url)
1158
1159 if dest_branch.startswith(R_HEADS):
1160 dest_branch = dest_branch[len(R_HEADS) :]
1161
1162 ref_spec = "%s:refs/for/%s" % (R_HEADS + branch.name, dest_branch)
1163 opts = []
1164 if auto_topic:
1165 opts += ["topic=" + branch.name]
1166 opts += ["t=%s" % p for p in hashtags]
1167 # NB: No need to encode labels as they've been validated above.
1168 opts += ["l=%s" % p for p in labels]
1169
1170 opts += ["r=%s" % p for p in people[0]]
1171 opts += ["cc=%s" % p for p in people[1]]
1172 if notify:
1173 opts += ["notify=" + notify]
1174 if private:
1175 opts += ["private"]
1176 if wip:
1177 opts += ["wip"]
1178 if ready:
1179 opts += ["ready"]
1180 if opts:
1181 ref_spec = ref_spec + "%" + ",".join(opts)
1182 cmd.append(ref_spec)
1183
Jason Chang1e9f7b92023-08-25 10:31:04 -07001184 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001185
1186 if not dryrun:
1187 msg = "posted to %s for %s" % (branch.remote.review, dest_branch)
1188 self.bare_git.UpdateRef(
1189 R_PUB + branch.name, R_HEADS + branch.name, message=msg
1190 )
1191
1192 def _ExtractArchive(self, tarpath, path=None):
1193 """Extract the given tar on its current location
1194
1195 Args:
1196 tarpath: The path to the actual tar file
1197
1198 """
1199 try:
1200 with tarfile.open(tarpath, "r") as tar:
1201 tar.extractall(path=path)
1202 return True
1203 except (IOError, tarfile.TarError) as e:
1204 _error("Cannot extract archive %s: %s", tarpath, str(e))
1205 return False
1206
1207 def Sync_NetworkHalf(
1208 self,
1209 quiet=False,
1210 verbose=False,
1211 output_redir=None,
1212 is_new=None,
1213 current_branch_only=None,
1214 force_sync=False,
1215 clone_bundle=True,
1216 tags=None,
1217 archive=False,
1218 optimized_fetch=False,
1219 retry_fetches=0,
1220 prune=False,
1221 submodules=False,
1222 ssh_proxy=None,
1223 clone_filter=None,
1224 partial_clone_exclude=set(),
Jason Chang17833322023-05-23 13:06:55 -07001225 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001226 ):
1227 """Perform only the network IO portion of the sync process.
1228 Local working directory/branch state is not affected.
1229 """
1230 if archive and not isinstance(self, MetaProject):
1231 if self.remote.url.startswith(("http://", "https://")):
Jason Chang32b59562023-07-14 16:45:35 -07001232 msg_template = (
1233 "%s: Cannot fetch archives from http/https remotes."
Gavin Makea2e3302023-03-11 06:46:20 +00001234 )
Jason Chang32b59562023-07-14 16:45:35 -07001235 msg_args = self.name
1236 msg = msg_template % msg_args
1237 _error(
1238 msg_template,
1239 msg_args,
1240 )
1241 return SyncNetworkHalfResult(
1242 False, SyncNetworkHalfError(msg, project=self.name)
1243 )
Gavin Makea2e3302023-03-11 06:46:20 +00001244
1245 name = self.relpath.replace("\\", "/")
1246 name = name.replace("/", "_")
1247 tarpath = "%s.tar" % name
1248 topdir = self.manifest.topdir
1249
1250 try:
1251 self._FetchArchive(tarpath, cwd=topdir)
1252 except GitError as e:
1253 _error("%s", e)
Jason Chang32b59562023-07-14 16:45:35 -07001254 return SyncNetworkHalfResult(False, e)
Gavin Makea2e3302023-03-11 06:46:20 +00001255
1256 # From now on, we only need absolute tarpath.
1257 tarpath = os.path.join(topdir, tarpath)
1258
1259 if not self._ExtractArchive(tarpath, path=topdir):
Jason Chang32b59562023-07-14 16:45:35 -07001260 return SyncNetworkHalfResult(
1261 True,
1262 SyncNetworkHalfError(
1263 f"Unable to Extract Archive {tarpath}",
1264 project=self.name,
1265 ),
1266 )
Gavin Makea2e3302023-03-11 06:46:20 +00001267 try:
1268 platform_utils.remove(tarpath)
1269 except OSError as e:
1270 _warn("Cannot remove archive %s: %s", tarpath, str(e))
1271 self._CopyAndLinkFiles()
Jason Chang32b59562023-07-14 16:45:35 -07001272 return SyncNetworkHalfResult(True)
Gavin Makea2e3302023-03-11 06:46:20 +00001273
1274 # If the shared object dir already exists, don't try to rebootstrap with
1275 # a clone bundle download. We should have the majority of objects
1276 # already.
1277 if clone_bundle and os.path.exists(self.objdir):
1278 clone_bundle = False
1279
1280 if self.name in partial_clone_exclude:
1281 clone_bundle = True
1282 clone_filter = None
1283
1284 if is_new is None:
1285 is_new = not self.Exists
1286 if is_new:
1287 self._InitGitDir(force_sync=force_sync, quiet=quiet)
1288 else:
1289 self._UpdateHooks(quiet=quiet)
1290 self._InitRemote()
1291
1292 if self.UseAlternates:
1293 # If gitdir/objects is a symlink, migrate it from the old layout.
1294 gitdir_objects = os.path.join(self.gitdir, "objects")
1295 if platform_utils.islink(gitdir_objects):
1296 platform_utils.remove(gitdir_objects, missing_ok=True)
1297 gitdir_alt = os.path.join(self.gitdir, "objects/info/alternates")
1298 if not os.path.exists(gitdir_alt):
1299 os.makedirs(os.path.dirname(gitdir_alt), exist_ok=True)
1300 _lwrite(
1301 gitdir_alt,
1302 os.path.join(
1303 os.path.relpath(self.objdir, gitdir_objects), "objects"
1304 )
1305 + "\n",
1306 )
1307
1308 if is_new:
1309 alt = os.path.join(self.objdir, "objects/info/alternates")
1310 try:
1311 with open(alt) as fd:
1312 # This works for both absolute and relative alternate
1313 # directories.
1314 alt_dir = os.path.join(
1315 self.objdir, "objects", fd.readline().rstrip()
1316 )
1317 except IOError:
1318 alt_dir = None
1319 else:
1320 alt_dir = None
1321
1322 if (
1323 clone_bundle
1324 and alt_dir is None
1325 and self._ApplyCloneBundle(
1326 initial=is_new, quiet=quiet, verbose=verbose
1327 )
1328 ):
1329 is_new = False
1330
1331 if current_branch_only is None:
1332 if self.sync_c:
1333 current_branch_only = True
1334 elif not self.manifest._loaded:
1335 # Manifest cannot check defaults until it syncs.
1336 current_branch_only = False
1337 elif self.manifest.default.sync_c:
1338 current_branch_only = True
1339
1340 if tags is None:
1341 tags = self.sync_tags
1342
1343 if self.clone_depth:
1344 depth = self.clone_depth
1345 else:
1346 depth = self.manifest.manifestProject.depth
1347
Jason Chang17833322023-05-23 13:06:55 -07001348 if depth and clone_filter_for_depth:
1349 depth = None
1350 clone_filter = clone_filter_for_depth
1351
Gavin Makea2e3302023-03-11 06:46:20 +00001352 # See if we can skip the network fetch entirely.
1353 remote_fetched = False
1354 if not (
1355 optimized_fetch
Sylvain56a5a012023-09-11 13:38:00 +02001356 and IsId(self.revisionExpr)
1357 and self._CheckForImmutableRevision()
Gavin Makea2e3302023-03-11 06:46:20 +00001358 ):
1359 remote_fetched = True
Jason Chang32b59562023-07-14 16:45:35 -07001360 try:
1361 if not self._RemoteFetch(
1362 initial=is_new,
1363 quiet=quiet,
1364 verbose=verbose,
1365 output_redir=output_redir,
1366 alt_dir=alt_dir,
1367 current_branch_only=current_branch_only,
1368 tags=tags,
1369 prune=prune,
1370 depth=depth,
1371 submodules=submodules,
1372 force_sync=force_sync,
1373 ssh_proxy=ssh_proxy,
1374 clone_filter=clone_filter,
1375 retry_fetches=retry_fetches,
1376 ):
1377 return SyncNetworkHalfResult(
1378 remote_fetched,
1379 SyncNetworkHalfError(
1380 f"Unable to remote fetch project {self.name}",
1381 project=self.name,
1382 ),
1383 )
1384 except RepoError as e:
1385 return SyncNetworkHalfResult(
1386 remote_fetched,
1387 e,
1388 )
Gavin Makea2e3302023-03-11 06:46:20 +00001389
1390 mp = self.manifest.manifestProject
1391 dissociate = mp.dissociate
1392 if dissociate:
1393 alternates_file = os.path.join(
1394 self.objdir, "objects/info/alternates"
1395 )
1396 if os.path.exists(alternates_file):
1397 cmd = ["repack", "-a", "-d"]
1398 p = GitCommand(
1399 self,
1400 cmd,
1401 bare=True,
1402 capture_stdout=bool(output_redir),
1403 merge_output=bool(output_redir),
1404 )
1405 if p.stdout and output_redir:
1406 output_redir.write(p.stdout)
1407 if p.Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07001408 return SyncNetworkHalfResult(
1409 remote_fetched,
1410 GitError(
1411 "Unable to repack alternates", project=self.name
1412 ),
1413 )
Gavin Makea2e3302023-03-11 06:46:20 +00001414 platform_utils.remove(alternates_file)
1415
1416 if self.worktree:
1417 self._InitMRef()
1418 else:
1419 self._InitMirrorHead()
1420 platform_utils.remove(
1421 os.path.join(self.gitdir, "FETCH_HEAD"), missing_ok=True
1422 )
Jason Chang32b59562023-07-14 16:45:35 -07001423 return SyncNetworkHalfResult(remote_fetched)
Gavin Makea2e3302023-03-11 06:46:20 +00001424
1425 def PostRepoUpgrade(self):
1426 self._InitHooks()
1427
1428 def _CopyAndLinkFiles(self):
1429 if self.client.isGitcClient:
1430 return
1431 for copyfile in self.copyfiles:
1432 copyfile._Copy()
1433 for linkfile in self.linkfiles:
1434 linkfile._Link()
1435
1436 def GetCommitRevisionId(self):
1437 """Get revisionId of a commit.
1438
1439 Use this method instead of GetRevisionId to get the id of the commit
1440 rather than the id of the current git object (for example, a tag)
1441
1442 """
Sylvaine9cb3912023-09-10 23:35:01 +02001443 if self.revisionId:
1444 return self.revisionId
Gavin Makea2e3302023-03-11 06:46:20 +00001445 if not self.revisionExpr.startswith(R_TAGS):
1446 return self.GetRevisionId(self._allrefs)
1447
1448 try:
1449 return self.bare_git.rev_list(self.revisionExpr, "-1")[0]
1450 except GitError:
1451 raise ManifestInvalidRevisionError(
1452 "revision %s in %s not found" % (self.revisionExpr, self.name)
1453 )
1454
1455 def GetRevisionId(self, all_refs=None):
1456 if self.revisionId:
1457 return self.revisionId
1458
1459 rem = self.GetRemote()
1460 rev = rem.ToLocal(self.revisionExpr)
1461
1462 if all_refs is not None and rev in all_refs:
1463 return all_refs[rev]
1464
1465 try:
1466 return self.bare_git.rev_parse("--verify", "%s^0" % rev)
1467 except GitError:
1468 raise ManifestInvalidRevisionError(
1469 "revision %s in %s not found" % (self.revisionExpr, self.name)
1470 )
1471
1472 def SetRevisionId(self, revisionId):
1473 if self.revisionExpr:
1474 self.upstream = self.revisionExpr
1475
1476 self.revisionId = revisionId
1477
Jason Chang32b59562023-07-14 16:45:35 -07001478 def Sync_LocalHalf(
1479 self, syncbuf, force_sync=False, submodules=False, errors=None
1480 ):
Gavin Makea2e3302023-03-11 06:46:20 +00001481 """Perform only the local IO portion of the sync process.
1482
1483 Network access is not required.
1484 """
Jason Chang32b59562023-07-14 16:45:35 -07001485 if errors is None:
1486 errors = []
1487
1488 def fail(error: Exception):
1489 errors.append(error)
1490 syncbuf.fail(self, error)
1491
Gavin Makea2e3302023-03-11 06:46:20 +00001492 if not os.path.exists(self.gitdir):
Jason Chang32b59562023-07-14 16:45:35 -07001493 fail(
1494 LocalSyncFail(
1495 "Cannot checkout %s due to missing network sync; Run "
1496 "`repo sync -n %s` first." % (self.name, self.name),
1497 project=self.name,
1498 )
Gavin Makea2e3302023-03-11 06:46:20 +00001499 )
1500 return
1501
1502 self._InitWorkTree(force_sync=force_sync, submodules=submodules)
1503 all_refs = self.bare_ref.all
1504 self.CleanPublishedCache(all_refs)
1505 revid = self.GetRevisionId(all_refs)
1506
1507 # Special case the root of the repo client checkout. Make sure it
1508 # doesn't contain files being checked out to dirs we don't allow.
1509 if self.relpath == ".":
1510 PROTECTED_PATHS = {".repo"}
1511 paths = set(
1512 self.work_git.ls_tree("-z", "--name-only", "--", revid).split(
1513 "\0"
1514 )
1515 )
1516 bad_paths = paths & PROTECTED_PATHS
1517 if bad_paths:
Jason Chang32b59562023-07-14 16:45:35 -07001518 fail(
1519 LocalSyncFail(
1520 "Refusing to checkout project that writes to protected "
1521 "paths: %s" % (", ".join(bad_paths),),
1522 project=self.name,
1523 )
Gavin Makea2e3302023-03-11 06:46:20 +00001524 )
1525 return
1526
1527 def _doff():
1528 self._FastForward(revid)
1529 self._CopyAndLinkFiles()
1530
1531 def _dosubmodules():
1532 self._SyncSubmodules(quiet=True)
1533
1534 head = self.work_git.GetHead()
1535 if head.startswith(R_HEADS):
1536 branch = head[len(R_HEADS) :]
1537 try:
1538 head = all_refs[head]
1539 except KeyError:
1540 head = None
1541 else:
1542 branch = None
1543
1544 if branch is None or syncbuf.detach_head:
1545 # Currently on a detached HEAD. The user is assumed to
1546 # not have any local modifications worth worrying about.
1547 if self.IsRebaseInProgress():
Jason Chang32b59562023-07-14 16:45:35 -07001548 fail(_PriorSyncFailedError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001549 return
1550
1551 if head == revid:
1552 # No changes; don't do anything further.
1553 # Except if the head needs to be detached.
1554 if not syncbuf.detach_head:
1555 # The copy/linkfile config may have changed.
1556 self._CopyAndLinkFiles()
1557 return
1558 else:
1559 lost = self._revlist(not_rev(revid), HEAD)
1560 if lost:
1561 syncbuf.info(self, "discarding %d commits", len(lost))
1562
1563 try:
1564 self._Checkout(revid, quiet=True)
1565 if submodules:
1566 self._SyncSubmodules(quiet=True)
1567 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001568 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001569 return
1570 self._CopyAndLinkFiles()
1571 return
1572
1573 if head == revid:
1574 # No changes; don't do anything further.
1575 #
1576 # The copy/linkfile config may have changed.
1577 self._CopyAndLinkFiles()
1578 return
1579
1580 branch = self.GetBranch(branch)
1581
1582 if not branch.LocalMerge:
1583 # The current branch has no tracking configuration.
1584 # Jump off it to a detached HEAD.
1585 syncbuf.info(
1586 self, "leaving %s; does not track upstream", branch.name
1587 )
1588 try:
1589 self._Checkout(revid, quiet=True)
1590 if submodules:
1591 self._SyncSubmodules(quiet=True)
1592 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001593 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001594 return
1595 self._CopyAndLinkFiles()
1596 return
1597
1598 upstream_gain = self._revlist(not_rev(HEAD), revid)
1599
1600 # See if we can perform a fast forward merge. This can happen if our
1601 # branch isn't in the exact same state as we last published.
1602 try:
1603 self.work_git.merge_base("--is-ancestor", HEAD, revid)
1604 # Skip the published logic.
1605 pub = False
1606 except GitError:
1607 pub = self.WasPublished(branch.name, all_refs)
1608
1609 if pub:
1610 not_merged = self._revlist(not_rev(revid), pub)
1611 if not_merged:
1612 if upstream_gain:
1613 # The user has published this branch and some of those
1614 # commits are not yet merged upstream. We do not want
1615 # to rewrite the published commits so we punt.
Jason Chang32b59562023-07-14 16:45:35 -07001616 fail(
1617 LocalSyncFail(
1618 "branch %s is published (but not merged) and is "
1619 "now %d commits behind"
1620 % (branch.name, len(upstream_gain)),
1621 project=self.name,
1622 )
Gavin Makea2e3302023-03-11 06:46:20 +00001623 )
1624 return
1625 elif pub == head:
1626 # All published commits are merged, and thus we are a
1627 # strict subset. We can fast-forward safely.
1628 syncbuf.later1(self, _doff)
1629 if submodules:
1630 syncbuf.later1(self, _dosubmodules)
1631 return
1632
1633 # Examine the local commits not in the remote. Find the
1634 # last one attributed to this user, if any.
1635 local_changes = self._revlist(not_rev(revid), HEAD, format="%H %ce")
1636 last_mine = None
1637 cnt_mine = 0
1638 for commit in local_changes:
1639 commit_id, committer_email = commit.split(" ", 1)
1640 if committer_email == self.UserEmail:
1641 last_mine = commit_id
1642 cnt_mine += 1
1643
1644 if not upstream_gain and cnt_mine == len(local_changes):
1645 # The copy/linkfile config may have changed.
1646 self._CopyAndLinkFiles()
1647 return
1648
1649 if self.IsDirty(consider_untracked=False):
Jason Chang32b59562023-07-14 16:45:35 -07001650 fail(_DirtyError(project=self.name))
Gavin Makea2e3302023-03-11 06:46:20 +00001651 return
1652
1653 # If the upstream switched on us, warn the user.
1654 if branch.merge != self.revisionExpr:
1655 if branch.merge and self.revisionExpr:
1656 syncbuf.info(
1657 self,
1658 "manifest switched %s...%s",
1659 branch.merge,
1660 self.revisionExpr,
1661 )
1662 elif branch.merge:
1663 syncbuf.info(self, "manifest no longer tracks %s", branch.merge)
1664
1665 if cnt_mine < len(local_changes):
1666 # Upstream rebased. Not everything in HEAD was created by this user.
1667 syncbuf.info(
1668 self,
1669 "discarding %d commits removed from upstream",
1670 len(local_changes) - cnt_mine,
1671 )
1672
1673 branch.remote = self.GetRemote()
Sylvain56a5a012023-09-11 13:38:00 +02001674 if not IsId(self.revisionExpr):
Gavin Makea2e3302023-03-11 06:46:20 +00001675 # In case of manifest sync the revisionExpr might be a SHA1.
1676 branch.merge = self.revisionExpr
1677 if not branch.merge.startswith("refs/"):
1678 branch.merge = R_HEADS + branch.merge
1679 branch.Save()
1680
1681 if cnt_mine > 0 and self.rebase:
1682
1683 def _docopyandlink():
1684 self._CopyAndLinkFiles()
1685
1686 def _dorebase():
1687 self._Rebase(upstream="%s^1" % last_mine, onto=revid)
1688
1689 syncbuf.later2(self, _dorebase)
1690 if submodules:
1691 syncbuf.later2(self, _dosubmodules)
1692 syncbuf.later2(self, _docopyandlink)
1693 elif local_changes:
1694 try:
1695 self._ResetHard(revid)
1696 if submodules:
1697 self._SyncSubmodules(quiet=True)
1698 self._CopyAndLinkFiles()
1699 except GitError as e:
Jason Chang32b59562023-07-14 16:45:35 -07001700 fail(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001701 return
1702 else:
1703 syncbuf.later1(self, _doff)
1704 if submodules:
1705 syncbuf.later1(self, _dosubmodules)
1706
1707 def AddCopyFile(self, src, dest, topdir):
1708 """Mark |src| for copying to |dest| (relative to |topdir|).
1709
1710 No filesystem changes occur here. Actual copying happens later on.
1711
1712 Paths should have basic validation run on them before being queued.
1713 Further checking will be handled when the actual copy happens.
1714 """
1715 self.copyfiles.append(_CopyFile(self.worktree, src, topdir, dest))
1716
1717 def AddLinkFile(self, src, dest, topdir):
1718 """Mark |dest| to create a symlink (relative to |topdir|) pointing to
1719 |src|.
1720
1721 No filesystem changes occur here. Actual linking happens later on.
1722
1723 Paths should have basic validation run on them before being queued.
1724 Further checking will be handled when the actual link happens.
1725 """
1726 self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest))
1727
1728 def AddAnnotation(self, name, value, keep):
1729 self.annotations.append(Annotation(name, value, keep))
1730
1731 def DownloadPatchSet(self, change_id, patch_id):
1732 """Download a single patch set of a single change to FETCH_HEAD."""
1733 remote = self.GetRemote()
1734
1735 cmd = ["fetch", remote.name]
1736 cmd.append(
1737 "refs/changes/%2.2d/%d/%d" % (change_id % 100, change_id, patch_id)
1738 )
Jason Chang1a3612f2023-08-08 14:12:53 -07001739 GitCommand(self, cmd, bare=True, verify_command=True).Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00001740 return DownloadedChange(
1741 self,
1742 self.GetRevisionId(),
1743 change_id,
1744 patch_id,
1745 self.bare_git.rev_parse("FETCH_HEAD"),
1746 )
1747
1748 def DeleteWorktree(self, quiet=False, force=False):
1749 """Delete the source checkout and any other housekeeping tasks.
1750
1751 This currently leaves behind the internal .repo/ cache state. This
1752 helps when switching branches or manifest changes get reverted as we
1753 don't have to redownload all the git objects. But we should do some GC
1754 at some point.
1755
1756 Args:
1757 quiet: Whether to hide normal messages.
1758 force: Always delete tree even if dirty.
Doug Anderson3ba5f952011-04-07 12:51:04 -07001759
1760 Returns:
Gavin Makea2e3302023-03-11 06:46:20 +00001761 True if the worktree was completely cleaned out.
1762 """
1763 if self.IsDirty():
1764 if force:
1765 print(
1766 "warning: %s: Removing dirty project: uncommitted changes "
1767 "lost." % (self.RelPath(local=False),),
1768 file=sys.stderr,
1769 )
1770 else:
Jason Chang32b59562023-07-14 16:45:35 -07001771 msg = (
1772 "error: %s: Cannot remove project: uncommitted"
1773 "changes are present.\n" % self.RelPath(local=False)
Gavin Makea2e3302023-03-11 06:46:20 +00001774 )
Jason Chang32b59562023-07-14 16:45:35 -07001775 print(msg, file=sys.stderr)
1776 raise DeleteDirtyWorktreeError(msg, project=self)
Wink Saville02d79452009-04-10 13:01:24 -07001777
Gavin Makea2e3302023-03-11 06:46:20 +00001778 if not quiet:
1779 print(
1780 "%s: Deleting obsolete checkout." % (self.RelPath(local=False),)
1781 )
Wink Saville02d79452009-04-10 13:01:24 -07001782
Gavin Makea2e3302023-03-11 06:46:20 +00001783 # Unlock and delink from the main worktree. We don't use git's worktree
1784 # remove because it will recursively delete projects -- we handle that
1785 # ourselves below. https://crbug.com/git/48
1786 if self.use_git_worktrees:
1787 needle = platform_utils.realpath(self.gitdir)
1788 # Find the git worktree commondir under .repo/worktrees/.
1789 output = self.bare_git.worktree("list", "--porcelain").splitlines()[
1790 0
1791 ]
1792 assert output.startswith("worktree "), output
1793 commondir = output[9:]
1794 # Walk each of the git worktrees to see where they point.
1795 configs = os.path.join(commondir, "worktrees")
1796 for name in os.listdir(configs):
1797 gitdir = os.path.join(configs, name, "gitdir")
1798 with open(gitdir) as fp:
1799 relpath = fp.read().strip()
1800 # Resolve the checkout path and see if it matches this project.
1801 fullpath = platform_utils.realpath(
1802 os.path.join(configs, name, relpath)
1803 )
1804 if fullpath == needle:
1805 platform_utils.rmtree(os.path.join(configs, name))
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001806
Gavin Makea2e3302023-03-11 06:46:20 +00001807 # Delete the .git directory first, so we're less likely to have a
1808 # partially working git repository around. There shouldn't be any git
1809 # projects here, so rmtree works.
Shawn O. Pearce89e717d2009-04-18 15:04:41 -07001810
Gavin Makea2e3302023-03-11 06:46:20 +00001811 # Try to remove plain files first in case of git worktrees. If this
1812 # fails for any reason, we'll fall back to rmtree, and that'll display
1813 # errors if it can't remove things either.
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001814 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001815 platform_utils.remove(self.gitdir)
1816 except OSError:
1817 pass
1818 try:
1819 platform_utils.rmtree(self.gitdir)
1820 except OSError as e:
1821 if e.errno != errno.ENOENT:
1822 print("error: %s: %s" % (self.gitdir, e), file=sys.stderr)
1823 print(
1824 "error: %s: Failed to delete obsolete checkout; remove "
1825 "manually, then run `repo sync -l`."
1826 % (self.RelPath(local=False),),
1827 file=sys.stderr,
1828 )
Jason Chang32b59562023-07-14 16:45:35 -07001829 raise DeleteWorktreeError(aggregate_errors=[e])
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001830
Gavin Makea2e3302023-03-11 06:46:20 +00001831 # Delete everything under the worktree, except for directories that
1832 # contain another git project.
1833 dirs_to_remove = []
1834 failed = False
Jason Chang32b59562023-07-14 16:45:35 -07001835 errors = []
Gavin Makea2e3302023-03-11 06:46:20 +00001836 for root, dirs, files in platform_utils.walk(self.worktree):
1837 for f in files:
1838 path = os.path.join(root, f)
1839 try:
1840 platform_utils.remove(path)
1841 except OSError as e:
1842 if e.errno != errno.ENOENT:
1843 print(
1844 "error: %s: Failed to remove: %s" % (path, e),
1845 file=sys.stderr,
1846 )
1847 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001848 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001849 dirs[:] = [
1850 d
1851 for d in dirs
1852 if not os.path.lexists(os.path.join(root, d, ".git"))
1853 ]
1854 dirs_to_remove += [
1855 os.path.join(root, d)
1856 for d in dirs
1857 if os.path.join(root, d) not in dirs_to_remove
1858 ]
1859 for d in reversed(dirs_to_remove):
1860 if platform_utils.islink(d):
1861 try:
1862 platform_utils.remove(d)
1863 except OSError as e:
1864 if e.errno != errno.ENOENT:
1865 print(
1866 "error: %s: Failed to remove: %s" % (d, e),
1867 file=sys.stderr,
1868 )
1869 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001870 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001871 elif not platform_utils.listdir(d):
1872 try:
1873 platform_utils.rmdir(d)
1874 except OSError as e:
1875 if e.errno != errno.ENOENT:
1876 print(
1877 "error: %s: Failed to remove: %s" % (d, e),
1878 file=sys.stderr,
1879 )
1880 failed = True
Jason Chang32b59562023-07-14 16:45:35 -07001881 errors.append(e)
Gavin Makea2e3302023-03-11 06:46:20 +00001882 if failed:
1883 print(
1884 "error: %s: Failed to delete obsolete checkout."
1885 % (self.RelPath(local=False),),
1886 file=sys.stderr,
1887 )
1888 print(
1889 " Remove manually, then run `repo sync -l`.",
1890 file=sys.stderr,
1891 )
Jason Chang32b59562023-07-14 16:45:35 -07001892 raise DeleteWorktreeError(aggregate_errors=errors)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07001893
Gavin Makea2e3302023-03-11 06:46:20 +00001894 # Try deleting parent dirs if they are empty.
1895 path = self.worktree
1896 while path != self.manifest.topdir:
1897 try:
1898 platform_utils.rmdir(path)
1899 except OSError as e:
1900 if e.errno != errno.ENOENT:
1901 break
1902 path = os.path.dirname(path)
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001903
Gavin Makea2e3302023-03-11 06:46:20 +00001904 return True
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001905
Gavin Makea2e3302023-03-11 06:46:20 +00001906 def StartBranch(self, name, branch_merge="", revision=None):
1907 """Create a new branch off the manifest's revision."""
1908 if not branch_merge:
1909 branch_merge = self.revisionExpr
1910 head = self.work_git.GetHead()
1911 if head == (R_HEADS + name):
1912 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001913
David Pursehouse8a68ff92012-09-24 12:15:13 +09001914 all_refs = self.bare_ref.all
Gavin Makea2e3302023-03-11 06:46:20 +00001915 if R_HEADS + name in all_refs:
Jason Chang1a3612f2023-08-08 14:12:53 -07001916 GitCommand(
1917 self, ["checkout", "-q", name, "--"], verify_command=True
1918 ).Wait()
1919 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001920
Gavin Makea2e3302023-03-11 06:46:20 +00001921 branch = self.GetBranch(name)
1922 branch.remote = self.GetRemote()
1923 branch.merge = branch_merge
Sylvain56a5a012023-09-11 13:38:00 +02001924 if not branch.merge.startswith("refs/") and not IsId(branch_merge):
Gavin Makea2e3302023-03-11 06:46:20 +00001925 branch.merge = R_HEADS + branch_merge
Shawn O. Pearce88443382010-10-08 10:02:09 +02001926
Gavin Makea2e3302023-03-11 06:46:20 +00001927 if revision is None:
1928 revid = self.GetRevisionId(all_refs)
Shawn O. Pearce88443382010-10-08 10:02:09 +02001929 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001930 revid = self.work_git.rev_parse(revision)
Brian Harring14a66742012-09-28 20:21:57 -07001931
Gavin Makea2e3302023-03-11 06:46:20 +00001932 if head.startswith(R_HEADS):
Kevin Degiabaa7f32014-11-12 11:27:45 -07001933 try:
Gavin Makea2e3302023-03-11 06:46:20 +00001934 head = all_refs[head]
1935 except KeyError:
1936 head = None
1937 if revid and head and revid == head:
1938 ref = R_HEADS + name
1939 self.work_git.update_ref(ref, revid)
1940 self.work_git.symbolic_ref(HEAD, ref)
1941 branch.Save()
1942 return True
Kevin Degib1a07b82015-07-27 13:33:43 -06001943
Jason Chang1a3612f2023-08-08 14:12:53 -07001944 GitCommand(
1945 self,
1946 ["checkout", "-q", "-b", branch.name, revid],
1947 verify_command=True,
1948 ).Wait()
1949 branch.Save()
1950 return True
Kevin Degi384b3c52014-10-16 16:02:58 -06001951
Gavin Makea2e3302023-03-11 06:46:20 +00001952 def CheckoutBranch(self, name):
1953 """Checkout a local topic branch.
Shawn O. Pearce2816d4f2009-03-03 17:53:18 -08001954
Gavin Makea2e3302023-03-11 06:46:20 +00001955 Args:
1956 name: The name of the branch to checkout.
Shawn O. Pearce88443382010-10-08 10:02:09 +02001957
Gavin Makea2e3302023-03-11 06:46:20 +00001958 Returns:
Jason Chang1a3612f2023-08-08 14:12:53 -07001959 True if the checkout succeeded; False if the
1960 branch doesn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00001961 """
1962 rev = R_HEADS + name
1963 head = self.work_git.GetHead()
1964 if head == rev:
1965 # Already on the branch.
1966 return True
Shawn O. Pearce88443382010-10-08 10:02:09 +02001967
Gavin Makea2e3302023-03-11 06:46:20 +00001968 all_refs = self.bare_ref.all
1969 try:
1970 revid = all_refs[rev]
1971 except KeyError:
1972 # Branch does not exist in this project.
Jason Chang1a3612f2023-08-08 14:12:53 -07001973 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001974
Gavin Makea2e3302023-03-11 06:46:20 +00001975 if head.startswith(R_HEADS):
1976 try:
1977 head = all_refs[head]
1978 except KeyError:
1979 head = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001980
Gavin Makea2e3302023-03-11 06:46:20 +00001981 if head == revid:
1982 # Same revision; just update HEAD to point to the new
1983 # target branch, but otherwise take no other action.
1984 _lwrite(
1985 self.work_git.GetDotgitPath(subpath=HEAD),
1986 "ref: %s%s\n" % (R_HEADS, name),
1987 )
1988 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05001989
Jason Chang1a3612f2023-08-08 14:12:53 -07001990 GitCommand(
1991 self,
1992 ["checkout", name, "--"],
1993 capture_stdout=True,
1994 capture_stderr=True,
1995 verify_command=True,
1996 ).Wait()
1997 return True
Mike Frysinger98bb7652021-12-20 21:15:59 -05001998
Gavin Makea2e3302023-03-11 06:46:20 +00001999 def AbandonBranch(self, name):
2000 """Destroy a local topic branch.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002001
Gavin Makea2e3302023-03-11 06:46:20 +00002002 Args:
2003 name: The name of the branch to abandon.
Shawn O. Pearce9452e4e2009-08-22 18:17:46 -07002004
Gavin Makea2e3302023-03-11 06:46:20 +00002005 Returns:
Jason Changf9aacd42023-08-03 14:38:00 -07002006 True if the abandon succeeded; Raises GitCommandError if it didn't;
2007 None if the branch didn't exist.
Gavin Makea2e3302023-03-11 06:46:20 +00002008 """
2009 rev = R_HEADS + name
2010 all_refs = self.bare_ref.all
2011 if rev not in all_refs:
2012 # Doesn't exist
2013 return None
2014
2015 head = self.work_git.GetHead()
2016 if head == rev:
2017 # We can't destroy the branch while we are sitting
2018 # on it. Switch to a detached HEAD.
2019 head = all_refs[head]
2020
2021 revid = self.GetRevisionId(all_refs)
2022 if head == revid:
2023 _lwrite(
2024 self.work_git.GetDotgitPath(subpath=HEAD), "%s\n" % revid
2025 )
2026 else:
2027 self._Checkout(revid, quiet=True)
Jason Changf9aacd42023-08-03 14:38:00 -07002028 GitCommand(
2029 self,
2030 ["branch", "-D", name],
2031 capture_stdout=True,
2032 capture_stderr=True,
2033 verify_command=True,
2034 ).Wait()
2035 return True
Gavin Makea2e3302023-03-11 06:46:20 +00002036
2037 def PruneHeads(self):
2038 """Prune any topic branches already merged into upstream."""
2039 cb = self.CurrentBranch
2040 kill = []
2041 left = self._allrefs
2042 for name in left.keys():
2043 if name.startswith(R_HEADS):
2044 name = name[len(R_HEADS) :]
2045 if cb is None or name != cb:
2046 kill.append(name)
2047
2048 # Minor optimization: If there's nothing to prune, then don't try to
2049 # read any project state.
2050 if not kill and not cb:
2051 return []
2052
2053 rev = self.GetRevisionId(left)
2054 if (
2055 cb is not None
2056 and not self._revlist(HEAD + "..." + rev)
2057 and not self.IsDirty(consider_untracked=False)
2058 ):
2059 self.work_git.DetachHead(HEAD)
2060 kill.append(cb)
2061
2062 if kill:
2063 old = self.bare_git.GetHead()
2064
2065 try:
2066 self.bare_git.DetachHead(rev)
2067
2068 b = ["branch", "-d"]
2069 b.extend(kill)
2070 b = GitCommand(
2071 self, b, bare=True, capture_stdout=True, capture_stderr=True
2072 )
2073 b.Wait()
2074 finally:
Sylvain56a5a012023-09-11 13:38:00 +02002075 if IsId(old):
Gavin Makea2e3302023-03-11 06:46:20 +00002076 self.bare_git.DetachHead(old)
2077 else:
2078 self.bare_git.SetHead(old)
2079 left = self._allrefs
2080
2081 for branch in kill:
2082 if (R_HEADS + branch) not in left:
2083 self.CleanPublishedCache()
2084 break
2085
2086 if cb and cb not in kill:
2087 kill.append(cb)
2088 kill.sort()
2089
2090 kept = []
2091 for branch in kill:
2092 if R_HEADS + branch in left:
2093 branch = self.GetBranch(branch)
2094 base = branch.LocalMerge
2095 if not base:
2096 base = rev
2097 kept.append(ReviewableBranch(self, branch, base))
2098 return kept
2099
2100 def GetRegisteredSubprojects(self):
2101 result = []
2102
2103 def rec(subprojects):
2104 if not subprojects:
2105 return
2106 result.extend(subprojects)
2107 for p in subprojects:
2108 rec(p.subprojects)
2109
2110 rec(self.subprojects)
2111 return result
2112
2113 def _GetSubmodules(self):
2114 # Unfortunately we cannot call `git submodule status --recursive` here
2115 # because the working tree might not exist yet, and it cannot be used
2116 # without a working tree in its current implementation.
2117
2118 def get_submodules(gitdir, rev):
2119 # Parse .gitmodules for submodule sub_paths and sub_urls.
2120 sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
2121 if not sub_paths:
2122 return []
2123 # Run `git ls-tree` to read SHAs of submodule object, which happen
2124 # to be revision of submodule repository.
2125 sub_revs = git_ls_tree(gitdir, rev, sub_paths)
2126 submodules = []
2127 for sub_path, sub_url in zip(sub_paths, sub_urls):
2128 try:
2129 sub_rev = sub_revs[sub_path]
2130 except KeyError:
2131 # Ignore non-exist submodules.
2132 continue
2133 submodules.append((sub_rev, sub_path, sub_url))
2134 return submodules
2135
2136 re_path = re.compile(r"^submodule\.(.+)\.path=(.*)$")
2137 re_url = re.compile(r"^submodule\.(.+)\.url=(.*)$")
2138
2139 def parse_gitmodules(gitdir, rev):
2140 cmd = ["cat-file", "blob", "%s:.gitmodules" % rev]
2141 try:
2142 p = GitCommand(
2143 None,
2144 cmd,
2145 capture_stdout=True,
2146 capture_stderr=True,
2147 bare=True,
2148 gitdir=gitdir,
2149 )
2150 except GitError:
2151 return [], []
2152 if p.Wait() != 0:
2153 return [], []
2154
2155 gitmodules_lines = []
2156 fd, temp_gitmodules_path = tempfile.mkstemp()
2157 try:
2158 os.write(fd, p.stdout.encode("utf-8"))
2159 os.close(fd)
2160 cmd = ["config", "--file", temp_gitmodules_path, "--list"]
2161 p = GitCommand(
2162 None,
2163 cmd,
2164 capture_stdout=True,
2165 capture_stderr=True,
2166 bare=True,
2167 gitdir=gitdir,
2168 )
2169 if p.Wait() != 0:
2170 return [], []
2171 gitmodules_lines = p.stdout.split("\n")
2172 except GitError:
2173 return [], []
2174 finally:
2175 platform_utils.remove(temp_gitmodules_path)
2176
2177 names = set()
2178 paths = {}
2179 urls = {}
2180 for line in gitmodules_lines:
2181 if not line:
2182 continue
2183 m = re_path.match(line)
2184 if m:
2185 names.add(m.group(1))
2186 paths[m.group(1)] = m.group(2)
2187 continue
2188 m = re_url.match(line)
2189 if m:
2190 names.add(m.group(1))
2191 urls[m.group(1)] = m.group(2)
2192 continue
2193 names = sorted(names)
2194 return (
2195 [paths.get(name, "") for name in names],
2196 [urls.get(name, "") for name in names],
2197 )
2198
2199 def git_ls_tree(gitdir, rev, paths):
2200 cmd = ["ls-tree", rev, "--"]
2201 cmd.extend(paths)
2202 try:
2203 p = GitCommand(
2204 None,
2205 cmd,
2206 capture_stdout=True,
2207 capture_stderr=True,
2208 bare=True,
2209 gitdir=gitdir,
2210 )
2211 except GitError:
2212 return []
2213 if p.Wait() != 0:
2214 return []
2215 objects = {}
2216 for line in p.stdout.split("\n"):
2217 if not line.strip():
2218 continue
2219 object_rev, object_path = line.split()[2:4]
2220 objects[object_path] = object_rev
2221 return objects
2222
2223 try:
2224 rev = self.GetRevisionId()
2225 except GitError:
2226 return []
2227 return get_submodules(self.gitdir, rev)
2228
2229 def GetDerivedSubprojects(self):
2230 result = []
2231 if not self.Exists:
2232 # If git repo does not exist yet, querying its submodules will
2233 # mess up its states; so return here.
2234 return result
2235 for rev, path, url in self._GetSubmodules():
2236 name = self.manifest.GetSubprojectName(self, path)
2237 (
2238 relpath,
2239 worktree,
2240 gitdir,
2241 objdir,
2242 ) = self.manifest.GetSubprojectPaths(self, name, path)
2243 project = self.manifest.paths.get(relpath)
2244 if project:
2245 result.extend(project.GetDerivedSubprojects())
2246 continue
2247
2248 if url.startswith(".."):
2249 url = urllib.parse.urljoin("%s/" % self.remote.url, url)
2250 remote = RemoteSpec(
2251 self.remote.name,
2252 url=url,
2253 pushUrl=self.remote.pushUrl,
2254 review=self.remote.review,
2255 revision=self.remote.revision,
2256 )
2257 subproject = Project(
2258 manifest=self.manifest,
2259 name=name,
2260 remote=remote,
2261 gitdir=gitdir,
2262 objdir=objdir,
2263 worktree=worktree,
2264 relpath=relpath,
2265 revisionExpr=rev,
2266 revisionId=rev,
2267 rebase=self.rebase,
2268 groups=self.groups,
2269 sync_c=self.sync_c,
2270 sync_s=self.sync_s,
2271 sync_tags=self.sync_tags,
2272 parent=self,
2273 is_derived=True,
2274 )
2275 result.append(subproject)
2276 result.extend(subproject.GetDerivedSubprojects())
2277 return result
2278
2279 def EnableRepositoryExtension(self, key, value="true", version=1):
2280 """Enable git repository extension |key| with |value|.
2281
2282 Args:
2283 key: The extension to enabled. Omit the "extensions." prefix.
2284 value: The value to use for the extension.
2285 version: The minimum git repository version needed.
2286 """
2287 # Make sure the git repo version is new enough already.
2288 found_version = self.config.GetInt("core.repositoryFormatVersion")
2289 if found_version is None:
2290 found_version = 0
2291 if found_version < version:
2292 self.config.SetString("core.repositoryFormatVersion", str(version))
2293
2294 # Enable the extension!
2295 self.config.SetString("extensions.%s" % (key,), value)
2296
2297 def ResolveRemoteHead(self, name=None):
2298 """Find out what the default branch (HEAD) points to.
2299
2300 Normally this points to refs/heads/master, but projects are moving to
2301 main. Support whatever the server uses rather than hardcoding "master"
2302 ourselves.
2303 """
2304 if name is None:
2305 name = self.remote.name
2306
2307 # The output will look like (NB: tabs are separators):
2308 # ref: refs/heads/master HEAD
2309 # 5f6803b100bb3cd0f534e96e88c91373e8ed1c44 HEAD
2310 output = self.bare_git.ls_remote(
2311 "-q", "--symref", "--exit-code", name, "HEAD"
2312 )
2313
2314 for line in output.splitlines():
2315 lhs, rhs = line.split("\t", 1)
2316 if rhs == "HEAD" and lhs.startswith("ref:"):
2317 return lhs[4:].strip()
2318
2319 return None
2320
2321 def _CheckForImmutableRevision(self):
2322 try:
2323 # if revision (sha or tag) is not present then following function
2324 # throws an error.
2325 self.bare_git.rev_list(
2326 "-1", "--missing=allow-any", "%s^0" % self.revisionExpr, "--"
2327 )
2328 if self.upstream:
2329 rev = self.GetRemote().ToLocal(self.upstream)
2330 self.bare_git.rev_list(
2331 "-1", "--missing=allow-any", "%s^0" % rev, "--"
2332 )
2333 self.bare_git.merge_base(
2334 "--is-ancestor", self.revisionExpr, rev
2335 )
2336 return True
2337 except GitError:
2338 # There is no such persistent revision. We have to fetch it.
2339 return False
2340
2341 def _FetchArchive(self, tarpath, cwd=None):
2342 cmd = ["archive", "-v", "-o", tarpath]
2343 cmd.append("--remote=%s" % self.remote.url)
2344 cmd.append("--prefix=%s/" % self.RelPath(local=False))
2345 cmd.append(self.revisionExpr)
2346
2347 command = GitCommand(
Jason Chang32b59562023-07-14 16:45:35 -07002348 self,
2349 cmd,
2350 cwd=cwd,
2351 capture_stdout=True,
2352 capture_stderr=True,
2353 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00002354 )
Jason Chang32b59562023-07-14 16:45:35 -07002355 command.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00002356
2357 def _RemoteFetch(
2358 self,
2359 name=None,
2360 current_branch_only=False,
2361 initial=False,
2362 quiet=False,
2363 verbose=False,
2364 output_redir=None,
2365 alt_dir=None,
2366 tags=True,
2367 prune=False,
2368 depth=None,
2369 submodules=False,
2370 ssh_proxy=None,
2371 force_sync=False,
2372 clone_filter=None,
2373 retry_fetches=2,
2374 retry_sleep_initial_sec=4.0,
2375 retry_exp_factor=2.0,
Jason Chang32b59562023-07-14 16:45:35 -07002376 ) -> bool:
Gavin Makea2e3302023-03-11 06:46:20 +00002377 tag_name = None
2378 # The depth should not be used when fetching to a mirror because
2379 # it will result in a shallow repository that cannot be cloned or
2380 # fetched from.
2381 # The repo project should also never be synced with partial depth.
2382 if self.manifest.IsMirror or self.relpath == ".repo/repo":
2383 depth = None
2384
2385 if depth:
2386 current_branch_only = True
2387
Sylvain56a5a012023-09-11 13:38:00 +02002388 is_sha1 = bool(IsId(self.revisionExpr))
Gavin Makea2e3302023-03-11 06:46:20 +00002389
2390 if current_branch_only:
2391 if self.revisionExpr.startswith(R_TAGS):
2392 # This is a tag and its commit id should never change.
2393 tag_name = self.revisionExpr[len(R_TAGS) :]
2394 elif self.upstream and self.upstream.startswith(R_TAGS):
2395 # This is a tag and its commit id should never change.
2396 tag_name = self.upstream[len(R_TAGS) :]
2397
2398 if is_sha1 or tag_name is not None:
2399 if self._CheckForImmutableRevision():
2400 if verbose:
2401 print(
2402 "Skipped fetching project %s (already have "
2403 "persistent ref)" % self.name
2404 )
2405 return True
2406 if is_sha1 and not depth:
2407 # When syncing a specific commit and --depth is not set:
2408 # * if upstream is explicitly specified and is not a sha1, fetch
2409 # only upstream as users expect only upstream to be fetch.
2410 # Note: The commit might not be in upstream in which case the
2411 # sync will fail.
2412 # * otherwise, fetch all branches to make sure we end up with
2413 # the specific commit.
2414 if self.upstream:
Sylvain56a5a012023-09-11 13:38:00 +02002415 current_branch_only = not IsId(self.upstream)
Gavin Makea2e3302023-03-11 06:46:20 +00002416 else:
2417 current_branch_only = False
2418
2419 if not name:
2420 name = self.remote.name
2421
2422 remote = self.GetRemote(name)
2423 if not remote.PreConnectFetch(ssh_proxy):
2424 ssh_proxy = None
2425
2426 if initial:
2427 if alt_dir and "objects" == os.path.basename(alt_dir):
2428 ref_dir = os.path.dirname(alt_dir)
2429 packed_refs = os.path.join(self.gitdir, "packed-refs")
2430
2431 all_refs = self.bare_ref.all
2432 ids = set(all_refs.values())
2433 tmp = set()
2434
2435 for r, ref_id in GitRefs(ref_dir).all.items():
2436 if r not in all_refs:
2437 if r.startswith(R_TAGS) or remote.WritesTo(r):
2438 all_refs[r] = ref_id
2439 ids.add(ref_id)
2440 continue
2441
2442 if ref_id in ids:
2443 continue
2444
2445 r = "refs/_alt/%s" % ref_id
2446 all_refs[r] = ref_id
2447 ids.add(ref_id)
2448 tmp.add(r)
2449
2450 tmp_packed_lines = []
2451 old_packed_lines = []
2452
2453 for r in sorted(all_refs):
2454 line = "%s %s\n" % (all_refs[r], r)
2455 tmp_packed_lines.append(line)
2456 if r not in tmp:
2457 old_packed_lines.append(line)
2458
2459 tmp_packed = "".join(tmp_packed_lines)
2460 old_packed = "".join(old_packed_lines)
2461 _lwrite(packed_refs, tmp_packed)
2462 else:
2463 alt_dir = None
2464
2465 cmd = ["fetch"]
2466
2467 if clone_filter:
2468 git_require((2, 19, 0), fail=True, msg="partial clones")
2469 cmd.append("--filter=%s" % clone_filter)
2470 self.EnableRepositoryExtension("partialclone", self.remote.name)
2471
2472 if depth:
2473 cmd.append("--depth=%s" % depth)
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002474 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002475 # If this repo has shallow objects, then we don't know which refs
2476 # have shallow objects or not. Tell git to unshallow all fetched
2477 # refs. Don't do this with projects that don't have shallow
2478 # objects, since it is less efficient.
2479 if os.path.exists(os.path.join(self.gitdir, "shallow")):
2480 cmd.append("--depth=2147483647")
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -08002481
Gavin Makea2e3302023-03-11 06:46:20 +00002482 if not verbose:
2483 cmd.append("--quiet")
2484 if not quiet and sys.stdout.isatty():
2485 cmd.append("--progress")
2486 if not self.worktree:
2487 cmd.append("--update-head-ok")
2488 cmd.append(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002489
Gavin Makea2e3302023-03-11 06:46:20 +00002490 if force_sync:
2491 cmd.append("--force")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002492
Gavin Makea2e3302023-03-11 06:46:20 +00002493 if prune:
2494 cmd.append("--prune")
Mike Frysinger29626b42021-05-01 09:37:13 -04002495
Gavin Makea2e3302023-03-11 06:46:20 +00002496 # Always pass something for --recurse-submodules, git with GIT_DIR
2497 # behaves incorrectly when not given `--recurse-submodules=no`.
2498 # (b/218891912)
2499 cmd.append(
2500 f'--recurse-submodules={"on-demand" if submodules else "no"}'
2501 )
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002502
Gavin Makea2e3302023-03-11 06:46:20 +00002503 spec = []
2504 if not current_branch_only:
2505 # Fetch whole repo.
2506 spec.append(
2507 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2508 )
2509 elif tag_name is not None:
2510 spec.append("tag")
2511 spec.append(tag_name)
Remy Böhmer1469c282020-12-15 18:49:02 +01002512
Gavin Makea2e3302023-03-11 06:46:20 +00002513 if self.manifest.IsMirror and not current_branch_only:
2514 branch = None
Remy Böhmer1469c282020-12-15 18:49:02 +01002515 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002516 branch = self.revisionExpr
2517 if (
2518 not self.manifest.IsMirror
2519 and is_sha1
2520 and depth
2521 and git_require((1, 8, 3))
2522 ):
2523 # Shallow checkout of a specific commit, fetch from that commit and
2524 # not the heads only as the commit might be deeper in the history.
2525 spec.append(branch)
2526 if self.upstream:
2527 spec.append(self.upstream)
David James8d201162013-10-11 17:03:19 -07002528 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002529 if is_sha1:
2530 branch = self.upstream
2531 if branch is not None and branch.strip():
2532 if not branch.startswith("refs/"):
2533 branch = R_HEADS + branch
2534 spec.append(str(("+%s:" % branch) + remote.ToLocal(branch)))
David James8d201162013-10-11 17:03:19 -07002535
Gavin Makea2e3302023-03-11 06:46:20 +00002536 # If mirroring repo and we cannot deduce the tag or branch to fetch,
2537 # fetch whole repo.
2538 if self.manifest.IsMirror and not spec:
2539 spec.append(
2540 str(("+refs/heads/*:") + remote.ToLocal("refs/heads/*"))
2541 )
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002542
Gavin Makea2e3302023-03-11 06:46:20 +00002543 # If using depth then we should not get all the tags since they may
2544 # be outside of the depth.
2545 if not tags or depth:
2546 cmd.append("--no-tags")
2547 else:
2548 cmd.append("--tags")
2549 spec.append(str(("+refs/tags/*:") + remote.ToLocal("refs/tags/*")))
Mike Frysinger979d5bd2020-02-09 02:28:34 -05002550
Gavin Makea2e3302023-03-11 06:46:20 +00002551 cmd.extend(spec)
Mike Frysinger21b7fbe2020-02-26 23:53:36 -05002552
Gavin Makea2e3302023-03-11 06:46:20 +00002553 # At least one retry minimum due to git remote prune.
2554 retry_fetches = max(retry_fetches, 2)
2555 retry_cur_sleep = retry_sleep_initial_sec
2556 ok = prune_tried = False
2557 for try_n in range(retry_fetches):
Jason Chang32b59562023-07-14 16:45:35 -07002558 verify_command = try_n == retry_fetches - 1
Gavin Makea2e3302023-03-11 06:46:20 +00002559 gitcmd = GitCommand(
2560 self,
2561 cmd,
2562 bare=True,
2563 objdir=os.path.join(self.objdir, "objects"),
2564 ssh_proxy=ssh_proxy,
2565 merge_output=True,
2566 capture_stdout=quiet or bool(output_redir),
Jason Chang32b59562023-07-14 16:45:35 -07002567 verify_command=verify_command,
Gavin Makea2e3302023-03-11 06:46:20 +00002568 )
2569 if gitcmd.stdout and not quiet and output_redir:
2570 output_redir.write(gitcmd.stdout)
2571 ret = gitcmd.Wait()
2572 if ret == 0:
2573 ok = True
2574 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002575
Gavin Makea2e3302023-03-11 06:46:20 +00002576 # Retry later due to HTTP 429 Too Many Requests.
2577 elif (
2578 gitcmd.stdout
2579 and "error:" in gitcmd.stdout
2580 and "HTTP 429" in gitcmd.stdout
2581 ):
2582 # Fallthru to sleep+retry logic at the bottom.
2583 pass
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002584
Gavin Makea2e3302023-03-11 06:46:20 +00002585 # Try to prune remote branches once in case there are conflicts.
2586 # For example, if the remote had refs/heads/upstream, but deleted
2587 # that and now has refs/heads/upstream/foo.
2588 elif (
2589 gitcmd.stdout
2590 and "error:" in gitcmd.stdout
2591 and "git remote prune" in gitcmd.stdout
2592 and not prune_tried
2593 ):
2594 prune_tried = True
2595 prunecmd = GitCommand(
2596 self,
2597 ["remote", "prune", name],
2598 bare=True,
2599 ssh_proxy=ssh_proxy,
2600 )
2601 ret = prunecmd.Wait()
2602 if ret:
2603 break
2604 print(
2605 "retrying fetch after pruning remote branches",
2606 file=output_redir,
2607 )
2608 # Continue right away so we don't sleep as we shouldn't need to.
2609 continue
2610 elif current_branch_only and is_sha1 and ret == 128:
2611 # Exit code 128 means "couldn't find the ref you asked for"; if
2612 # we're in sha1 mode, we just tried sync'ing from the upstream
2613 # field; it doesn't exist, thus abort the optimization attempt
2614 # and do a full sync.
2615 break
2616 elif ret < 0:
2617 # Git died with a signal, exit immediately.
2618 break
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002619
Gavin Makea2e3302023-03-11 06:46:20 +00002620 # Figure out how long to sleep before the next attempt, if there is
2621 # one.
2622 if not verbose and gitcmd.stdout:
2623 print(
2624 "\n%s:\n%s" % (self.name, gitcmd.stdout),
2625 end="",
2626 file=output_redir,
2627 )
2628 if try_n < retry_fetches - 1:
2629 print(
2630 "%s: sleeping %s seconds before retrying"
2631 % (self.name, retry_cur_sleep),
2632 file=output_redir,
2633 )
2634 time.sleep(retry_cur_sleep)
2635 retry_cur_sleep = min(
2636 retry_exp_factor * retry_cur_sleep, MAXIMUM_RETRY_SLEEP_SEC
2637 )
2638 retry_cur_sleep *= 1 - random.uniform(
2639 -RETRY_JITTER_PERCENT, RETRY_JITTER_PERCENT
2640 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002641
Gavin Makea2e3302023-03-11 06:46:20 +00002642 if initial:
2643 if alt_dir:
2644 if old_packed != "":
2645 _lwrite(packed_refs, old_packed)
2646 else:
2647 platform_utils.remove(packed_refs)
2648 self.bare_git.pack_refs("--all", "--prune")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002649
Gavin Makea2e3302023-03-11 06:46:20 +00002650 if is_sha1 and current_branch_only:
2651 # We just synced the upstream given branch; verify we
2652 # got what we wanted, else trigger a second run of all
2653 # refs.
2654 if not self._CheckForImmutableRevision():
2655 # Sync the current branch only with depth set to None.
2656 # We always pass depth=None down to avoid infinite recursion.
2657 return self._RemoteFetch(
2658 name=name,
2659 quiet=quiet,
2660 verbose=verbose,
2661 output_redir=output_redir,
2662 current_branch_only=current_branch_only and depth,
2663 initial=False,
2664 alt_dir=alt_dir,
Nasser Grainawiaafed292023-05-24 12:51:03 -06002665 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00002666 depth=None,
2667 ssh_proxy=ssh_proxy,
2668 clone_filter=clone_filter,
2669 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002670
Gavin Makea2e3302023-03-11 06:46:20 +00002671 return ok
Mike Frysingerf4545122019-11-11 04:34:16 -05002672
Gavin Makea2e3302023-03-11 06:46:20 +00002673 def _ApplyCloneBundle(self, initial=False, quiet=False, verbose=False):
2674 if initial and (
2675 self.manifest.manifestProject.depth or self.clone_depth
2676 ):
2677 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002678
Gavin Makea2e3302023-03-11 06:46:20 +00002679 remote = self.GetRemote()
2680 bundle_url = remote.url + "/clone.bundle"
2681 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
2682 if GetSchemeFromUrl(bundle_url) not in (
2683 "http",
2684 "https",
2685 "persistent-http",
2686 "persistent-https",
2687 ):
2688 return False
Kevin Degi384b3c52014-10-16 16:02:58 -06002689
Gavin Makea2e3302023-03-11 06:46:20 +00002690 bundle_dst = os.path.join(self.gitdir, "clone.bundle")
2691 bundle_tmp = os.path.join(self.gitdir, "clone.bundle.tmp")
2692
2693 exist_dst = os.path.exists(bundle_dst)
2694 exist_tmp = os.path.exists(bundle_tmp)
2695
2696 if not initial and not exist_dst and not exist_tmp:
2697 return False
2698
2699 if not exist_dst:
2700 exist_dst = self._FetchBundle(
2701 bundle_url, bundle_tmp, bundle_dst, quiet, verbose
2702 )
2703 if not exist_dst:
2704 return False
2705
2706 cmd = ["fetch"]
2707 if not verbose:
2708 cmd.append("--quiet")
2709 if not quiet and sys.stdout.isatty():
2710 cmd.append("--progress")
2711 if not self.worktree:
2712 cmd.append("--update-head-ok")
2713 cmd.append(bundle_dst)
2714 for f in remote.fetch:
2715 cmd.append(str(f))
2716 cmd.append("+refs/tags/*:refs/tags/*")
2717
2718 ok = (
2719 GitCommand(
2720 self,
2721 cmd,
2722 bare=True,
2723 objdir=os.path.join(self.objdir, "objects"),
2724 ).Wait()
2725 == 0
2726 )
2727 platform_utils.remove(bundle_dst, missing_ok=True)
2728 platform_utils.remove(bundle_tmp, missing_ok=True)
2729 return ok
2730
2731 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
2732 platform_utils.remove(dstPath, missing_ok=True)
2733
2734 cmd = ["curl", "--fail", "--output", tmpPath, "--netrc", "--location"]
2735 if quiet:
2736 cmd += ["--silent", "--show-error"]
2737 if os.path.exists(tmpPath):
2738 size = os.stat(tmpPath).st_size
2739 if size >= 1024:
2740 cmd += ["--continue-at", "%d" % (size,)]
2741 else:
2742 platform_utils.remove(tmpPath)
2743 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
2744 if cookiefile:
2745 cmd += ["--cookie", cookiefile]
2746 if proxy:
2747 cmd += ["--proxy", proxy]
2748 elif "http_proxy" in os.environ and "darwin" == sys.platform:
2749 cmd += ["--proxy", os.environ["http_proxy"]]
2750 if srcUrl.startswith("persistent-https"):
2751 srcUrl = "http" + srcUrl[len("persistent-https") :]
2752 elif srcUrl.startswith("persistent-http"):
2753 srcUrl = "http" + srcUrl[len("persistent-http") :]
2754 cmd += [srcUrl]
2755
2756 proc = None
2757 with Trace("Fetching bundle: %s", " ".join(cmd)):
2758 if verbose:
2759 print("%s: Downloading bundle: %s" % (self.name, srcUrl))
2760 stdout = None if verbose else subprocess.PIPE
2761 stderr = None if verbose else subprocess.STDOUT
2762 try:
2763 proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
2764 except OSError:
2765 return False
2766
2767 (output, _) = proc.communicate()
2768 curlret = proc.returncode
2769
2770 if curlret == 22:
2771 # From curl man page:
2772 # 22: HTTP page not retrieved. The requested url was not found
2773 # or returned another error with the HTTP error code being 400
2774 # or above. This return code only appears if -f, --fail is used.
2775 if verbose:
2776 print(
2777 "%s: Unable to retrieve clone.bundle; ignoring."
2778 % self.name
2779 )
2780 if output:
2781 print("Curl output:\n%s" % output)
2782 return False
2783 elif curlret and not verbose and output:
2784 print("%s" % output, file=sys.stderr)
2785
2786 if os.path.exists(tmpPath):
2787 if curlret == 0 and self._IsValidBundle(tmpPath, quiet):
2788 platform_utils.rename(tmpPath, dstPath)
2789 return True
2790 else:
2791 platform_utils.remove(tmpPath)
2792 return False
2793 else:
2794 return False
2795
2796 def _IsValidBundle(self, path, quiet):
2797 try:
2798 with open(path, "rb") as f:
2799 if f.read(16) == b"# v2 git bundle\n":
2800 return True
2801 else:
2802 if not quiet:
2803 print(
2804 "Invalid clone.bundle file; ignoring.",
2805 file=sys.stderr,
2806 )
2807 return False
2808 except OSError:
2809 return False
2810
2811 def _Checkout(self, rev, quiet=False):
2812 cmd = ["checkout"]
2813 if quiet:
2814 cmd.append("-q")
2815 cmd.append(rev)
2816 cmd.append("--")
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002817 if GitCommand(self, cmd).Wait() != 0:
Gavin Makea2e3302023-03-11 06:46:20 +00002818 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002819 raise GitError(
2820 "%s checkout %s " % (self.name, rev), project=self.name
2821 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002822
Gavin Makea2e3302023-03-11 06:46:20 +00002823 def _CherryPick(self, rev, ffonly=False, record_origin=False):
2824 cmd = ["cherry-pick"]
2825 if ffonly:
2826 cmd.append("--ff")
2827 if record_origin:
2828 cmd.append("-x")
2829 cmd.append(rev)
2830 cmd.append("--")
2831 if GitCommand(self, cmd).Wait() != 0:
2832 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002833 raise GitError(
2834 "%s cherry-pick %s " % (self.name, rev), project=self.name
2835 )
Victor Boivie0960b5b2010-11-26 13:42:13 +01002836
Gavin Makea2e3302023-03-11 06:46:20 +00002837 def _LsRemote(self, refs):
2838 cmd = ["ls-remote", self.remote.name, refs]
2839 p = GitCommand(self, cmd, capture_stdout=True)
2840 if p.Wait() == 0:
2841 return p.stdout
2842 return None
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002843
Gavin Makea2e3302023-03-11 06:46:20 +00002844 def _Revert(self, rev):
2845 cmd = ["revert"]
2846 cmd.append("--no-edit")
2847 cmd.append(rev)
2848 cmd.append("--")
2849 if GitCommand(self, cmd).Wait() != 0:
2850 if self._allrefs:
Jason Chang32b59562023-07-14 16:45:35 -07002851 raise GitError(
2852 "%s revert %s " % (self.name, rev), project=self.name
2853 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002854
Gavin Makea2e3302023-03-11 06:46:20 +00002855 def _ResetHard(self, rev, quiet=True):
2856 cmd = ["reset", "--hard"]
2857 if quiet:
2858 cmd.append("-q")
2859 cmd.append(rev)
2860 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002861 raise GitError(
2862 "%s reset --hard %s " % (self.name, rev), project=self.name
2863 )
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002864
Gavin Makea2e3302023-03-11 06:46:20 +00002865 def _SyncSubmodules(self, quiet=True):
2866 cmd = ["submodule", "update", "--init", "--recursive"]
2867 if quiet:
2868 cmd.append("-q")
2869 if GitCommand(self, cmd).Wait() != 0:
2870 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07002871 "%s submodule update --init --recursive " % self.name,
2872 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00002873 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002874
Gavin Makea2e3302023-03-11 06:46:20 +00002875 def _Rebase(self, upstream, onto=None):
2876 cmd = ["rebase"]
2877 if onto is not None:
2878 cmd.extend(["--onto", onto])
2879 cmd.append(upstream)
2880 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002881 raise GitError(
2882 "%s rebase %s " % (self.name, upstream), project=self.name
2883 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002884
Gavin Makea2e3302023-03-11 06:46:20 +00002885 def _FastForward(self, head, ffonly=False):
2886 cmd = ["merge", "--no-stat", head]
2887 if ffonly:
2888 cmd.append("--ff-only")
2889 if GitCommand(self, cmd).Wait() != 0:
Jason Chang32b59562023-07-14 16:45:35 -07002890 raise GitError(
2891 "%s merge %s " % (self.name, head), project=self.name
2892 )
Mike Frysinger89ed8ac2022-01-06 05:42:24 -05002893
Gavin Makea2e3302023-03-11 06:46:20 +00002894 def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False):
2895 init_git_dir = not os.path.exists(self.gitdir)
2896 init_obj_dir = not os.path.exists(self.objdir)
2897 try:
2898 # Initialize the bare repository, which contains all of the objects.
2899 if init_obj_dir:
2900 os.makedirs(self.objdir)
2901 self.bare_objdir.init()
Mike Frysinger2a089cf2021-11-13 23:29:42 -05002902
Gavin Makea2e3302023-03-11 06:46:20 +00002903 self._UpdateHooks(quiet=quiet)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002904
Gavin Makea2e3302023-03-11 06:46:20 +00002905 if self.use_git_worktrees:
2906 # Enable per-worktree config file support if possible. This
2907 # is more a nice-to-have feature for users rather than a
2908 # hard requirement.
2909 if git_require((2, 20, 0)):
2910 self.EnableRepositoryExtension("worktreeConfig")
Renaud Paquay788e9622017-01-27 11:41:12 -08002911
Gavin Makea2e3302023-03-11 06:46:20 +00002912 # If we have a separate directory to hold refs, initialize it as
2913 # well.
2914 if self.objdir != self.gitdir:
2915 if init_git_dir:
2916 os.makedirs(self.gitdir)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002917
Gavin Makea2e3302023-03-11 06:46:20 +00002918 if init_obj_dir or init_git_dir:
2919 self._ReferenceGitDir(
2920 self.objdir, self.gitdir, copy_all=True
2921 )
2922 try:
2923 self._CheckDirReference(self.objdir, self.gitdir)
2924 except GitError as e:
2925 if force_sync:
2926 print(
2927 "Retrying clone after deleting %s" % self.gitdir,
2928 file=sys.stderr,
2929 )
2930 try:
2931 platform_utils.rmtree(
2932 platform_utils.realpath(self.gitdir)
2933 )
2934 if self.worktree and os.path.exists(
2935 platform_utils.realpath(self.worktree)
2936 ):
2937 platform_utils.rmtree(
2938 platform_utils.realpath(self.worktree)
2939 )
2940 return self._InitGitDir(
2941 mirror_git=mirror_git,
2942 force_sync=False,
2943 quiet=quiet,
2944 )
2945 except Exception:
2946 raise e
2947 raise e
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07002948
Gavin Makea2e3302023-03-11 06:46:20 +00002949 if init_git_dir:
2950 mp = self.manifest.manifestProject
2951 ref_dir = mp.reference or ""
Julien Camperguedd654222014-01-09 16:21:37 +01002952
Gavin Makea2e3302023-03-11 06:46:20 +00002953 def _expanded_ref_dirs():
2954 """Iterate through possible git reference dir paths."""
2955 name = self.name + ".git"
2956 yield mirror_git or os.path.join(ref_dir, name)
2957 for prefix in "", self.remote.name:
2958 yield os.path.join(
2959 ref_dir, ".repo", "project-objects", prefix, name
2960 )
2961 yield os.path.join(
2962 ref_dir, ".repo", "worktrees", prefix, name
2963 )
2964
2965 if ref_dir or mirror_git:
2966 found_ref_dir = None
2967 for path in _expanded_ref_dirs():
2968 if os.path.exists(path):
2969 found_ref_dir = path
2970 break
2971 ref_dir = found_ref_dir
2972
2973 if ref_dir:
2974 if not os.path.isabs(ref_dir):
2975 # The alternate directory is relative to the object
2976 # database.
2977 ref_dir = os.path.relpath(
2978 ref_dir, os.path.join(self.objdir, "objects")
2979 )
2980 _lwrite(
2981 os.path.join(
2982 self.objdir, "objects/info/alternates"
2983 ),
2984 os.path.join(ref_dir, "objects") + "\n",
2985 )
2986
2987 m = self.manifest.manifestProject.config
2988 for key in ["user.name", "user.email"]:
2989 if m.Has(key, include_defaults=False):
2990 self.config.SetString(key, m.GetString(key))
2991 if not self.manifest.EnableGitLfs:
2992 self.config.SetString(
2993 "filter.lfs.smudge", "git-lfs smudge --skip -- %f"
2994 )
2995 self.config.SetString(
2996 "filter.lfs.process", "git-lfs filter-process --skip"
2997 )
2998 self.config.SetBoolean(
2999 "core.bare", True if self.manifest.IsMirror else None
3000 )
3001 except Exception:
3002 if init_obj_dir and os.path.exists(self.objdir):
3003 platform_utils.rmtree(self.objdir)
3004 if init_git_dir and os.path.exists(self.gitdir):
3005 platform_utils.rmtree(self.gitdir)
3006 raise
3007
3008 def _UpdateHooks(self, quiet=False):
3009 if os.path.exists(self.objdir):
3010 self._InitHooks(quiet=quiet)
3011
3012 def _InitHooks(self, quiet=False):
3013 hooks = platform_utils.realpath(os.path.join(self.objdir, "hooks"))
3014 if not os.path.exists(hooks):
3015 os.makedirs(hooks)
3016
3017 # Delete sample hooks. They're noise.
3018 for hook in glob.glob(os.path.join(hooks, "*.sample")):
3019 try:
3020 platform_utils.remove(hook, missing_ok=True)
3021 except PermissionError:
3022 pass
3023
3024 for stock_hook in _ProjectHooks():
3025 name = os.path.basename(stock_hook)
3026
3027 if (
3028 name in ("commit-msg",)
3029 and not self.remote.review
3030 and self is not self.manifest.manifestProject
3031 ):
3032 # Don't install a Gerrit Code Review hook if this
3033 # project does not appear to use it for reviews.
3034 #
3035 # Since the manifest project is one of those, but also
3036 # managed through gerrit, it's excluded.
3037 continue
3038
3039 dst = os.path.join(hooks, name)
3040 if platform_utils.islink(dst):
3041 continue
3042 if os.path.exists(dst):
3043 # If the files are the same, we'll leave it alone. We create
3044 # symlinks below by default but fallback to hardlinks if the OS
3045 # blocks them. So if we're here, it's probably because we made a
3046 # hardlink below.
3047 if not filecmp.cmp(stock_hook, dst, shallow=False):
3048 if not quiet:
3049 _warn(
3050 "%s: Not replacing locally modified %s hook",
3051 self.RelPath(local=False),
3052 name,
3053 )
3054 continue
3055 try:
3056 platform_utils.symlink(
3057 os.path.relpath(stock_hook, os.path.dirname(dst)), dst
3058 )
3059 except OSError as e:
3060 if e.errno == errno.EPERM:
3061 try:
3062 os.link(stock_hook, dst)
3063 except OSError:
Jason Chang32b59562023-07-14 16:45:35 -07003064 raise GitError(
3065 self._get_symlink_error_message(), project=self.name
3066 )
Gavin Makea2e3302023-03-11 06:46:20 +00003067 else:
3068 raise
3069
3070 def _InitRemote(self):
3071 if self.remote.url:
3072 remote = self.GetRemote()
3073 remote.url = self.remote.url
3074 remote.pushUrl = self.remote.pushUrl
3075 remote.review = self.remote.review
3076 remote.projectname = self.name
3077
3078 if self.worktree:
3079 remote.ResetFetch(mirror=False)
3080 else:
3081 remote.ResetFetch(mirror=True)
3082 remote.Save()
3083
3084 def _InitMRef(self):
3085 """Initialize the pseudo m/<manifest branch> ref."""
3086 if self.manifest.branch:
3087 if self.use_git_worktrees:
3088 # Set up the m/ space to point to the worktree-specific ref
3089 # space. We'll update the worktree-specific ref space on each
3090 # checkout.
3091 ref = R_M + self.manifest.branch
3092 if not self.bare_ref.symref(ref):
3093 self.bare_git.symbolic_ref(
3094 "-m",
3095 "redirecting to worktree scope",
3096 ref,
3097 R_WORKTREE_M + self.manifest.branch,
3098 )
3099
3100 # We can't update this ref with git worktrees until it exists.
3101 # We'll wait until the initial checkout to set it.
3102 if not os.path.exists(self.worktree):
3103 return
3104
3105 base = R_WORKTREE_M
3106 active_git = self.work_git
3107
3108 self._InitAnyMRef(HEAD, self.bare_git, detach=True)
3109 else:
3110 base = R_M
3111 active_git = self.bare_git
3112
3113 self._InitAnyMRef(base + self.manifest.branch, active_git)
3114
3115 def _InitMirrorHead(self):
3116 self._InitAnyMRef(HEAD, self.bare_git)
3117
3118 def _InitAnyMRef(self, ref, active_git, detach=False):
3119 """Initialize |ref| in |active_git| to the value in the manifest.
3120
3121 This points |ref| to the <project> setting in the manifest.
3122
3123 Args:
3124 ref: The branch to update.
3125 active_git: The git repository to make updates in.
3126 detach: Whether to update target of symbolic refs, or overwrite the
3127 ref directly (and thus make it non-symbolic).
3128 """
3129 cur = self.bare_ref.symref(ref)
3130
3131 if self.revisionId:
3132 if cur != "" or self.bare_ref.get(ref) != self.revisionId:
3133 msg = "manifest set to %s" % self.revisionId
3134 dst = self.revisionId + "^0"
3135 active_git.UpdateRef(ref, dst, message=msg, detach=True)
Julien Camperguedd654222014-01-09 16:21:37 +01003136 else:
Gavin Makea2e3302023-03-11 06:46:20 +00003137 remote = self.GetRemote()
3138 dst = remote.ToLocal(self.revisionExpr)
3139 if cur != dst:
3140 msg = "manifest set to %s" % self.revisionExpr
3141 if detach:
3142 active_git.UpdateRef(ref, dst, message=msg, detach=True)
3143 else:
3144 active_git.symbolic_ref("-m", msg, ref, dst)
Julien Camperguedd654222014-01-09 16:21:37 +01003145
Gavin Makea2e3302023-03-11 06:46:20 +00003146 def _CheckDirReference(self, srcdir, destdir):
3147 # Git worktrees don't use symlinks to share at all.
3148 if self.use_git_worktrees:
3149 return
Julien Camperguedd654222014-01-09 16:21:37 +01003150
Gavin Makea2e3302023-03-11 06:46:20 +00003151 for name in self.shareable_dirs:
3152 # Try to self-heal a bit in simple cases.
3153 dst_path = os.path.join(destdir, name)
3154 src_path = os.path.join(srcdir, name)
Julien Camperguedd654222014-01-09 16:21:37 +01003155
Gavin Makea2e3302023-03-11 06:46:20 +00003156 dst = platform_utils.realpath(dst_path)
3157 if os.path.lexists(dst):
3158 src = platform_utils.realpath(src_path)
3159 # Fail if the links are pointing to the wrong place.
3160 if src != dst:
3161 _error("%s is different in %s vs %s", name, destdir, srcdir)
3162 raise GitError(
3163 "--force-sync not enabled; cannot overwrite a local "
3164 "work tree. If you're comfortable with the "
3165 "possibility of losing the work tree's git metadata,"
3166 " use `repo sync --force-sync {0}` to "
Jason Chang32b59562023-07-14 16:45:35 -07003167 "proceed.".format(self.RelPath(local=False)),
3168 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003169 )
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003170
Gavin Makea2e3302023-03-11 06:46:20 +00003171 def _ReferenceGitDir(self, gitdir, dotgit, copy_all):
3172 """Update |dotgit| to reference |gitdir|, using symlinks where possible.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003173
Gavin Makea2e3302023-03-11 06:46:20 +00003174 Args:
3175 gitdir: The bare git repository. Must already be initialized.
3176 dotgit: The repository you would like to initialize.
3177 copy_all: If true, copy all remaining files from |gitdir| ->
3178 |dotgit|. This saves you the effort of initializing |dotgit|
3179 yourself.
3180 """
3181 symlink_dirs = self.shareable_dirs[:]
3182 to_symlink = symlink_dirs
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003183
Gavin Makea2e3302023-03-11 06:46:20 +00003184 to_copy = []
3185 if copy_all:
3186 to_copy = platform_utils.listdir(gitdir)
Kimiyuki Onaka0501b292020-08-28 10:05:27 +09003187
Gavin Makea2e3302023-03-11 06:46:20 +00003188 dotgit = platform_utils.realpath(dotgit)
3189 for name in set(to_copy).union(to_symlink):
3190 try:
3191 src = platform_utils.realpath(os.path.join(gitdir, name))
3192 dst = os.path.join(dotgit, name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003193
Gavin Makea2e3302023-03-11 06:46:20 +00003194 if os.path.lexists(dst):
3195 continue
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003196
Gavin Makea2e3302023-03-11 06:46:20 +00003197 # If the source dir doesn't exist, create an empty dir.
3198 if name in symlink_dirs and not os.path.lexists(src):
3199 os.makedirs(src)
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003200
Gavin Makea2e3302023-03-11 06:46:20 +00003201 if name in to_symlink:
3202 platform_utils.symlink(
3203 os.path.relpath(src, os.path.dirname(dst)), dst
3204 )
3205 elif copy_all and not platform_utils.islink(dst):
3206 if platform_utils.isdir(src):
3207 shutil.copytree(src, dst)
3208 elif os.path.isfile(src):
3209 shutil.copy(src, dst)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003210
Gavin Makea2e3302023-03-11 06:46:20 +00003211 except OSError as e:
3212 if e.errno == errno.EPERM:
3213 raise DownloadError(self._get_symlink_error_message())
3214 else:
3215 raise
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003216
Gavin Makea2e3302023-03-11 06:46:20 +00003217 def _InitGitWorktree(self):
3218 """Init the project using git worktrees."""
3219 self.bare_git.worktree("prune")
3220 self.bare_git.worktree(
3221 "add",
3222 "-ff",
3223 "--checkout",
3224 "--detach",
3225 "--lock",
3226 self.worktree,
3227 self.GetRevisionId(),
3228 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003229
Gavin Makea2e3302023-03-11 06:46:20 +00003230 # Rewrite the internal state files to use relative paths between the
3231 # checkouts & worktrees.
3232 dotgit = os.path.join(self.worktree, ".git")
3233 with open(dotgit, "r") as fp:
3234 # Figure out the checkout->worktree path.
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003235 setting = fp.read()
Gavin Makea2e3302023-03-11 06:46:20 +00003236 assert setting.startswith("gitdir:")
3237 git_worktree_path = setting.split(":", 1)[1].strip()
3238 # Some platforms (e.g. Windows) won't let us update dotgit in situ
3239 # because of file permissions. Delete it and recreate it from scratch
3240 # to avoid.
3241 platform_utils.remove(dotgit)
3242 # Use relative path from checkout->worktree & maintain Unix line endings
3243 # on all OS's to match git behavior.
3244 with open(dotgit, "w", newline="\n") as fp:
3245 print(
3246 "gitdir:",
3247 os.path.relpath(git_worktree_path, self.worktree),
3248 file=fp,
3249 )
3250 # Use relative path from worktree->checkout & maintain Unix line endings
3251 # on all OS's to match git behavior.
3252 with open(
3253 os.path.join(git_worktree_path, "gitdir"), "w", newline="\n"
3254 ) as fp:
3255 print(os.path.relpath(dotgit, git_worktree_path), file=fp)
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003256
Gavin Makea2e3302023-03-11 06:46:20 +00003257 self._InitMRef()
Mike Frysinger4b0eb5a2020-02-23 23:22:34 -05003258
Gavin Makea2e3302023-03-11 06:46:20 +00003259 def _InitWorkTree(self, force_sync=False, submodules=False):
3260 """Setup the worktree .git path.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003261
Gavin Makea2e3302023-03-11 06:46:20 +00003262 This is the user-visible path like src/foo/.git/.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003263
Gavin Makea2e3302023-03-11 06:46:20 +00003264 With non-git-worktrees, this will be a symlink to the .repo/projects/
3265 path. With git-worktrees, this will be a .git file using "gitdir: ..."
3266 syntax.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003267
Gavin Makea2e3302023-03-11 06:46:20 +00003268 Older checkouts had .git/ directories. If we see that, migrate it.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003269
Gavin Makea2e3302023-03-11 06:46:20 +00003270 This also handles changes in the manifest. Maybe this project was
3271 backed by "foo/bar" on the server, but now it's "new/foo/bar". We have
3272 to update the path we point to under .repo/projects/ to match.
3273 """
3274 dotgit = os.path.join(self.worktree, ".git")
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003275
Gavin Makea2e3302023-03-11 06:46:20 +00003276 # If using an old layout style (a directory), migrate it.
3277 if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
Jason Chang32b59562023-07-14 16:45:35 -07003278 self._MigrateOldWorkTreeGitDir(dotgit, project=self.name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003279
Gavin Makea2e3302023-03-11 06:46:20 +00003280 init_dotgit = not os.path.exists(dotgit)
3281 if self.use_git_worktrees:
3282 if init_dotgit:
3283 self._InitGitWorktree()
3284 self._CopyAndLinkFiles()
3285 else:
3286 if not init_dotgit:
3287 # See if the project has changed.
3288 if platform_utils.realpath(
3289 self.gitdir
3290 ) != platform_utils.realpath(dotgit):
3291 platform_utils.remove(dotgit)
Doug Anderson37282b42011-03-04 11:54:18 -08003292
Gavin Makea2e3302023-03-11 06:46:20 +00003293 if init_dotgit or not os.path.exists(dotgit):
3294 os.makedirs(self.worktree, exist_ok=True)
3295 platform_utils.symlink(
3296 os.path.relpath(self.gitdir, self.worktree), dotgit
3297 )
Doug Anderson37282b42011-03-04 11:54:18 -08003298
Gavin Makea2e3302023-03-11 06:46:20 +00003299 if init_dotgit:
3300 _lwrite(
3301 os.path.join(dotgit, HEAD), "%s\n" % self.GetRevisionId()
3302 )
Doug Anderson37282b42011-03-04 11:54:18 -08003303
Gavin Makea2e3302023-03-11 06:46:20 +00003304 # Finish checking out the worktree.
3305 cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
3306 if GitCommand(self, cmd).Wait() != 0:
3307 raise GitError(
Jason Chang32b59562023-07-14 16:45:35 -07003308 "Cannot initialize work tree for " + self.name,
3309 project=self.name,
Gavin Makea2e3302023-03-11 06:46:20 +00003310 )
Doug Anderson37282b42011-03-04 11:54:18 -08003311
Gavin Makea2e3302023-03-11 06:46:20 +00003312 if submodules:
3313 self._SyncSubmodules(quiet=True)
3314 self._CopyAndLinkFiles()
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003315
Gavin Makea2e3302023-03-11 06:46:20 +00003316 @classmethod
Jason Chang32b59562023-07-14 16:45:35 -07003317 def _MigrateOldWorkTreeGitDir(cls, dotgit, project=None):
Gavin Makea2e3302023-03-11 06:46:20 +00003318 """Migrate the old worktree .git/ dir style to a symlink.
3319
3320 This logic specifically only uses state from |dotgit| to figure out
3321 where to move content and not |self|. This way if the backing project
3322 also changed places, we only do the .git/ dir to .git symlink migration
3323 here. The path updates will happen independently.
3324 """
3325 # Figure out where in .repo/projects/ it's pointing to.
3326 if not os.path.islink(os.path.join(dotgit, "refs")):
Jason Chang32b59562023-07-14 16:45:35 -07003327 raise GitError(
3328 f"{dotgit}: unsupported checkout state", project=project
3329 )
Gavin Makea2e3302023-03-11 06:46:20 +00003330 gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, "refs")))
3331
3332 # Remove known symlink paths that exist in .repo/projects/.
3333 KNOWN_LINKS = {
3334 "config",
3335 "description",
3336 "hooks",
3337 "info",
3338 "logs",
3339 "objects",
3340 "packed-refs",
3341 "refs",
3342 "rr-cache",
3343 "shallow",
3344 "svn",
3345 }
3346 # Paths that we know will be in both, but are safe to clobber in
3347 # .repo/projects/.
3348 SAFE_TO_CLOBBER = {
3349 "COMMIT_EDITMSG",
3350 "FETCH_HEAD",
3351 "HEAD",
3352 "gc.log",
3353 "gitk.cache",
3354 "index",
3355 "ORIG_HEAD",
3356 }
3357
3358 # First see if we'd succeed before starting the migration.
3359 unknown_paths = []
3360 for name in platform_utils.listdir(dotgit):
3361 # Ignore all temporary/backup names. These are common with vim &
3362 # emacs.
3363 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3364 continue
3365
3366 dotgit_path = os.path.join(dotgit, name)
3367 if name in KNOWN_LINKS:
3368 if not platform_utils.islink(dotgit_path):
3369 unknown_paths.append(f"{dotgit_path}: should be a symlink")
3370 else:
3371 gitdir_path = os.path.join(gitdir, name)
3372 if name not in SAFE_TO_CLOBBER and os.path.exists(gitdir_path):
3373 unknown_paths.append(
3374 f"{dotgit_path}: unknown file; please file a bug"
3375 )
3376 if unknown_paths:
Jason Chang32b59562023-07-14 16:45:35 -07003377 raise GitError(
3378 "Aborting migration: " + "\n".join(unknown_paths),
3379 project=project,
3380 )
Gavin Makea2e3302023-03-11 06:46:20 +00003381
3382 # Now walk the paths and sync the .git/ to .repo/projects/.
3383 for name in platform_utils.listdir(dotgit):
3384 dotgit_path = os.path.join(dotgit, name)
3385
3386 # Ignore all temporary/backup names. These are common with vim &
3387 # emacs.
3388 if name.endswith("~") or (name[0] == "#" and name[-1] == "#"):
3389 platform_utils.remove(dotgit_path)
3390 elif name in KNOWN_LINKS:
3391 platform_utils.remove(dotgit_path)
3392 else:
3393 gitdir_path = os.path.join(gitdir, name)
3394 platform_utils.remove(gitdir_path, missing_ok=True)
3395 platform_utils.rename(dotgit_path, gitdir_path)
3396
3397 # Now that the dir should be empty, clear it out, and symlink it over.
3398 platform_utils.rmdir(dotgit)
3399 platform_utils.symlink(
3400 os.path.relpath(gitdir, os.path.dirname(dotgit)), dotgit
3401 )
3402
3403 def _get_symlink_error_message(self):
3404 if platform_utils.isWindows():
3405 return (
3406 "Unable to create symbolic link. Please re-run the command as "
3407 "Administrator, or see "
3408 "https://github.com/git-for-windows/git/wiki/Symbolic-Links "
3409 "for other options."
3410 )
3411 return "filesystem must support symlinks"
3412
3413 def _revlist(self, *args, **kw):
3414 a = []
3415 a.extend(args)
3416 a.append("--")
3417 return self.work_git.rev_list(*a, **kw)
3418
3419 @property
3420 def _allrefs(self):
3421 return self.bare_ref.all
3422
3423 def _getLogs(
3424 self, rev1, rev2, oneline=False, color=True, pretty_format=None
3425 ):
3426 """Get logs between two revisions of this project."""
3427 comp = ".."
3428 if rev1:
3429 revs = [rev1]
3430 if rev2:
3431 revs.extend([comp, rev2])
3432 cmd = ["log", "".join(revs)]
3433 out = DiffColoring(self.config)
3434 if out.is_on and color:
3435 cmd.append("--color")
3436 if pretty_format is not None:
3437 cmd.append("--pretty=format:%s" % pretty_format)
3438 if oneline:
3439 cmd.append("--oneline")
3440
3441 try:
3442 log = GitCommand(
3443 self, cmd, capture_stdout=True, capture_stderr=True
3444 )
3445 if log.Wait() == 0:
3446 return log.stdout
3447 except GitError:
3448 # worktree may not exist if groups changed for example. In that
3449 # case, try in gitdir instead.
3450 if not os.path.exists(self.worktree):
3451 return self.bare_git.log(*cmd[1:])
3452 else:
3453 raise
3454 return None
3455
3456 def getAddedAndRemovedLogs(
3457 self, toProject, oneline=False, color=True, pretty_format=None
3458 ):
3459 """Get the list of logs from this revision to given revisionId"""
3460 logs = {}
3461 selfId = self.GetRevisionId(self._allrefs)
3462 toId = toProject.GetRevisionId(toProject._allrefs)
3463
3464 logs["added"] = self._getLogs(
3465 selfId,
3466 toId,
3467 oneline=oneline,
3468 color=color,
3469 pretty_format=pretty_format,
3470 )
3471 logs["removed"] = self._getLogs(
3472 toId,
3473 selfId,
3474 oneline=oneline,
3475 color=color,
3476 pretty_format=pretty_format,
3477 )
3478 return logs
3479
3480 class _GitGetByExec(object):
3481 def __init__(self, project, bare, gitdir):
3482 self._project = project
3483 self._bare = bare
3484 self._gitdir = gitdir
3485
3486 # __getstate__ and __setstate__ are required for pickling because
3487 # __getattr__ exists.
3488 def __getstate__(self):
3489 return (self._project, self._bare, self._gitdir)
3490
3491 def __setstate__(self, state):
3492 self._project, self._bare, self._gitdir = state
3493
3494 def LsOthers(self):
3495 p = GitCommand(
3496 self._project,
3497 ["ls-files", "-z", "--others", "--exclude-standard"],
3498 bare=False,
3499 gitdir=self._gitdir,
3500 capture_stdout=True,
3501 capture_stderr=True,
3502 )
3503 if p.Wait() == 0:
3504 out = p.stdout
3505 if out:
3506 # Backslash is not anomalous.
3507 return out[:-1].split("\0")
3508 return []
3509
3510 def DiffZ(self, name, *args):
3511 cmd = [name]
3512 cmd.append("-z")
3513 cmd.append("--ignore-submodules")
3514 cmd.extend(args)
3515 p = GitCommand(
3516 self._project,
3517 cmd,
3518 gitdir=self._gitdir,
3519 bare=False,
3520 capture_stdout=True,
3521 capture_stderr=True,
3522 )
3523 p.Wait()
3524 r = {}
3525 out = p.stdout
3526 if out:
3527 out = iter(out[:-1].split("\0"))
3528 while out:
3529 try:
3530 info = next(out)
3531 path = next(out)
3532 except StopIteration:
3533 break
3534
3535 class _Info(object):
3536 def __init__(self, path, omode, nmode, oid, nid, state):
3537 self.path = path
3538 self.src_path = None
3539 self.old_mode = omode
3540 self.new_mode = nmode
3541 self.old_id = oid
3542 self.new_id = nid
3543
3544 if len(state) == 1:
3545 self.status = state
3546 self.level = None
3547 else:
3548 self.status = state[:1]
3549 self.level = state[1:]
3550 while self.level.startswith("0"):
3551 self.level = self.level[1:]
3552
3553 info = info[1:].split(" ")
3554 info = _Info(path, *info)
3555 if info.status in ("R", "C"):
3556 info.src_path = info.path
3557 info.path = next(out)
3558 r[info.path] = info
3559 return r
3560
3561 def GetDotgitPath(self, subpath=None):
3562 """Return the full path to the .git dir.
3563
3564 As a convenience, append |subpath| if provided.
3565 """
3566 if self._bare:
3567 dotgit = self._gitdir
3568 else:
3569 dotgit = os.path.join(self._project.worktree, ".git")
3570 if os.path.isfile(dotgit):
3571 # Git worktrees use a "gitdir:" syntax to point to the
3572 # scratch space.
3573 with open(dotgit) as fp:
3574 setting = fp.read()
3575 assert setting.startswith("gitdir:")
3576 gitdir = setting.split(":", 1)[1].strip()
3577 dotgit = os.path.normpath(
3578 os.path.join(self._project.worktree, gitdir)
3579 )
3580
3581 return dotgit if subpath is None else os.path.join(dotgit, subpath)
3582
3583 def GetHead(self):
3584 """Return the ref that HEAD points to."""
3585 path = self.GetDotgitPath(subpath=HEAD)
3586 try:
3587 with open(path) as fd:
3588 line = fd.readline()
3589 except IOError as e:
3590 raise NoManifestException(path, str(e))
3591 try:
3592 line = line.decode()
3593 except AttributeError:
3594 pass
3595 if line.startswith("ref: "):
3596 return line[5:-1]
3597 return line[:-1]
3598
3599 def SetHead(self, ref, message=None):
3600 cmdv = []
3601 if message is not None:
3602 cmdv.extend(["-m", message])
3603 cmdv.append(HEAD)
3604 cmdv.append(ref)
3605 self.symbolic_ref(*cmdv)
3606
3607 def DetachHead(self, new, message=None):
3608 cmdv = ["--no-deref"]
3609 if message is not None:
3610 cmdv.extend(["-m", message])
3611 cmdv.append(HEAD)
3612 cmdv.append(new)
3613 self.update_ref(*cmdv)
3614
3615 def UpdateRef(self, name, new, old=None, message=None, detach=False):
3616 cmdv = []
3617 if message is not None:
3618 cmdv.extend(["-m", message])
3619 if detach:
3620 cmdv.append("--no-deref")
3621 cmdv.append(name)
3622 cmdv.append(new)
3623 if old is not None:
3624 cmdv.append(old)
3625 self.update_ref(*cmdv)
3626
3627 def DeleteRef(self, name, old=None):
3628 if not old:
3629 old = self.rev_parse(name)
3630 self.update_ref("-d", name, old)
3631 self._project.bare_ref.deleted(name)
3632
3633 def rev_list(self, *args, **kw):
3634 if "format" in kw:
3635 cmdv = ["log", "--pretty=format:%s" % kw["format"]]
3636 else:
3637 cmdv = ["rev-list"]
3638 cmdv.extend(args)
3639 p = GitCommand(
3640 self._project,
3641 cmdv,
3642 bare=self._bare,
3643 gitdir=self._gitdir,
3644 capture_stdout=True,
3645 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003646 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00003647 )
Jason Chang32b59562023-07-14 16:45:35 -07003648 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003649 return p.stdout.splitlines()
3650
3651 def __getattr__(self, name):
3652 """Allow arbitrary git commands using pythonic syntax.
3653
3654 This allows you to do things like:
3655 git_obj.rev_parse('HEAD')
3656
3657 Since we don't have a 'rev_parse' method defined, the __getattr__
3658 will run. We'll replace the '_' with a '-' and try to run a git
3659 command. Any other positional arguments will be passed to the git
3660 command, and the following keyword arguments are supported:
3661 config: An optional dict of git config options to be passed with
3662 '-c'.
3663
3664 Args:
3665 name: The name of the git command to call. Any '_' characters
3666 will be replaced with '-'.
3667
3668 Returns:
3669 A callable object that will try to call git with the named
3670 command.
3671 """
3672 name = name.replace("_", "-")
3673
3674 def runner(*args, **kwargs):
3675 cmdv = []
3676 config = kwargs.pop("config", None)
3677 for k in kwargs:
3678 raise TypeError(
3679 "%s() got an unexpected keyword argument %r" % (name, k)
3680 )
3681 if config is not None:
3682 for k, v in config.items():
3683 cmdv.append("-c")
3684 cmdv.append("%s=%s" % (k, v))
3685 cmdv.append(name)
3686 cmdv.extend(args)
3687 p = GitCommand(
3688 self._project,
3689 cmdv,
3690 bare=self._bare,
3691 gitdir=self._gitdir,
3692 capture_stdout=True,
3693 capture_stderr=True,
Jason Chang32b59562023-07-14 16:45:35 -07003694 verify_command=True,
Gavin Makea2e3302023-03-11 06:46:20 +00003695 )
Jason Chang32b59562023-07-14 16:45:35 -07003696 p.Wait()
Gavin Makea2e3302023-03-11 06:46:20 +00003697 r = p.stdout
3698 if r.endswith("\n") and r.index("\n") == len(r) - 1:
3699 return r[:-1]
3700 return r
3701
3702 return runner
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003703
3704
Jason Chang32b59562023-07-14 16:45:35 -07003705class LocalSyncFail(RepoError):
3706 """Default error when there is an Sync_LocalHalf error."""
3707
3708
3709class _PriorSyncFailedError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003710 def __str__(self):
3711 return "prior sync failed; rebase still in progress"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003712
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003713
Jason Chang32b59562023-07-14 16:45:35 -07003714class _DirtyError(LocalSyncFail):
Gavin Makea2e3302023-03-11 06:46:20 +00003715 def __str__(self):
3716 return "contains uncommitted changes"
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003717
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003718
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003719class _InfoMessage(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003720 def __init__(self, project, text):
3721 self.project = project
3722 self.text = text
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003723
Gavin Makea2e3302023-03-11 06:46:20 +00003724 def Print(self, syncbuf):
3725 syncbuf.out.info(
3726 "%s/: %s", self.project.RelPath(local=False), self.text
3727 )
3728 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003729
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003730
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003731class _Failure(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003732 def __init__(self, project, why):
3733 self.project = project
3734 self.why = why
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003735
Gavin Makea2e3302023-03-11 06:46:20 +00003736 def Print(self, syncbuf):
3737 syncbuf.out.fail(
3738 "error: %s/: %s", self.project.RelPath(local=False), str(self.why)
3739 )
3740 syncbuf.out.nl()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003741
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003742
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003743class _Later(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003744 def __init__(self, project, action):
3745 self.project = project
3746 self.action = action
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003747
Gavin Makea2e3302023-03-11 06:46:20 +00003748 def Run(self, syncbuf):
3749 out = syncbuf.out
3750 out.project("project %s/", self.project.RelPath(local=False))
3751 out.nl()
3752 try:
3753 self.action()
3754 out.nl()
3755 return True
3756 except GitError:
3757 out.nl()
3758 return False
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003759
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003760
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003761class _SyncColoring(Coloring):
Gavin Makea2e3302023-03-11 06:46:20 +00003762 def __init__(self, config):
3763 super().__init__(config, "reposync")
3764 self.project = self.printer("header", attr="bold")
3765 self.info = self.printer("info")
3766 self.fail = self.printer("fail", fg="red")
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003767
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003768
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003769class SyncBuffer(object):
Gavin Makea2e3302023-03-11 06:46:20 +00003770 def __init__(self, config, detach_head=False):
3771 self._messages = []
3772 self._failures = []
3773 self._later_queue1 = []
3774 self._later_queue2 = []
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003775
Gavin Makea2e3302023-03-11 06:46:20 +00003776 self.out = _SyncColoring(config)
3777 self.out.redirect(sys.stderr)
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003778
Gavin Makea2e3302023-03-11 06:46:20 +00003779 self.detach_head = detach_head
3780 self.clean = True
3781 self.recent_clean = True
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003782
Gavin Makea2e3302023-03-11 06:46:20 +00003783 def info(self, project, fmt, *args):
3784 self._messages.append(_InfoMessage(project, fmt % args))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003785
Gavin Makea2e3302023-03-11 06:46:20 +00003786 def fail(self, project, err=None):
3787 self._failures.append(_Failure(project, err))
David Rileye0684ad2017-04-05 00:02:59 -07003788 self._MarkUnclean()
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003789
Gavin Makea2e3302023-03-11 06:46:20 +00003790 def later1(self, project, what):
3791 self._later_queue1.append(_Later(project, what))
Mike Frysinger70d861f2019-08-26 15:22:36 -04003792
Gavin Makea2e3302023-03-11 06:46:20 +00003793 def later2(self, project, what):
3794 self._later_queue2.append(_Later(project, what))
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003795
Gavin Makea2e3302023-03-11 06:46:20 +00003796 def Finish(self):
3797 self._PrintMessages()
3798 self._RunLater()
3799 self._PrintMessages()
3800 return self.clean
3801
3802 def Recently(self):
3803 recent_clean = self.recent_clean
3804 self.recent_clean = True
3805 return recent_clean
3806
3807 def _MarkUnclean(self):
3808 self.clean = False
3809 self.recent_clean = False
3810
3811 def _RunLater(self):
3812 for q in ["_later_queue1", "_later_queue2"]:
3813 if not self._RunQueue(q):
3814 return
3815
3816 def _RunQueue(self, queue):
3817 for m in getattr(self, queue):
3818 if not m.Run(self):
3819 self._MarkUnclean()
3820 return False
3821 setattr(self, queue, [])
3822 return True
3823
3824 def _PrintMessages(self):
3825 if self._messages or self._failures:
3826 if os.isatty(2):
3827 self.out.write(progress.CSI_ERASE_LINE)
3828 self.out.write("\r")
3829
3830 for m in self._messages:
3831 m.Print(self)
3832 for m in self._failures:
3833 m.Print(self)
3834
3835 self._messages = []
3836 self._failures = []
Shawn O. Pearce350cde42009-04-16 11:21:18 -07003837
3838
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003839class MetaProject(Project):
Gavin Makea2e3302023-03-11 06:46:20 +00003840 """A special project housed under .repo."""
Mark E. Hamilton30b0f4e2016-02-10 10:44:30 -07003841
Gavin Makea2e3302023-03-11 06:46:20 +00003842 def __init__(self, manifest, name, gitdir, worktree):
3843 Project.__init__(
3844 self,
3845 manifest=manifest,
3846 name=name,
3847 gitdir=gitdir,
3848 objdir=gitdir,
3849 worktree=worktree,
3850 remote=RemoteSpec("origin"),
3851 relpath=".repo/%s" % name,
3852 revisionExpr="refs/heads/master",
3853 revisionId=None,
3854 groups=None,
3855 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003856
Gavin Makea2e3302023-03-11 06:46:20 +00003857 def PreSync(self):
3858 if self.Exists:
3859 cb = self.CurrentBranch
3860 if cb:
3861 base = self.GetBranch(cb).merge
3862 if base:
3863 self.revisionExpr = base
3864 self.revisionId = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07003865
Gavin Makea2e3302023-03-11 06:46:20 +00003866 @property
3867 def HasChanges(self):
3868 """Has the remote received new commits not yet checked out?"""
3869 if not self.remote or not self.revisionExpr:
3870 return False
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003871
Gavin Makea2e3302023-03-11 06:46:20 +00003872 all_refs = self.bare_ref.all
3873 revid = self.GetRevisionId(all_refs)
3874 head = self.work_git.GetHead()
3875 if head.startswith(R_HEADS):
3876 try:
3877 head = all_refs[head]
3878 except KeyError:
3879 head = None
Shawn O. Pearce336f7bd2009-04-18 10:39:28 -07003880
Gavin Makea2e3302023-03-11 06:46:20 +00003881 if revid == head:
3882 return False
3883 elif self._revlist(not_rev(HEAD), revid):
3884 return True
3885 return False
LaMont Jones9b72cf22022-03-29 21:54:22 +00003886
3887
3888class RepoProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003889 """The MetaProject for repo itself."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003890
Gavin Makea2e3302023-03-11 06:46:20 +00003891 @property
3892 def LastFetch(self):
3893 try:
3894 fh = os.path.join(self.gitdir, "FETCH_HEAD")
3895 return os.path.getmtime(fh)
3896 except OSError:
3897 return 0
LaMont Jones9b72cf22022-03-29 21:54:22 +00003898
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +01003899
LaMont Jones9b72cf22022-03-29 21:54:22 +00003900class ManifestProject(MetaProject):
Gavin Makea2e3302023-03-11 06:46:20 +00003901 """The MetaProject for manifests."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003902
Gavin Makea2e3302023-03-11 06:46:20 +00003903 def MetaBranchSwitch(self, submodules=False):
3904 """Prepare for manifest branch switch."""
LaMont Jones9b72cf22022-03-29 21:54:22 +00003905
Gavin Makea2e3302023-03-11 06:46:20 +00003906 # detach and delete manifest branch, allowing a new
3907 # branch to take over
3908 syncbuf = SyncBuffer(self.config, detach_head=True)
3909 self.Sync_LocalHalf(syncbuf, submodules=submodules)
3910 syncbuf.Finish()
LaMont Jones9b72cf22022-03-29 21:54:22 +00003911
Gavin Makea2e3302023-03-11 06:46:20 +00003912 return (
3913 GitCommand(
3914 self,
3915 ["update-ref", "-d", "refs/heads/default"],
3916 capture_stdout=True,
3917 capture_stderr=True,
3918 ).Wait()
3919 == 0
3920 )
LaMont Jones9b03f152022-03-29 23:01:18 +00003921
Gavin Makea2e3302023-03-11 06:46:20 +00003922 @property
3923 def standalone_manifest_url(self):
3924 """The URL of the standalone manifest, or None."""
3925 return self.config.GetString("manifest.standalone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003926
Gavin Makea2e3302023-03-11 06:46:20 +00003927 @property
3928 def manifest_groups(self):
3929 """The manifest groups string."""
3930 return self.config.GetString("manifest.groups")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003931
Gavin Makea2e3302023-03-11 06:46:20 +00003932 @property
3933 def reference(self):
3934 """The --reference for this manifest."""
3935 return self.config.GetString("repo.reference")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003936
Gavin Makea2e3302023-03-11 06:46:20 +00003937 @property
3938 def dissociate(self):
3939 """Whether to dissociate."""
3940 return self.config.GetBoolean("repo.dissociate")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003941
Gavin Makea2e3302023-03-11 06:46:20 +00003942 @property
3943 def archive(self):
3944 """Whether we use archive."""
3945 return self.config.GetBoolean("repo.archive")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003946
Gavin Makea2e3302023-03-11 06:46:20 +00003947 @property
3948 def mirror(self):
3949 """Whether we use mirror."""
3950 return self.config.GetBoolean("repo.mirror")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003951
Gavin Makea2e3302023-03-11 06:46:20 +00003952 @property
3953 def use_worktree(self):
3954 """Whether we use worktree."""
3955 return self.config.GetBoolean("repo.worktree")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003956
Gavin Makea2e3302023-03-11 06:46:20 +00003957 @property
3958 def clone_bundle(self):
3959 """Whether we use clone_bundle."""
3960 return self.config.GetBoolean("repo.clonebundle")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003961
Gavin Makea2e3302023-03-11 06:46:20 +00003962 @property
3963 def submodules(self):
3964 """Whether we use submodules."""
3965 return self.config.GetBoolean("repo.submodules")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003966
Gavin Makea2e3302023-03-11 06:46:20 +00003967 @property
3968 def git_lfs(self):
3969 """Whether we use git_lfs."""
3970 return self.config.GetBoolean("repo.git-lfs")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003971
Gavin Makea2e3302023-03-11 06:46:20 +00003972 @property
3973 def use_superproject(self):
3974 """Whether we use superproject."""
3975 return self.config.GetBoolean("repo.superproject")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003976
Gavin Makea2e3302023-03-11 06:46:20 +00003977 @property
3978 def partial_clone(self):
3979 """Whether this is a partial clone."""
3980 return self.config.GetBoolean("repo.partialclone")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003981
Gavin Makea2e3302023-03-11 06:46:20 +00003982 @property
3983 def depth(self):
3984 """Partial clone depth."""
3985 return self.config.GetString("repo.depth")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003986
Gavin Makea2e3302023-03-11 06:46:20 +00003987 @property
3988 def clone_filter(self):
3989 """The clone filter."""
3990 return self.config.GetString("repo.clonefilter")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00003991
Gavin Makea2e3302023-03-11 06:46:20 +00003992 @property
3993 def partial_clone_exclude(self):
3994 """Partial clone exclude string"""
3995 return self.config.GetString("repo.partialcloneexclude")
LaMont Jones4ada0432022-04-14 15:10:43 +00003996
Gavin Makea2e3302023-03-11 06:46:20 +00003997 @property
Jason Chang17833322023-05-23 13:06:55 -07003998 def clone_filter_for_depth(self):
3999 """Replace shallow clone with partial clone."""
4000 return self.config.GetString("repo.clonefilterfordepth")
4001
4002 @property
Gavin Makea2e3302023-03-11 06:46:20 +00004003 def manifest_platform(self):
4004 """The --platform argument from `repo init`."""
4005 return self.config.GetString("manifest.platform")
LaMont Jonesd82be3e2022-04-05 19:30:46 +00004006
Gavin Makea2e3302023-03-11 06:46:20 +00004007 @property
4008 def _platform_name(self):
4009 """Return the name of the platform."""
4010 return platform.system().lower()
LaMont Jones9b03f152022-03-29 23:01:18 +00004011
Gavin Makea2e3302023-03-11 06:46:20 +00004012 def SyncWithPossibleInit(
4013 self,
4014 submanifest,
4015 verbose=False,
4016 current_branch_only=False,
4017 tags="",
4018 git_event_log=None,
4019 ):
4020 """Sync a manifestProject, possibly for the first time.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004021
Gavin Makea2e3302023-03-11 06:46:20 +00004022 Call Sync() with arguments from the most recent `repo init`. If this is
4023 a new sub manifest, then inherit options from the parent's
4024 manifestProject.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004025
Gavin Makea2e3302023-03-11 06:46:20 +00004026 This is used by subcmds.Sync() to do an initial download of new sub
4027 manifests.
LaMont Jonesbdcba7d2022-04-11 22:50:11 +00004028
Gavin Makea2e3302023-03-11 06:46:20 +00004029 Args:
4030 submanifest: an XmlSubmanifest, the submanifest to re-sync.
4031 verbose: a boolean, whether to show all output, rather than only
4032 errors.
4033 current_branch_only: a boolean, whether to only fetch the current
4034 manifest branch from the server.
4035 tags: a boolean, whether to fetch tags.
4036 git_event_log: an EventLog, for git tracing.
4037 """
4038 # TODO(lamontjones): when refactoring sync (and init?) consider how to
4039 # better get the init options that we should use for new submanifests
4040 # that are added when syncing an existing workspace.
4041 git_event_log = git_event_log or EventLog()
LaMont Jonesb90a4222022-04-14 15:00:09 +00004042 spec = submanifest.ToSubmanifestSpec()
Gavin Makea2e3302023-03-11 06:46:20 +00004043 # Use the init options from the existing manifestProject, or the parent
4044 # if it doesn't exist.
4045 #
4046 # Today, we only support changing manifest_groups on the sub-manifest,
4047 # with no supported-for-the-user way to change the other arguments from
4048 # those specified by the outermost manifest.
4049 #
4050 # TODO(lamontjones): determine which of these should come from the
4051 # outermost manifest and which should come from the parent manifest.
4052 mp = self if self.Exists else submanifest.parent.manifestProject
4053 return self.Sync(
LaMont Jones55ee3042022-04-06 17:10:21 +00004054 manifest_url=spec.manifestUrl,
4055 manifest_branch=spec.revision,
Gavin Makea2e3302023-03-11 06:46:20 +00004056 standalone_manifest=mp.standalone_manifest_url,
4057 groups=mp.manifest_groups,
4058 platform=mp.manifest_platform,
4059 mirror=mp.mirror,
4060 dissociate=mp.dissociate,
4061 reference=mp.reference,
4062 worktree=mp.use_worktree,
4063 submodules=mp.submodules,
4064 archive=mp.archive,
4065 partial_clone=mp.partial_clone,
4066 clone_filter=mp.clone_filter,
4067 partial_clone_exclude=mp.partial_clone_exclude,
4068 clone_bundle=mp.clone_bundle,
4069 git_lfs=mp.git_lfs,
4070 use_superproject=mp.use_superproject,
LaMont Jones55ee3042022-04-06 17:10:21 +00004071 verbose=verbose,
4072 current_branch_only=current_branch_only,
4073 tags=tags,
Gavin Makea2e3302023-03-11 06:46:20 +00004074 depth=mp.depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004075 git_event_log=git_event_log,
4076 manifest_name=spec.manifestName,
Gavin Makea2e3302023-03-11 06:46:20 +00004077 this_manifest_only=True,
LaMont Jones55ee3042022-04-06 17:10:21 +00004078 outer_manifest=False,
Jason Chang17833322023-05-23 13:06:55 -07004079 clone_filter_for_depth=mp.clone_filter_for_depth,
LaMont Jones55ee3042022-04-06 17:10:21 +00004080 )
LaMont Jones409407a2022-04-05 21:21:56 +00004081
Gavin Makea2e3302023-03-11 06:46:20 +00004082 def Sync(
4083 self,
4084 _kwargs_only=(),
4085 manifest_url="",
4086 manifest_branch=None,
4087 standalone_manifest=False,
4088 groups="",
4089 mirror=False,
4090 reference="",
4091 dissociate=False,
4092 worktree=False,
4093 submodules=False,
4094 archive=False,
4095 partial_clone=None,
4096 depth=None,
4097 clone_filter="blob:none",
4098 partial_clone_exclude=None,
4099 clone_bundle=None,
4100 git_lfs=None,
4101 use_superproject=None,
4102 verbose=False,
4103 current_branch_only=False,
4104 git_event_log=None,
4105 platform="",
4106 manifest_name="default.xml",
4107 tags="",
4108 this_manifest_only=False,
4109 outer_manifest=True,
Jason Chang17833322023-05-23 13:06:55 -07004110 clone_filter_for_depth=None,
Gavin Makea2e3302023-03-11 06:46:20 +00004111 ):
4112 """Sync the manifest and all submanifests.
LaMont Jones409407a2022-04-05 21:21:56 +00004113
Gavin Makea2e3302023-03-11 06:46:20 +00004114 Args:
4115 manifest_url: a string, the URL of the manifest project.
4116 manifest_branch: a string, the manifest branch to use.
4117 standalone_manifest: a boolean, whether to store the manifest as a
4118 static file.
4119 groups: a string, restricts the checkout to projects with the
4120 specified groups.
4121 mirror: a boolean, whether to create a mirror of the remote
4122 repository.
4123 reference: a string, location of a repo instance to use as a
4124 reference.
4125 dissociate: a boolean, whether to dissociate from reference mirrors
4126 after clone.
4127 worktree: a boolean, whether to use git-worktree to manage projects.
4128 submodules: a boolean, whether sync submodules associated with the
4129 manifest project.
4130 archive: a boolean, whether to checkout each project as an archive.
4131 See git-archive.
4132 partial_clone: a boolean, whether to perform a partial clone.
4133 depth: an int, how deep of a shallow clone to create.
4134 clone_filter: a string, filter to use with partial_clone.
4135 partial_clone_exclude : a string, comma-delimeted list of project
4136 names to exclude from partial clone.
4137 clone_bundle: a boolean, whether to enable /clone.bundle on
4138 HTTP/HTTPS.
4139 git_lfs: a boolean, whether to enable git LFS support.
4140 use_superproject: a boolean, whether to use the manifest
4141 superproject to sync projects.
4142 verbose: a boolean, whether to show all output, rather than only
4143 errors.
4144 current_branch_only: a boolean, whether to only fetch the current
4145 manifest branch from the server.
4146 platform: a string, restrict the checkout to projects with the
4147 specified platform group.
4148 git_event_log: an EventLog, for git tracing.
4149 tags: a boolean, whether to fetch tags.
4150 manifest_name: a string, the name of the manifest file to use.
4151 this_manifest_only: a boolean, whether to only operate on the
4152 current sub manifest.
4153 outer_manifest: a boolean, whether to start at the outermost
4154 manifest.
Jason Chang17833322023-05-23 13:06:55 -07004155 clone_filter_for_depth: a string, when specified replaces shallow
4156 clones with partial.
LaMont Jones9b03f152022-03-29 23:01:18 +00004157
Gavin Makea2e3302023-03-11 06:46:20 +00004158 Returns:
4159 a boolean, whether the sync was successful.
4160 """
4161 assert _kwargs_only == (), "Sync only accepts keyword arguments."
LaMont Jones9b03f152022-03-29 23:01:18 +00004162
Gavin Makea2e3302023-03-11 06:46:20 +00004163 groups = groups or self.manifest.GetDefaultGroupsStr(
4164 with_platform=False
4165 )
4166 platform = platform or "auto"
4167 git_event_log = git_event_log or EventLog()
4168 if outer_manifest and self.manifest.is_submanifest:
4169 # In a multi-manifest checkout, use the outer manifest unless we are
4170 # told not to.
4171 return self.client.outer_manifest.manifestProject.Sync(
4172 manifest_url=manifest_url,
4173 manifest_branch=manifest_branch,
4174 standalone_manifest=standalone_manifest,
4175 groups=groups,
4176 platform=platform,
4177 mirror=mirror,
4178 dissociate=dissociate,
4179 reference=reference,
4180 worktree=worktree,
4181 submodules=submodules,
4182 archive=archive,
4183 partial_clone=partial_clone,
4184 clone_filter=clone_filter,
4185 partial_clone_exclude=partial_clone_exclude,
4186 clone_bundle=clone_bundle,
4187 git_lfs=git_lfs,
4188 use_superproject=use_superproject,
4189 verbose=verbose,
4190 current_branch_only=current_branch_only,
4191 tags=tags,
4192 depth=depth,
4193 git_event_log=git_event_log,
4194 manifest_name=manifest_name,
4195 this_manifest_only=this_manifest_only,
4196 outer_manifest=False,
4197 )
LaMont Jones9b03f152022-03-29 23:01:18 +00004198
Gavin Makea2e3302023-03-11 06:46:20 +00004199 # If repo has already been initialized, we take -u with the absence of
4200 # --standalone-manifest to mean "transition to a standard repo set up",
4201 # which necessitates starting fresh.
4202 # If --standalone-manifest is set, we always tear everything down and
4203 # start anew.
4204 if self.Exists:
4205 was_standalone_manifest = self.config.GetString(
4206 "manifest.standalone"
4207 )
4208 if was_standalone_manifest and not manifest_url:
4209 print(
4210 "fatal: repo was initialized with a standlone manifest, "
4211 "cannot be re-initialized without --manifest-url/-u"
4212 )
4213 return False
4214
4215 if standalone_manifest or (
4216 was_standalone_manifest and manifest_url
4217 ):
4218 self.config.ClearCache()
4219 if self.gitdir and os.path.exists(self.gitdir):
4220 platform_utils.rmtree(self.gitdir)
4221 if self.worktree and os.path.exists(self.worktree):
4222 platform_utils.rmtree(self.worktree)
4223
4224 is_new = not self.Exists
4225 if is_new:
4226 if not manifest_url:
4227 print("fatal: manifest url is required.", file=sys.stderr)
4228 return False
4229
4230 if verbose:
4231 print(
4232 "Downloading manifest from %s"
4233 % (GitConfig.ForUser().UrlInsteadOf(manifest_url),),
4234 file=sys.stderr,
4235 )
4236
4237 # The manifest project object doesn't keep track of the path on the
4238 # server where this git is located, so let's save that here.
4239 mirrored_manifest_git = None
4240 if reference:
4241 manifest_git_path = urllib.parse.urlparse(manifest_url).path[1:]
4242 mirrored_manifest_git = os.path.join(
4243 reference, manifest_git_path
4244 )
4245 if not mirrored_manifest_git.endswith(".git"):
4246 mirrored_manifest_git += ".git"
4247 if not os.path.exists(mirrored_manifest_git):
4248 mirrored_manifest_git = os.path.join(
4249 reference, ".repo/manifests.git"
4250 )
4251
4252 self._InitGitDir(mirror_git=mirrored_manifest_git)
4253
4254 # If standalone_manifest is set, mark the project as "standalone" --
4255 # we'll still do much of the manifests.git set up, but will avoid actual
4256 # syncs to a remote.
4257 if standalone_manifest:
4258 self.config.SetString("manifest.standalone", manifest_url)
4259 elif not manifest_url and not manifest_branch:
4260 # If -u is set and --standalone-manifest is not, then we're not in
4261 # standalone mode. Otherwise, use config to infer what we were in
4262 # the last init.
4263 standalone_manifest = bool(
4264 self.config.GetString("manifest.standalone")
4265 )
4266 if not standalone_manifest:
4267 self.config.SetString("manifest.standalone", None)
4268
4269 self._ConfigureDepth(depth)
4270
4271 # Set the remote URL before the remote branch as we might need it below.
4272 if manifest_url:
4273 r = self.GetRemote()
4274 r.url = manifest_url
4275 r.ResetFetch()
4276 r.Save()
4277
4278 if not standalone_manifest:
4279 if manifest_branch:
4280 if manifest_branch == "HEAD":
4281 manifest_branch = self.ResolveRemoteHead()
4282 if manifest_branch is None:
4283 print("fatal: unable to resolve HEAD", file=sys.stderr)
4284 return False
4285 self.revisionExpr = manifest_branch
4286 else:
4287 if is_new:
4288 default_branch = self.ResolveRemoteHead()
4289 if default_branch is None:
4290 # If the remote doesn't have HEAD configured, default to
4291 # master.
4292 default_branch = "refs/heads/master"
4293 self.revisionExpr = default_branch
4294 else:
4295 self.PreSync()
4296
4297 groups = re.split(r"[,\s]+", groups or "")
4298 all_platforms = ["linux", "darwin", "windows"]
4299 platformize = lambda x: "platform-" + x
4300 if platform == "auto":
4301 if not mirror and not self.mirror:
4302 groups.append(platformize(self._platform_name))
4303 elif platform == "all":
4304 groups.extend(map(platformize, all_platforms))
4305 elif platform in all_platforms:
4306 groups.append(platformize(platform))
4307 elif platform != "none":
4308 print("fatal: invalid platform flag", file=sys.stderr)
4309 return False
4310 self.config.SetString("manifest.platform", platform)
4311
4312 groups = [x for x in groups if x]
4313 groupstr = ",".join(groups)
4314 if (
4315 platform == "auto"
4316 and groupstr == self.manifest.GetDefaultGroupsStr()
4317 ):
4318 groupstr = None
4319 self.config.SetString("manifest.groups", groupstr)
4320
4321 if reference:
4322 self.config.SetString("repo.reference", reference)
4323
4324 if dissociate:
4325 self.config.SetBoolean("repo.dissociate", dissociate)
4326
4327 if worktree:
4328 if mirror:
4329 print(
4330 "fatal: --mirror and --worktree are incompatible",
4331 file=sys.stderr,
4332 )
4333 return False
4334 if submodules:
4335 print(
4336 "fatal: --submodules and --worktree are incompatible",
4337 file=sys.stderr,
4338 )
4339 return False
4340 self.config.SetBoolean("repo.worktree", worktree)
4341 if is_new:
4342 self.use_git_worktrees = True
4343 print("warning: --worktree is experimental!", file=sys.stderr)
4344
4345 if archive:
4346 if is_new:
4347 self.config.SetBoolean("repo.archive", archive)
4348 else:
4349 print(
4350 "fatal: --archive is only supported when initializing a "
4351 "new workspace.",
4352 file=sys.stderr,
4353 )
4354 print(
4355 "Either delete the .repo folder in this workspace, or "
4356 "initialize in another location.",
4357 file=sys.stderr,
4358 )
4359 return False
4360
4361 if mirror:
4362 if is_new:
4363 self.config.SetBoolean("repo.mirror", mirror)
4364 else:
4365 print(
4366 "fatal: --mirror is only supported when initializing a new "
4367 "workspace.",
4368 file=sys.stderr,
4369 )
4370 print(
4371 "Either delete the .repo folder in this workspace, or "
4372 "initialize in another location.",
4373 file=sys.stderr,
4374 )
4375 return False
4376
4377 if partial_clone is not None:
4378 if mirror:
4379 print(
4380 "fatal: --mirror and --partial-clone are mutually "
4381 "exclusive",
4382 file=sys.stderr,
4383 )
4384 return False
4385 self.config.SetBoolean("repo.partialclone", partial_clone)
4386 if clone_filter:
4387 self.config.SetString("repo.clonefilter", clone_filter)
4388 elif self.partial_clone:
4389 clone_filter = self.clone_filter
4390 else:
4391 clone_filter = None
4392
4393 if partial_clone_exclude is not None:
4394 self.config.SetString(
4395 "repo.partialcloneexclude", partial_clone_exclude
4396 )
4397
4398 if clone_bundle is None:
4399 clone_bundle = False if partial_clone else True
4400 else:
4401 self.config.SetBoolean("repo.clonebundle", clone_bundle)
4402
4403 if submodules:
4404 self.config.SetBoolean("repo.submodules", submodules)
4405
4406 if git_lfs is not None:
4407 if git_lfs:
4408 git_require((2, 17, 0), fail=True, msg="Git LFS support")
4409
4410 self.config.SetBoolean("repo.git-lfs", git_lfs)
4411 if not is_new:
4412 print(
4413 "warning: Changing --git-lfs settings will only affect new "
4414 "project checkouts.\n"
4415 " Existing projects will require manual updates.\n",
4416 file=sys.stderr,
4417 )
4418
Jason Chang17833322023-05-23 13:06:55 -07004419 if clone_filter_for_depth is not None:
4420 self.ConfigureCloneFilterForDepth(clone_filter_for_depth)
4421
Gavin Makea2e3302023-03-11 06:46:20 +00004422 if use_superproject is not None:
4423 self.config.SetBoolean("repo.superproject", use_superproject)
4424
4425 if not standalone_manifest:
4426 success = self.Sync_NetworkHalf(
4427 is_new=is_new,
4428 quiet=not verbose,
4429 verbose=verbose,
4430 clone_bundle=clone_bundle,
4431 current_branch_only=current_branch_only,
4432 tags=tags,
4433 submodules=submodules,
4434 clone_filter=clone_filter,
4435 partial_clone_exclude=self.manifest.PartialCloneExclude,
Jason Chang17833322023-05-23 13:06:55 -07004436 clone_filter_for_depth=self.manifest.CloneFilterForDepth,
Gavin Makea2e3302023-03-11 06:46:20 +00004437 ).success
4438 if not success:
4439 r = self.GetRemote()
4440 print(
4441 "fatal: cannot obtain manifest %s" % r.url, file=sys.stderr
4442 )
4443
4444 # Better delete the manifest git dir if we created it; otherwise
4445 # next time (when user fixes problems) we won't go through the
4446 # "is_new" logic.
4447 if is_new:
4448 platform_utils.rmtree(self.gitdir)
4449 return False
4450
4451 if manifest_branch:
4452 self.MetaBranchSwitch(submodules=submodules)
4453
4454 syncbuf = SyncBuffer(self.config)
4455 self.Sync_LocalHalf(syncbuf, submodules=submodules)
4456 syncbuf.Finish()
4457
4458 if is_new or self.CurrentBranch is None:
Jason Chang1a3612f2023-08-08 14:12:53 -07004459 try:
4460 self.StartBranch("default")
4461 except GitError as e:
4462 msg = str(e)
Gavin Makea2e3302023-03-11 06:46:20 +00004463 print(
Jason Chang1a3612f2023-08-08 14:12:53 -07004464 f"fatal: cannot create default in manifest {msg}",
Gavin Makea2e3302023-03-11 06:46:20 +00004465 file=sys.stderr,
4466 )
4467 return False
4468
4469 if not manifest_name:
4470 print("fatal: manifest name (-m) is required.", file=sys.stderr)
4471 return False
4472
4473 elif is_new:
4474 # This is a new standalone manifest.
4475 manifest_name = "default.xml"
4476 manifest_data = fetch.fetch_file(manifest_url, verbose=verbose)
4477 dest = os.path.join(self.worktree, manifest_name)
4478 os.makedirs(os.path.dirname(dest), exist_ok=True)
4479 with open(dest, "wb") as f:
4480 f.write(manifest_data)
4481
4482 try:
4483 self.manifest.Link(manifest_name)
4484 except ManifestParseError as e:
4485 print(
4486 "fatal: manifest '%s' not available" % manifest_name,
4487 file=sys.stderr,
4488 )
4489 print("fatal: %s" % str(e), file=sys.stderr)
4490 return False
4491
4492 if not this_manifest_only:
4493 for submanifest in self.manifest.submanifests.values():
4494 spec = submanifest.ToSubmanifestSpec()
4495 submanifest.repo_client.manifestProject.Sync(
4496 manifest_url=spec.manifestUrl,
4497 manifest_branch=spec.revision,
4498 standalone_manifest=standalone_manifest,
4499 groups=self.manifest_groups,
4500 platform=platform,
4501 mirror=mirror,
4502 dissociate=dissociate,
4503 reference=reference,
4504 worktree=worktree,
4505 submodules=submodules,
4506 archive=archive,
4507 partial_clone=partial_clone,
4508 clone_filter=clone_filter,
4509 partial_clone_exclude=partial_clone_exclude,
4510 clone_bundle=clone_bundle,
4511 git_lfs=git_lfs,
4512 use_superproject=use_superproject,
4513 verbose=verbose,
4514 current_branch_only=current_branch_only,
4515 tags=tags,
4516 depth=depth,
4517 git_event_log=git_event_log,
4518 manifest_name=spec.manifestName,
4519 this_manifest_only=False,
4520 outer_manifest=False,
4521 )
4522
4523 # Lastly, if the manifest has a <superproject> then have the
4524 # superproject sync it (if it will be used).
4525 if git_superproject.UseSuperproject(use_superproject, self.manifest):
4526 sync_result = self.manifest.superproject.Sync(git_event_log)
4527 if not sync_result.success:
4528 submanifest = ""
4529 if self.manifest.path_prefix:
4530 submanifest = f"for {self.manifest.path_prefix} "
4531 print(
4532 f"warning: git update of superproject {submanifest}failed, "
4533 "repo sync will not use superproject to fetch source; "
4534 "while this error is not fatal, and you can continue to "
4535 "run repo sync, please run repo init with the "
4536 "--no-use-superproject option to stop seeing this warning",
4537 file=sys.stderr,
4538 )
4539 if sync_result.fatal and use_superproject is not None:
4540 return False
4541
4542 return True
4543
Jason Chang17833322023-05-23 13:06:55 -07004544 def ConfigureCloneFilterForDepth(self, clone_filter_for_depth):
4545 """Configure clone filter to replace shallow clones.
4546
4547 Args:
4548 clone_filter_for_depth: a string or None, e.g. 'blob:none' will
4549 disable shallow clones and replace with partial clone. None will
4550 enable shallow clones.
4551 """
4552 self.config.SetString(
4553 "repo.clonefilterfordepth", clone_filter_for_depth
4554 )
4555
Gavin Makea2e3302023-03-11 06:46:20 +00004556 def _ConfigureDepth(self, depth):
4557 """Configure the depth we'll sync down.
4558
4559 Args:
4560 depth: an int, how deep of a partial clone to create.
4561 """
4562 # Opt.depth will be non-None if user actually passed --depth to repo
4563 # init.
4564 if depth is not None:
4565 if depth > 0:
4566 # Positive values will set the depth.
4567 depth = str(depth)
4568 else:
4569 # Negative numbers will clear the depth; passing None to
4570 # SetString will do that.
4571 depth = None
4572
4573 # We store the depth in the main manifest project.
4574 self.config.SetString("repo.depth", depth)