| # 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 + b'^!'), '--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)) |