1# Copyright 2020 The Pigweed Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may not 4# use this file except in compliance with the License. You may obtain a copy of 5# the License at 6# 7# https://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, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations under 13# the License. 14"""Install and check status of Git repository-based packages.""" 15 16import os 17import pathlib 18import shutil 19import subprocess 20from typing import Union 21import urllib.parse 22 23import pw_package.package_manager 24 25PathOrStr = Union[pathlib.Path, str] 26 27 28def git_stdout(*args: PathOrStr, 29 show_stderr=False, 30 repo: PathOrStr = '.') -> str: 31 return subprocess.run(['git', '-C', repo, *args], 32 stdout=subprocess.PIPE, 33 stderr=None if show_stderr else subprocess.DEVNULL, 34 check=True).stdout.decode().strip() 35 36 37def git(*args: PathOrStr, 38 repo: PathOrStr = '.') -> subprocess.CompletedProcess: 39 return subprocess.run(['git', '-C', repo, *args], check=True) 40 41 42class GitRepo(pw_package.package_manager.Package): 43 """Install and check status of Git repository-based packages.""" 44 def __init__(self, url, commit, *args, **kwargs): 45 super().__init__(*args, **kwargs) 46 self._url = url 47 self._commit = commit 48 49 def status(self, path: pathlib.Path) -> bool: 50 if not os.path.isdir(path / '.git'): 51 return False 52 53 remote = git_stdout('remote', 'get-url', 'origin', repo=path) 54 url = urllib.parse.urlparse(remote) 55 if url.scheme == 'sso' or '.git.corp.google.com' in url.netloc: 56 host = url.netloc.replace( 57 '.git.corp.google.com', 58 '.googlesource.com', 59 ) 60 if not host.endswith('.googlesource.com'): 61 host += '.googlesource.com' 62 remote = 'https://{}{}'.format(host, url.path) 63 64 commit = git_stdout('rev-parse', 'HEAD', repo=path) 65 status = git_stdout('status', '--porcelain=v1', repo=path) 66 return remote == self._url and commit == self._commit and not status 67 68 def install(self, path: pathlib.Path) -> None: 69 # If already installed and at correct version exit now. 70 if self.status(path): 71 return 72 73 # Otherwise delete current version and clone again. 74 if os.path.isdir(path): 75 shutil.rmtree(path) 76 77 # --filter=blob:none means we don't get history, just the current 78 # revision. If we later run commands that need history it will be 79 # retrieved on-demand. For small repositories the effect is negligible 80 # but for large repositories this should be a significant improvement. 81 git('clone', '--filter=blob:none', self._url, path) 82 git('reset', '--hard', self._commit, repo=path) 83