git-hyper-blame: Fix unicode handling.

git hyper_blame might use a subprocess' stdin for its stdout,
which is opened to accept byte input.
The text must be encoded before printing to stdout to avoid
unicode errors.

Bug: 1028709
Change-Id: If2a270a7f3f69a818d367616f6732245de364db9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/1937500
Reviewed-by: Andy Perelson <ajp@chromium.org>
Commit-Queue: Edward Lesmes <ehmaldonado@chromium.org>
changes/00/1937500/4
Edward Lemur 5 years ago committed by Commit Bot
parent 5604ce9f20
commit 5e94b80c34

@ -677,7 +677,9 @@ def less(): # pragma: no cover
running less and just yields sys.stdout. running less and just yields sys.stdout.
""" """
if not setup_color.IS_TTY: if not setup_color.IS_TTY:
yield sys.stdout # On Python 3, sys.stdout doesn't accept bytes, and sys.stdout.buffer must
# be used.
yield getattr(sys.stdout, 'buffer', sys.stdout)
return return
# Run with the same options that git uses (see setup_pager in git repo). # Run with the same options that git uses (see setup_pager in git repo).

@ -95,7 +95,7 @@ def parse_blame(blameoutput):
yield BlameLine(commit, context, lineno_then, lineno_now, False) yield BlameLine(commit, context, lineno_then, lineno_now, False)
def print_table(table, colsep=' ', rowsep='\n', align=None, out=sys.stdout): def print_table(table, align=None, out=sys.stdout):
"""Print a 2D rectangular array, aligning columns with spaces. """Print a 2D rectangular array, aligning columns with spaces.
Args: Args:
@ -124,9 +124,10 @@ def print_table(table, colsep=' ', rowsep='\n', align=None, out=sys.stdout):
elif i < len(row) - 1: elif i < len(row) - 1:
# Do not pad the final column if left-aligned. # Do not pad the final column if left-aligned.
cell += padding cell += padding
cell = cell.encode('utf-8', 'replace')
cells.append(cell) cells.append(cell)
try: try:
print(*cells, sep=colsep, end=rowsep, file=out) out.write(b' '.join(cells) + b'\n')
except IOError: # pragma: no cover except IOError: # pragma: no cover
# Can happen on Windows if the pipe is closed early. # Can happen on Windows if the pipe is closed early.
pass pass

@ -14,6 +14,7 @@ import sys
import tempfile import tempfile
import unittest import unittest
from io import BytesIO
if sys.version_info.major == 2: if sys.version_info.major == 2:
from StringIO import StringIO from StringIO import StringIO
else: else:
@ -38,12 +39,12 @@ class GitHyperBlameTestBase(git_test_utils.GitRepoReadOnlyTestBase):
cls.git_hyper_blame = git_hyper_blame cls.git_hyper_blame = git_hyper_blame
def run_hyperblame(self, ignored, filename, revision): def run_hyperblame(self, ignored, filename, revision):
stdout = StringIO() stdout = BytesIO()
stderr = StringIO() stderr = StringIO()
ignored = [self.repo[c] for c in ignored] ignored = [self.repo[c] for c in ignored]
retval = self.repo.run(self.git_hyper_blame.hyper_blame, ignored, filename, retval = self.repo.run(self.git_hyper_blame.hyper_blame, ignored, filename,
revision=revision, out=stdout, err=stderr) revision=revision, out=stdout, err=stderr)
return retval, stdout.getvalue().rstrip().split('\n') return retval, stdout.getvalue().rstrip().split(b'\n')
def blame_line(self, commit_name, rest, author=None, filename=None): def blame_line(self, commit_name, rest, author=None, filename=None):
"""Generate a blame line from a commit. """Generate a blame line from a commit.
@ -60,7 +61,7 @@ class GitHyperBlameTestBase(git_test_utils.GitRepoReadOnlyTestBase):
author = self.repo.show_commit(commit_name, format_string='%an %ai') author = self.repo.show_commit(commit_name, format_string='%an %ai')
else: else:
author += self.repo.show_commit(commit_name, format_string=' %ai') author += self.repo.show_commit(commit_name, format_string=' %ai')
return '%s (%s %s' % (start, author, rest) return ('%s (%s %s' % (start, author, rest)).encode('utf-8')
class GitHyperBlameMainTest(GitHyperBlameTestBase): class GitHyperBlameMainTest(GitHyperBlameTestBase):
"""End-to-end tests on a very simple repo.""" """End-to-end tests on a very simple repo."""
@ -95,26 +96,26 @@ class GitHyperBlameMainTest(GitHyperBlameTestBase):
"""Tests the main function (simple end-to-end test with no ignores).""" """Tests the main function (simple end-to-end test with no ignores)."""
expected_output = [self.blame_line('C', '1) line 1.1'), expected_output = [self.blame_line('C', '1) line 1.1'),
self.blame_line('B', '2) line 2.1')] self.blame_line('B', '2) line 2.1')]
stdout = StringIO() stdout = BytesIO()
stderr = StringIO() stderr = StringIO()
retval = self.repo.run(self.git_hyper_blame.main, retval = self.repo.run(self.git_hyper_blame.main,
args=['tag_C', 'some/files/file'], stdout=stdout, args=['tag_C', 'some/files/file'], stdout=stdout,
stderr=stderr) stderr=stderr)
self.assertEqual(0, retval) self.assertEqual(0, retval)
self.assertEqual(expected_output, stdout.getvalue().rstrip().split('\n')) self.assertEqual(expected_output, stdout.getvalue().rstrip().split(b'\n'))
self.assertEqual('', stderr.getvalue()) self.assertEqual('', stderr.getvalue())
def testIgnoreSimple(self): def testIgnoreSimple(self):
"""Tests the main function (simple end-to-end test with ignores).""" """Tests the main function (simple end-to-end test with ignores)."""
expected_output = [self.blame_line('C', ' 1) line 1.1'), expected_output = [self.blame_line('C', ' 1) line 1.1'),
self.blame_line('A', '2*) line 2.1')] self.blame_line('A', '2*) line 2.1')]
stdout = StringIO() stdout = BytesIO()
stderr = StringIO() stderr = StringIO()
retval = self.repo.run(self.git_hyper_blame.main, retval = self.repo.run(self.git_hyper_blame.main,
args=['-i', 'tag_B', 'tag_C', 'some/files/file'], args=['-i', 'tag_B', 'tag_C', 'some/files/file'],
stdout=stdout, stderr=stderr) stdout=stdout, stderr=stderr)
self.assertEqual(0, retval) self.assertEqual(0, retval)
self.assertEqual(expected_output, stdout.getvalue().rstrip().split('\n')) self.assertEqual(expected_output, stdout.getvalue().rstrip().split(b'\n'))
self.assertEqual('', stderr.getvalue()) self.assertEqual('', stderr.getvalue())
def testBadRepo(self): def testBadRepo(self):
@ -124,7 +125,7 @@ class GitHyperBlameMainTest(GitHyperBlameTestBase):
tempdir = tempfile.mkdtemp(suffix='_nogit', prefix='git_repo') tempdir = tempfile.mkdtemp(suffix='_nogit', prefix='git_repo')
try: try:
os.chdir(tempdir) os.chdir(tempdir)
stdout = StringIO() stdout = BytesIO()
stderr = StringIO() stderr = StringIO()
retval = self.git_hyper_blame.main( retval = self.git_hyper_blame.main(
args=['-i', 'tag_B', 'tag_C', 'some/files/file'], stdout=stdout, args=['-i', 'tag_B', 'tag_C', 'some/files/file'], stdout=stdout,
@ -134,19 +135,19 @@ class GitHyperBlameMainTest(GitHyperBlameTestBase):
os.chdir(curdir) os.chdir(curdir)
self.assertNotEqual(0, retval) self.assertNotEqual(0, retval)
self.assertEqual('', stdout.getvalue()) self.assertEqual(b'', stdout.getvalue())
r = re.compile('^fatal: Not a git repository', re.I) r = re.compile('^fatal: Not a git repository', re.I)
self.assertRegexpMatches(stderr.getvalue(), r) self.assertRegexpMatches(stderr.getvalue(), r)
def testBadFilename(self): def testBadFilename(self):
"""Tests the main function (bad filename).""" """Tests the main function (bad filename)."""
stdout = StringIO() stdout = BytesIO()
stderr = StringIO() stderr = StringIO()
retval = self.repo.run(self.git_hyper_blame.main, retval = self.repo.run(self.git_hyper_blame.main,
args=['-i', 'tag_B', 'tag_C', 'some/files/xxxx'], args=['-i', 'tag_B', 'tag_C', 'some/files/xxxx'],
stdout=stdout, stderr=stderr) stdout=stdout, stderr=stderr)
self.assertNotEqual(0, retval) self.assertNotEqual(0, retval)
self.assertEqual('', stdout.getvalue()) self.assertEqual(b'', stdout.getvalue())
# TODO(mgiuca): This test used to test the exact string, but it broke due to # TODO(mgiuca): This test used to test the exact string, but it broke due to
# an upstream bug in git-blame. For now, just check the start of the string. # an upstream bug in git-blame. For now, just check the start of the string.
# A patch has been sent upstream; when it rolls out we can revert back to # A patch has been sent upstream; when it rolls out we can revert back to
@ -156,13 +157,13 @@ class GitHyperBlameMainTest(GitHyperBlameTestBase):
def testBadRevision(self): def testBadRevision(self):
"""Tests the main function (bad revision to blame from).""" """Tests the main function (bad revision to blame from)."""
stdout = StringIO() stdout = BytesIO()
stderr = StringIO() stderr = StringIO()
retval = self.repo.run(self.git_hyper_blame.main, retval = self.repo.run(self.git_hyper_blame.main,
args=['-i', 'tag_B', 'xxxx', 'some/files/file'], args=['-i', 'tag_B', 'xxxx', 'some/files/file'],
stdout=stdout, stderr=stderr) stdout=stdout, stderr=stderr)
self.assertNotEqual(0, retval) self.assertNotEqual(0, retval)
self.assertEqual('', stdout.getvalue()) self.assertEqual(b'', stdout.getvalue())
self.assertRegexpMatches(stderr.getvalue(), self.assertRegexpMatches(stderr.getvalue(),
'^fatal: ambiguous argument \'xxxx\': unknown ' '^fatal: ambiguous argument \'xxxx\': unknown '
'revision or path not in the working tree.') 'revision or path not in the working tree.')
@ -171,20 +172,20 @@ class GitHyperBlameMainTest(GitHyperBlameTestBase):
"""Tests the main function (bad revision passed to -i).""" """Tests the main function (bad revision passed to -i)."""
expected_output = [self.blame_line('C', '1) line 1.1'), expected_output = [self.blame_line('C', '1) line 1.1'),
self.blame_line('B', '2) line 2.1')] self.blame_line('B', '2) line 2.1')]
stdout = StringIO() stdout = BytesIO()
stderr = StringIO() stderr = StringIO()
retval = self.repo.run(self.git_hyper_blame.main, retval = self.repo.run(self.git_hyper_blame.main,
args=['-i', 'xxxx', 'tag_C', 'some/files/file'], args=['-i', 'xxxx', 'tag_C', 'some/files/file'],
stdout=stdout, stderr=stderr) stdout=stdout, stderr=stderr)
self.assertEqual(0, retval) self.assertEqual(0, retval)
self.assertEqual(expected_output, stdout.getvalue().rstrip().split('\n')) self.assertEqual(expected_output, stdout.getvalue().rstrip().split(b'\n'))
self.assertEqual('warning: unknown revision \'xxxx\'.\n', stderr.getvalue()) self.assertEqual('warning: unknown revision \'xxxx\'.\n', stderr.getvalue())
def testIgnoreFile(self): def testIgnoreFile(self):
"""Tests passing the ignore list in a file.""" """Tests passing the ignore list in a file."""
expected_output = [self.blame_line('C', ' 1) line 1.1'), expected_output = [self.blame_line('C', ' 1) line 1.1'),
self.blame_line('A', '2*) line 2.1')] self.blame_line('A', '2*) line 2.1')]
stdout = StringIO() stdout = BytesIO()
stderr = StringIO() stderr = StringIO()
with tempfile.NamedTemporaryFile(mode='w+', prefix='ignore') as ignore_file: with tempfile.NamedTemporaryFile(mode='w+', prefix='ignore') as ignore_file:
@ -200,7 +201,7 @@ class GitHyperBlameMainTest(GitHyperBlameTestBase):
stdout=stdout, stderr=stderr) stdout=stdout, stderr=stderr)
self.assertEqual(0, retval) self.assertEqual(0, retval)
self.assertEqual(expected_output, stdout.getvalue().rstrip().split('\n')) self.assertEqual(expected_output, stdout.getvalue().rstrip().split(b'\n'))
self.assertEqual('warning: unknown revision \'xxxx\'.\n', stderr.getvalue()) self.assertEqual('warning: unknown revision \'xxxx\'.\n', stderr.getvalue())
def testDefaultIgnoreFile(self): def testDefaultIgnoreFile(self):
@ -211,7 +212,7 @@ class GitHyperBlameMainTest(GitHyperBlameTestBase):
expected_output = [self.blame_line('A', '1*) line 1.1'), expected_output = [self.blame_line('A', '1*) line 1.1'),
self.blame_line('B', ' 2) line 2.1')] self.blame_line('B', ' 2) line 2.1')]
stdout = StringIO() stdout = BytesIO()
stderr = StringIO() stderr = StringIO()
retval = self.repo.run(self.git_hyper_blame.main, retval = self.repo.run(self.git_hyper_blame.main,
@ -219,13 +220,13 @@ class GitHyperBlameMainTest(GitHyperBlameTestBase):
stdout=stdout, stderr=stderr) stdout=stdout, stderr=stderr)
self.assertEqual(0, retval) self.assertEqual(0, retval)
self.assertEqual(expected_output, stdout.getvalue().rstrip().split('\n')) self.assertEqual(expected_output, stdout.getvalue().rstrip().split(b'\n'))
self.assertEqual('', stderr.getvalue()) self.assertEqual('', stderr.getvalue())
# Test blame from a different revision. Despite the default ignore file # Test blame from a different revision. Despite the default ignore file
# *not* being committed at that revision, it should still be picked up # *not* being committed at that revision, it should still be picked up
# because D is currently checked out. # because D is currently checked out.
stdout = StringIO() stdout = BytesIO()
stderr = StringIO() stderr = StringIO()
retval = self.repo.run(self.git_hyper_blame.main, retval = self.repo.run(self.git_hyper_blame.main,
@ -233,7 +234,7 @@ class GitHyperBlameMainTest(GitHyperBlameTestBase):
stdout=stdout, stderr=stderr) stdout=stdout, stderr=stderr)
self.assertEqual(0, retval) self.assertEqual(0, retval)
self.assertEqual(expected_output, stdout.getvalue().rstrip().split('\n')) self.assertEqual(expected_output, stdout.getvalue().rstrip().split(b'\n'))
self.assertEqual('', stderr.getvalue()) self.assertEqual('', stderr.getvalue())
def testNoDefaultIgnores(self): def testNoDefaultIgnores(self):
@ -244,7 +245,7 @@ class GitHyperBlameMainTest(GitHyperBlameTestBase):
expected_output = [self.blame_line('C', '1) line 1.1'), expected_output = [self.blame_line('C', '1) line 1.1'),
self.blame_line('B', '2) line 2.1')] self.blame_line('B', '2) line 2.1')]
stdout = StringIO() stdout = BytesIO()
stderr = StringIO() stderr = StringIO()
retval = self.repo.run( retval = self.repo.run(
@ -253,7 +254,7 @@ class GitHyperBlameMainTest(GitHyperBlameTestBase):
stdout=stdout, stderr=stderr) stdout=stdout, stderr=stderr)
self.assertEqual(0, retval) self.assertEqual(0, retval)
self.assertEqual(expected_output, stdout.getvalue().rstrip().split('\n')) self.assertEqual(expected_output, stdout.getvalue().rstrip().split(b'\n'))
self.assertEqual('', stderr.getvalue()) self.assertEqual('', stderr.getvalue())
class GitHyperBlameSimpleTest(GitHyperBlameTestBase): class GitHyperBlameSimpleTest(GitHyperBlameTestBase):
@ -305,14 +306,14 @@ class GitHyperBlameSimpleTest(GitHyperBlameTestBase):
def testBlameError(self): def testBlameError(self):
"""Tests a blame on a non-existent file.""" """Tests a blame on a non-existent file."""
expected_output = [''] expected_output = [b'']
retval, output = self.run_hyperblame([], 'some/other/file2', 'tag_D') retval, output = self.run_hyperblame([], 'some/other/file2', 'tag_D')
self.assertNotEqual(0, retval) self.assertNotEqual(0, retval)
self.assertEqual(expected_output, output) self.assertEqual(expected_output, output)
def testBlameEmpty(self): def testBlameEmpty(self):
"""Tests a blame of an empty file with no ignores.""" """Tests a blame of an empty file with no ignores."""
expected_output = [''] expected_output = [b'']
retval, output = self.run_hyperblame([], 'some/files/empty', 'tag_A') retval, output = self.run_hyperblame([], 'some/files/empty', 'tag_A')
self.assertEqual(0, retval) self.assertEqual(0, retval)
self.assertEqual(expected_output, output) self.assertEqual(expected_output, output)

Loading…
Cancel
Save