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-d872f2087c98changes/01/332501/1
parent
b1f0581df4
commit
819375653b
@ -0,0 +1,6 @@
|
||||
#!/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)
|
@ -0,0 +1,62 @@
|
||||
# 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)
|
@ -0,0 +1,266 @@
|
||||
#!/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))
|
@ -0,0 +1,871 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
|
||||
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=UTF-8" />
|
||||
<meta name="generator" content="AsciiDoc 8.6.9" />
|
||||
<title>git-hyper-blame(1)</title>
|
||||
<style type="text/css">
|
||||
/* Shared CSS for AsciiDoc xhtml11 and html5 backends */
|
||||
|
||||
/* Default font. */
|
||||
body {
|
||||
font-family: Georgia,serif;
|
||||
}
|
||||
|
||||
/* Title font. */
|
||||
h1, h2, h3, h4, h5, h6,
|
||||
div.title, caption.title,
|
||||
thead, p.table.header,
|
||||
#toctitle,
|
||||
#author, #revnumber, #revdate, #revremark,
|
||||
#footer {
|
||||
font-family: Arial,Helvetica,sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 1em 5% 1em 5%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:visited {
|
||||
color: fuchsia;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
color: navy;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
color: #083194;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: #527bbd;
|
||||
margin-top: 1.2em;
|
||||
margin-bottom: 0.5em;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
border-bottom: 2px solid silver;
|
||||
}
|
||||
h2 {
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
h3 {
|
||||
float: left;
|
||||
}
|
||||
h3 + * {
|
||||
clear: left;
|
||||
}
|
||||
h5 {
|
||||
font-size: 1.0em;
|
||||
}
|
||||
|
||||
div.sectionbody {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1px solid silver;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
ul, ol, li > p {
|
||||
margin-top: 0;
|
||||
}
|
||||
ul > li { color: #aaa; }
|
||||
ul > li > * { color: black; }
|
||||
|
||||
.monospaced, code, pre {
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
font-size: inherit;
|
||||
color: navy;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
#author {
|
||||
color: #527bbd;
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
#email {
|
||||
}
|
||||
#revnumber, #revdate, #revremark {
|
||||
}
|
||||
|
||||
#footer {
|
||||
font-size: small;
|
||||
border-top: 2px solid silver;
|
||||
padding-top: 0.5em;
|
||||
margin-top: 4.0em;
|
||||
}
|
||||
#footer-text {
|
||||
float: left;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
#footer-badges {
|
||||
float: right;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
#preamble {
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
div.imageblock, div.exampleblock, div.verseblock,
|
||||
div.quoteblock, div.literalblock, div.listingblock, div.sidebarblock,
|
||||
div.admonitionblock {
|
||||
margin-top: 1.0em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
div.admonitionblock {
|
||||
margin-top: 2.0em;
|
||||
margin-bottom: 2.0em;
|
||||
margin-right: 10%;
|
||||
color: #606060;
|
||||
}
|
||||
|
||||
div.content { /* Block element content. */
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Block element titles. */
|
||||
div.title, caption.title {
|
||||
color: #527bbd;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
margin-top: 1.0em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
div.title + * {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
td div.title:first-child {
|
||||
margin-top: 0.0em;
|
||||
}
|
||||
div.content div.title:first-child {
|
||||
margin-top: 0.0em;
|
||||
}
|
||||
div.content + div.title {
|
||||
margin-top: 0.0em;
|
||||
}
|
||||
|
||||
div.sidebarblock > div.content {
|
||||
background: #ffffee;
|
||||
border: 1px solid #dddddd;
|
||||
border-left: 4px solid #f0f0f0;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
div.listingblock > div.content {
|
||||
border: 1px solid #dddddd;
|
||||
border-left: 5px solid #f0f0f0;
|
||||
background: #f8f8f8;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
div.quoteblock, div.verseblock {
|
||||
padding-left: 1.0em;
|
||||
margin-left: 1.0em;
|
||||
margin-right: 10%;
|
||||
border-left: 5px solid #f0f0f0;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
div.quoteblock > div.attribution {
|
||||
padding-top: 0.5em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div.verseblock > pre.content {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
div.verseblock > div.attribution {
|
||||
padding-top: 0.75em;
|
||||
text-align: left;
|
||||
}
|
||||
/* DEPRECATED: Pre version 8.2.7 verse style literal block. */
|
||||
div.verseblock + div.attribution {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.admonitionblock .icon {
|
||||
vertical-align: top;
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
color: #527bbd;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
div.admonitionblock td.content {
|
||||
padding-left: 0.5em;
|
||||
border-left: 3px solid #dddddd;
|
||||
}
|
||||
|
||||
div.exampleblock > div.content {
|
||||
border-left: 3px solid #dddddd;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
div.imageblock div.content { padding-left: 0; }
|
||||
span.image img { border-style: none; vertical-align: text-bottom; }
|
||||
a.image:visited { color: white; }
|
||||
|
||||
dl {
|
||||
margin-top: 0.8em;
|
||||
margin-bottom: 0.8em;
|
||||
}
|
||||
dt {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0;
|
||||
font-style: normal;
|
||||
color: navy;
|
||||
}
|
||||
dd > *:first-child {
|
||||
margin-top: 0.1em;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
list-style-position: outside;
|
||||
}
|
||||
ol.arabic {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
ol.loweralpha {
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
ol.upperalpha {
|
||||
list-style-type: upper-alpha;
|
||||
}
|
||||
ol.lowerroman {
|
||||
list-style-type: lower-roman;
|
||||
}
|
||||
ol.upperroman {
|
||||
list-style-type: upper-roman;
|
||||
}
|
||||
|
||||
div.compact ul, div.compact ol,
|
||||
div.compact p, div.compact p,
|
||||
div.compact div, div.compact div {
|
||||
margin-top: 0.1em;
|
||||
margin-bottom: 0.1em;
|
||||
}
|
||||
|
||||
tfoot {
|
||||
font-weight: bold;
|
||||
}
|
||||
td > div.verse {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
div.hdlist {
|
||||
margin-top: 0.8em;
|
||||
margin-bottom: 0.8em;
|
||||
}
|
||||
div.hdlist tr {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
dt.hdlist1.strong, td.hdlist1.strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
td.hdlist1 {
|
||||
vertical-align: top;
|
||||
font-style: normal;
|
||||
padding-right: 0.8em;
|
||||
color: navy;
|
||||
}
|
||||
td.hdlist2 {
|
||||
vertical-align: top;
|
||||
}
|
||||
div.hdlist.compact tr {
|
||||
margin: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.comment {
|
||||
background: yellow;
|
||||
}
|
||||
|
||||
.footnote, .footnoteref {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
span.footnote, span.footnoteref {
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
#footnotes {
|
||||
margin: 20px 0 20px 0;
|
||||
padding: 7px 0 0 0;
|
||||
}
|
||||
|
||||
#footnotes div.footnote {
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
|
||||
#footnotes hr {
|
||||
border: none;
|
||||
border-top: 1px solid silver;
|
||||
height: 1px;
|
||||
text-align: left;
|
||||
margin-left: 0;
|
||||
width: 20%;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
div.colist td {
|
||||
padding-right: 0.5em;
|
||||
padding-bottom: 0.3em;
|
||||
vertical-align: top;
|
||||
}
|
||||
div.colist td img {
|
||||
margin-top: 0.3em;
|
||||
}
|
||||
|
||||
@media print {
|
||||
#footer-badges { display: none; }
|
||||
}
|
||||
|
||||
#toc {
|
||||
margin-bottom: 2.5em;
|
||||
}
|
||||
|
||||
#toctitle {
|
||||
color: #527bbd;
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
margin-top: 1.0em;
|
||||
margin-bottom: 0.1em;
|
||||
}
|
||||
|
||||
div.toclevel0, div.toclevel1, div.toclevel2, div.toclevel3, div.toclevel4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
div.toclevel2 {
|
||||
margin-left: 2em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
div.toclevel3 {
|
||||
margin-left: 4em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
div.toclevel4 {
|
||||
margin-left: 6em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
span.aqua { color: aqua; }
|
||||
span.black { color: black; }
|
||||
span.blue { color: blue; }
|
||||
span.fuchsia { color: fuchsia; }
|
||||
span.gray { color: gray; }
|
||||
span.green { color: green; }
|
||||
span.lime { color: lime; }
|
||||
span.maroon { color: maroon; }
|
||||
span.navy { color: navy; }
|
||||
span.olive { color: olive; }
|
||||
span.purple { color: purple; }
|
||||
span.red { color: red; }
|
||||
span.silver { color: silver; }
|
||||
span.teal { color: teal; }
|
||||
span.white { color: white; }
|
||||
span.yellow { color: yellow; }
|
||||
|
||||
span.aqua-background { background: aqua; }
|
||||
span.black-background { background: black; }
|
||||
span.blue-background { background: blue; }
|
||||
span.fuchsia-background { background: fuchsia; }
|
||||
span.gray-background { background: gray; }
|
||||
span.green-background { background: green; }
|
||||
span.lime-background { background: lime; }
|
||||
span.maroon-background { background: maroon; }
|
||||
span.navy-background { background: navy; }
|
||||
span.olive-background { background: olive; }
|
||||
span.purple-background { background: purple; }
|
||||
span.red-background { background: red; }
|
||||
span.silver-background { background: silver; }
|
||||
span.teal-background { background: teal; }
|
||||
span.white-background { background: white; }
|
||||
span.yellow-background { background: yellow; }
|
||||
|
||||
span.big { font-size: 2em; }
|
||||
span.small { font-size: 0.6em; }
|
||||
|
||||
span.underline { text-decoration: underline; }
|
||||
span.overline { text-decoration: overline; }
|
||||
span.line-through { text-decoration: line-through; }
|
||||
|
||||
div.unbreakable { page-break-inside: avoid; }
|
||||
|
||||
|
||||
/*
|
||||
* xhtml11 specific
|
||||
*
|
||||
* */
|
||||
|
||||
div.tableblock {
|
||||
margin-top: 1.0em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
div.tableblock > table {
|
||||
border: 3px solid #527bbd;
|
||||
}
|
||||
thead, p.table.header {
|
||||
font-weight: bold;
|
||||
color: #527bbd;
|
||||
}
|
||||
p.table {
|
||||
margin-top: 0;
|
||||
}
|
||||
/* Because the table frame attribute is overriden by CSS in most browsers. */
|
||||
div.tableblock > table[frame="void"] {
|
||||
border-style: none;
|
||||
}
|
||||
div.tableblock > table[frame="hsides"] {
|
||||
border-left-style: none;
|
||||
border-right-style: none;
|
||||
}
|
||||
div.tableblock > table[frame="vsides"] {
|
||||
border-top-style: none;
|
||||
border-bottom-style: none;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* html5 specific
|
||||
*
|
||||
* */
|
||||
|
||||
table.tableblock {
|
||||
margin-top: 1.0em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
thead, p.tableblock.header {
|
||||
font-weight: bold;
|
||||
color: #527bbd;
|
||||
}
|
||||
p.tableblock {
|
||||
margin-top: 0;
|
||||
}
|
||||
table.tableblock {
|
||||
border-width: 3px;
|
||||
border-spacing: 0px;
|
||||
border-style: solid;
|
||||
border-color: #527bbd;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th.tableblock, td.tableblock {
|
||||
border-width: 1px;
|
||||
padding: 4px;
|
||||
border-style: solid;
|
||||
border-color: #527bbd;
|
||||
}
|
||||
|
||||
table.tableblock.frame-topbot {
|
||||
border-left-style: hidden;
|
||||
border-right-style: hidden;
|
||||
}
|
||||
table.tableblock.frame-sides {
|
||||
border-top-style: hidden;
|
||||
border-bottom-style: hidden;
|
||||
}
|
||||
table.tableblock.frame-none {
|
||||
border-style: hidden;
|
||||
}
|
||||
|
||||
th.tableblock.halign-left, td.tableblock.halign-left {
|
||||
text-align: left;
|
||||
}
|
||||
th.tableblock.halign-center, td.tableblock.halign-center {
|
||||
text-align: center;
|
||||
}
|
||||
th.tableblock.halign-right, td.tableblock.halign-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
th.tableblock.valign-top, td.tableblock.valign-top {
|
||||
vertical-align: top;
|
||||
}
|
||||
th.tableblock.valign-middle, td.tableblock.valign-middle {
|
||||
vertical-align: middle;
|
||||
}
|
||||
th.tableblock.valign-bottom, td.tableblock.valign-bottom {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* manpage specific
|
||||
*
|
||||
* */
|
||||
|
||||
body.manpage h1 {
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
border-top: 2px solid silver;
|
||||
border-bottom: 2px solid silver;
|
||||
}
|
||||
body.manpage h2 {
|
||||
border-style: none;
|
||||
}
|
||||
body.manpage div.sectionbody {
|
||||
margin-left: 3em;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body.manpage div#toc { display: none; }
|
||||
}
|
||||
|
||||
|
||||
div.listingblock > div.content {
|
||||
background: rgb(28, 28, 28);
|
||||
}
|
||||
|
||||
div.listingblock > div > pre > code {
|
||||
color: rgb(187, 187, 187);
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
/*<+'])');
|
||||
// Function that scans the DOM tree for header elements (the DOM2
|
||||
// nodeIterator API would be a better technique but not supported by all
|
||||
// browsers).
|
||||
var iterate = function (el) {
|
||||
for (var i = el.firstChild; i != null; i = i.nextSibling) {
|
||||
if (i.nodeType == 1 /* Node.ELEMENT_NODE */) {
|
||||
var mo = re.exec(i.tagName);
|
||||
if (mo && (i.getAttribute("class") || i.getAttribute("className")) != "float") {
|
||||
result[result.length] = new TocEntry(i, getText(i), mo[1]-1);
|
||||
}
|
||||
iterate(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
iterate(el);
|
||||
return result;
|
||||
}
|
||||
|
||||
var toc = document.getElementById("toc");
|
||||
if (!toc) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete existing TOC entries in case we're reloading the TOC.
|
||||
var tocEntriesToRemove = [];
|
||||
var i;
|
||||
for (i = 0; i < toc.childNodes.length; i++) {
|
||||
var entry = toc.childNodes[i];
|
||||
if (entry.nodeName.toLowerCase() == 'div'
|
||||
&& entry.getAttribute("class")
|
||||
&& entry.getAttribute("class").match(/^toclevel/))
|
||||
tocEntriesToRemove.push(entry);
|
||||
}
|
||||
for (i = 0; i < tocEntriesToRemove.length; i++) {
|
||||
toc.removeChild(tocEntriesToRemove[i]);
|
||||
}
|
||||
|
||||
// Rebuild TOC entries.
|
||||
var entries = tocEntries(document.getElementById("content"), toclevels);
|
||||
for (var i = 0; i < entries.length; ++i) {
|
||||
var entry = entries[i];
|
||||
if (entry.element.id == "")
|
||||
entry.element.id = "_toc_" + i;
|
||||
var a = document.createElement("a");
|
||||
a.href = "#" + entry.element.id;
|
||||
a.appendChild(document.createTextNode(entry.text));
|
||||
var div = document.createElement("div");
|
||||
div.appendChild(a);
|
||||
div.className = "toclevel" + entry.toclevel;
|
||||
toc.appendChild(div);
|
||||
}
|
||||
if (entries.length == 0)
|
||||
toc.parentNode.removeChild(toc);
|
||||
},
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// Footnotes generator
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
/* Based on footnote generation code from:
|
||||
* http://www.brandspankingnew.net/archive/2005/07/format_footnote.html
|
||||
*/
|
||||
|
||||
footnotes: function () {
|
||||
// Delete existing footnote entries in case we're reloading the footnodes.
|
||||
var i;
|
||||
var noteholder = document.getElementById("footnotes");
|
||||
if (!noteholder) {
|
||||
return;
|
||||
}
|
||||
var entriesToRemove = [];
|
||||
for (i = 0; i < noteholder.childNodes.length; i++) {
|
||||
var entry = noteholder.childNodes[i];
|
||||
if (entry.nodeName.toLowerCase() == 'div' && entry.getAttribute("class") == "footnote")
|
||||
entriesToRemove.push(entry);
|
||||
}
|
||||
for (i = 0; i < entriesToRemove.length; i++) {
|
||||
noteholder.removeChild(entriesToRemove[i]);
|
||||
}
|
||||
|
||||
// Rebuild footnote entries.
|
||||
var cont = document.getElementById("content");
|
||||
var spans = cont.getElementsByTagName("span");
|
||||
var refs = {};
|
||||
var n = 0;
|
||||
for (i=0; i<spans.length; i++) {
|
||||
if (spans[i].className == "footnote") {
|
||||
n++;
|
||||
var note = spans[i].getAttribute("data-note");
|
||||
if (!note) {
|
||||
// Use [\s\S] in place of . so multi-line matches work.
|
||||
// Because JavaScript has no s (dotall) regex flag.
|
||||
note = spans[i].innerHTML.match(/\s*\[([\s\S]*)]\s*/)[1];
|
||||
spans[i].innerHTML =
|
||||
"[<a id='_footnoteref_" + n + "' href='#_footnote_" + n +
|
||||
"' title='View footnote' class='footnote'>" + n + "</a>]";
|
||||
spans[i].setAttribute("data-note", note);
|
||||
}
|
||||
noteholder.innerHTML +=
|
||||
"<div class='footnote' id='_footnote_" + n + "'>" +
|
||||
"<a href='#_footnoteref_" + n + "' title='Return to text'>" +
|
||||
n + "</a>. " + note + "</div>";
|
||||
var id =spans[i].getAttribute("id");
|
||||
if (id != null) refs["#"+id] = n;
|
||||
}
|
||||
}
|
||||
if (n == 0)
|
||||
noteholder.parentNode.removeChild(noteholder);
|
||||
else {
|
||||
// Process footnoterefs.
|
||||
for (i=0; i<spans.length; i++) {
|
||||
if (spans[i].className == "footnoteref") {
|
||||
var href = spans[i].getElementsByTagName("a")[0].getAttribute("href");
|
||||
href = href.match(/#.*/)[0]; // Because IE return full URL.
|
||||
n = refs[href];
|
||||
spans[i].innerHTML =
|
||||
"[<a href='#_footnote_" + n +
|
||||
"' title='View footnote' class='footnote'>" + n + "</a>]";
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
install: function(toclevels) {
|
||||
var timerId;
|
||||
|
||||
function reinstall() {
|
||||
asciidoc.footnotes();
|
||||
if (toclevels) {
|
||||
asciidoc.toc(toclevels);
|
||||
}
|
||||
}
|
||||
|
||||
function reinstallAndRemoveTimer() {
|
||||
clearInterval(timerId);
|
||||
reinstall();
|
||||
}
|
||||
|
||||
timerId = setInterval(reinstall, 500);
|
||||
if (document.addEventListener)
|
||||
document.addEventListener("DOMContentLoaded", reinstallAndRemoveTimer, false);
|
||||
else
|
||||
window.onload = reinstallAndRemoveTimer;
|
||||
}
|
||||
|
||||
}
|
||||
asciidoc.install();
|
||||
/*]]>*/
|
||||
</script>
|
||||
</head>
|
||||
<body class="manpage">
|
||||
<div id="header">
|
||||
<h1>
|
||||
git-hyper-blame(1) Manual Page
|
||||
</h1>
|
||||
<h2>NAME</h2>
|
||||
<div class="sectionbody">
|
||||
<p>git-hyper-blame -
|
||||
Like git blame, but with the ability to ignore or bypass certain commits.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="content">
|
||||
<div class="sect1">
|
||||
<h2 id="_synopsis">SYNOPSIS</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="verseblock">
|
||||
<pre class="content"><em>git hyper-blame</em> [-i <rev> [-i <rev> …]] [<rev>] [--] <file></pre>
|
||||
<div class="attribution">
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sect1">
|
||||
<h2 id="_description">DESCRIPTION</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="paragraph"><p><code>git hyper-blame</code> is like <code>git blame</code> but it can ignore or "look through" a
|
||||
given set of commits, to find the real culprit.</p></div>
|
||||
<div class="paragraph"><p>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, <code>git
|
||||
hyper-blame</code> will look past these commits to find the previous commit that
|
||||
touched a given line.</p></div>
|
||||
<div class="paragraph"><p>Follows the normal <code>blame</code> syntax: annotates <code><file></code> with the revision that
|
||||
last modified each line. Optional <code><rev></code> specifies the revision of <code><file></code> to
|
||||
start from.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sect1">
|
||||
<h2 id="_options">OPTIONS</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="dlist"><dl>
|
||||
<dt class="hdlist1">
|
||||
-i <rev>
|
||||
</dt>
|
||||
<dd>
|
||||
<p>
|
||||
A revision to ignore. Can be specified as many times as needed.
|
||||
</p>
|
||||
</dd>
|
||||
</dl></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sect1">
|
||||
<h2 id="_example">EXAMPLE</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="paragraph"><p>Let’s run <code>git blame</code> on a file:</p></div>
|
||||
<div class="paragraph"><p></p></div><div class="listingblock"><div class="content"><pre><code><span style="font-weight: bold; color: #ffffff">$ git blame ipsum.txt</span>
|
||||
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.
|
||||
</code></pre></div></div><p><div class="paragraph"></p></div>
|
||||
<div class="paragraph"><p>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 <code>hyper-blame</code> to ignore
|
||||
that commit:</p></div>
|
||||
<div class="paragraph"><p></p></div><div class="listingblock"><div class="content"><pre><code><span style="font-weight: bold; color: #ffffff">$ git hyper-blame -i 3ddda43c ipsum.txt</span>
|
||||
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.
|
||||
</code></pre></div></div><p><div class="paragraph"></p></div>
|
||||
<div class="paragraph"><p><code>hyper-blame</code> places a <code>*</code> 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.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sect1">
|
||||
<h2 id="_bugs">BUGS</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="ulist"><ul>
|
||||
<li>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
There is currently no way to pass the ignore list as a file.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
It should be possible for a git repository to configure an automatic list of
|
||||
commits to ignore (like <code>.gitignore</code>), so that project owners can maintain a
|
||||
list of "big change" commits that are ignored by hyper-blame by default.
|
||||
</p>
|
||||
</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sect1">
|
||||
<h2 id="_see_also">SEE ALSO</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="paragraph"><p><a href="git-blame.html">git-blame(1)</a></p></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sect1">
|
||||
<h2 id="_chromium_depot_tools">CHROMIUM DEPOT_TOOLS</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="paragraph"><p>Part of the chromium <a href="depot_tools.html">depot_tools(7)</a> suite. These tools are meant to
|
||||
assist with the development of chromium and related projects. Download the tools
|
||||
from <a href="https://chromium.googlesource.com/chromium/tools/depot_tools.git">here</a>.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="footnotes"><hr /></div>
|
||||
<div id="footer">
|
||||
<div id="footer-text">
|
||||
Last updated 2016-01-28 16:40:21 AEDT
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,142 @@
|
||||
'\" 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
|
@ -0,0 +1 @@
|
||||
Like git blame, but with the ability to ignore or bypass certain commits.
|
@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
. git-hyper-blame.demo.common.sh
|
||||
run git blame ipsum.txt
|
@ -0,0 +1,4 @@
|
||||
#!/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
|
@ -0,0 +1,57 @@
|
||||
#!/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."
|
@ -0,0 +1,72 @@
|
||||
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:
|
@ -0,0 +1,78 @@
|
||||
#!/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')))
|
@ -0,0 +1,367 @@
|
||||
#!/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 shutil
|
||||
import StringIO
|
||||
import sys
|
||||
import tempfile
|
||||
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
|
||||
from testing_support import git_test_utils
|
||||
|
||||
import git_common
|
||||
|
||||
|
||||
class GitHyperBlameTestBase(git_test_utils.GitRepoReadOnlyTestBase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(GitHyperBlameTestBase, cls).setUpClass()
|
||||
import git_hyper_blame
|
||||
cls.git_hyper_blame = git_hyper_blame
|
||||
|
||||
def run_hyperblame(self, ignored, filename, revision):
|
||||
stdout = StringIO.StringIO()
|
||||
stderr = StringIO.StringIO()
|
||||
ignored = [self.repo[c] for c in ignored]
|
||||
retval = self.repo.run(self.git_hyper_blame.hyper_blame, ignored, filename,
|
||||
revision=revision, out=stdout, err=stderr)
|
||||
return retval, stdout.getvalue().rstrip().split('\n')
|
||||
|
||||
def blame_line(self, commit_name, rest, filename=None):
|
||||
"""Generate a blame line from a commit.
|
||||
|
||||
Args:
|
||||
commit_name: The commit's schema name.
|
||||
rest: The blame line after the timestamp. e.g., '2) file2 - merged'.
|
||||
"""
|
||||
short = self.repo[commit_name][:8]
|
||||
start = '%s %s' % (short, filename) if filename else short
|
||||
author = self.repo.show_commit(commit_name, format_string='%an %ai')
|
||||
return '%s (%s %s' % (start, author, rest)
|
||||
|
||||
class GitHyperBlameMainTest(GitHyperBlameTestBase):
|
||||
"""End-to-end tests on a very simple repo."""
|
||||
REPO_SCHEMA = "A B C"
|
||||
|
||||
COMMIT_A = {
|
||||
'some/files/file': {'data': 'line 1\nline 2\n'},
|
||||
}
|
||||
|
||||
COMMIT_B = {
|
||||
'some/files/file': {'data': 'line 1\nline 2.1\n'},
|
||||
}
|
||||
|
||||
COMMIT_C = {
|
||||
'some/files/file': {'data': 'line 1.1\nline 2.1\n'},
|
||||
}
|
||||
|
||||
def testBasicBlame(self):
|
||||
"""Tests the main function (simple end-to-end test with no ignores)."""
|
||||
expected_output = [self.blame_line('C', '1) line 1.1'),
|
||||
self.blame_line('B', '2) line 2.1')]
|
||||
stdout = StringIO.StringIO()
|
||||
stderr = StringIO.StringIO()
|
||||
retval = self.repo.run(self.git_hyper_blame.main,
|
||||
args=['tag_C', 'some/files/file'], stdout=stdout,
|
||||
stderr=stderr)
|
||||
self.assertEqual(0, retval)
|
||||
self.assertEqual(expected_output, stdout.getvalue().rstrip().split('\n'))
|
||||
self.assertEqual('', stderr.getvalue())
|
||||
|
||||
def testIgnoreSimple(self):
|
||||
"""Tests the main function (simple end-to-end test with ignores)."""
|
||||
expected_output = [self.blame_line('C', ' 1) line 1.1'),
|
||||
self.blame_line('A', '2*) line 2.1')]
|
||||
stdout = StringIO.StringIO()
|
||||
stderr = StringIO.StringIO()
|
||||
retval = self.repo.run(self.git_hyper_blame.main,
|
||||
args=['-i', 'tag_B', 'tag_C', 'some/files/file'],
|
||||
stdout=stdout, stderr=stderr)
|
||||
self.assertEqual(0, retval)
|
||||
self.assertEqual(expected_output, stdout.getvalue().rstrip().split('\n'))
|
||||
self.assertEqual('', stderr.getvalue())
|
||||
|
||||
def testBadRepo(self):
|
||||
"""Tests the main function (not in a repo)."""
|
||||
# Make a temp dir that has no .git directory.
|
||||
curdir = os.getcwd()
|
||||
tempdir = tempfile.mkdtemp(suffix='_nogit', prefix='git_repo')
|
||||
try:
|
||||
os.chdir(tempdir)
|
||||
stdout = StringIO.StringIO()
|
||||
stderr = StringIO.StringIO()
|
||||
retval = self.git_hyper_blame.main(
|
||||
args=['-i', 'tag_B', 'tag_C', 'some/files/file'], stdout=stdout,
|
||||
stderr=stderr)
|
||||
finally:
|
||||
shutil.rmtree(tempdir)
|
||||
os.chdir(curdir)
|
||||
|
||||
self.assertNotEqual(0, retval)
|
||||
self.assertEqual('', stdout.getvalue())
|
||||
self.assertRegexpMatches(stderr.getvalue(), '^fatal: Not a git repository')
|
||||
|
||||
def testBadFilename(self):
|
||||
"""Tests the main function (bad filename)."""
|
||||
stdout = StringIO.StringIO()
|
||||
stderr = StringIO.StringIO()
|
||||
retval = self.repo.run(self.git_hyper_blame.main,
|
||||
args=['-i', 'tag_B', 'tag_C', 'some/files/xxxx'],
|
||||
stdout=stdout, stderr=stderr)
|
||||
self.assertNotEqual(0, retval)
|
||||
self.assertEqual('', stdout.getvalue())
|
||||
self.assertEqual('fatal: no such path some/files/xxxx in %s\n' %
|
||||
self.repo['C'], stderr.getvalue())
|
||||
|
||||
def testBadRevision(self):
|
||||
"""Tests the main function (bad revision to blame from)."""
|
||||
stdout = StringIO.StringIO()
|
||||
stderr = StringIO.StringIO()
|
||||
retval = self.repo.run(self.git_hyper_blame.main,
|
||||
args=['-i', 'tag_B', 'xxxx', 'some/files/file'],
|
||||
stdout=stdout, stderr=stderr)
|
||||
self.assertNotEqual(0, retval)
|
||||
self.assertEqual('', stdout.getvalue())
|
||||
self.assertRegexpMatches(stderr.getvalue(),
|
||||
'^fatal: ambiguous argument \'xxxx\': unknown '
|
||||
'revision or path not in the working tree.')
|
||||
|
||||
def testBadIgnore(self):
|
||||
"""Tests the main function (bad revision passed to -i)."""
|
||||
stdout = StringIO.StringIO()
|
||||
stderr = StringIO.StringIO()
|
||||
retval = self.repo.run(self.git_hyper_blame.main,
|
||||
args=['-i', 'xxxx', 'tag_C', 'some/files/file'],
|
||||
stdout=stdout, stderr=stderr)
|
||||
self.assertNotEqual(0, retval)
|
||||
self.assertEqual('', stdout.getvalue())
|
||||
self.assertEqual('fatal: unknown revision \'xxxx\'.\n', stderr.getvalue())
|
||||
|
||||
class GitHyperBlameSimpleTest(GitHyperBlameTestBase):
|
||||
REPO_SCHEMA = """
|
||||
A B D E F G H
|
||||
A C D
|
||||
"""
|
||||
|
||||
COMMIT_A = {
|
||||
'some/files/file1': {'data': 'file1'},
|
||||
'some/files/file2': {'data': 'file2'},
|
||||
'some/files/empty': {'data': ''},
|
||||
'some/other/file': {'data': 'otherfile'},
|
||||
}
|
||||
|
||||
COMMIT_B = {
|
||||
'some/files/file2': {
|
||||
'mode': 0755,
|
||||
'data': 'file2 - vanilla\n'},
|
||||
'some/files/empty': {'data': 'not anymore'},
|
||||
'some/files/file3': {'data': 'file3'},
|
||||
}
|
||||
|
||||
COMMIT_C = {
|
||||
'some/files/file2': {'data': 'file2 - merged\n'},
|
||||
}
|
||||
|
||||
COMMIT_D = {
|
||||
'some/files/file2': {'data': 'file2 - vanilla\nfile2 - merged\n'},
|
||||
}
|
||||
|
||||
COMMIT_E = {
|
||||
'some/files/file2': {'data': 'file2 - vanilla\nfile_x - merged\n'},
|
||||
}
|
||||
|
||||
COMMIT_F = {
|
||||
'some/files/file2': {'data': 'file2 - vanilla\nfile_y - merged\n'},
|
||||
}
|
||||
|
||||
# Move file2 from files to other.
|
||||
COMMIT_G = {
|
||||
'some/files/file2': {'data': None},
|
||||
'some/other/file2': {'data': 'file2 - vanilla\nfile_y - merged\n'},
|
||||
}
|
||||
|
||||
COMMIT_H = {
|
||||
'some/other/file2': {'data': 'file2 - vanilla\nfile_z - merged\n'},
|
||||
}
|
||||
|
||||
def testBlameError(self):
|
||||
"""Tests a blame on a non-existent file."""
|
||||
expected_output = ['']
|
||||
retval, output = self.run_hyperblame([], 'some/other/file2', 'tag_D')
|
||||
self.assertNotEqual(0, retval)
|
||||
self.assertEqual(expected_output, output)
|
||||
|
||||
def testBlameEmpty(self):
|
||||
"""Tests a blame of an empty file with no ignores."""
|
||||
expected_output = ['']
|
||||
retval, output = self.run_hyperblame([], 'some/files/empty', 'tag_A')
|
||||
self.assertEqual(0, retval)
|
||||
self.assertEqual(expected_output, output)
|
||||
|
||||
def testBasicBlame(self):
|
||||
"""Tests a basic blame with no ignores."""
|
||||
# Expect to blame line 1 on B, line 2 on C.
|
||||
expected_output = [self.blame_line('B', '1) file2 - vanilla'),
|
||||
self.blame_line('C', '2) file2 - merged')]
|
||||
retval, output = self.run_hyperblame([], 'some/files/file2', 'tag_D')
|
||||
self.assertEqual(0, retval)
|
||||
self.assertEqual(expected_output, output)
|
||||
|
||||
def testBlameRenamed(self):
|
||||
"""Tests a blame with no ignores on a renamed file."""
|
||||
# Expect to blame line 1 on B, line 2 on H.
|
||||
# Because the file has a different name than it had when (some of) these
|
||||
# lines were changed, expect the filenames to be displayed.
|
||||
expected_output = [self.blame_line('B', '1) file2 - vanilla',
|
||||
filename='some/files/file2'),
|
||||
self.blame_line('H', '2) file_z - merged',
|
||||
filename='some/other/file2')]
|
||||
retval, output = self.run_hyperblame([], 'some/other/file2', 'tag_H')
|
||||
self.assertEqual(0, retval)
|
||||
self.assertEqual(expected_output, output)
|
||||
|
||||
def testIgnoreSimpleEdits(self):
|
||||
"""Tests a blame with simple (line-level changes) commits ignored."""
|
||||
# Expect to blame line 1 on B, line 2 on E.
|
||||
expected_output = [self.blame_line('B', '1) file2 - vanilla'),
|
||||
self.blame_line('E', '2) file_x - merged')]
|
||||
retval, output = self.run_hyperblame([], 'some/files/file2', 'tag_E')
|
||||
self.assertEqual(0, retval)
|
||||
self.assertEqual(expected_output, output)
|
||||
|
||||
# Ignore E; blame line 1 on B, line 2 on C.
|
||||
expected_output = [self.blame_line('B', ' 1) file2 - vanilla'),
|
||||
self.blame_line('C', '2*) file_x - merged')]
|
||||
retval, output = self.run_hyperblame(['E'], 'some/files/file2', 'tag_E')
|
||||
self.assertEqual(0, retval)
|
||||
self.assertEqual(expected_output, output)
|
||||
|
||||
# Ignore E and F; blame line 1 on B, line 2 on C.
|
||||
expected_output = [self.blame_line('B', ' 1) file2 - vanilla'),
|
||||
self.blame_line('C', '2*) file_y - merged')]
|
||||
retval, output = self.run_hyperblame(['E', 'F'], 'some/files/file2',
|
||||
'tag_F')
|
||||
self.assertEqual(0, retval)
|
||||
self.assertEqual(expected_output, output)
|
||||
|
||||
def testIgnoreInitialCommit(self):
|
||||
"""Tests a blame with the initial commit ignored."""
|
||||
# Ignore A. Expect A to get blamed anyway.
|
||||
expected_output = [self.blame_line('A', '1) file1')]
|
||||
retval, output = self.run_hyperblame(['A'], 'some/files/file1', 'tag_A')
|
||||
self.assertEqual(0, retval)
|
||||
self.assertEqual(expected_output, output)
|
||||
|
||||
def testIgnoreFileAdd(self):
|
||||
"""Tests a blame ignoring the commit that added this file."""
|
||||
# Ignore A. Expect A to get blamed anyway.
|
||||
expected_output = [self.blame_line('B', '1) file3')]
|
||||
retval, output = self.run_hyperblame(['B'], 'some/files/file3', 'tag_B')
|
||||
self.assertEqual(0, retval)
|
||||
self.assertEqual(expected_output, output)
|
||||
|
||||
def testIgnoreFilePopulate(self):
|
||||
"""Tests a blame ignoring the commit that added data to an empty file."""
|
||||
# Ignore A. Expect A to get blamed anyway.
|
||||
expected_output = [self.blame_line('B', '1) not anymore')]
|
||||
retval, output = self.run_hyperblame(['B'], 'some/files/empty', 'tag_B')
|
||||
self.assertEqual(0, retval)
|
||||
self.assertEqual(expected_output, output)
|
||||
|
||||
class GitHyperBlameLineMotionTest(GitHyperBlameTestBase):
|
||||
REPO_SCHEMA = """
|
||||
A B C D E
|
||||
"""
|
||||
|
||||
COMMIT_A = {
|
||||
'file': {'data': 'A\ngreen\nblue\n'},
|
||||
}
|
||||
|
||||
# Change "green" to "yellow".
|
||||
COMMIT_B = {
|
||||
'file': {'data': 'A\nyellow\nblue\n'},
|
||||
}
|
||||
|
||||
# Insert 2 lines at the top,
|
||||
# Change "yellow" to "red".
|
||||
COMMIT_C = {
|
||||
'file': {'data': 'X\nY\nA\nred\nblue\n'},
|
||||
}
|
||||
|
||||
# Insert 2 more lines at the top.
|
||||
COMMIT_D = {
|
||||
'file': {'data': 'earth\nfire\nX\nY\nA\nred\nblue\n'},
|
||||
}
|
||||
|
||||
# Insert a line before "red", and indent "red" and "blue".
|
||||
COMMIT_E = {
|
||||
'file': {'data': 'earth\nfire\nX\nY\nA\ncolors:\n red\n blue\n'},
|
||||
}
|
||||
|
||||
def testInterHunkLineMotion(self):
|
||||
"""Tests a blame with line motion in another hunk in the ignored commit."""
|
||||
# This test was mostly written as a demonstration of the limitations of the
|
||||
# current algorithm (it exhibits non-ideal behaviour).
|
||||
|
||||
# Blame from D, ignoring C.
|
||||
# Lines 1, 2 were added by D.
|
||||
# Lines 3, 4 were added by C (but ignored, so blame A, B, respectively).
|
||||
# TODO(mgiuca): Ideally, this would blame both of these lines on A, because
|
||||
# they add lines nowhere near the changes made by B.
|
||||
# Line 5 was added by A.
|
||||
# Line 6 was modified by C (but ignored, so blame A).
|
||||
# TODO(mgiuca): Ideally, Line 6 would be blamed on B, because that was the
|
||||
# last commit to touch that line (changing "green" to "yellow"), but the
|
||||
# algorithm isn't yet able to figure out that Line 6 in D == Line 4 in C ~=
|
||||
# Line 2 in B.
|
||||
# Line 7 was added by A.
|
||||
expected_output = [self.blame_line('D', ' 1) earth'),
|
||||
self.blame_line('D', ' 2) fire'),
|
||||
self.blame_line('A', '3*) X'),
|
||||
self.blame_line('B', '4*) Y'),
|
||||
self.blame_line('A', ' 5) A'),
|
||||
self.blame_line('A', '6*) red'),
|
||||
self.blame_line('A', ' 7) blue'),
|
||||
]
|
||||
retval, output = self.run_hyperblame(['C'], 'file', 'tag_D')
|
||||
self.assertEqual(0, retval)
|
||||
self.assertEqual(expected_output, output)
|
||||
|
||||
def testIntraHunkLineMotion(self):
|
||||
"""Tests a blame with line motion in the same hunk in the ignored commit."""
|
||||
# This test was mostly written as a demonstration of the limitations of the
|
||||
# current algorithm (it exhibits non-ideal behaviour).
|
||||
|
||||
# Blame from E, ignoring E.
|
||||
# Line 6 was added by E (but ignored, so blame C).
|
||||
# Lines 7, 8 were modified by E (but ignored, so blame A).
|
||||
# TODO(mgiuca): Ideally, this would blame Line 7 on C, because the line
|
||||
# "red" was added by C, and this is just a small change to that line. But
|
||||
# the current algorithm can't deal with line motion within a hunk, so it
|
||||
# just assumes Line 7 in E ~= Line 7 in D == Line 3 in A (which was "blue").
|
||||
expected_output = [self.blame_line('D', ' 1) earth'),
|
||||
self.blame_line('D', ' 2) fire'),
|
||||
self.blame_line('C', ' 3) X'),
|
||||
self.blame_line('C', ' 4) Y'),
|
||||
self.blame_line('A', ' 5) A'),
|
||||
self.blame_line('C', '6*) colors:'),
|
||||
self.blame_line('A', '7*) red'),
|
||||
self.blame_line('A', '8*) blue'),
|
||||
]
|
||||
retval, output = self.run_hyperblame(['E'], 'file', 'tag_E')
|
||||
self.assertEqual(0, retval)
|
||||
self.assertEqual(expected_output, output)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(coverage_utils.covered_main(
|
||||
os.path.join(DEPOT_TOOLS_ROOT, 'git_hyper_blame.py')))
|
Loading…
Reference in New Issue