Commit 81937565 authored by mgiuca@chromium.org's avatar mgiuca@chromium.org

Added git hyper-blame, a tool that skips unwanted commits in git blame.

Currently, the script requires you to pass the unwanted commits on the
command line, but eventually, you could configure it with a file
(checked into the repo) that provides a fixed set of commits to always
skip (such as commits that do a huge amount of renaming and nothing
else).

BUG=574290

Review URL: https://codereview.chromium.org/1559943003

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@298544 0039d316-1c4b-4281-b951-d872f2087c98
parent b1f0581d
#!/usr/bin/env bash
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
. $(type -P python_runner.sh)
......@@ -281,6 +281,16 @@ def once(function):
## Git functions
def blame(filename, revision=None, porcelain=False, *args):
command = ['blame']
if porcelain:
command.append('-p')
if revision is not None:
command.append(revision)
command.extend(['--', filename])
return run(*command)
def branch_config(branch, option, default=None):
return config('branch.%s.%s' % (branch, option), default=default)
......@@ -546,10 +556,39 @@ def remove_merge_base(branch):
del_branch_config(branch, 'base-upstream')
def repo_root():
"""Returns the absolute path to the repository root."""
return run('rev-parse', '--show-toplevel')
def root():
return config('depot-tools.upstream', 'origin/master')
@contextlib.contextmanager
def less(): # pragma: no cover
"""Runs 'less' as context manager yielding its stdin as a PIPE.
Automatically checks if sys.stdout is a non-TTY stream. If so, it avoids
running less and just yields sys.stdout.
"""
if not sys.stdout.isatty():
yield sys.stdout
return
# Run with the same options that git uses (see setup_pager in git repo).
# -F: Automatically quit if the output is less than one screen.
# -R: Don't escape ANSI color codes.
# -X: Don't clear the screen before starting.
cmd = ('less', '-FRX')
try:
proc = subprocess2.Popen(cmd, stdin=subprocess2.PIPE)
yield proc.stdin
finally:
proc.stdin.close()
proc.wait()
def run(*cmd, **kwargs):
"""The same as run_with_stderr, except it only returns stdout."""
return run_with_stderr(*cmd, **kwargs)[0]
......
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Utility module for dealing with Git timestamps."""
import datetime
def timestamp_offset_to_datetime(timestamp, offset):
"""Converts a timestamp + offset into a datetime.datetime.
Useful for dealing with the output of porcelain commands, which provide times
as timestamp and offset strings.
Args:
timestamp: An int UTC timestamp, or a string containing decimal digits.
offset: A str timezone offset. e.g., '-0800'.
Returns:
A tz-aware datetime.datetime for this timestamp.
"""
timestamp = int(timestamp)
tz = FixedOffsetTZ.from_offset_string(offset)
return datetime.datetime.fromtimestamp(timestamp, tz)
def datetime_string(dt):
"""Converts a tz-aware datetime.datetime into a string in git format."""
return dt.strftime('%Y-%m-%d %H:%M:%S %z')
# Adapted from: https://docs.python.org/2/library/datetime.html#tzinfo-objects
class FixedOffsetTZ(datetime.tzinfo):
def __init__(self, offset, name):
datetime.tzinfo.__init__(self)
self.__offset = offset
self.__name = name
def __repr__(self): # pragma: no cover
return '{}({!r}, {!r})'.format(type(self).__name__, self.__offset,
self.__name)
@classmethod
def from_offset_string(cls, offset):
try:
hours = int(offset[:-2])
minutes = int(offset[-2:])
except ValueError:
return cls(datetime.timedelta(0), 'UTC')
delta = datetime.timedelta(hours=hours, minutes=minutes)
return cls(delta, offset)
def utcoffset(self, dt):
return self.__offset
def tzname(self, dt):
return self.__name
def dst(self, dt):
return datetime.timedelta(0)
#!/usr/bin/env python
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Wrapper around git blame that ignores certain commits.
"""
from __future__ import print_function
import argparse
import collections
import logging
import os
import subprocess2
import sys
import git_common
import git_dates
logging.getLogger().setLevel(logging.INFO)
class Commit(object):
"""Info about a commit."""
def __init__(self, commithash):
self.commithash = commithash
self.author = None
self.author_mail = None
self.author_time = None
self.author_tz = None
self.committer = None
self.committer_mail = None
self.committer_time = None
self.committer_tz = None
self.summary = None
self.boundary = None
self.previous = None
self.filename = None
def __repr__(self): # pragma: no cover
return '<Commit %s>' % self.commithash
BlameLine = collections.namedtuple(
'BlameLine',
'commit context lineno_then lineno_now modified')
def parse_blame(blameoutput):
"""Parses the output of git blame -p into a data structure."""
lines = blameoutput.split('\n')
i = 0
commits = {}
while i < len(lines):
# Read a commit line and parse it.
line = lines[i]
i += 1
if not line.strip():
continue
commitline = line.split()
commithash = commitline[0]
lineno_then = int(commitline[1])
lineno_now = int(commitline[2])
try:
commit = commits[commithash]
except KeyError:
commit = Commit(commithash)
commits[commithash] = commit
# Read commit details until we find a context line.
while i < len(lines):
line = lines[i]
i += 1
if line.startswith('\t'):
break
try:
key, value = line.split(' ', 1)
except ValueError:
key = line
value = True
setattr(commit, key.replace('-', '_'), value)
context = line[1:]
yield BlameLine(commit, context, lineno_then, lineno_now, False)
def print_table(table, colsep=' ', rowsep='\n', align=None, out=sys.stdout):
"""Print a 2D rectangular array, aligning columns with spaces.
Args:
align: Optional string of 'l' and 'r', designating whether each column is
left- or right-aligned. Defaults to left aligned.
"""
if len(table) == 0:
return
colwidths = None
for row in table:
if colwidths is None:
colwidths = [len(x) for x in row]
else:
colwidths = [max(colwidths[i], len(x)) for i, x in enumerate(row)]
if align is None: # pragma: no cover
align = 'l' * len(colwidths)
for row in table:
cells = []
for i, cell in enumerate(row):
padding = ' ' * (colwidths[i] - len(cell))
if align[i] == 'r':
cell = padding + cell
elif i < len(row) - 1:
# Do not pad the final column if left-aligned.
cell += padding
cells.append(cell)
try:
print(*cells, sep=colsep, end=rowsep, file=out)
except IOError: # pragma: no cover
# Can happen on Windows if the pipe is closed early.
pass
def pretty_print(parsedblame, show_filenames=False, out=sys.stdout):
"""Pretty-prints the output of parse_blame."""
table = []
for line in parsedblame:
author_time = git_dates.timestamp_offset_to_datetime(
line.commit.author_time, line.commit.author_tz)
row = [line.commit.commithash[:8],
'(' + line.commit.author,
git_dates.datetime_string(author_time),
str(line.lineno_now) + ('*' if line.modified else '') + ')',
line.context]
if show_filenames:
row.insert(1, line.commit.filename)
table.append(row)
print_table(table, align='llllrl' if show_filenames else 'lllrl', out=out)
def get_parsed_blame(filename, revision='HEAD'):
blame = git_common.blame(filename, revision=revision, porcelain=True)
return list(parse_blame(blame))
def hyper_blame(ignored, filename, revision='HEAD', out=sys.stdout,
err=sys.stderr):
# Map from commit to parsed blame from that commit.
blame_from = {}
def cache_blame_from(filename, commithash):
try:
return blame_from[commithash]
except KeyError:
parsed = get_parsed_blame(filename, commithash)
blame_from[commithash] = parsed
return parsed
try:
parsed = cache_blame_from(filename, git_common.hash_one(revision))
except subprocess2.CalledProcessError as e:
err.write(e.stderr)
return e.returncode
new_parsed = []
# We don't show filenames in blame output unless we have to.
show_filenames = False
for line in parsed:
# If a line references an ignored commit, blame that commit's parent
# repeatedly until we find a non-ignored commit.
while line.commit.commithash in ignored:
if line.commit.previous is None:
# You can't ignore the commit that added this file.
break
previouscommit, previousfilename = line.commit.previous.split(' ', 1)
parent_blame = cache_blame_from(previousfilename, previouscommit)
if len(parent_blame) == 0:
# The previous version of this file was empty, therefore, you can't
# ignore this commit.
break
# line.lineno_then is the line number in question at line.commit.
# TODO(mgiuca): This will be incorrect if line.commit added or removed
# lines. Translate that line number so that it refers to the position of
# the same line on previouscommit.
lineno_previous = line.lineno_then
logging.debug('ignore commit %s on line p%d/t%d/n%d',
line.commit.commithash, lineno_previous, line.lineno_then,
line.lineno_now)
# Get the line at lineno_previous in the parent commit.
assert lineno_previous > 0
try:
newline = parent_blame[lineno_previous - 1]
except IndexError:
# lineno_previous is a guess, so it may be past the end of the file.
# Just grab the last line in the file.
newline = parent_blame[-1]
# Replace the commit and lineno_then, but not the lineno_now or context.
logging.debug(' replacing with %r', newline)
line = BlameLine(newline.commit, line.context, lineno_previous,
line.lineno_now, True)
# If any line has a different filename to the file's current name, turn on
# filename display for the entire blame output.
if line.commit.filename != filename:
show_filenames = True
new_parsed.append(line)
pretty_print(new_parsed, show_filenames=show_filenames, out=out)
return 0
def main(args, stdout=sys.stdout, stderr=sys.stderr):
parser = argparse.ArgumentParser(
prog='git hyper-blame',
description='git blame with support for ignoring certain commits.')
parser.add_argument('-i', metavar='REVISION', action='append', dest='ignored',
default=[], help='a revision to ignore')
parser.add_argument('revision', nargs='?', default='HEAD', metavar='REVISION',
help='revision to look at')
parser.add_argument('filename', metavar='FILE', help='filename to blame')
args = parser.parse_args(args)
try:
repo_root = git_common.repo_root()
except subprocess2.CalledProcessError as e:
stderr.write(e.stderr)
return e.returncode
# Make filename relative to the repository root, and cd to the root dir (so
# all filenames throughout this script are relative to the root).
filename = os.path.relpath(args.filename, repo_root)
os.chdir(repo_root)
# Normalize filename so we can compare it to other filenames git gives us.
filename = os.path.normpath(filename)
filename = os.path.normcase(filename)
ignored = set()
for c in args.ignored:
try:
ignored.add(git_common.hash_one(c))
except subprocess2.CalledProcessError as e:
# Custom error message (the message from git-rev-parse is inappropriate).
stderr.write('fatal: unknown revision \'%s\'.\n' % c)
return e.returncode
return hyper_blame(ignored, filename, args.revision, out=stdout, err=stderr)
if __name__ == '__main__': # pragma: no cover
with git_common.less() as less_input:
sys.exit(main(sys.argv[1:], stdout=less_input))
......@@ -804,6 +804,14 @@ Freeze all changes on a branch (indexed and unindexed).
</p>
</dd>
<dt class="hdlist1">
<a href="git-hyper-blame.html">git-hyper-blame(1)</a>
</dt>
<dd>
<p>
Like git blame, but with the ability to ignore or bypass certain commits.
</p>
</dd>
<dt class="hdlist1">
<a href="git-map-branches.html">git-map-branches(1)</a>
</dt>
<dd>
......@@ -919,7 +927,7 @@ Print a diff of the current branch, compared to its upstream.
<div id="footnotes"><hr /></div>
<div id="footer">
<div id="footer-text">
Last updated 2014-09-09 14:15:07 PDT
Last updated 2016-01-28 16:40:21 AEDT
</div>
</div>
</body>
......
This diff is collapsed.
'\" t
.\" Title: git-hyper-blame
.\" Author: [FIXME: author] [see http://docbook.sf.net/el/author]
.\" Generator: DocBook XSL Stylesheets v1.78.1 <http://docbook.sf.net/>
.\" Date: 01/28/2016
.\" Manual: Chromium depot_tools Manual
.\" Source: depot_tools 7143379
.\" Language: English
.\"
.TH "GIT\-HYPER\-BLAME" "1" "01/28/2016" "depot_tools 7143379" "Chromium depot_tools Manual"
.\" -----------------------------------------------------------------
.\" * Define some portability stuff
.\" -----------------------------------------------------------------
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.\" http://bugs.debian.org/507673
.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.\" -----------------------------------------------------------------
.\" * set default formatting
.\" -----------------------------------------------------------------
.\" disable hyphenation
.nh
.\" disable justification (adjust text to left margin only)
.ad l
.\" -----------------------------------------------------------------
.\" * MAIN CONTENT STARTS HERE *
.\" -----------------------------------------------------------------
.SH "NAME"
git-hyper-blame \- Like git blame, but with the ability to ignore or bypass certain commits\&.
.SH "SYNOPSIS"
.sp
.nf
\fIgit hyper\-blame\fR [\-i <rev> [\-i <rev> \&...]] [<rev>] [\-\-] <file>
.fi
.sp
.SH "DESCRIPTION"
.sp
git hyper\-blame is like git blame but it can ignore or "look through" a given set of commits, to find the real culprit\&.
.sp
This is useful if you have a commit that makes sweeping changes that are unlikely to be what you are looking for in a blame, such as mass reformatting or renaming\&. By adding these commits to the hyper\-blame ignore list, git hyper\-blame will look past these commits to find the previous commit that touched a given line\&.
.sp
Follows the normal blame syntax: annotates <file> with the revision that last modified each line\&. Optional <rev> specifies the revision of <file> to start from\&.
.SH "OPTIONS"
.PP
\-i <rev>
.RS 4
A revision to ignore\&. Can be specified as many times as needed\&.
.RE
.SH "EXAMPLE"
.sp
Let\(cqs run git blame on a file:
.sp
.sp
.if n \{\
.RS 4
.\}
.nf
\fB$ git blame ipsum\&.txt\fR
c6eb3bfa (lorem 2014\-08\-11 23:15:57 +0000 1) LOREM IPSUM DOLOR SIT AMET, CONSECTETUR
3ddda43c (auto\-uppercaser 2014\-07\-05 02:05:18 +0000 2) ADIPISCING ELIT, SED DO EIUSMOD TEMPOR
3ddda43c (auto\-uppercaser 2014\-07\-05 02:05:18 +0000 3) INCIDIDUNT UT LABORE ET DOLORE MAGNA
3ddda43c (auto\-uppercaser 2014\-07\-05 02:05:18 +0000 4) ALIQUA\&. UT ENIM AD MINIM VENIAM, QUIS
c6eb3bfa (lorem 2014\-08\-11 23:15:57 +0000 5) NOSTRUD EXERCITATION ULLAMCO LABORIS
3ddda43c (auto\-uppercaser 2014\-07\-05 02:05:18 +0000 6) NISI UT ALIQUIP EX EA COMMODO CONSEQUAT\&.
.fi
.if n \{\
.RE
.\}
.sp
.sp
Notice that almost the entire file has been blamed on a formatting change? You aren\(cqt interested in the uppercasing of the file\&. You want to know who wrote/modified those lines in the first place\&. Just tell hyper\-blame to ignore that commit:
.sp
.sp
.if n \{\
.RS 4
.\}
.nf
\fB$ git hyper\-blame \-i 3ddda43c ipsum\&.txt\fR
c6eb3bfa (lorem 2014\-08\-11 23:15:57 +0000 1) LOREM IPSUM DOLOR SIT AMET, CONSECTETUR
134200d1 (lorem 2014\-04\-10 08:54:46 +0000 2*) ADIPISCING ELIT, SED DO EIUSMOD TEMPOR
a34a1d0d (ipsum 2014\-04\-11 11:25:04 +0000 3*) INCIDIDUNT UT LABORE ET DOLORE MAGNA
134200d1 (lorem 2014\-04\-10 08:54:46 +0000 4*) ALIQUA\&. UT ENIM AD MINIM VENIAM, QUIS
c6eb3bfa (lorem 2014\-08\-11 23:15:57 +0000 5) NOSTRUD EXERCITATION ULLAMCO LABORIS
0f0d17bd (dolor 2014\-06\-02 11:31:48 +0000 6*) NISI UT ALIQUIP EX EA COMMODO CONSEQUAT\&.
.fi
.if n \{\
.RE
.\}
.sp
.sp
hyper\-blame places a * next to any line where it has skipped over an ignored commit, so you know that the line in question has been changed (by an ignored commit) since the given person wrote it\&.
.SH "BUGS"
.sp
.RS 4
.ie n \{\
\h'-04'\(bu\h'+03'\c
.\}
.el \{\
.sp -1
.IP \(bu 2.3
.\}
When a commit is ignored, hyper\-blame currently just blames the same line in the previous version of the file\&. This can be wildly inaccurate if the ignored commit adds or removes lines, resulting in a completely wrong commit being blamed\&.
.RE
.sp
.RS 4
.ie n \{\
\h'-04'\(bu\h'+03'\c
.\}
.el \{\
.sp -1
.IP \(bu 2.3
.\}
There is currently no way to pass the ignore list as a file\&.
.RE
.sp
.RS 4
.ie n \{\
\h'-04'\(bu\h'+03'\c
.\}
.el \{\
.sp -1
.IP \(bu 2.3
.\}
It should be possible for a git repository to configure an automatic list of commits to ignore (like
\&.gitignore), so that project owners can maintain a list of "big change" commits that are ignored by hyper\-blame by default\&.
.RE
.SH "SEE ALSO"
.sp
\fBgit-blame\fR(1)
.SH "CHROMIUM DEPOT_TOOLS"
.sp
Part of the chromium \fBdepot_tools\fR(7) suite\&. These tools are meant to assist with the development of chromium and related projects\&. Download the tools from \m[blue]\fBhere\fR\m[]\&\s-2\u[1]\d\s+2\&.
.SH "NOTES"
.IP " 1." 4
here
.RS 4
\%https://chromium.googlesource.com/chromium/tools/depot_tools.git
.RE
'\" t
.\" Title: depot_tools
.\" Author: [FIXME: author] [see http://docbook.sf.net/el/author]
.\" Generator: DocBook XSL Stylesheets v1.76.1 <http://docbook.sf.net/>
.\" Date: 09/09/2014
.\" Generator: DocBook XSL Stylesheets v1.78.1 <http://docbook.sf.net/>
.\" Date: 01/28/2016
.\" Manual: Chromium depot_tools Manual
.\" Source: depot_tools 40ce153
.\" Source: depot_tools 7143379
.\" Language: English
.\"
.TH "DEPOT_TOOLS" "7" "09/09/2014" "depot_tools 40ce153" "Chromium depot_tools Manual"
.TH "DEPOT_TOOLS" "7" "01/28/2016" "depot_tools 7143379" "Chromium depot_tools Manual"
.\" -----------------------------------------------------------------
.\" * Define some portability stuff
.\" -----------------------------------------------------------------
......@@ -62,6 +62,11 @@ Extract meta\-information expressed as footers in a commit message\&.
Freeze all changes on a branch (indexed and unindexed)\&.
.RE
.PP
\fBgit-hyper-blame\fR(1)
.RS 4
Like git blame, but with the ability to ignore or bypass certain commits\&.
.RE
.PP
\fBgit-map-branches\fR(1)
.RS 4
Helper script to display all local git branches with \(oqupstream\(cq hierarchy in colorized terminal format\&.
......
Like git blame, but with the ability to ignore or bypass certain commits.
#!/usr/bin/env bash
. git-hyper-blame.demo.common.sh
run git blame ipsum.txt
#!/usr/bin/env bash
. git-hyper-blame.demo.common.sh
IGNORE=$(git rev-parse HEAD^)
run git hyper-blame -i ${IGNORE:0:8} ipsum.txt
#!/usr/bin/env bash
. demo_repo.sh
# Construct a plausible file history.
set_user "lorem"
V1="Lorem ipsum dolor sit amet, consectetur*
adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna*
aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris**
nisi ut aliquip ex ea commodo consequat.*"
add "ipsum.txt" "$V1"
c "Added Lorem Ipsum"
tick 95408
set_user "ipsum"
V2="Lorem ipsum dolor sit amet, consectetur*
adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris**
nisi ut aliquip ex ea commodo consequat.*"
add "ipsum.txt" "$V2"
c "Change 1"
tick 4493194
set_user "dolor"
V3="Lorem ipsum dolor sit amet, consectetur*
adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris*
nisi ut aliquip ex ea commodo consequat."
add "ipsum.txt" "$V3"
c "Change 2"
tick 2817200
set_user "auto-uppercaser"
V4="LOREM IPSUM DOLOR SIT AMET, CONSECTETUR*
ADIPISCING ELIT, SED DO EIUSMOD TEMPOR
INCIDIDUNT UT LABORE ET DOLORE MAGNA
ALIQUA. UT ENIM AD MINIM VENIAM, QUIS
NOSTRUD EXERCITATION ULLAMCO LABORIS*
NISI UT ALIQUIP EX EA COMMODO CONSEQUAT."
add "ipsum.txt" "$V4"
c "Automatic upper-casing of all text."
tick 3273029
set_user "lorem"
V4="LOREM IPSUM DOLOR SIT AMET, CONSECTETUR
ADIPISCING ELIT, SED DO EIUSMOD TEMPOR
INCIDIDUNT UT LABORE ET DOLORE MAGNA
ALIQUA. UT ENIM AD MINIM VENIAM, QUIS
NOSTRUD EXERCITATION ULLAMCO LABORIS
NISI UT ALIQUIP EX EA COMMODO CONSEQUAT."
add "ipsum.txt" "$V4"
c "Change 3."
git-hyper-blame(1)
==================
NAME
----
git-hyper-blame -
include::_git-hyper-blame_desc.helper.txt[]
SYNOPSIS
--------
[verse]
'git hyper-blame' [-i <rev> [-i <rev> ...]] [<rev>] [--] <file>
DESCRIPTION
-----------
`git hyper-blame` is like `git blame` but it can ignore or "look through" a
given set of commits, to find the real culprit.
This is useful if you have a commit that makes sweeping changes that are
unlikely to be what you are looking for in a blame, such as mass reformatting or
renaming. By adding these commits to the hyper-blame ignore list, `git
hyper-blame` will look past these commits to find the previous commit that
touched a given line.
Follows the normal `blame` syntax: annotates `<file>` with the revision that
last modified each line. Optional `<rev>` specifies the revision of `<file>` to
start from.
OPTIONS
-------
-i <rev>::
A revision to ignore. Can be specified as many times as needed.
EXAMPLE
-------
Let's run `git blame` on a file:
demo:1[]
Notice that almost the entire file has been blamed on a formatting change? You
aren't interested in the uppercasing of the file. You want to know who
wrote/modified those lines in the first place. Just tell `hyper-blame` to ignore
that commit:
demo:2[]
`hyper-blame` places a `*` next to any line where it has skipped over an ignored
commit, so you know that the line in question has been changed (by an ignored
commit) since the given person wrote it.
BUGS
----
- When a commit is ignored, hyper-blame currently just blames the same line in
the previous version of the file. This can be wildly inaccurate if the ignored
commit adds or removes lines, resulting in a completely wrong commit being
blamed.
- There is currently no way to pass the ignore list as a file.
- It should be possible for a git repository to configure an automatic list of
commits to ignore (like `.gitignore`), so that project owners can maintain a
list of "big change" commits that are ignored by hyper-blame by default.
SEE ALSO
--------
linkgit:git-blame[1]
include::_footer.txt[]
// vim: ft=asciidoc:
......@@ -176,15 +176,15 @@ class GitReadOnlyFunctionsTest(git_test_utils.GitRepoReadOnlyTestBase,
COMMIT_C = {
'some/files/file2': {
'mode': 0755,
'data': 'file2 - vanilla'},
'data': 'file2 - vanilla\n'},
}
COMMIT_E = {
'some/files/file2': {'data': 'file2 - merged'},
'some/files/file2': {'data': 'file2 - merged\n'},
}
COMMIT_D = {
'some/files/file2': {'data': 'file2 - vanilla\nfile2 - merged'},
'some/files/file2': {'data': 'file2 - vanilla\nfile2 - merged\n'},
}
def testHashes(self):
......@@ -259,6 +259,40 @@ class GitReadOnlyFunctionsTest(git_test_utils.GitRepoReadOnlyTestBase,
self.repo.git('config', 'branch.master.dormant', 'true')
self.assertTrue(self.repo.run(self.gc.is_dormant, 'master'))
def testBlame(self):
def get_porcelain_for_commit(commit_name, lines):
format_string = ('%H {}\nauthor %an\nauthor-mail <%ae>\nauthor-time %at\n'
'author-tz +0000\ncommitter %cn\ncommitter-mail <%ce>\n'
'committer-time %ct\ncommitter-tz +0000\nsummary {}')
format_string = format_string.format(lines, commit_name)
info = self.repo.show_commit(commit_name, format_string=format_string)
return info.split('\n')
# Expect to blame line 1 on C, line 2 on E.
c_short = self.repo['C'][:8]
c_author = self.repo.show_commit('C', format_string='%an %ai')
e_short = self.repo['E'][:8]
e_author = self.repo.show_commit('E', format_string='%an %ai')
expected_output = ['%s (%s 1) file2 - vanilla' % (c_short, c_author),
'%s (%s 2) file2 - merged' % (e_short, e_author)]
self.assertEqual(expected_output,
self.repo.run(self.gc.blame, 'some/files/file2',
'tag_D').split('\n'))
# Test porcelain.
expected_output = []
expected_output.extend(get_porcelain_for_commit('C', '1 1 1'))
expected_output.append('previous %s some/files/file2' % self.repo['B'])
expected_output.append('filename some/files/file2')
expected_output.append('\tfile2 - vanilla')
expected_output.extend(get_porcelain_for_commit('E', '1 2 1'))
expected_output.append('previous %s some/files/file2' % self.repo['B'])
expected_output.append('filename some/files/file2')
expected_output.append('\tfile2 - merged')
self.assertEqual(expected_output,
self.repo.run(self.gc.blame, 'some/files/file2',
'tag_D', porcelain=True).split('\n'))
def testParseCommitrefs(self):
ret = self.repo.run(
self.gc.parse_commitrefs, *[
......@@ -280,6 +314,17 @@ class GitReadOnlyFunctionsTest(git_test_utils.GitRepoReadOnlyTestBase,
with self.assertRaisesRegexp(Exception, r"one of \('master', 'bananas'\)"):
self.repo.run(self.gc.parse_commitrefs, 'master', 'bananas')
def testRepoRoot(self):
def cd_and_repo_root(path):
print(os.getcwd())
os.chdir(path)
return self.gc.repo_root()
self.assertEqual(self.repo.repo_path, self.repo.run(self.gc.repo_root))
# cd to a subdirectory; repo_root should still return the root dir.
self.assertEqual(self.repo.repo_path,
self.repo.run(cd_and_repo_root, 'some/files'))
def testTags(self):
self.assertEqual(set(self.repo.run(self.gc.tags)),
{'tag_'+l for l in 'ABCDE'})
......
#!/usr/bin/env python
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Tests for git_dates."""
import datetime
import os
import sys
import unittest
DEPOT_TOOLS_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, DEPOT_TOOLS_ROOT)
from testing_support import coverage_utils
class GitDatesTestBase(unittest.TestCase):
@classmethod
def setUpClass(cls):
super(GitDatesTestBase, cls).setUpClass()
import git_dates
cls.git_dates = git_dates
class GitDatesTest(GitDatesTestBase):
def testTimestampOffsetToDatetime(self):
# 2016-01-25 06:25:43 UTC
timestamp = 1453703143
offset = '+1100'
expected_tz = self.git_dates.FixedOffsetTZ(datetime.timedelta(hours=11), '')
expected = datetime.datetime(2016, 1, 25, 17, 25, 43, tzinfo=expected_tz)
result = self.git_dates.timestamp_offset_to_datetime(timestamp, offset)
self.assertEquals(expected, result)
self.assertEquals(datetime.timedelta(hours=11), result.utcoffset())
self.assertEquals('+1100', result.tzname())
self.assertEquals(datetime.timedelta(0), result.dst())
offset = '-0800'
expected_tz = self.git_dates.FixedOffsetTZ(datetime.timedelta(hours=-8), '')
expected = datetime.datetime(2016, 1, 24, 22, 25, 43, tzinfo=expected_tz)
result = self.git_dates.timestamp_offset_to_datetime(timestamp, offset)
self.assertEquals(expected, result)
self.assertEquals(datetime.timedelta(hours=-8), result.utcoffset())
self.assertEquals('-0800', result.tzname())
self.assertEquals(datetime.timedelta(0), result.dst())
# Invalid offset.
offset = '-08xx'
expected_tz = self.git_dates.FixedOffsetTZ(datetime.timedelta(hours=0), '')
expected = datetime.datetime(2016, 1, 25, 6, 25, 43, tzinfo=expected_tz)
result = self.git_dates.timestamp_offset_to_datetime(timestamp, offset)
self.assertEquals(expected, result)
self.assertEquals(datetime.timedelta(hours=0), result.utcoffset())
self.assertEquals('UTC', result.tzname())
self.assertEquals(datetime.timedelta(0), result.dst())
# Offset out of range.
offset = '+2400'
self.assertRaises(ValueError, self.git_dates.timestamp_offset_to_datetime,
timestamp, offset)
def testDatetimeString(self):
tz = self.git_dates.FixedOffsetTZ(datetime.timedelta(hours=11), '')
dt = datetime.datetime(2016, 1, 25, 17, 25, 43, tzinfo=tz)
self.assertEquals('2016-01-25 17:25:43 +1100',
self.git_dates.datetime_string(dt))
tz = self.git_dates.FixedOffsetTZ(datetime.timedelta(hours=-8), '')
dt = datetime.datetime(2016, 1, 24, 22, 25, 43, tzinfo=tz)
self.assertEquals('2016-01-24 22:25:43 -0800',
self.git_dates.datetime_string(dt))
if __name__ == '__main__':
sys.exit(coverage_utils.covered_main(
os.path.join(DEPOT_TOOLS_ROOT, 'git_dates.py')))
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment