File: //snap/google-cloud-cli/396/lib/googlecloudsdk/api_lib/source/git.py
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Wrapper to manipulate GCP git repository."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import errno
import os
import re
import subprocess
import textwrap
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core.util import encoding
from googlecloudsdk.core.util import files
from googlecloudsdk.core.util import platforms
import six
import uritemplate
# This is the minimum version of git required to use credential helpers.
_HELPER_MIN = (2, 0, 1)
_WINDOWS_HELPER_MIN = (2, 15, 0)
class Error(exceptions.Error):
"""Exceptions for this module."""
class UnknownRepositoryAliasException(Error):
"""Exception to be thrown when a repository alias provided cannot be found."""
class CannotInitRepositoryException(Error):
"""Exception to be thrown when a repository cannot be created."""
class CannotFetchRepositoryException(Error):
"""Exception to be thrown when a repository cannot be fetched."""
class GitVersionException(Error):
"""Exceptions for when git version is too old."""
def __init__(self, fmtstr, cur_version, min_version):
self.cur_version = cur_version
super(GitVersionException, self).__init__(
fmtstr.format(cur_version=cur_version, min_version=min_version))
class InvalidGitException(Error):
"""Exceptions for when git version is empty or invalid."""
class GcloudIsNotInPath(Error):
"""Exception for when the gcloud cannot be found."""
def CheckGitVersion(version_lower_bound=None):
"""Returns true when version of git is >= min_version.
Args:
version_lower_bound: (int,int,int), The lowest allowed version, or None to
just check for the presence of git.
Returns:
True if version >= min_version.
Raises:
GitVersionException: if `git` was found, but the version is incorrect.
InvalidGitException: if `git` was found, but the output of `git version` is
not as expected.
NoGitException: if `git` was not found.
"""
try:
cur_version = encoding.Decode(subprocess.check_output(['git', 'version']))
if not cur_version:
raise InvalidGitException('The git version string is empty.')
if not cur_version.startswith('git version '):
raise InvalidGitException(('The git version string must start with '
'git version .'))
match = re.search(r'(\d+)\.(\d+)\.(\d+)', cur_version)
if not match:
raise InvalidGitException('The git version string must contain a '
'version number.')
current_version = tuple([int(item) for item in match.group(1, 2, 3)])
if version_lower_bound and current_version < version_lower_bound:
min_version = '.'.join(six.text_type(i) for i in version_lower_bound)
raise GitVersionException(
'Your git version {cur_version} is older than the minimum version '
'{min_version}. Please install a newer version of git.',
cur_version=cur_version, min_version=min_version)
except OSError as e:
if e.errno == errno.ENOENT:
raise NoGitException()
raise
return True
class NoGitException(Error):
"""Exceptions for when git is not available."""
def __init__(self):
super(NoGitException, self).__init__(
textwrap.dedent("""\
Cannot find git. Please install git and try again.
You can find git installers at [http://git-scm.com/downloads], or use
your favorite package manager to install it on your computer. Make sure
it can be found on your system PATH.
"""))
def _GetRepositoryURI(project, alias):
"""Get the URI for a repository, given its project and alias.
Args:
project: str, The project name.
alias: str, The repository alias.
Returns:
str, The repository URI.
"""
return uritemplate.expand(
'https://source.developers.google.com/p/{project}/r/{alias}',
{'project': project, 'alias': alias})
def _GetGcloudScript(full_path=False):
"""Get name of the gcloud script.
Args:
full_path: boolean, True if the gcloud full path should be used if free
of spaces.
Returns:
str, command to use to execute gcloud
Raises:
GcloudIsNotInPath: if gcloud is not found in the path
"""
if (platforms.OperatingSystem.Current() ==
platforms.OperatingSystem.WINDOWS):
gcloud_ext = '.cmd'
else:
gcloud_ext = ''
gcloud_name = 'gcloud'
gcloud = files.FindExecutableOnPath(gcloud_name, pathext=[gcloud_ext])
if not gcloud:
raise GcloudIsNotInPath(
'Could not verify that gcloud is in the PATH. '
'Please make sure the Cloud SDK bin folder is in PATH.')
if full_path:
if not re.match(r'[-a-zA-Z0-9_/]+$', gcloud):
log.warning(
textwrap.dedent("""\
You specified the option to use the full gcloud path in the git
credential.helper, but the path contains non alphanumberic characters
so the credential helper may not work correctly."""))
return gcloud
else:
return gcloud_name + gcloud_ext
def _GetCredHelperCommand(uri, full_path=False, min_version=_HELPER_MIN):
"""Returns the gcloud credential helper command for a remote repository.
The command will be of the form '!gcloud auth git-helper --account=EMAIL
--ignore-unknown $@`. See https://git-scm.com/docs/git-config. If the
installed version of git or the remote repository does not support
the gcloud credential helper, then returns None.
Args:
uri: str, The uri of the remote repository.
full_path: bool, If true, use the full path to gcloud.
min_version: minimum git version; if found git is earlier than this, warn
and return None
Returns:
str, The credential helper command if it is available.
"""
credentialed_hosts = ['source.developers.google.com']
extra = properties.VALUES.core.credentialed_hosted_repo_domains.Get()
if extra:
credentialed_hosts.extend(extra.split(','))
if any(
uri.startswith('https://' + host + '/') for host in credentialed_hosts):
try:
CheckGitVersion(min_version)
except GitVersionException as e:
helper_min_str = '.'.join(six.text_type(i) for i in min_version)
log.warning(
textwrap.dedent("""\
You are using a Google-hosted repository with a
{current} which is older than {min_version}. If you upgrade
to {min_version} or later, gcloud can handle authentication to
this repository. Otherwise, to authenticate, use your Google
account and the password found by running the following command.
$ gcloud auth print-access-token""".format(
current=e.cur_version, min_version=helper_min_str)))
return None
# Use git alias "!shell command" syntax so we can configure
# the helper with options. Also git-credential is not
# prefixed when it starts with "!".
return '!{0} auth git-helper --account={1} --ignore-unknown $@'.format(
_GetGcloudScript(full_path),
properties.VALUES.core.account.Get(required=True))
return None
class Git(object):
"""Represents project git repo."""
def __init__(self, project_id, repo_name, uri=None):
"""Constructor.
Args:
project_id: str, The name of the project that has a repository associated
with it.
repo_name: str, The name of the repository to clone.
uri: str, The URI of the repository, or None if it will be inferred from
the name.
Raises:
UnknownRepositoryAliasException: If the repo name is not known to be
associated with the project.
"""
self._project_id = project_id
self._repo_name = repo_name
self._uri = uri or _GetRepositoryURI(project_id, repo_name)
if not self._uri:
raise UnknownRepositoryAliasException()
def GetName(self):
return self._repo_name
def Clone(self, destination_path, dry_run=False, full_path=False):
"""Clone a git repository into a gcloud workspace.
If the resulting clone does not have a .gcloud directory, create one. Also,
sets the credential.helper to use the gcloud credential helper.
Args:
destination_path: str, The relative path for the repository clone.
dry_run: bool, If true do not run but print commands instead.
full_path: bool, If true use the full path to gcloud.
Returns:
str, The absolute path of cloned repository.
Raises:
CannotInitRepositoryException: If there is already a file or directory in
the way of creating this repository.
CannotFetchRepositoryException: If there is a problem fetching the
repository from the remote host, or if the repository is otherwise
misconfigured.
"""
abs_repository_path = os.path.abspath(destination_path)
if os.path.exists(abs_repository_path):
CheckGitVersion() # Do this here, before we start running git commands
if os.listdir(abs_repository_path):
# Raise exception if dir is not empty and not a git repository
raise CannotInitRepositoryException(
'Directory path specified exists and is not empty')
# Make a brand new repository if directory does not exist or
# clone if directory exists and is empty
try:
# If this is a Google-hosted repo, clone with the cred helper.
cmd = ['git', 'clone', self._uri, abs_repository_path]
min_git = _HELPER_MIN
if (platforms.OperatingSystem.Current() ==
platforms.OperatingSystem.WINDOWS):
min_git = _WINDOWS_HELPER_MIN
cred_helper_command = _GetCredHelperCommand(
self._uri, full_path=full_path, min_version=min_git)
if cred_helper_command:
cmd += [
'--config',
'credential.https://source.developers.google.com/.helper=',
'--config',
'credential.https://source.developers.google.com/.helper=' +
cred_helper_command
]
self._RunCommand(cmd, dry_run)
except subprocess.CalledProcessError as e:
raise CannotFetchRepositoryException(e)
return abs_repository_path
def _RunCommand(self, cmd, dry_run):
log.debug('Executing %s', cmd)
if dry_run:
log.out.Print(' '.join(cmd))
else:
subprocess.check_call(cmd)