|
|
@@ -1,5 +1,3 @@
|
|
|
-# -*- coding:utf-8 -*-
|
|
|
-#
|
|
|
# Copyright (C) 2008 The Android Open Source Project
|
|
|
#
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
@@ -14,11 +12,9 @@
|
|
|
# See the License for the specific language governing permissions and
|
|
|
# limitations under the License.
|
|
|
|
|
|
-from __future__ import print_function
|
|
|
import errno
|
|
|
import filecmp
|
|
|
import glob
|
|
|
-import json
|
|
|
import os
|
|
|
import random
|
|
|
import re
|
|
|
@@ -29,13 +25,13 @@ import sys
|
|
|
import tarfile
|
|
|
import tempfile
|
|
|
import time
|
|
|
-import traceback
|
|
|
+import urllib.parse
|
|
|
|
|
|
from color import Coloring
|
|
|
from git_command import GitCommand, git_require
|
|
|
from git_config import GitConfig, IsId, GetSchemeFromUrl, GetUrlCookieFile, \
|
|
|
ID_RE
|
|
|
-from error import GitError, HookError, UploadError, DownloadError
|
|
|
+from error import GitError, UploadError, DownloadError
|
|
|
from error import ManifestInvalidRevisionError, ManifestInvalidPathError
|
|
|
from error import NoManifestException
|
|
|
import platform_utils
|
|
|
@@ -44,21 +40,18 @@ from repo_trace import IsTrace, Trace
|
|
|
|
|
|
from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M, R_WORKTREE_M
|
|
|
|
|
|
-from pyversion import is_python3
|
|
|
-if is_python3():
|
|
|
- import urllib.parse
|
|
|
-else:
|
|
|
- import imp
|
|
|
- import urlparse
|
|
|
- urllib = imp.new_module('urllib')
|
|
|
- urllib.parse = urlparse
|
|
|
- input = raw_input # noqa: F821
|
|
|
+
|
|
|
+# Maximum sleep time allowed during retries.
|
|
|
+MAXIMUM_RETRY_SLEEP_SEC = 3600.0
|
|
|
+# +-10% random jitter is added to each Fetches retry sleep duration.
|
|
|
+RETRY_JITTER_PERCENT = 0.1
|
|
|
|
|
|
|
|
|
def _lwrite(path, content):
|
|
|
lock = '%s.lock' % path
|
|
|
|
|
|
- with open(lock, 'w') as fd:
|
|
|
+ # Maintain Unix line endings on all OS's to match git behavior.
|
|
|
+ with open(lock, 'w', newline='\n') as fd:
|
|
|
fd.write(content)
|
|
|
|
|
|
try:
|
|
|
@@ -399,8 +392,8 @@ class _LinkFile(object):
|
|
|
else:
|
|
|
src = _SafeExpandPath(self.git_worktree, self.src)
|
|
|
|
|
|
- if os.path.exists(src):
|
|
|
- # Entity exists so just a simple one to one link operation.
|
|
|
+ if not glob.has_magic(src):
|
|
|
+ # Entity does not contain a wild card so just a simple one to one link operation.
|
|
|
dest = _SafeExpandPath(self.topdir, self.dest, skipfinal=True)
|
|
|
# dest & src are absolute paths at this point. Make sure the target of
|
|
|
# the symlink is relative in the context of the repo client checkout.
|
|
|
@@ -408,7 +401,7 @@ class _LinkFile(object):
|
|
|
self.__linkIt(relpath, dest)
|
|
|
else:
|
|
|
dest = _SafeExpandPath(self.topdir, self.dest)
|
|
|
- # Entity doesn't exist assume there is a wild card
|
|
|
+ # Entity contains a wild card.
|
|
|
if os.path.exists(dest) and not platform_utils.isdir(dest):
|
|
|
_error('Link error: src with wildcard, %s must be a directory', dest)
|
|
|
else:
|
|
|
@@ -445,406 +438,6 @@ class RemoteSpec(object):
|
|
|
self.orig_name = orig_name
|
|
|
self.fetchUrl = fetchUrl
|
|
|
|
|
|
-
|
|
|
-class RepoHook(object):
|
|
|
-
|
|
|
- """A RepoHook contains information about a script to run as a hook.
|
|
|
-
|
|
|
- Hooks are used to run a python script before running an upload (for instance,
|
|
|
- to run presubmit checks). Eventually, we may have hooks for other actions.
|
|
|
-
|
|
|
- This shouldn't be confused with files in the 'repo/hooks' directory. Those
|
|
|
- files are copied into each '.git/hooks' folder for each project. Repo-level
|
|
|
- hooks are associated instead with repo actions.
|
|
|
-
|
|
|
- Hooks are always python. When a hook is run, we will load the hook into the
|
|
|
- interpreter and execute its main() function.
|
|
|
- """
|
|
|
-
|
|
|
- def __init__(self,
|
|
|
- hook_type,
|
|
|
- hooks_project,
|
|
|
- topdir,
|
|
|
- manifest_url,
|
|
|
- abort_if_user_denies=False):
|
|
|
- """RepoHook constructor.
|
|
|
-
|
|
|
- Params:
|
|
|
- hook_type: A string representing the type of hook. This is also used
|
|
|
- to figure out the name of the file containing the hook. For
|
|
|
- example: 'pre-upload'.
|
|
|
- hooks_project: The project containing the repo hooks. If you have a
|
|
|
- manifest, this is manifest.repo_hooks_project. OK if this is None,
|
|
|
- which will make the hook a no-op.
|
|
|
- topdir: Repo's top directory (the one containing the .repo directory).
|
|
|
- Scripts will run with CWD as this directory. If you have a manifest,
|
|
|
- this is manifest.topdir
|
|
|
- manifest_url: The URL to the manifest git repo.
|
|
|
- abort_if_user_denies: If True, we'll throw a HookError() if the user
|
|
|
- doesn't allow us to run the hook.
|
|
|
- """
|
|
|
- self._hook_type = hook_type
|
|
|
- self._hooks_project = hooks_project
|
|
|
- self._manifest_url = manifest_url
|
|
|
- self._topdir = topdir
|
|
|
- self._abort_if_user_denies = abort_if_user_denies
|
|
|
-
|
|
|
- # Store the full path to the script for convenience.
|
|
|
- if self._hooks_project:
|
|
|
- self._script_fullpath = os.path.join(self._hooks_project.worktree,
|
|
|
- self._hook_type + '.py')
|
|
|
- else:
|
|
|
- self._script_fullpath = None
|
|
|
-
|
|
|
- def _GetHash(self):
|
|
|
- """Return a hash of the contents of the hooks directory.
|
|
|
-
|
|
|
- We'll just use git to do this. This hash has the property that if anything
|
|
|
- changes in the directory we will return a different has.
|
|
|
-
|
|
|
- SECURITY CONSIDERATION:
|
|
|
- This hash only represents the contents of files in the hook directory, not
|
|
|
- any other files imported or called by hooks. Changes to imported files
|
|
|
- can change the script behavior without affecting the hash.
|
|
|
-
|
|
|
- Returns:
|
|
|
- A string representing the hash. This will always be ASCII so that it can
|
|
|
- be printed to the user easily.
|
|
|
- """
|
|
|
- assert self._hooks_project, "Must have hooks to calculate their hash."
|
|
|
-
|
|
|
- # We will use the work_git object rather than just calling GetRevisionId().
|
|
|
- # That gives us a hash of the latest checked in version of the files that
|
|
|
- # the user will actually be executing. Specifically, GetRevisionId()
|
|
|
- # doesn't appear to change even if a user checks out a different version
|
|
|
- # of the hooks repo (via git checkout) nor if a user commits their own revs.
|
|
|
- #
|
|
|
- # NOTE: Local (non-committed) changes will not be factored into this hash.
|
|
|
- # I think this is OK, since we're really only worried about warning the user
|
|
|
- # about upstream changes.
|
|
|
- return self._hooks_project.work_git.rev_parse('HEAD')
|
|
|
-
|
|
|
- def _GetMustVerb(self):
|
|
|
- """Return 'must' if the hook is required; 'should' if not."""
|
|
|
- if self._abort_if_user_denies:
|
|
|
- return 'must'
|
|
|
- else:
|
|
|
- return 'should'
|
|
|
-
|
|
|
- def _CheckForHookApproval(self):
|
|
|
- """Check to see whether this hook has been approved.
|
|
|
-
|
|
|
- We'll accept approval of manifest URLs if they're using secure transports.
|
|
|
- This way the user can say they trust the manifest hoster. For insecure
|
|
|
- hosts, we fall back to checking the hash of the hooks repo.
|
|
|
-
|
|
|
- Note that we ask permission for each individual hook even though we use
|
|
|
- the hash of all hooks when detecting changes. We'd like the user to be
|
|
|
- able to approve / deny each hook individually. We only use the hash of all
|
|
|
- hooks because there is no other easy way to detect changes to local imports.
|
|
|
-
|
|
|
- Returns:
|
|
|
- True if this hook is approved to run; False otherwise.
|
|
|
-
|
|
|
- Raises:
|
|
|
- HookError: Raised if the user doesn't approve and abort_if_user_denies
|
|
|
- was passed to the consturctor.
|
|
|
- """
|
|
|
- if self._ManifestUrlHasSecureScheme():
|
|
|
- return self._CheckForHookApprovalManifest()
|
|
|
- else:
|
|
|
- return self._CheckForHookApprovalHash()
|
|
|
-
|
|
|
- def _CheckForHookApprovalHelper(self, subkey, new_val, main_prompt,
|
|
|
- changed_prompt):
|
|
|
- """Check for approval for a particular attribute and hook.
|
|
|
-
|
|
|
- Args:
|
|
|
- subkey: The git config key under [repo.hooks.<hook_type>] to store the
|
|
|
- last approved string.
|
|
|
- new_val: The new value to compare against the last approved one.
|
|
|
- main_prompt: Message to display to the user to ask for approval.
|
|
|
- changed_prompt: Message explaining why we're re-asking for approval.
|
|
|
-
|
|
|
- Returns:
|
|
|
- True if this hook is approved to run; False otherwise.
|
|
|
-
|
|
|
- Raises:
|
|
|
- HookError: Raised if the user doesn't approve and abort_if_user_denies
|
|
|
- was passed to the consturctor.
|
|
|
- """
|
|
|
- hooks_config = self._hooks_project.config
|
|
|
- git_approval_key = 'repo.hooks.%s.%s' % (self._hook_type, subkey)
|
|
|
-
|
|
|
- # Get the last value that the user approved for this hook; may be None.
|
|
|
- old_val = hooks_config.GetString(git_approval_key)
|
|
|
-
|
|
|
- if old_val is not None:
|
|
|
- # User previously approved hook and asked not to be prompted again.
|
|
|
- if new_val == old_val:
|
|
|
- # Approval matched. We're done.
|
|
|
- return True
|
|
|
- else:
|
|
|
- # Give the user a reason why we're prompting, since they last told
|
|
|
- # us to "never ask again".
|
|
|
- prompt = 'WARNING: %s\n\n' % (changed_prompt,)
|
|
|
- else:
|
|
|
- prompt = ''
|
|
|
-
|
|
|
- # Prompt the user if we're not on a tty; on a tty we'll assume "no".
|
|
|
- if sys.stdout.isatty():
|
|
|
- prompt += main_prompt + ' (yes/always/NO)? '
|
|
|
- response = input(prompt).lower()
|
|
|
- print()
|
|
|
-
|
|
|
- # User is doing a one-time approval.
|
|
|
- if response in ('y', 'yes'):
|
|
|
- return True
|
|
|
- elif response == 'always':
|
|
|
- hooks_config.SetString(git_approval_key, new_val)
|
|
|
- return True
|
|
|
-
|
|
|
- # For anything else, we'll assume no approval.
|
|
|
- if self._abort_if_user_denies:
|
|
|
- raise HookError('You must allow the %s hook or use --no-verify.' %
|
|
|
- self._hook_type)
|
|
|
-
|
|
|
- return False
|
|
|
-
|
|
|
- def _ManifestUrlHasSecureScheme(self):
|
|
|
- """Check if the URI for the manifest is a secure transport."""
|
|
|
- secure_schemes = ('file', 'https', 'ssh', 'persistent-https', 'sso', 'rpc')
|
|
|
- parse_results = urllib.parse.urlparse(self._manifest_url)
|
|
|
- return parse_results.scheme in secure_schemes
|
|
|
-
|
|
|
- def _CheckForHookApprovalManifest(self):
|
|
|
- """Check whether the user has approved this manifest host.
|
|
|
-
|
|
|
- Returns:
|
|
|
- True if this hook is approved to run; False otherwise.
|
|
|
- """
|
|
|
- return self._CheckForHookApprovalHelper(
|
|
|
- 'approvedmanifest',
|
|
|
- self._manifest_url,
|
|
|
- 'Run hook scripts from %s' % (self._manifest_url,),
|
|
|
- 'Manifest URL has changed since %s was allowed.' % (self._hook_type,))
|
|
|
-
|
|
|
- def _CheckForHookApprovalHash(self):
|
|
|
- """Check whether the user has approved the hooks repo.
|
|
|
-
|
|
|
- Returns:
|
|
|
- True if this hook is approved to run; False otherwise.
|
|
|
- """
|
|
|
- prompt = ('Repo %s run the script:\n'
|
|
|
- ' %s\n'
|
|
|
- '\n'
|
|
|
- 'Do you want to allow this script to run')
|
|
|
- return self._CheckForHookApprovalHelper(
|
|
|
- 'approvedhash',
|
|
|
- self._GetHash(),
|
|
|
- prompt % (self._GetMustVerb(), self._script_fullpath),
|
|
|
- 'Scripts have changed since %s was allowed.' % (self._hook_type,))
|
|
|
-
|
|
|
- @staticmethod
|
|
|
- def _ExtractInterpFromShebang(data):
|
|
|
- """Extract the interpreter used in the shebang.
|
|
|
-
|
|
|
- Try to locate the interpreter the script is using (ignoring `env`).
|
|
|
-
|
|
|
- Args:
|
|
|
- data: The file content of the script.
|
|
|
-
|
|
|
- Returns:
|
|
|
- The basename of the main script interpreter, or None if a shebang is not
|
|
|
- used or could not be parsed out.
|
|
|
- """
|
|
|
- firstline = data.splitlines()[:1]
|
|
|
- if not firstline:
|
|
|
- return None
|
|
|
-
|
|
|
- # The format here can be tricky.
|
|
|
- shebang = firstline[0].strip()
|
|
|
- m = re.match(r'^#!\s*([^\s]+)(?:\s+([^\s]+))?', shebang)
|
|
|
- if not m:
|
|
|
- return None
|
|
|
-
|
|
|
- # If the using `env`, find the target program.
|
|
|
- interp = m.group(1)
|
|
|
- if os.path.basename(interp) == 'env':
|
|
|
- interp = m.group(2)
|
|
|
-
|
|
|
- return interp
|
|
|
-
|
|
|
- def _ExecuteHookViaReexec(self, interp, context, **kwargs):
|
|
|
- """Execute the hook script through |interp|.
|
|
|
-
|
|
|
- Note: Support for this feature should be dropped ~Jun 2021.
|
|
|
-
|
|
|
- Args:
|
|
|
- interp: The Python program to run.
|
|
|
- context: Basic Python context to execute the hook inside.
|
|
|
- kwargs: Arbitrary arguments to pass to the hook script.
|
|
|
-
|
|
|
- Raises:
|
|
|
- HookError: When the hooks failed for any reason.
|
|
|
- """
|
|
|
- # This logic needs to be kept in sync with _ExecuteHookViaImport below.
|
|
|
- script = """
|
|
|
-import json, os, sys
|
|
|
-path = '''%(path)s'''
|
|
|
-kwargs = json.loads('''%(kwargs)s''')
|
|
|
-context = json.loads('''%(context)s''')
|
|
|
-sys.path.insert(0, os.path.dirname(path))
|
|
|
-data = open(path).read()
|
|
|
-exec(compile(data, path, 'exec'), context)
|
|
|
-context['main'](**kwargs)
|
|
|
-""" % {
|
|
|
- 'path': self._script_fullpath,
|
|
|
- 'kwargs': json.dumps(kwargs),
|
|
|
- 'context': json.dumps(context),
|
|
|
- }
|
|
|
-
|
|
|
- # We pass the script via stdin to avoid OS argv limits. It also makes
|
|
|
- # unhandled exception tracebacks less verbose/confusing for users.
|
|
|
- cmd = [interp, '-c', 'import sys; exec(sys.stdin.read())']
|
|
|
- proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
|
|
|
- proc.communicate(input=script.encode('utf-8'))
|
|
|
- if proc.returncode:
|
|
|
- raise HookError('Failed to run %s hook.' % (self._hook_type,))
|
|
|
-
|
|
|
- def _ExecuteHookViaImport(self, data, context, **kwargs):
|
|
|
- """Execute the hook code in |data| directly.
|
|
|
-
|
|
|
- Args:
|
|
|
- data: The code of the hook to execute.
|
|
|
- context: Basic Python context to execute the hook inside.
|
|
|
- kwargs: Arbitrary arguments to pass to the hook script.
|
|
|
-
|
|
|
- Raises:
|
|
|
- HookError: When the hooks failed for any reason.
|
|
|
- """
|
|
|
- # Exec, storing global context in the context dict. We catch exceptions
|
|
|
- # and convert to a HookError w/ just the failing traceback.
|
|
|
- try:
|
|
|
- exec(compile(data, self._script_fullpath, 'exec'), context)
|
|
|
- except Exception:
|
|
|
- raise HookError('%s\nFailed to import %s hook; see traceback above.' %
|
|
|
- (traceback.format_exc(), self._hook_type))
|
|
|
-
|
|
|
- # Running the script should have defined a main() function.
|
|
|
- if 'main' not in context:
|
|
|
- raise HookError('Missing main() in: "%s"' % self._script_fullpath)
|
|
|
-
|
|
|
- # Call the main function in the hook. If the hook should cause the
|
|
|
- # build to fail, it will raise an Exception. We'll catch that convert
|
|
|
- # to a HookError w/ just the failing traceback.
|
|
|
- try:
|
|
|
- context['main'](**kwargs)
|
|
|
- except Exception:
|
|
|
- raise HookError('%s\nFailed to run main() for %s hook; see traceback '
|
|
|
- 'above.' % (traceback.format_exc(), self._hook_type))
|
|
|
-
|
|
|
- def _ExecuteHook(self, **kwargs):
|
|
|
- """Actually execute the given hook.
|
|
|
-
|
|
|
- This will run the hook's 'main' function in our python interpreter.
|
|
|
-
|
|
|
- Args:
|
|
|
- kwargs: Keyword arguments to pass to the hook. These are often specific
|
|
|
- to the hook type. For instance, pre-upload hooks will contain
|
|
|
- a project_list.
|
|
|
- """
|
|
|
- # Keep sys.path and CWD stashed away so that we can always restore them
|
|
|
- # upon function exit.
|
|
|
- orig_path = os.getcwd()
|
|
|
- orig_syspath = sys.path
|
|
|
-
|
|
|
- try:
|
|
|
- # Always run hooks with CWD as topdir.
|
|
|
- os.chdir(self._topdir)
|
|
|
-
|
|
|
- # Put the hook dir as the first item of sys.path so hooks can do
|
|
|
- # relative imports. We want to replace the repo dir as [0] so
|
|
|
- # hooks can't import repo files.
|
|
|
- sys.path = [os.path.dirname(self._script_fullpath)] + sys.path[1:]
|
|
|
-
|
|
|
- # Initial global context for the hook to run within.
|
|
|
- context = {'__file__': self._script_fullpath}
|
|
|
-
|
|
|
- # Add 'hook_should_take_kwargs' to the arguments to be passed to main.
|
|
|
- # We don't actually want hooks to define their main with this argument--
|
|
|
- # it's there to remind them that their hook should always take **kwargs.
|
|
|
- # For instance, a pre-upload hook should be defined like:
|
|
|
- # def main(project_list, **kwargs):
|
|
|
- #
|
|
|
- # This allows us to later expand the API without breaking old hooks.
|
|
|
- kwargs = kwargs.copy()
|
|
|
- kwargs['hook_should_take_kwargs'] = True
|
|
|
-
|
|
|
- # See what version of python the hook has been written against.
|
|
|
- data = open(self._script_fullpath).read()
|
|
|
- interp = self._ExtractInterpFromShebang(data)
|
|
|
- reexec = False
|
|
|
- if interp:
|
|
|
- prog = os.path.basename(interp)
|
|
|
- if prog.startswith('python2') and sys.version_info.major != 2:
|
|
|
- reexec = True
|
|
|
- elif prog.startswith('python3') and sys.version_info.major == 2:
|
|
|
- reexec = True
|
|
|
-
|
|
|
- # Attempt to execute the hooks through the requested version of Python.
|
|
|
- if reexec:
|
|
|
- try:
|
|
|
- self._ExecuteHookViaReexec(interp, context, **kwargs)
|
|
|
- except OSError as e:
|
|
|
- if e.errno == errno.ENOENT:
|
|
|
- # We couldn't find the interpreter, so fallback to importing.
|
|
|
- reexec = False
|
|
|
- else:
|
|
|
- raise
|
|
|
-
|
|
|
- # Run the hook by importing directly.
|
|
|
- if not reexec:
|
|
|
- self._ExecuteHookViaImport(data, context, **kwargs)
|
|
|
- finally:
|
|
|
- # Restore sys.path and CWD.
|
|
|
- sys.path = orig_syspath
|
|
|
- os.chdir(orig_path)
|
|
|
-
|
|
|
- def Run(self, user_allows_all_hooks, **kwargs):
|
|
|
- """Run the hook.
|
|
|
-
|
|
|
- If the hook doesn't exist (because there is no hooks project or because
|
|
|
- this particular hook is not enabled), this is a no-op.
|
|
|
-
|
|
|
- Args:
|
|
|
- user_allows_all_hooks: If True, we will never prompt about running the
|
|
|
- hook--we'll just assume it's OK to run it.
|
|
|
- kwargs: Keyword arguments to pass to the hook. These are often specific
|
|
|
- to the hook type. For instance, pre-upload hooks will contain
|
|
|
- a project_list.
|
|
|
-
|
|
|
- Raises:
|
|
|
- HookError: If there was a problem finding the hook or the user declined
|
|
|
- to run a required hook (from _CheckForHookApproval).
|
|
|
- """
|
|
|
- # No-op if there is no hooks project or if hook is disabled.
|
|
|
- if ((not self._hooks_project) or (self._hook_type not in
|
|
|
- self._hooks_project.enabled_repo_hooks)):
|
|
|
- return
|
|
|
-
|
|
|
- # Bail with a nice error if we can't find the hook.
|
|
|
- if not os.path.isfile(self._script_fullpath):
|
|
|
- raise HookError('Couldn\'t find repo hook: "%s"' % self._script_fullpath)
|
|
|
-
|
|
|
- # Make sure the user is OK with running the hook.
|
|
|
- if (not user_allows_all_hooks) and (not self._CheckForHookApproval()):
|
|
|
- return
|
|
|
-
|
|
|
- # Run the hook with the same version of python we're using.
|
|
|
- self._ExecuteHook(**kwargs)
|
|
|
-
|
|
|
-
|
|
|
class Project(object):
|
|
|
# These objects can be shared between several working trees.
|
|
|
shareable_files = ['description', 'info']
|
|
|
@@ -875,6 +468,7 @@ class Project(object):
|
|
|
is_derived=False,
|
|
|
dest_branch=None,
|
|
|
optimized_fetch=False,
|
|
|
+ retry_fetches=0,
|
|
|
old_revision=None):
|
|
|
"""Init a Project object.
|
|
|
|
|
|
@@ -901,9 +495,11 @@ class Project(object):
|
|
|
dest_branch: The branch to which to push changes for review by default.
|
|
|
optimized_fetch: If True, when a project is set to a sha1 revision, only
|
|
|
fetch from the remote if the sha1 is not present locally.
|
|
|
+ retry_fetches: Retry remote fetches n times upon receiving transient error
|
|
|
+ with exponential backoff and jitter.
|
|
|
old_revision: saved git commit id for open GITC projects.
|
|
|
"""
|
|
|
- self.manifest = manifest
|
|
|
+ self.client = self.manifest = manifest
|
|
|
self.name = name
|
|
|
self.remote = remote
|
|
|
self.gitdir = gitdir.replace('\\', '/')
|
|
|
@@ -936,6 +532,7 @@ class Project(object):
|
|
|
self.use_git_worktrees = use_git_worktrees
|
|
|
self.is_derived = is_derived
|
|
|
self.optimized_fetch = optimized_fetch
|
|
|
+ self.retry_fetches = max(0, retry_fetches)
|
|
|
self.subprojects = []
|
|
|
|
|
|
self.snapshots = {}
|
|
|
@@ -943,7 +540,7 @@ class Project(object):
|
|
|
self.linkfiles = []
|
|
|
self.annotations = []
|
|
|
self.config = GitConfig.ForRepository(gitdir=self.gitdir,
|
|
|
- defaults=self.manifest.globalConfig)
|
|
|
+ defaults=self.client.globalConfig)
|
|
|
|
|
|
if self.worktree:
|
|
|
self.work_git = self._GitGetByExec(self, bare=False, gitdir=gitdir)
|
|
|
@@ -1418,10 +1015,11 @@ class Project(object):
|
|
|
if GitCommand(self, cmd, bare=True).Wait() != 0:
|
|
|
raise UploadError('Upload failed')
|
|
|
|
|
|
- msg = "posted to %s for %s" % (branch.remote.review, dest_branch)
|
|
|
- self.bare_git.UpdateRef(R_PUB + branch.name,
|
|
|
- R_HEADS + branch.name,
|
|
|
- message=msg)
|
|
|
+ if not dryrun:
|
|
|
+ msg = "posted to %s for %s" % (branch.remote.review, dest_branch)
|
|
|
+ self.bare_git.UpdateRef(R_PUB + branch.name,
|
|
|
+ R_HEADS + branch.name,
|
|
|
+ message=msg)
|
|
|
|
|
|
# Sync ##
|
|
|
def _ExtractArchive(self, tarpath, path=None):
|
|
|
@@ -1449,6 +1047,7 @@ class Project(object):
|
|
|
tags=True,
|
|
|
archive=False,
|
|
|
optimized_fetch=False,
|
|
|
+ retry_fetches=0,
|
|
|
prune=False,
|
|
|
submodules=False,
|
|
|
clone_filter=None):
|
|
|
@@ -1532,7 +1131,7 @@ class Project(object):
|
|
|
current_branch_only=current_branch_only,
|
|
|
tags=tags, prune=prune, depth=depth,
|
|
|
submodules=submodules, force_sync=force_sync,
|
|
|
- clone_filter=clone_filter):
|
|
|
+ clone_filter=clone_filter, retry_fetches=retry_fetches):
|
|
|
return False
|
|
|
|
|
|
mp = self.manifest.manifestProject
|
|
|
@@ -1559,7 +1158,7 @@ class Project(object):
|
|
|
self._InitHooks()
|
|
|
|
|
|
def _CopyAndLinkFiles(self):
|
|
|
- if self.manifest.isGitcClient:
|
|
|
+ if self.client.isGitcClient:
|
|
|
return
|
|
|
for copyfile in self.copyfiles:
|
|
|
copyfile._Copy()
|
|
|
@@ -2300,6 +1899,27 @@ class Project(object):
|
|
|
# Enable the extension!
|
|
|
self.config.SetString('extensions.%s' % (key,), value)
|
|
|
|
|
|
+ def ResolveRemoteHead(self, name=None):
|
|
|
+ """Find out what the default branch (HEAD) points to.
|
|
|
+
|
|
|
+ Normally this points to refs/heads/master, but projects are moving to main.
|
|
|
+ Support whatever the server uses rather than hardcoding "master" ourselves.
|
|
|
+ """
|
|
|
+ if name is None:
|
|
|
+ name = self.remote.name
|
|
|
+
|
|
|
+ # The output will look like (NB: tabs are separators):
|
|
|
+ # ref: refs/heads/master HEAD
|
|
|
+ # 5f6803b100bb3cd0f534e96e88c91373e8ed1c44 HEAD
|
|
|
+ output = self.bare_git.ls_remote('-q', '--symref', '--exit-code', name, 'HEAD')
|
|
|
+
|
|
|
+ for line in output.splitlines():
|
|
|
+ lhs, rhs = line.split('\t', 1)
|
|
|
+ if rhs == 'HEAD' and lhs.startswith('ref:'):
|
|
|
+ return lhs[4:].strip()
|
|
|
+
|
|
|
+ return None
|
|
|
+
|
|
|
def _CheckForImmutableRevision(self):
|
|
|
try:
|
|
|
# if revision (sha or tag) is not present then following function
|
|
|
@@ -2334,8 +1954,10 @@ class Project(object):
|
|
|
depth=None,
|
|
|
submodules=False,
|
|
|
force_sync=False,
|
|
|
- clone_filter=None):
|
|
|
-
|
|
|
+ clone_filter=None,
|
|
|
+ retry_fetches=2,
|
|
|
+ retry_sleep_initial_sec=4.0,
|
|
|
+ retry_exp_factor=2.0):
|
|
|
is_sha1 = False
|
|
|
tag_name = None
|
|
|
# The depth should not be used when fetching to a mirror because
|
|
|
@@ -2497,18 +2119,37 @@ class Project(object):
|
|
|
|
|
|
cmd.extend(spec)
|
|
|
|
|
|
- ok = False
|
|
|
- for _i in range(2):
|
|
|
+ # At least one retry minimum due to git remote prune.
|
|
|
+ retry_fetches = max(retry_fetches, 2)
|
|
|
+ retry_cur_sleep = retry_sleep_initial_sec
|
|
|
+ ok = prune_tried = False
|
|
|
+ for try_n in range(retry_fetches):
|
|
|
gitcmd = GitCommand(self, cmd, bare=True, ssh_proxy=ssh_proxy,
|
|
|
merge_output=True, capture_stdout=quiet)
|
|
|
ret = gitcmd.Wait()
|
|
|
if ret == 0:
|
|
|
ok = True
|
|
|
break
|
|
|
- # If needed, run the 'git remote prune' the first time through the loop
|
|
|
- elif (not _i and
|
|
|
- "error:" in gitcmd.stderr and
|
|
|
- "git remote prune" in gitcmd.stderr):
|
|
|
+
|
|
|
+ # Retry later due to HTTP 429 Too Many Requests.
|
|
|
+ elif ('error:' in gitcmd.stderr and
|
|
|
+ 'HTTP 429' in gitcmd.stderr):
|
|
|
+ if not quiet:
|
|
|
+ print('429 received, sleeping: %s sec' % retry_cur_sleep,
|
|
|
+ file=sys.stderr)
|
|
|
+ time.sleep(retry_cur_sleep)
|
|
|
+ retry_cur_sleep = min(retry_exp_factor * retry_cur_sleep,
|
|
|
+ MAXIMUM_RETRY_SLEEP_SEC)
|
|
|
+ retry_cur_sleep *= (1 - random.uniform(-RETRY_JITTER_PERCENT,
|
|
|
+ RETRY_JITTER_PERCENT))
|
|
|
+ continue
|
|
|
+
|
|
|
+ # If this is not last attempt, try 'git remote prune'.
|
|
|
+ elif (try_n < retry_fetches - 1 and
|
|
|
+ 'error:' in gitcmd.stderr and
|
|
|
+ 'git remote prune' in gitcmd.stderr and
|
|
|
+ not prune_tried):
|
|
|
+ prune_tried = True
|
|
|
prunecmd = GitCommand(self, ['remote', 'prune', name], bare=True,
|
|
|
ssh_proxy=ssh_proxy)
|
|
|
ret = prunecmd.Wait()
|
|
|
@@ -2644,7 +2285,9 @@ class Project(object):
|
|
|
# returned another error with the HTTP error code being 400 or above.
|
|
|
# This return code only appears if -f, --fail is used.
|
|
|
if verbose:
|
|
|
- print('Server does not provide clone.bundle; ignoring.')
|
|
|
+ print('%s: Unable to retrieve clone.bundle; ignoring.' % self.name)
|
|
|
+ if output:
|
|
|
+ print('Curl output:\n%s' % output)
|
|
|
return False
|
|
|
elif curlret and not verbose and output:
|
|
|
print('%s' % output, file=sys.stderr)
|
|
|
@@ -2759,7 +2402,7 @@ class Project(object):
|
|
|
|
|
|
# Enable per-worktree config file support if possible. This is more a
|
|
|
# nice-to-have feature for users rather than a hard requirement.
|
|
|
- if git_require((2, 19, 0)):
|
|
|
+ if git_require((2, 20, 0)):
|
|
|
self.EnableRepositoryExtension('worktreeConfig')
|
|
|
|
|
|
# If we have a separate directory to hold refs, initialize it as well.
|
|
|
@@ -2903,6 +2546,8 @@ class Project(object):
|
|
|
|
|
|
base = R_WORKTREE_M
|
|
|
active_git = self.work_git
|
|
|
+
|
|
|
+ self._InitAnyMRef(HEAD, self.bare_git, detach=True)
|
|
|
else:
|
|
|
base = R_M
|
|
|
active_git = self.bare_git
|
|
|
@@ -2912,7 +2557,7 @@ class Project(object):
|
|
|
def _InitMirrorHead(self):
|
|
|
self._InitAnyMRef(HEAD, self.bare_git)
|
|
|
|
|
|
- def _InitAnyMRef(self, ref, active_git):
|
|
|
+ def _InitAnyMRef(self, ref, active_git, detach=False):
|
|
|
cur = self.bare_ref.symref(ref)
|
|
|
|
|
|
if self.revisionId:
|
|
|
@@ -2925,7 +2570,10 @@ class Project(object):
|
|
|
dst = remote.ToLocal(self.revisionExpr)
|
|
|
if cur != dst:
|
|
|
msg = 'manifest set to %s' % self.revisionExpr
|
|
|
- active_git.symbolic_ref('-m', msg, ref, dst)
|
|
|
+ if detach:
|
|
|
+ active_git.UpdateRef(ref, dst, message=msg, detach=True)
|
|
|
+ else:
|
|
|
+ active_git.symbolic_ref('-m', msg, ref, dst)
|
|
|
|
|
|
def _CheckDirReference(self, srcdir, destdir, share_refs):
|
|
|
# Git worktrees don't use symlinks to share at all.
|
|
|
@@ -3048,12 +2696,14 @@ class Project(object):
|
|
|
# Some platforms (e.g. Windows) won't let us update dotgit in situ because
|
|
|
# of file permissions. Delete it and recreate it from scratch to avoid.
|
|
|
platform_utils.remove(dotgit)
|
|
|
- # Use relative path from checkout->worktree.
|
|
|
- with open(dotgit, 'w') as fp:
|
|
|
+ # Use relative path from checkout->worktree & maintain Unix line endings
|
|
|
+ # on all OS's to match git behavior.
|
|
|
+ with open(dotgit, 'w', newline='\n') as fp:
|
|
|
print('gitdir:', os.path.relpath(git_worktree_path, self.worktree),
|
|
|
file=fp)
|
|
|
- # Use relative path from worktree->checkout.
|
|
|
- with open(os.path.join(git_worktree_path, 'gitdir'), 'w') as fp:
|
|
|
+ # Use relative path from worktree->checkout & maintain Unix line endings
|
|
|
+ # on all OS's to match git behavior.
|
|
|
+ with open(os.path.join(git_worktree_path, 'gitdir'), 'w', newline='\n') as fp:
|
|
|
print(os.path.relpath(dotgit, git_worktree_path), file=fp)
|
|
|
|
|
|
self._InitMRef()
|
|
|
@@ -3174,6 +2824,13 @@ class Project(object):
|
|
|
self._bare = bare
|
|
|
self._gitdir = gitdir
|
|
|
|
|
|
+ # __getstate__ and __setstate__ are required for pickling because __getattr__ exists.
|
|
|
+ def __getstate__(self):
|
|
|
+ return (self._project, self._bare, self._gitdir)
|
|
|
+
|
|
|
+ def __setstate__(self, state):
|
|
|
+ self._project, self._bare, self._gitdir = state
|
|
|
+
|
|
|
def LsOthers(self):
|
|
|
p = GitCommand(self._project,
|
|
|
['ls-files',
|