普通文本  |  159行  |  4.63 KB

# Copyright (C) 2018 The Android Open Source Project
#
# 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.
'''Helper functions to communicate with Git.'''

import datetime
import re
import subprocess


def _run(cmd, cwd, redirect=True):
    """Runs a command with stdout and stderr redirected."""
    out = subprocess.PIPE if redirect else None
    return subprocess.run(cmd, stdout=out, stderr=out,
                          check=True, cwd=cwd)


def fetch(proj_path, remote_names):
    """Runs git fetch.

    Args:
        proj_path: Path to Git repository.
        remote_names: Array of string to specify remote names.
    """
    _run(['git', 'fetch', '--multiple'] + remote_names, cwd=proj_path)


def add_remote(proj_path, name, url):
    """Adds a git remote.

    Args:
        proj_path: Path to Git repository.
        name: Name of the new remote.
        url: Url of the new remote.
    """
    _run(['git', 'remote', 'add', name, url], cwd=proj_path)


def list_remotes(proj_path):
    """Lists all Git remotes.

    Args:
        proj_path: Path to Git repository.

    Returns:
        A dict from remote name to remote url.
    """
    out = _run(['git', 'remote', '-v'], proj_path)
    lines = out.stdout.decode('utf-8').splitlines()
    return dict([line.split()[0:2] for line in lines])


def get_commits_ahead(proj_path, branch, base_branch):
    """Lists commits in `branch` but not `base_branch`."""
    out = _run(['git', 'rev-list', '--left-only', '--ancestry-path',
                '{}...{}'.format(branch, base_branch)],
               proj_path)
    return out.stdout.decode('utf-8').splitlines()


def get_commit_time(proj_path, commit):
    """Gets commit time of one commit."""
    out = _run(['git', 'show', '-s', '--format=%ct', commit], cwd=proj_path)
    return datetime.datetime.fromtimestamp(int(out.stdout))


def list_remote_branches(proj_path, remote_name):
    """Lists all branches for a remote."""
    out = _run(['git', 'branch', '-r'], cwd=proj_path)
    lines = out.stdout.decode('utf-8').splitlines()
    stripped = [line.strip() for line in lines]
    remote_path = remote_name + '/'
    remote_path_len = len(remote_path)
    return [line[remote_path_len:] for line in stripped
            if line.startswith(remote_path)]


def _parse_remote_tag(line):
    tag_prefix = 'refs/tags/'
    tag_suffix = '^{}'
    try:
        line = line[line.index(tag_prefix):]
    except ValueError:
        return None
    line = line[len(tag_prefix):]
    if line.endswith(tag_suffix):
        line = line[:-len(tag_suffix)]
    return line


def list_remote_tags(proj_path, remote_name):
    """Lists all tags for a remote."""
    out = _run(['git', "ls-remote", "--tags", remote_name],
               cwd=proj_path)
    lines = out.stdout.decode('utf-8').splitlines()
    tags = [_parse_remote_tag(line) for line in lines]
    return list(set(tags))


COMMIT_PATTERN = r'^[a-f0-9]{40}$'
COMMIT_RE = re.compile(COMMIT_PATTERN)


def is_commit(commit):
    """Whether a string looks like a SHA1 hash."""
    return bool(COMMIT_RE.match(commit))


def merge(proj_path, branch):
    """Merges a branch."""
    try:
        out = _run(['git', 'merge', branch, '--no-commit'],
                   cwd=proj_path)
    except subprocess.CalledProcessError:
        # Merge failed. Error is already written to console.
        subprocess.run(['git', 'merge', '--abort'], cwd=proj_path)
        raise


def add_file(proj_path, file_name):
    """Stages a file."""
    _run(['git', 'add', file_name], cwd=proj_path)


def delete_branch(proj_path, branch_name):
    """Force delete a branch."""
    _run(['git', 'branch', '-D', branch_name], cwd=proj_path)


def start_branch(proj_path, branch_name):
    """Starts a new repo branch."""
    _run(['repo', 'start', branch_name], cwd=proj_path)


def commit(proj_path, message):
    """Commits changes."""
    _run(['git', 'commit', '-m', message], cwd=proj_path)


def checkout(proj_path, branch_name):
    """Checkouts a branch."""
    _run(['git', 'checkout', branch_name], cwd=proj_path)


def push(proj_path, remote_name):
    """Pushes change to remote."""
    return _run(['git', 'push', remote_name, 'HEAD:refs/for/master'],
                cwd=proj_path, redirect=False)