]> source.dussan.org Git - gitea.git/commitdiff
Support `.git-blame-ignore-revs` file (#26395)
authorKN4CK3R <admin@oldschoolhack.me>
Sat, 16 Sep 2023 17:42:34 +0000 (19:42 +0200)
committerGitHub <noreply@github.com>
Sat, 16 Sep 2023 17:42:34 +0000 (17:42 +0000)
Closes #26329

This PR adds the ability to ignore revisions specified in the
`.git-blame-ignore-revs` file in the root of the repository.

![grafik](https://github.com/go-gitea/gitea/assets/1666336/9e91be0c-6e9c-431c-bbe9-5f80154251c8)

The banner is displayed in this case. I intentionally did not add a UI
way to bypass the ignore file (same behaviour as Github) but you can add
`?bypass-blame-ignore=true` to the url manually.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
19 files changed:
docs/content/usage/blame.en-us.md [new file with mode: 0644]
docs/static/octicon-versions.svg [new file with mode: 0644]
modules/git/blame.go
modules/git/blame_test.go
modules/git/tests/repos/repo6_blame/HEAD [new file with mode: 0644]
modules/git/tests/repos/repo6_blame/config [new file with mode: 0644]
modules/git/tests/repos/repo6_blame/objects/31/bb4b42cecf0a98fc9a32fc5aaeaf53ec52643c [new file with mode: 0644]
modules/git/tests/repos/repo6_blame/objects/3b/0f66d8b065f8adbf2fef7d986528c655b98cb1 [new file with mode: 0644]
modules/git/tests/repos/repo6_blame/objects/45/fb6cbc12f970b04eacd5cd4165edd11c8d7376 [new file with mode: 0644]
modules/git/tests/repos/repo6_blame/objects/49/7701e5bb8676e419b93875d8f0808c7b31aed9 [new file with mode: 0644]
modules/git/tests/repos/repo6_blame/objects/54/4d8f7a3b15927cddf2299b4b562d6ebd71b6a7 [new file with mode: 0644]
modules/git/tests/repos/repo6_blame/objects/a8/9199e8dea077e4a8ba0bc01bc155275cfdd044 [new file with mode: 0644]
modules/git/tests/repos/repo6_blame/objects/af/7486bd54cfc39eea97207ca666aa69c9d6df93 [new file with mode: 0644]
modules/git/tests/repos/repo6_blame/objects/b8/d1ba1ccb58ee3744b3d1434aae7d26ce2d9421 [new file with mode: 0644]
modules/git/tests/repos/repo6_blame/objects/ca/411a3b842c3caec045772da42de16b3ffdafe8 [new file with mode: 0644]
modules/git/tests/repos/repo6_blame/refs/heads/master [new file with mode: 0644]
options/locale/locale_en-US.ini
routers/web/repo/blame.go
templates/repo/blame.tmpl

diff --git a/docs/content/usage/blame.en-us.md b/docs/content/usage/blame.en-us.md
new file mode 100644 (file)
index 0000000..7772bbc
--- /dev/null
@@ -0,0 +1,38 @@
+---
+date: "2023-08-14T00:00:00+00:00"
+title: "Blame File View"
+slug: "blame"
+sidebar_position: 13
+toc: false
+draft: false
+aliases:
+  - /en-us/blame
+menu:
+  sidebar:
+    parent: "usage"
+    name: "Blame"
+    sidebar_position: 13
+    identifier: "blame"
+---
+
+# Blame File View
+
+Gitea supports viewing the line-by-line revision history for a file also known as blame view.
+You can also use [`git blame`](https://git-scm.com/docs/git-blame) on the command line to view the revision history of lines within a file.
+
+1. Navigate to and open the file whose line history you want to view.
+1. Click the `Blame` button in the file header bar.
+1. The new view shows the line-by-line revision history for a file with author and commit information on the left side.
+1. To navigate to an older commit, click the ![versions](/octicon-versions.svg) icon.
+
+## Ignore commits in the blame view
+
+All revisions specified in the `.git-blame-ignore-revs` file are hidden from the blame view.
+This is especially useful to hide reformatting changes and keep the benefits of `git blame`.
+Lines that were changed or added by an ignored commit will be blamed on the previous commit that changed that line or nearby lines.
+The `.git-blame-ignore-revs` file must be located in the root directory of the repository.
+For more information like the file format, see [the `git blame --ignore-revs-file` documentation](https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revs-fileltfilegt).
+
+### Bypassing `.git-blame-ignore-revs` in the blame view
+
+If the blame view for a file shows a message about ignored revisions, you can see the normal blame view by appending the url parameter `?bypass-blame-ignore=true`.
diff --git a/docs/static/octicon-versions.svg b/docs/static/octicon-versions.svg
new file mode 100644 (file)
index 0000000..aaf5f9c
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M7.75 14A1.75 1.75 0 0 1 6 12.25v-8.5C6 2.784 6.784 2 7.75 2h6.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0 1 14.25 14Zm-.25-1.75c0 .138.112.25.25.25h6.5a.25.25 0 0 0 .25-.25v-8.5a.25.25 0 0 0-.25-.25h-6.5a.25.25 0 0 0-.25.25ZM4.9 3.508a.75.75 0 0 1-.274 1.025.249.249 0 0 0-.126.217v6.5c0 .09.048.173.126.217a.75.75 0 0 1-.752 1.298A1.75 1.75 0 0 1 3 11.25v-6.5c0-.649.353-1.214.874-1.516a.75.75 0 0 1 1.025.274ZM1.625 5.533h.001a.249.249 0 0 0-.126.217v4.5c0 .09.048.173.126.217a.75.75 0 0 1-.752 1.298A1.748 1.748 0 0 1 0 10.25v-4.5a1.748 1.748 0 0 1 .873-1.516.75.75 0 1 1 .752 1.299Z"></path></svg>
\ No newline at end of file
index 4bd13dc32d27e2c2125c211d12c6da67a5476b03..6728a6bed85f1b57bd58d9497fd4f147256b40df 100644 (file)
@@ -13,6 +13,7 @@ import (
        "regexp"
 
        "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/util"
 )
 
 // BlamePart represents block of blame - continuous lines with one sha
@@ -23,12 +24,16 @@ type BlamePart struct {
 
 // BlameReader returns part of file blame one by one
 type BlameReader struct {
-       cmd            *Command
        output         io.WriteCloser
        reader         io.ReadCloser
        bufferedReader *bufio.Reader
        done           chan error
        lastSha        *string
+       ignoreRevsFile *string
+}
+
+func (r *BlameReader) UsesIgnoreRevs() bool {
+       return r.ignoreRevsFile != nil
 }
 
 var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})")
@@ -101,28 +106,44 @@ func (r *BlameReader) Close() error {
        r.bufferedReader = nil
        _ = r.reader.Close()
        _ = r.output.Close()
+       if r.ignoreRevsFile != nil {
+               _ = util.Remove(*r.ignoreRevsFile)
+       }
        return err
 }
 
 // CreateBlameReader creates reader for given repository, commit and file
-func CreateBlameReader(ctx context.Context, repoPath, commitID, file string) (*BlameReader, error) {
-       cmd := NewCommandContextNoGlobals(ctx, "blame", "--porcelain").
-               AddDynamicArguments(commitID).
+func CreateBlameReader(ctx context.Context, repoPath string, commit *Commit, file string, bypassBlameIgnore bool) (*BlameReader, error) {
+       var ignoreRevsFile *string
+       if CheckGitVersionAtLeast("2.23") == nil && !bypassBlameIgnore {
+               ignoreRevsFile = tryCreateBlameIgnoreRevsFile(commit)
+       }
+
+       cmd := NewCommandContextNoGlobals(ctx, "blame", "--porcelain")
+       if ignoreRevsFile != nil {
+               // Possible improvement: use --ignore-revs-file /dev/stdin on unix
+               // There is no equivalent on Windows. May be implemented if Gitea uses an external git backend.
+               cmd.AddOptionValues("--ignore-revs-file", *ignoreRevsFile)
+       }
+       cmd.AddDynamicArguments(commit.ID.String()).
                AddDashesAndList(file).
                SetDescription(fmt.Sprintf("GetBlame [repo_path: %s]", repoPath))
        reader, stdout, err := os.Pipe()
        if err != nil {
+               if ignoreRevsFile != nil {
+                       _ = util.Remove(*ignoreRevsFile)
+               }
                return nil, err
        }
 
        done := make(chan error, 1)
 
-       go func(cmd *Command, dir string, stdout io.WriteCloser, done chan error) {
+       go func() {
                stderr := bytes.Buffer{}
                // TODO: it doesn't work for directories (the directories shouldn't be "blamed"), and the "err" should be returned by "Read" but not by "Close"
                err := cmd.Run(&RunOpts{
                        UseContextTimeout: true,
-                       Dir:               dir,
+                       Dir:               repoPath,
                        Stdout:            stdout,
                        Stderr:            &stderr,
                })
@@ -131,15 +152,42 @@ func CreateBlameReader(ctx context.Context, repoPath, commitID, file string) (*B
                if err != nil {
                        log.Error("Error running git blame (dir: %v): %v, stderr: %v", repoPath, err, stderr.String())
                }
-       }(cmd, repoPath, stdout, done)
+       }()
 
        bufferedReader := bufio.NewReader(reader)
 
        return &BlameReader{
-               cmd:            cmd,
                output:         stdout,
                reader:         reader,
                bufferedReader: bufferedReader,
                done:           done,
+               ignoreRevsFile: ignoreRevsFile,
        }, nil
 }
+
+func tryCreateBlameIgnoreRevsFile(commit *Commit) *string {
+       entry, err := commit.GetTreeEntryByPath(".git-blame-ignore-revs")
+       if err != nil {
+               return nil
+       }
+
+       r, err := entry.Blob().DataAsync()
+       if err != nil {
+               return nil
+       }
+       defer r.Close()
+
+       f, err := os.CreateTemp("", "gitea_git-blame-ignore-revs")
+       if err != nil {
+               return nil
+       }
+
+       _, err = io.Copy(f, r)
+       _ = f.Close()
+       if err != nil {
+               _ = util.Remove(f.Name())
+               return nil
+       }
+
+       return util.ToPointer(f.Name())
+}
index 1c0cd5c4aa20d20805d50f4c69140006463da24f..013350ac2f4ebba03070aef625e4501cca779529 100644 (file)
@@ -14,27 +14,127 @@ func TestReadingBlameOutput(t *testing.T) {
        ctx, cancel := context.WithCancel(context.Background())
        defer cancel()
 
-       blameReader, err := CreateBlameReader(ctx, "./tests/repos/repo5_pulls", "f32b0a9dfd09a60f616f29158f772cedd89942d2", "README.md")
-       assert.NoError(t, err)
-       defer blameReader.Close()
-
-       parts := []*BlamePart{
-               {
-                       "72866af952e98d02a73003501836074b286a78f6",
-                       []string{
-                               "# test_repo",
-                               "Test repository for testing migration from github to gitea",
-                       },
-               },
-               {
-                       "f32b0a9dfd09a60f616f29158f772cedd89942d2",
-                       []string{"", "Do not make any changes to this repo it is used for unit testing"},
-               },
-       }
-
-       for _, part := range parts {
-               actualPart, err := blameReader.NextPart()
+       t.Run("Without .git-blame-ignore-revs", func(t *testing.T) {
+               repo, err := OpenRepository(ctx, "./tests/repos/repo5_pulls")
                assert.NoError(t, err)
-               assert.Equal(t, part, actualPart)
-       }
+               defer repo.Close()
+
+               commit, err := repo.GetCommit("f32b0a9dfd09a60f616f29158f772cedd89942d2")
+               assert.NoError(t, err)
+
+               parts := []*BlamePart{
+                       {
+                               "72866af952e98d02a73003501836074b286a78f6",
+                               []string{
+                                       "# test_repo",
+                                       "Test repository for testing migration from github to gitea",
+                               },
+                       },
+                       {
+                               "f32b0a9dfd09a60f616f29158f772cedd89942d2",
+                               []string{"", "Do not make any changes to this repo it is used for unit testing"},
+                       },
+               }
+
+               for _, bypass := range []bool{false, true} {
+                       blameReader, err := CreateBlameReader(ctx, "./tests/repos/repo5_pulls", commit, "README.md", bypass)
+                       assert.NoError(t, err)
+                       assert.NotNil(t, blameReader)
+                       defer blameReader.Close()
+
+                       assert.False(t, blameReader.UsesIgnoreRevs())
+
+                       for _, part := range parts {
+                               actualPart, err := blameReader.NextPart()
+                               assert.NoError(t, err)
+                               assert.Equal(t, part, actualPart)
+                       }
+
+                       // make sure all parts have been read
+                       actualPart, err := blameReader.NextPart()
+                       assert.Nil(t, actualPart)
+                       assert.NoError(t, err)
+               }
+       })
+
+       t.Run("With .git-blame-ignore-revs", func(t *testing.T) {
+               repo, err := OpenRepository(ctx, "./tests/repos/repo6_blame")
+               assert.NoError(t, err)
+               defer repo.Close()
+
+               full := []*BlamePart{
+                       {
+                               "af7486bd54cfc39eea97207ca666aa69c9d6df93",
+                               []string{"line", "line"},
+                       },
+                       {
+                               "45fb6cbc12f970b04eacd5cd4165edd11c8d7376",
+                               []string{"changed line"},
+                       },
+                       {
+                               "af7486bd54cfc39eea97207ca666aa69c9d6df93",
+                               []string{"line", "line", ""},
+                       },
+               }
+
+               cases := []struct {
+                       CommitID       string
+                       UsesIgnoreRevs bool
+                       Bypass         bool
+                       Parts          []*BlamePart
+               }{
+                       {
+                               CommitID:       "544d8f7a3b15927cddf2299b4b562d6ebd71b6a7",
+                               UsesIgnoreRevs: true,
+                               Bypass:         false,
+                               Parts: []*BlamePart{
+                                       {
+                                               "af7486bd54cfc39eea97207ca666aa69c9d6df93",
+                                               []string{"line", "line", "changed line", "line", "line", ""},
+                                       },
+                               },
+                       },
+                       {
+                               CommitID:       "544d8f7a3b15927cddf2299b4b562d6ebd71b6a7",
+                               UsesIgnoreRevs: false,
+                               Bypass:         true,
+                               Parts:          full,
+                       },
+                       {
+                               CommitID:       "45fb6cbc12f970b04eacd5cd4165edd11c8d7376",
+                               UsesIgnoreRevs: false,
+                               Bypass:         false,
+                               Parts:          full,
+                       },
+                       {
+                               CommitID:       "45fb6cbc12f970b04eacd5cd4165edd11c8d7376",
+                               UsesIgnoreRevs: false,
+                               Bypass:         false,
+                               Parts:          full,
+                       },
+               }
+
+               for _, c := range cases {
+                       commit, err := repo.GetCommit(c.CommitID)
+                       assert.NoError(t, err)
+
+                       blameReader, err := CreateBlameReader(ctx, "./tests/repos/repo6_blame", commit, "blame.txt", c.Bypass)
+                       assert.NoError(t, err)
+                       assert.NotNil(t, blameReader)
+                       defer blameReader.Close()
+
+                       assert.Equal(t, c.UsesIgnoreRevs, blameReader.UsesIgnoreRevs())
+
+                       for _, part := range c.Parts {
+                               actualPart, err := blameReader.NextPart()
+                               assert.NoError(t, err)
+                               assert.Equal(t, part, actualPart)
+                       }
+
+                       // make sure all parts have been read
+                       actualPart, err := blameReader.NextPart()
+                       assert.Nil(t, actualPart)
+                       assert.NoError(t, err)
+               }
+       })
 }
diff --git a/modules/git/tests/repos/repo6_blame/HEAD b/modules/git/tests/repos/repo6_blame/HEAD
new file mode 100644 (file)
index 0000000..cb089cd
--- /dev/null
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/modules/git/tests/repos/repo6_blame/config b/modules/git/tests/repos/repo6_blame/config
new file mode 100644 (file)
index 0000000..07d359d
--- /dev/null
@@ -0,0 +1,4 @@
+[core]
+       repositoryformatversion = 0
+       filemode = true
+       bare = true
diff --git a/modules/git/tests/repos/repo6_blame/objects/31/bb4b42cecf0a98fc9a32fc5aaeaf53ec52643c b/modules/git/tests/repos/repo6_blame/objects/31/bb4b42cecf0a98fc9a32fc5aaeaf53ec52643c
new file mode 100644 (file)
index 0000000..6cde910
Binary files /dev/null and b/modules/git/tests/repos/repo6_blame/objects/31/bb4b42cecf0a98fc9a32fc5aaeaf53ec52643c differ
diff --git a/modules/git/tests/repos/repo6_blame/objects/3b/0f66d8b065f8adbf2fef7d986528c655b98cb1 b/modules/git/tests/repos/repo6_blame/objects/3b/0f66d8b065f8adbf2fef7d986528c655b98cb1
new file mode 100644 (file)
index 0000000..b8db01d
Binary files /dev/null and b/modules/git/tests/repos/repo6_blame/objects/3b/0f66d8b065f8adbf2fef7d986528c655b98cb1 differ
diff --git a/modules/git/tests/repos/repo6_blame/objects/45/fb6cbc12f970b04eacd5cd4165edd11c8d7376 b/modules/git/tests/repos/repo6_blame/objects/45/fb6cbc12f970b04eacd5cd4165edd11c8d7376
new file mode 100644 (file)
index 0000000..6c0ae47
Binary files /dev/null and b/modules/git/tests/repos/repo6_blame/objects/45/fb6cbc12f970b04eacd5cd4165edd11c8d7376 differ
diff --git a/modules/git/tests/repos/repo6_blame/objects/49/7701e5bb8676e419b93875d8f0808c7b31aed9 b/modules/git/tests/repos/repo6_blame/objects/49/7701e5bb8676e419b93875d8f0808c7b31aed9
new file mode 100644 (file)
index 0000000..5c2b564
Binary files /dev/null and b/modules/git/tests/repos/repo6_blame/objects/49/7701e5bb8676e419b93875d8f0808c7b31aed9 differ
diff --git a/modules/git/tests/repos/repo6_blame/objects/54/4d8f7a3b15927cddf2299b4b562d6ebd71b6a7 b/modules/git/tests/repos/repo6_blame/objects/54/4d8f7a3b15927cddf2299b4b562d6ebd71b6a7
new file mode 100644 (file)
index 0000000..3c64718
Binary files /dev/null and b/modules/git/tests/repos/repo6_blame/objects/54/4d8f7a3b15927cddf2299b4b562d6ebd71b6a7 differ
diff --git a/modules/git/tests/repos/repo6_blame/objects/a8/9199e8dea077e4a8ba0bc01bc155275cfdd044 b/modules/git/tests/repos/repo6_blame/objects/a8/9199e8dea077e4a8ba0bc01bc155275cfdd044
new file mode 100644 (file)
index 0000000..847b7bc
Binary files /dev/null and b/modules/git/tests/repos/repo6_blame/objects/a8/9199e8dea077e4a8ba0bc01bc155275cfdd044 differ
diff --git a/modules/git/tests/repos/repo6_blame/objects/af/7486bd54cfc39eea97207ca666aa69c9d6df93 b/modules/git/tests/repos/repo6_blame/objects/af/7486bd54cfc39eea97207ca666aa69c9d6df93
new file mode 100644 (file)
index 0000000..206ef1e
Binary files /dev/null and b/modules/git/tests/repos/repo6_blame/objects/af/7486bd54cfc39eea97207ca666aa69c9d6df93 differ
diff --git a/modules/git/tests/repos/repo6_blame/objects/b8/d1ba1ccb58ee3744b3d1434aae7d26ce2d9421 b/modules/git/tests/repos/repo6_blame/objects/b8/d1ba1ccb58ee3744b3d1434aae7d26ce2d9421
new file mode 100644 (file)
index 0000000..bb26889
Binary files /dev/null and b/modules/git/tests/repos/repo6_blame/objects/b8/d1ba1ccb58ee3744b3d1434aae7d26ce2d9421 differ
diff --git a/modules/git/tests/repos/repo6_blame/objects/ca/411a3b842c3caec045772da42de16b3ffdafe8 b/modules/git/tests/repos/repo6_blame/objects/ca/411a3b842c3caec045772da42de16b3ffdafe8
new file mode 100644 (file)
index 0000000..1653ed9
Binary files /dev/null and b/modules/git/tests/repos/repo6_blame/objects/ca/411a3b842c3caec045772da42de16b3ffdafe8 differ
diff --git a/modules/git/tests/repos/repo6_blame/refs/heads/master b/modules/git/tests/repos/repo6_blame/refs/heads/master
new file mode 100644 (file)
index 0000000..01c9922
--- /dev/null
@@ -0,0 +1 @@
+544d8f7a3b15927cddf2299b4b562d6ebd71b6a7
index ad7d35127eb591bccd7ae960b68e4a5ce61f0bd3..c38c9d9e4667d89fe2d1c35ce217f3b1b4706303 100644 (file)
@@ -1007,6 +1007,8 @@ delete_preexisting = Delete pre-existing files
 delete_preexisting_content = Delete files in %s
 delete_preexisting_success = Deleted unadopted files in %s
 blame_prior = View blame prior to this change
+blame.ignore_revs = Ignoring revisions in <a href="%s">.git-blame-ignore-revs</a>. Click <a href="%s">here to bypass</a> and see the normal blame view.
+blame.ignore_revs.failed = Failed to ignore revisions in <a href="%s">.git-blame-ignore-revs</a>.
 author_search_tooltip = Shows a maximum of 30 users
 
 transfer.accept = Accept Transfer
index b1cb42297c1a64244dbcdd567aa124015074e515..e4506a857e7304009fd6cc2b3fb27bbd3451cf45 100644 (file)
@@ -8,9 +8,9 @@ import (
        gotemplate "html/template"
        "net/http"
        "net/url"
+       "strconv"
        "strings"
 
-       repo_model "code.gitea.io/gitea/models/repo"
        user_model "code.gitea.io/gitea/models/user"
        "code.gitea.io/gitea/modules/charset"
        "code.gitea.io/gitea/modules/context"
@@ -45,10 +45,6 @@ func RefBlame(ctx *context.Context) {
                return
        }
 
-       userName := ctx.Repo.Owner.Name
-       repoName := ctx.Repo.Repository.Name
-       commitID := ctx.Repo.CommitID
-
        branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
        treeLink := branchLink
        rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL()
@@ -101,26 +97,16 @@ func RefBlame(ctx *context.Context) {
                return
        }
 
-       blameReader, err := git.CreateBlameReader(ctx, repo_model.RepoPath(userName, repoName), commitID, fileName)
+       bypassBlameIgnore, _ := strconv.ParseBool(ctx.FormString("bypass-blame-ignore"))
+
+       result, err := performBlame(ctx, ctx.Repo.Repository.RepoPath(), ctx.Repo.Commit, fileName, bypassBlameIgnore)
        if err != nil {
                ctx.NotFound("CreateBlameReader", err)
                return
        }
-       defer blameReader.Close()
-
-       blameParts := make([]git.BlamePart, 0)
 
-       for {
-               blamePart, err := blameReader.NextPart()
-               if err != nil {
-                       ctx.NotFound("NextPart", err)
-                       return
-               }
-               if blamePart == nil {
-                       break
-               }
-               blameParts = append(blameParts, *blamePart)
-       }
+       ctx.Data["UsesIgnoreRevs"] = result.UsesIgnoreRevs
+       ctx.Data["FaultyIgnoreRevsFile"] = result.FaultyIgnoreRevsFile
 
        // Get Topics of this repo
        renderRepoTopics(ctx)
@@ -128,16 +114,77 @@ func RefBlame(ctx *context.Context) {
                return
        }
 
-       commitNames, previousCommits := processBlameParts(ctx, blameParts)
+       commitNames, previousCommits := processBlameParts(ctx, result.Parts)
        if ctx.Written() {
                return
        }
 
-       renderBlame(ctx, blameParts, commitNames, previousCommits)
+       renderBlame(ctx, result.Parts, commitNames, previousCommits)
 
        ctx.HTML(http.StatusOK, tplRepoHome)
 }
 
+type blameResult struct {
+       Parts                []git.BlamePart
+       UsesIgnoreRevs       bool
+       FaultyIgnoreRevsFile bool
+}
+
+func performBlame(ctx *context.Context, repoPath string, commit *git.Commit, file string, bypassBlameIgnore bool) (*blameResult, error) {
+       blameReader, err := git.CreateBlameReader(ctx, repoPath, commit, file, bypassBlameIgnore)
+       if err != nil {
+               return nil, err
+       }
+
+       r := &blameResult{}
+       if err := fillBlameResult(blameReader, r); err != nil {
+               _ = blameReader.Close()
+               return nil, err
+       }
+
+       err = blameReader.Close()
+       if err != nil {
+               if len(r.Parts) == 0 && r.UsesIgnoreRevs {
+                       // try again without ignored revs
+
+                       blameReader, err = git.CreateBlameReader(ctx, repoPath, commit, file, true)
+                       if err != nil {
+                               return nil, err
+                       }
+
+                       r := &blameResult{
+                               FaultyIgnoreRevsFile: true,
+                       }
+                       if err := fillBlameResult(blameReader, r); err != nil {
+                               _ = blameReader.Close()
+                               return nil, err
+                       }
+
+                       return r, blameReader.Close()
+               }
+               return nil, err
+       }
+       return r, nil
+}
+
+func fillBlameResult(br *git.BlameReader, r *blameResult) error {
+       r.UsesIgnoreRevs = br.UsesIgnoreRevs()
+
+       r.Parts = make([]git.BlamePart, 0, 5)
+       for {
+               blamePart, err := br.NextPart()
+               if err != nil {
+                       return fmt.Errorf("BlameReader.NextPart failed: %w", err)
+               }
+               if blamePart == nil {
+                       break
+               }
+               r.Parts = append(r.Parts, *blamePart)
+       }
+
+       return nil
+}
+
 func processBlameParts(ctx *context.Context, blameParts []git.BlamePart) (map[string]*user_model.UserCommit, map[string]string) {
        // store commit data by SHA to look up avatar info etc
        commitNames := make(map[string]*user_model.UserCommit)
index b253c4d90143b52956b38befc8426f4ff925129d..3078e9bef36034ca05d08391c53331de2d940d59 100644 (file)
@@ -1,3 +1,15 @@
+{{if or .UsesIgnoreRevs .FaultyIgnoreRevsFile}}
+       {{$revsFileLink := URLJoin .RepoLink "src" .BranchNameSubURL "/.git-blame-ignore-revs"}}
+       {{if .UsesIgnoreRevs}}
+               <div class="ui info message">
+                       <p>{{.locale.Tr "repo.blame.ignore_revs" $revsFileLink (print $revsFileLink "?bypass-blame-ignore=true") | Str2html}}</p>
+               </div>
+       {{else}}
+               <div class="ui error message">
+                       <p>{{.locale.Tr "repo.blame.ignore_revs.failed" $revsFileLink | Str2html}}</p>
+               </div>
+       {{end}}
+{{end}}
 <div class="{{TabSizeClass .Editorconfig .FileName}} non-diff-file-content">
        <h4 class="file-header ui top attached header gt-df gt-ac gt-sb gt-fw">
                <div class="file-header-left gt-df gt-ac gt-py-3 gt-pr-4">