#!/usr/bin/env vpython3
# Copyright 2016 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Tests for git_dates."""

from io import BytesIO, StringIO
import os
import re
import shutil
import sys
import tempfile
from unittest import mock

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  # noqa: E402
from testing_support import git_test_utils  # noqa: E402

import gclient_utils  # noqa: E402

GitRepo = git_test_utils.GitRepo


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 setUp(self):
        mock.patch("sys.stderr", StringIO()).start()
        self.addCleanup(mock.patch.stopall)

    def run_hyperblame(self, ignored, filename, revision):
        outbuf = BytesIO()
        ignored = [self.repo[c] for c in ignored]
        retval = self.repo.run(
            self.git_hyper_blame.hyper_blame,
            outbuf,
            ignored,
            filename,
            revision,
        )
        return retval, outbuf.getvalue().rstrip().split(b"\n")

    def blame_line(self, commit_name, rest, author=None, 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'.
            author: The author's name. If omitted, reads the name out of the commit.
            filename: The filename. If omitted, not shown in the blame line.
        """
        short = self.repo[commit_name][:8]
        start = "%s %s" % (short, filename) if filename else short
        if author is None:
            author = self.repo.show_commit(commit_name, format_string="%an %ai")
        else:
            author += self.repo.show_commit(commit_name, format_string=" %ai")
        return ("%s (%s %s" % (start, author, rest)).encode("utf-8")


class GitHyperBlameMainTest(GitHyperBlameTestBase):
    """End-to-end tests on a very simple repo."""

    REPO_SCHEMA = "A B C D"

    COMMIT_A = {
        "some/files/file": {"data": b"line 1\nline 2\n"},
    }

    COMMIT_B = {
        "some/files/file": {"data": b"line 1\nline 2.1\n"},
    }

    COMMIT_C = {
        "some/files/file": {"data": b"line 1.1\nline 2.1\n"},
    }

    COMMIT_D = {
        # This file should be automatically considered for ignore.
        ".git-blame-ignore-revs": {"data": b"tag_C"},
        # This file should not be considered.
        "some/files/.git-blame-ignore-revs": {"data": b"tag_B"},
    }

    def setUp(self):
        super(GitHyperBlameMainTest, self).setUp()
        # Most tests want to check out C (so the .git-blame-ignore-revs is not
        # used).
        self.repo.git("checkout", "-f", "tag_C")

    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"),
        ]
        outbuf = BytesIO()
        retval = self.repo.run(
            self.git_hyper_blame.main, ["tag_C", "some/files/file"], outbuf
        )
        self.assertEqual(0, retval)
        self.assertEqual(
            expected_output, outbuf.getvalue().rstrip().split(b"\n")
        )
        self.assertEqual("", sys.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"),
        ]
        outbuf = BytesIO()
        retval = self.repo.run(
            self.git_hyper_blame.main,
            ["-i", "tag_B", "tag_C", "some/files/file"],
            outbuf,
        )
        self.assertEqual(0, retval)
        self.assertEqual(
            expected_output, outbuf.getvalue().rstrip().split(b"\n")
        )
        self.assertEqual("", sys.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)
            outbuf = BytesIO()
            retval = self.git_hyper_blame.main(
                ["-i", "tag_B", "tag_C", "some/files/file"], outbuf
            )
        finally:
            os.chdir(curdir)
            shutil.rmtree(tempdir)

        self.assertNotEqual(0, retval)
        self.assertEqual(b"", outbuf.getvalue())
        r = re.compile("^fatal: Not a git repository", re.I)
        self.assertRegexpMatches(sys.stderr.getvalue(), r)

    def testBadFilename(self):
        """Tests the main function (bad filename)."""
        outbuf = BytesIO()
        retval = self.repo.run(
            self.git_hyper_blame.main,
            ["-i", "tag_B", "tag_C", "some/files/xxxx"],
            outbuf,
        )
        self.assertNotEqual(0, retval)
        self.assertEqual(b"", outbuf.getvalue())
        # 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. A patch has been sent upstream; when it rolls out we can
        # revert back to the original test logic.
        self.assertTrue(
            sys.stderr.getvalue().startswith(
                "fatal: no such path some/files/xxxx in "
            )
        )

    def testBadRevision(self):
        """Tests the main function (bad revision to blame from)."""
        outbuf = BytesIO()
        retval = self.repo.run(
            self.git_hyper_blame.main,
            ["-i", "tag_B", "xxxx", "some/files/file"],
            outbuf,
        )
        self.assertNotEqual(0, retval)
        self.assertEqual(b"", outbuf.getvalue())
        self.assertRegexpMatches(
            sys.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)."""
        expected_output = [
            self.blame_line("C", "1) line 1.1"),
            self.blame_line("B", "2) line 2.1"),
        ]
        outbuf = BytesIO()
        retval = self.repo.run(
            self.git_hyper_blame.main,
            ["-i", "xxxx", "tag_C", "some/files/file"],
            outbuf,
        )
        self.assertEqual(0, retval)
        self.assertEqual(
            expected_output, outbuf.getvalue().rstrip().split(b"\n")
        )
        self.assertEqual(
            "warning: unknown revision 'xxxx'.\n", sys.stderr.getvalue()
        )

    def testIgnoreFile(self):
        """Tests passing the ignore list in a file."""
        expected_output = [
            self.blame_line("C", " 1) line 1.1"),
            self.blame_line("A", "2*) line 2.1"),
        ]
        outbuf = BytesIO()

        with gclient_utils.temporary_file() as ignore_file:
            gclient_utils.FileWrite(
                ignore_file,
                "# Line comments are allowed.\n\n{}\nxxxx\n".format(
                    self.repo["B"]
                ),
            )
            retval = self.repo.run(
                self.git_hyper_blame.main,
                ["--ignore-file", ignore_file, "tag_C", "some/files/file"],
                outbuf,
            )

        self.assertEqual(0, retval)
        self.assertEqual(
            expected_output, outbuf.getvalue().rstrip().split(b"\n")
        )
        self.assertEqual(
            "warning: unknown revision 'xxxx'.\n", sys.stderr.getvalue()
        )

    def testDefaultIgnoreFile(self):
        """Tests automatically using a default ignore list."""
        # Check out revision D. We expect the script to use the default ignore
        # list that is checked out, *not* the one committed at the given
        # revision.
        self.repo.git("checkout", "-f", "tag_D")

        expected_output = [
            self.blame_line("A", "1*) line 1.1"),
            self.blame_line("B", " 2) line 2.1"),
        ]
        outbuf = BytesIO()

        retval = self.repo.run(
            self.git_hyper_blame.main, ["tag_D", "some/files/file"], outbuf
        )

        self.assertEqual(0, retval)
        self.assertEqual(
            expected_output, outbuf.getvalue().rstrip().split(b"\n")
        )
        self.assertEqual("", sys.stderr.getvalue())

        # Test blame from a different revision. Despite the default ignore file
        # *not* being committed at that revision, it should still be picked up
        # because D is currently checked out.
        outbuf = BytesIO()

        retval = self.repo.run(
            self.git_hyper_blame.main, ["tag_C", "some/files/file"], outbuf
        )

        self.assertEqual(0, retval)
        self.assertEqual(
            expected_output, outbuf.getvalue().rstrip().split(b"\n")
        )
        self.assertEqual("", sys.stderr.getvalue())

    def testNoDefaultIgnores(self):
        """Tests the --no-default-ignores switch."""
        # Check out revision D. This has a .git-blame-ignore-revs file, which we
        # expect to be ignored due to --no-default-ignores.
        self.repo.git("checkout", "-f", "tag_D")

        expected_output = [
            self.blame_line("C", "1) line 1.1"),
            self.blame_line("B", "2) line 2.1"),
        ]
        outbuf = BytesIO()

        retval = self.repo.run(
            self.git_hyper_blame.main,
            ["tag_D", "some/files/file", "--no-default-ignores"],
            outbuf,
        )

        self.assertEqual(0, retval)
        self.assertEqual(
            expected_output, outbuf.getvalue().rstrip().split(b"\n")
        )
        self.assertEqual("", sys.stderr.getvalue())


class GitHyperBlameSimpleTest(GitHyperBlameTestBase):
    REPO_SCHEMA = """
  A B D E F G H
  A C D
  """

    COMMIT_A = {
        "some/files/file1": {"data": b"file1"},
        "some/files/file2": {"data": b"file2"},
        "some/files/empty": {"data": b""},
        "some/other/file": {"data": b"otherfile"},
    }

    COMMIT_B = {
        "some/files/file2": {"mode": 0o755, "data": b"file2 - vanilla\n"},
        "some/files/empty": {"data": b"not anymore"},
        "some/files/file3": {"data": b"file3"},
    }

    COMMIT_C = {
        "some/files/file2": {"data": b"file2 - merged\n"},
    }

    COMMIT_D = {
        "some/files/file2": {"data": b"file2 - vanilla\nfile2 - merged\n"},
    }

    COMMIT_E = {
        "some/files/file2": {"data": b"file2 - vanilla\nfile_x - merged\n"},
    }

    COMMIT_F = {
        "some/files/file2": {"data": b"file2 - vanilla\nfile_y - merged\n"},
    }

    # Move file2 from files to other.
    COMMIT_G = {
        "some/files/file2": {"data": None},
        "some/other/file2": {"data": b"file2 - vanilla\nfile_y - merged\n"},
    }

    COMMIT_H = {
        "some/other/file2": {"data": b"file2 - vanilla\nfile_z - merged\n"},
    }

    def testBlameError(self):
        """Tests a blame on a non-existent file."""
        expected_output = [b""]
        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 = [b""]
        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 F
  """

    COMMIT_A = {
        "file": {"data": b"A\ngreen\nblue\n"},
    }

    # Change "green" to "yellow".
    COMMIT_B = {
        "file": {"data": b"A\nyellow\nblue\n"},
    }

    # Insert 2 lines at the top,
    # Change "yellow" to "red".
    # Insert 1 line at the bottom.
    COMMIT_C = {
        "file": {"data": b"X\nY\nA\nred\nblue\nZ\n"},
    }

    # Insert 2 more lines at the top.
    COMMIT_D = {
        "file": {"data": b"earth\nfire\nX\nY\nA\nred\nblue\nZ\n"},
    }

    # Insert a line before "red", and indent "red" and "blue".
    COMMIT_E = {
        "file": {"data": b"earth\nfire\nX\nY\nA\ncolors:\n red\n blue\nZ\n"},
    }

    # Insert a line between "A" and "colors".
    COMMIT_F = {
        "file": {"data": b"earth\nfire\nX\nY\nA\nB\ncolors:\n red\n blue\nZ\n"},
    }

    def testCacheDiffHunks(self):
        """Tests the cache_diff_hunks internal function."""
        expected_hunks = [
            ((0, 0), (1, 2)),
            ((2, 1), (4, 1)),
            ((3, 0), (6, 1)),
        ]
        hunks = self.repo.run(
            self.git_hyper_blame.cache_diff_hunks, "tag_B", "tag_C"
        )
        self.assertEqual(expected_hunks, hunks)

    def testApproxLinenoAcrossRevs(self):
        """Tests the approx_lineno_across_revs internal function."""
        # Note: For all of these tests, the "old revision" and "new revision"
        # are reversed, which matches the usage by hyper_blame.

        # Test an unchanged line before any hunks in the diff. Should be
        # unchanged.
        lineno = self.repo.run(
            self.git_hyper_blame.approx_lineno_across_revs,
            "file",
            "file",
            "tag_B",
            "tag_A",
            1,
        )
        self.assertEqual(1, lineno)

        # Test an unchanged line after all hunks in the diff. Should be matched
        # to the line's previous position in the file.
        lineno = self.repo.run(
            self.git_hyper_blame.approx_lineno_across_revs,
            "file",
            "file",
            "tag_D",
            "tag_C",
            6,
        )
        self.assertEqual(4, lineno)

        # Test a line added in a new hunk. Should be matched to the line
        # *before* where the hunk was inserted in the old version of the file.
        lineno = self.repo.run(
            self.git_hyper_blame.approx_lineno_across_revs,
            "file",
            "file",
            "tag_F",
            "tag_E",
            6,
        )
        self.assertEqual(5, lineno)

        # Test lines added in a new hunk at the very start of the file. This
        # tests an edge case: normally it would be matched to the line *before*
        # where the hunk was inserted (Line 0), but since the hunk is at the
        # start of the file, we match to Line 1.
        lineno = self.repo.run(
            self.git_hyper_blame.approx_lineno_across_revs,
            "file",
            "file",
            "tag_C",
            "tag_B",
            1,
        )
        self.assertEqual(1, lineno)
        lineno = self.repo.run(
            self.git_hyper_blame.approx_lineno_across_revs,
            "file",
            "file",
            "tag_C",
            "tag_B",
            2,
        )
        self.assertEqual(1, lineno)

        # Test an unchanged line in between hunks in the diff. Should be matched
        # to the line's previous position in the file.
        lineno = self.repo.run(
            self.git_hyper_blame.approx_lineno_across_revs,
            "file",
            "file",
            "tag_C",
            "tag_B",
            3,
        )
        self.assertEqual(1, lineno)

        # Test a changed line. Should be matched to the hunk's previous position
        # in the file.
        lineno = self.repo.run(
            self.git_hyper_blame.approx_lineno_across_revs,
            "file",
            "file",
            "tag_C",
            "tag_B",
            4,
        )
        self.assertEqual(2, lineno)

        # Test a line added in a new hunk at the very end of the file. Should be
        # matched to the line *before* where the hunk was inserted (the last
        # line of the file). Technically same as the case above but good to
        # boundary test.
        lineno = self.repo.run(
            self.git_hyper_blame.approx_lineno_across_revs,
            "file",
            "file",
            "tag_C",
            "tag_B",
            6,
        )
        self.assertEqual(3, lineno)

    def testInterHunkLineMotion(self):
        """Tests a blame with line motion in another hunk in the ignored commit."""
        # Blame from D, ignoring C.

        # Lines 1, 2 were added by D.
        # Lines 3, 4 were added by C (but ignored, so blame A).
        # Line 5 was added by A.
        # Line 6 was modified by C (but ignored, so blame B). (Note: This
        # requires the algorithm to figure out that Line 6 in D == Line 4 in C
        # ~= Line 2 in B, so it blames B. Otherwise, it would blame A.) Line 7
        # was added by A. Line 8 was added by C (but ignored, so blame A).
        expected_output = [
            self.blame_line("D", " 1) earth"),
            self.blame_line("D", " 2) fire"),
            self.blame_line("A", "3*) X"),
            self.blame_line("A", "4*) Y"),
            self.blame_line("A", " 5) A"),
            self.blame_line("B", "6*) red"),
            self.blame_line("A", " 7) blue"),
            self.blame_line("A", "8*) Z"),
        ]
        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"),
            self.blame_line("C", " 9) Z"),
        ]
        retval, output = self.run_hyperblame(["E"], "file", "tag_E")
        self.assertEqual(0, retval)
        self.assertEqual(expected_output, output)


class GitHyperBlameLineNumberTest(GitHyperBlameTestBase):
    REPO_SCHEMA = """
  A B C D
  """

    COMMIT_A = {
        "file": {"data": b"red\nblue\n"},
    }

    # Change "blue" to "green".
    COMMIT_B = {
        "file": {"data": b"red\ngreen\n"},
    }

    # Insert 2 lines at the top,
    COMMIT_C = {
        "file": {"data": b"\n\nred\ngreen\n"},
    }

    # Change "green" to "yellow".
    COMMIT_D = {
        "file": {"data": b"\n\nred\nyellow\n"},
    }

    def testTwoChangesWithAddedLines(self):
        """Regression test for https://crbug.com/709831.

        Tests a line with multiple ignored edits, and a line number change in
        between (such that the line number in the current revision is bigger than
        the file's line count at the older ignored revision).
        """
        expected_output = [
            self.blame_line("C", " 1) "),
            self.blame_line("C", " 2) "),
            self.blame_line("A", " 3) red"),
            self.blame_line("A", "4*) yellow"),
        ]
        # Due to https://crbug.com/709831, ignoring both B and D would crash,
        # because of C (in between those revisions) which moves Line 2 to Line
        # 4. The algorithm would incorrectly think that Line 4 was still on Line
        # 4 in Commit B, even though it was Line 2 at that time. Its index is
        # out of range in the number of lines in Commit B.
        retval, output = self.run_hyperblame(["B", "D"], "file", "tag_D")
        self.assertEqual(0, retval)
        self.assertEqual(expected_output, output)


class GitHyperBlameUnicodeTest(GitHyperBlameTestBase):
    REPO_SCHEMA = """
  A B C
  """

    COMMIT_A = {
        GitRepo.AUTHOR_NAME: "ASCII Author",
        "file": {"data": b"red\nblue\n"},
    }

    # Add a line.
    COMMIT_B = {
        GitRepo.AUTHOR_NAME: "\u4e2d\u56fd\u4f5c\u8005",
        "file": {"data": b"red\ngreen\nblue\n"},
    }

    # Modify a line with non-UTF-8 author and file text.
    COMMIT_C = {
        GitRepo.AUTHOR_NAME: "Lat\xedn-1 Author",
        "file": {"data": "red\ngre\u00e9n\nblue\n".encode("latin-1")},
    }

    def testNonUTF8Data(self):
        """Ensures correct behaviour even if author or file data is not UTF-8.

        There is no guarantee that a file will be UTF-8-encoded, so this is
        realistic.
        """
        expected_output = [
            self.blame_line("A", "1) red", author="ASCII Author  "),
            # The Author has been re-encoded as UTF-8. The file data is
            # converted to UTF8 and unknown characters replaced.
            self.blame_line("C", "2) gre\ufffdn", author="Lat\xedn-1 Author"),
            self.blame_line("A", "3) blue", author="ASCII Author  "),
        ]
        retval, output = self.run_hyperblame([], "file", "tag_C")
        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")
        )
    )
