普通文本  |  137行  |  4.63 KB

# Copyright 2013-2014 Sebastian Kreft
#
# 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.
"""Functions to get information from git."""

import os.path
import subprocess

import gitlint.utils as utils


def repository_root():
    """Returns the root of the repository as an absolute path."""
    try:
        root = subprocess.check_output(['git', 'rev-parse', '--show-toplevel'],
                                       stderr=subprocess.STDOUT).strip()
        # Convert to unicode first
        return root.decode('utf-8')
    except subprocess.CalledProcessError:
        return None


def last_commit():
    """Returns the SHA1 of the last commit."""
    try:
        root = subprocess.check_output(['git', 'rev-parse', 'HEAD'],
                                       stderr=subprocess.STDOUT).strip()
        # Convert to unicode first
        return root.decode('utf-8')
    except subprocess.CalledProcessError:
        return None


def _remove_filename_quotes(filename):
    """Removes the quotes from a filename returned by git status."""
    if filename.startswith('"') and filename.endswith('"'):
        return filename[1:-1]

    return filename


def modified_files(root, tracked_only=False, commit=None):
    """Returns a list of files that has been modified since the last commit.

    Args:
      root: the root of the repository, it has to be an absolute path.
      tracked_only: exclude untracked files when True.
      commit: SHA1 of the commit. If None, it will get the modified files in the
        working copy.

    Returns: a dictionary with the modified files as keys, and additional
      information as value. In this case it adds the status returned by
      git status.
    """
    assert os.path.isabs(root), "Root has to be absolute, got: %s" % root

    if commit:
        return _modified_files_with_commit(root, commit)

    # Convert to unicode and split
    status_lines = subprocess.check_output([
        'git', 'status', '--porcelain', '--untracked-files=all',
        '--ignore-submodules=all']).decode('utf-8').split(os.linesep)

    modes = ['M ', ' M', 'A ', 'AM', 'MM']
    if not tracked_only:
        modes.append(r'\?\?')
    modes_str = '|'.join(modes)

    modified_file_status = utils.filter_lines(
        status_lines,
        r'(?P<mode>%s) (?P<filename>.+)' % modes_str,
        groups=('filename', 'mode'))

    return dict((os.path.join(root, _remove_filename_quotes(filename)), mode)
                for filename, mode in modified_file_status)


def _modified_files_with_commit(root, commit):
    # Convert to unicode and split
    status_lines = subprocess.check_output(
        ['git', 'diff-tree', '-r', '--root', '--no-commit-id', '--name-status',
         commit]).decode('utf-8').split(os.linesep)

    modified_file_status = utils.filter_lines(
        status_lines,
        r'(?P<mode>A|M)\s(?P<filename>.+)',
        groups=('filename', 'mode'))

    # We need to add a space to the mode, so to be compatible with the output
    # generated by modified files.
    return dict((os.path.join(root, _remove_filename_quotes(filename)),
                 mode + ' ') for filename, mode in modified_file_status)


def modified_lines(filename, extra_data, commit=None):
    """Returns the lines that have been modifed for this file.

    Args:
      filename: the file to check.
      extra_data: is the extra_data returned by modified_files. Additionally, a
        value of None means that the file was not modified.
      commit: the complete sha1 (40 chars) of the commit.

    Returns: a list of lines that were modified, or None in case all lines are
      new.
    """
    if extra_data is None:
        return []
    if extra_data not in ('M ', ' M', 'MM'):
        return None

    if commit is None:
        commit = '0' * 40
    commit = commit.encode('utf-8')

    # Split as bytes, as the output may have some non unicode characters.
    blame_lines = subprocess.check_output(
        ['git', 'blame', commit, '--porcelain', '--', filename]).split(
            os.linesep.encode('utf-8'))
    modified_line_numbers = utils.filter_lines(
        blame_lines,
        commit + br' (?P<line>\d+) (\d+)',
        groups=('line',))

    return list(map(int, modified_line_numbers))