From 3cfafc4d151f04757be7a8bd384fdf2ccf48cfc7 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 14 Dec 2025 13:08:44 -0800 Subject: [PATCH 1/2] Move blame to gitrepo --- modules/gitrepo/blame.go | 208 +++++++++++++++++++++++++++ modules/gitrepo/blame_sha256_test.go | 156 ++++++++++++++++++++ modules/gitrepo/blame_test.go | 151 +++++++++++++++++++ 3 files changed, 515 insertions(+) create mode 100644 modules/gitrepo/blame_sha256_test.go create mode 100644 modules/gitrepo/blame_test.go diff --git a/modules/gitrepo/blame.go b/modules/gitrepo/blame.go index 3ce808d9b3cdb..bd64c748d44b4 100644 --- a/modules/gitrepo/blame.go +++ b/modules/gitrepo/blame.go @@ -4,9 +4,16 @@ package gitrepo import ( + "bufio" + "bytes" "context" + "io" + "os" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" ) func LineBlame(ctx context.Context, repo Repository, revision, file string, line uint) (string, error) { @@ -16,3 +23,204 @@ func LineBlame(ctx context.Context, repo Repository, revision, file string, line AddOptionValues("-p", revision). AddDashesAndList(file)) } + +// BlamePart represents block of blame - continuous lines with one sha +type BlamePart struct { + Sha string + Lines []string + PreviousSha string + PreviousPath string +} + +// BlameReader returns part of file blame one by one +type BlameReader struct { + output io.WriteCloser + reader io.ReadCloser + bufferedReader *bufio.Reader + done chan error + lastSha *string + ignoreRevsFile string + objectFormat git.ObjectFormat + cleanupFuncs []func() +} + +func (r *BlameReader) UsesIgnoreRevs() bool { + return r.ignoreRevsFile != "" +} + +// NextPart returns next part of blame (sequential code lines with the same commit) +func (r *BlameReader) NextPart() (*BlamePart, error) { + var blamePart *BlamePart + + if r.lastSha != nil { + blamePart = &BlamePart{ + Sha: *r.lastSha, + Lines: make([]string, 0), + } + } + + const previousHeader = "previous " + var lineBytes []byte + var isPrefix bool + var err error + + for err != io.EOF { + lineBytes, isPrefix, err = r.bufferedReader.ReadLine() + if err != nil && err != io.EOF { + return blamePart, err + } + + if len(lineBytes) == 0 { + // isPrefix will be false + continue + } + + var objectID string + objectFormatLength := r.objectFormat.FullLength() + + if len(lineBytes) > objectFormatLength && lineBytes[objectFormatLength] == ' ' && r.objectFormat.IsValid(string(lineBytes[0:objectFormatLength])) { + objectID = string(lineBytes[0:objectFormatLength]) + } + if len(objectID) > 0 { + if blamePart == nil { + blamePart = &BlamePart{ + Sha: objectID, + Lines: make([]string, 0), + } + } + + if blamePart.Sha != objectID { + r.lastSha = &objectID + // need to munch to end of line... + for isPrefix { + _, isPrefix, err = r.bufferedReader.ReadLine() + if err != nil && err != io.EOF { + return blamePart, err + } + } + return blamePart, nil + } + } else if lineBytes[0] == '\t' { + blamePart.Lines = append(blamePart.Lines, string(lineBytes[1:])) + } else if bytes.HasPrefix(lineBytes, []byte(previousHeader)) { + offset := len(previousHeader) // already includes a space + blamePart.PreviousSha = string(lineBytes[offset : offset+objectFormatLength]) + offset += objectFormatLength + 1 // +1 for space + blamePart.PreviousPath = string(lineBytes[offset:]) + } + + // need to munch to end of line... + for isPrefix { + _, isPrefix, err = r.bufferedReader.ReadLine() + if err != nil && err != io.EOF { + return blamePart, err + } + } + } + + r.lastSha = nil + + return blamePart, nil +} + +// Close BlameReader - don't run NextPart after invoking that +func (r *BlameReader) Close() error { + if r.bufferedReader == nil { + return nil + } + + err := <-r.done + r.bufferedReader = nil + _ = r.reader.Close() + _ = r.output.Close() + for _, cleanup := range r.cleanupFuncs { + if cleanup != nil { + cleanup() + } + } + return err +} + +// CreateBlameReader creates reader for given repository, commit and file +func CreateBlameReader(ctx context.Context, objectFormat git.ObjectFormat, repo Repository, commit *git.Commit, file string, bypassBlameIgnore bool) (rd *BlameReader, err error) { + var ignoreRevsFileName string + var ignoreRevsFileCleanup func() + defer func() { + if err != nil && ignoreRevsFileCleanup != nil { + ignoreRevsFileCleanup() + } + }() + + cmd := gitcmd.NewCommand("blame", "--porcelain") + + if git.DefaultFeatures().CheckVersionAtLeast("2.23") && !bypassBlameIgnore { + ignoreRevsFileName, ignoreRevsFileCleanup, err = tryCreateBlameIgnoreRevsFile(commit) + if err != nil && !git.IsErrNotExist(err) { + return nil, err + } + if ignoreRevsFileName != "" { + // 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", ignoreRevsFileName) + } + } + + cmd.AddDynamicArguments(commit.ID.String()).AddDashesAndList(file) + + done := make(chan error, 1) + reader, stdout, err := os.Pipe() + if err != nil { + return nil, err + } + 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 := RunCmd(ctx, repo, cmd.WithUseContextTimeout(true). + WithStdout(stdout). + WithStderr(&stderr), + ) + done <- err + _ = stdout.Close() + if err != nil { + log.Error("Error running git blame (dir: %v): %v, stderr: %v", repoPath, err, stderr.String()) + } + }() + + bufferedReader := bufio.NewReader(reader) + return &BlameReader{ + output: stdout, + reader: reader, + bufferedReader: bufferedReader, + done: done, + ignoreRevsFile: ignoreRevsFileName, + objectFormat: objectFormat, + cleanupFuncs: []func(){ignoreRevsFileCleanup}, + }, nil +} + +func tryCreateBlameIgnoreRevsFile(commit *git.Commit) (string, func(), error) { + entry, err := commit.GetTreeEntryByPath(".git-blame-ignore-revs") + if err != nil { + return "", nil, err + } + + r, err := entry.Blob().DataAsync() + if err != nil { + return "", nil, err + } + defer r.Close() + + f, cleanup, err := setting.AppDataTempDir("git-repo-content").CreateTempFileRandom("git-blame-ignore-revs") + if err != nil { + return "", nil, err + } + filename := f.Name() + _, err = io.Copy(f, r) + _ = f.Close() + if err != nil { + cleanup() + return "", nil, err + } + + return filename, cleanup, nil +} diff --git a/modules/gitrepo/blame_sha256_test.go b/modules/gitrepo/blame_sha256_test.go new file mode 100644 index 0000000000000..e92931d596a1c --- /dev/null +++ b/modules/gitrepo/blame_sha256_test.go @@ -0,0 +1,156 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + "testing" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + + "github.com/stretchr/testify/assert" +) + +func TestReadingBlameOutputSha256(t *testing.T) { + setting.AppDataPath = t.TempDir() + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + + if git.DefaultFeatures().UsingGogit { + t.Skip("Skipping test since gogit does not support sha256") + return + } + + t.Run("Without .git-blame-ignore-revs", func(t *testing.T) { + storage := &mockRepository{path: "repo5_pulls_sha256"} + repo, err := OpenRepository(ctx, storage) + assert.NoError(t, err) + defer repo.Close() + + commit, err := repo.GetCommit("0b69b7bb649b5d46e14cabb6468685e5dd721290acc7ffe604d37cde57927345") + assert.NoError(t, err) + + parts := []*BlamePart{ + { + Sha: "1e35a51dc00fd7de730344c07061acfe80e8117e075ac979b6a29a3a045190ca", + Lines: []string{ + "# test_repo", + "Test repository for testing migration from github to gitea", + }, + }, + { + Sha: "0b69b7bb649b5d46e14cabb6468685e5dd721290acc7ffe604d37cde57927345", + Lines: []string{"", "Do not make any changes to this repo it is used for unit testing"}, + PreviousSha: "1e35a51dc00fd7de730344c07061acfe80e8117e075ac979b6a29a3a045190ca", + PreviousPath: "README.md", + }, + } + + for _, bypass := range []bool{false, true} { + blameReader, err := CreateBlameReader(ctx, git.Sha256ObjectFormat, storage, 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) { + storage := &mockRepository{path: "repo6_blame_sha256"} + repo, err := OpenRepository(ctx, storage) + assert.NoError(t, err) + defer repo.Close() + + full := []*BlamePart{ + { + Sha: "ab2b57a4fa476fb2edb74dafa577caf918561abbaa8fba0c8dc63c412e17a7cc", + Lines: []string{"line", "line"}, + }, + { + Sha: "9347b0198cd1f25017579b79d0938fa89dba34ad2514f0dd92f6bc975ed1a2fe", + Lines: []string{"changed line"}, + PreviousSha: "ab2b57a4fa476fb2edb74dafa577caf918561abbaa8fba0c8dc63c412e17a7cc", + PreviousPath: "blame.txt", + }, + { + Sha: "ab2b57a4fa476fb2edb74dafa577caf918561abbaa8fba0c8dc63c412e17a7cc", + Lines: []string{"line", "line", ""}, + }, + } + + cases := []struct { + CommitID string + UsesIgnoreRevs bool + Bypass bool + Parts []*BlamePart + }{ + { + CommitID: "e2f5660e15159082902960af0ed74fc144921d2b0c80e069361853b3ece29ba3", + UsesIgnoreRevs: true, + Bypass: false, + Parts: []*BlamePart{ + { + Sha: "ab2b57a4fa476fb2edb74dafa577caf918561abbaa8fba0c8dc63c412e17a7cc", + Lines: []string{"line", "line", "changed line", "line", "line", ""}, + }, + }, + }, + { + CommitID: "e2f5660e15159082902960af0ed74fc144921d2b0c80e069361853b3ece29ba3", + UsesIgnoreRevs: false, + Bypass: true, + Parts: full, + }, + { + CommitID: "9347b0198cd1f25017579b79d0938fa89dba34ad2514f0dd92f6bc975ed1a2fe", + UsesIgnoreRevs: false, + Bypass: false, + Parts: full, + }, + { + CommitID: "9347b0198cd1f25017579b79d0938fa89dba34ad2514f0dd92f6bc975ed1a2fe", + UsesIgnoreRevs: false, + Bypass: false, + Parts: full, + }, + } + + objectFormat, err := repo.GetObjectFormat() + assert.NoError(t, err) + for _, c := range cases { + commit, err := repo.GetCommit(c.CommitID) + assert.NoError(t, err) + blameReader, err := CreateBlameReader(ctx, objectFormat, storage, 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/gitrepo/blame_test.go b/modules/gitrepo/blame_test.go new file mode 100644 index 0000000000000..0307a5fd33c7a --- /dev/null +++ b/modules/gitrepo/blame_test.go @@ -0,0 +1,151 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + "testing" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + + "github.com/stretchr/testify/assert" +) + +func TestReadingBlameOutput(t *testing.T) { + setting.AppDataPath = t.TempDir() + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + + t.Run("Without .git-blame-ignore-revs", func(t *testing.T) { + storage := &mockRepository{path: "repo5_pulls"} + repo, err := OpenRepository(ctx, storage) + assert.NoError(t, err) + defer repo.Close() + commit, err := repo.GetCommit("f32b0a9dfd09a60f616f29158f772cedd89942d2") + assert.NoError(t, err) + + parts := []*BlamePart{ + { + Sha: "72866af952e98d02a73003501836074b286a78f6", + Lines: []string{ + "# test_repo", + "Test repository for testing migration from github to gitea", + }, + }, + { + Sha: "f32b0a9dfd09a60f616f29158f772cedd89942d2", + Lines: []string{"", "Do not make any changes to this repo it is used for unit testing"}, + PreviousSha: "72866af952e98d02a73003501836074b286a78f6", + PreviousPath: "README.md", + }, + } + + for _, bypass := range []bool{false, true} { + blameReader, err := CreateBlameReader(ctx, git.Sha1ObjectFormat, storage, 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) { + storage := &mockRepository{path: "repo6_blame"} + repo, err := OpenRepository(ctx, storage) + assert.NoError(t, err) + defer repo.Close() + + full := []*BlamePart{ + { + Sha: "af7486bd54cfc39eea97207ca666aa69c9d6df93", + Lines: []string{"line", "line"}, + }, + { + Sha: "45fb6cbc12f970b04eacd5cd4165edd11c8d7376", + Lines: []string{"changed line"}, + PreviousSha: "af7486bd54cfc39eea97207ca666aa69c9d6df93", + PreviousPath: "blame.txt", + }, + { + Sha: "af7486bd54cfc39eea97207ca666aa69c9d6df93", + Lines: []string{"line", "line", ""}, + }, + } + + cases := []struct { + CommitID string + UsesIgnoreRevs bool + Bypass bool + Parts []*BlamePart + }{ + { + CommitID: "544d8f7a3b15927cddf2299b4b562d6ebd71b6a7", + UsesIgnoreRevs: true, + Bypass: false, + Parts: []*BlamePart{ + { + Sha: "af7486bd54cfc39eea97207ca666aa69c9d6df93", + Lines: []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, + }, + } + + objectFormat, err := repo.GetObjectFormat() + assert.NoError(t, err) + for _, c := range cases { + commit, err := repo.GetCommit(c.CommitID) + assert.NoError(t, err) + + blameReader, err := CreateBlameReader(ctx, objectFormat, storage, 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) + } + }) +} From 987024cfd439c951f882bf94eb1c1d213a645b3c Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 14 Dec 2025 13:12:28 -0800 Subject: [PATCH 2/2] Remove duplicated code --- modules/git/blame.go | 218 ------------------------------- modules/git/blame_sha256_test.go | 153 ---------------------- modules/git/blame_test.go | 149 --------------------- routers/web/repo/blame.go | 19 +-- 4 files changed, 10 insertions(+), 529 deletions(-) delete mode 100644 modules/git/blame.go delete mode 100644 modules/git/blame_sha256_test.go delete mode 100644 modules/git/blame_test.go diff --git a/modules/git/blame.go b/modules/git/blame.go deleted file mode 100644 index 601be96f05efb..0000000000000 --- a/modules/git/blame.go +++ /dev/null @@ -1,218 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package git - -import ( - "bufio" - "bytes" - "context" - "io" - "os" - - "code.gitea.io/gitea/modules/git/gitcmd" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" -) - -// BlamePart represents block of blame - continuous lines with one sha -type BlamePart struct { - Sha string - Lines []string - PreviousSha string - PreviousPath string -} - -// BlameReader returns part of file blame one by one -type BlameReader struct { - output io.WriteCloser - reader io.ReadCloser - bufferedReader *bufio.Reader - done chan error - lastSha *string - ignoreRevsFile string - objectFormat ObjectFormat - cleanupFuncs []func() -} - -func (r *BlameReader) UsesIgnoreRevs() bool { - return r.ignoreRevsFile != "" -} - -// NextPart returns next part of blame (sequential code lines with the same commit) -func (r *BlameReader) NextPart() (*BlamePart, error) { - var blamePart *BlamePart - - if r.lastSha != nil { - blamePart = &BlamePart{ - Sha: *r.lastSha, - Lines: make([]string, 0), - } - } - - const previousHeader = "previous " - var lineBytes []byte - var isPrefix bool - var err error - - for err != io.EOF { - lineBytes, isPrefix, err = r.bufferedReader.ReadLine() - if err != nil && err != io.EOF { - return blamePart, err - } - - if len(lineBytes) == 0 { - // isPrefix will be false - continue - } - - var objectID string - objectFormatLength := r.objectFormat.FullLength() - - if len(lineBytes) > objectFormatLength && lineBytes[objectFormatLength] == ' ' && r.objectFormat.IsValid(string(lineBytes[0:objectFormatLength])) { - objectID = string(lineBytes[0:objectFormatLength]) - } - if len(objectID) > 0 { - if blamePart == nil { - blamePart = &BlamePart{ - Sha: objectID, - Lines: make([]string, 0), - } - } - - if blamePart.Sha != objectID { - r.lastSha = &objectID - // need to munch to end of line... - for isPrefix { - _, isPrefix, err = r.bufferedReader.ReadLine() - if err != nil && err != io.EOF { - return blamePart, err - } - } - return blamePart, nil - } - } else if lineBytes[0] == '\t' { - blamePart.Lines = append(blamePart.Lines, string(lineBytes[1:])) - } else if bytes.HasPrefix(lineBytes, []byte(previousHeader)) { - offset := len(previousHeader) // already includes a space - blamePart.PreviousSha = string(lineBytes[offset : offset+objectFormatLength]) - offset += objectFormatLength + 1 // +1 for space - blamePart.PreviousPath = string(lineBytes[offset:]) - } - - // need to munch to end of line... - for isPrefix { - _, isPrefix, err = r.bufferedReader.ReadLine() - if err != nil && err != io.EOF { - return blamePart, err - } - } - } - - r.lastSha = nil - - return blamePart, nil -} - -// Close BlameReader - don't run NextPart after invoking that -func (r *BlameReader) Close() error { - if r.bufferedReader == nil { - return nil - } - - err := <-r.done - r.bufferedReader = nil - _ = r.reader.Close() - _ = r.output.Close() - for _, cleanup := range r.cleanupFuncs { - if cleanup != nil { - cleanup() - } - } - return err -} - -// CreateBlameReader creates reader for given repository, commit and file -func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath string, commit *Commit, file string, bypassBlameIgnore bool) (rd *BlameReader, err error) { - var ignoreRevsFileName string - var ignoreRevsFileCleanup func() - defer func() { - if err != nil && ignoreRevsFileCleanup != nil { - ignoreRevsFileCleanup() - } - }() - - cmd := gitcmd.NewCommand("blame", "--porcelain") - - if DefaultFeatures().CheckVersionAtLeast("2.23") && !bypassBlameIgnore { - ignoreRevsFileName, ignoreRevsFileCleanup, err = tryCreateBlameIgnoreRevsFile(commit) - if err != nil && !IsErrNotExist(err) { - return nil, err - } - if ignoreRevsFileName != "" { - // 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", ignoreRevsFileName) - } - } - - cmd.AddDynamicArguments(commit.ID.String()).AddDashesAndList(file) - - done := make(chan error, 1) - reader, stdout, err := os.Pipe() - if err != nil { - return nil, err - } - 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.WithDir(repoPath). - WithUseContextTimeout(true). - WithStdout(stdout). - WithStderr(&stderr). - Run(ctx) - done <- err - _ = stdout.Close() - if err != nil { - log.Error("Error running git blame (dir: %v): %v, stderr: %v", repoPath, err, stderr.String()) - } - }() - - bufferedReader := bufio.NewReader(reader) - return &BlameReader{ - output: stdout, - reader: reader, - bufferedReader: bufferedReader, - done: done, - ignoreRevsFile: ignoreRevsFileName, - objectFormat: objectFormat, - cleanupFuncs: []func(){ignoreRevsFileCleanup}, - }, nil -} - -func tryCreateBlameIgnoreRevsFile(commit *Commit) (string, func(), error) { - entry, err := commit.GetTreeEntryByPath(".git-blame-ignore-revs") - if err != nil { - return "", nil, err - } - - r, err := entry.Blob().DataAsync() - if err != nil { - return "", nil, err - } - defer r.Close() - - f, cleanup, err := setting.AppDataTempDir("git-repo-content").CreateTempFileRandom("git-blame-ignore-revs") - if err != nil { - return "", nil, err - } - filename := f.Name() - _, err = io.Copy(f, r) - _ = f.Close() - if err != nil { - cleanup() - return "", nil, err - } - - return filename, cleanup, nil -} diff --git a/modules/git/blame_sha256_test.go b/modules/git/blame_sha256_test.go deleted file mode 100644 index c0a97bed3bd35..0000000000000 --- a/modules/git/blame_sha256_test.go +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package git - -import ( - "context" - "testing" - - "code.gitea.io/gitea/modules/setting" - - "github.com/stretchr/testify/assert" -) - -func TestReadingBlameOutputSha256(t *testing.T) { - setting.AppDataPath = t.TempDir() - ctx, cancel := context.WithCancel(t.Context()) - defer cancel() - - if isGogit { - t.Skip("Skipping test since gogit does not support sha256") - return - } - - t.Run("Without .git-blame-ignore-revs", func(t *testing.T) { - repo, err := OpenRepository(ctx, "./tests/repos/repo5_pulls_sha256") - assert.NoError(t, err) - defer repo.Close() - - commit, err := repo.GetCommit("0b69b7bb649b5d46e14cabb6468685e5dd721290acc7ffe604d37cde57927345") - assert.NoError(t, err) - - parts := []*BlamePart{ - { - Sha: "1e35a51dc00fd7de730344c07061acfe80e8117e075ac979b6a29a3a045190ca", - Lines: []string{ - "# test_repo", - "Test repository for testing migration from github to gitea", - }, - }, - { - Sha: "0b69b7bb649b5d46e14cabb6468685e5dd721290acc7ffe604d37cde57927345", - Lines: []string{"", "Do not make any changes to this repo it is used for unit testing"}, - PreviousSha: "1e35a51dc00fd7de730344c07061acfe80e8117e075ac979b6a29a3a045190ca", - PreviousPath: "README.md", - }, - } - - for _, bypass := range []bool{false, true} { - blameReader, err := CreateBlameReader(ctx, Sha256ObjectFormat, "./tests/repos/repo5_pulls_sha256", 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_sha256") - assert.NoError(t, err) - defer repo.Close() - - full := []*BlamePart{ - { - Sha: "ab2b57a4fa476fb2edb74dafa577caf918561abbaa8fba0c8dc63c412e17a7cc", - Lines: []string{"line", "line"}, - }, - { - Sha: "9347b0198cd1f25017579b79d0938fa89dba34ad2514f0dd92f6bc975ed1a2fe", - Lines: []string{"changed line"}, - PreviousSha: "ab2b57a4fa476fb2edb74dafa577caf918561abbaa8fba0c8dc63c412e17a7cc", - PreviousPath: "blame.txt", - }, - { - Sha: "ab2b57a4fa476fb2edb74dafa577caf918561abbaa8fba0c8dc63c412e17a7cc", - Lines: []string{"line", "line", ""}, - }, - } - - cases := []struct { - CommitID string - UsesIgnoreRevs bool - Bypass bool - Parts []*BlamePart - }{ - { - CommitID: "e2f5660e15159082902960af0ed74fc144921d2b0c80e069361853b3ece29ba3", - UsesIgnoreRevs: true, - Bypass: false, - Parts: []*BlamePart{ - { - Sha: "ab2b57a4fa476fb2edb74dafa577caf918561abbaa8fba0c8dc63c412e17a7cc", - Lines: []string{"line", "line", "changed line", "line", "line", ""}, - }, - }, - }, - { - CommitID: "e2f5660e15159082902960af0ed74fc144921d2b0c80e069361853b3ece29ba3", - UsesIgnoreRevs: false, - Bypass: true, - Parts: full, - }, - { - CommitID: "9347b0198cd1f25017579b79d0938fa89dba34ad2514f0dd92f6bc975ed1a2fe", - UsesIgnoreRevs: false, - Bypass: false, - Parts: full, - }, - { - CommitID: "9347b0198cd1f25017579b79d0938fa89dba34ad2514f0dd92f6bc975ed1a2fe", - UsesIgnoreRevs: false, - Bypass: false, - Parts: full, - }, - } - - objectFormat, err := repo.GetObjectFormat() - assert.NoError(t, err) - for _, c := range cases { - commit, err := repo.GetCommit(c.CommitID) - assert.NoError(t, err) - blameReader, err := CreateBlameReader(ctx, objectFormat, "./tests/repos/repo6_blame_sha256", 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/blame_test.go b/modules/git/blame_test.go deleted file mode 100644 index 809d6fbcf7381..0000000000000 --- a/modules/git/blame_test.go +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package git - -import ( - "context" - "testing" - - "code.gitea.io/gitea/modules/setting" - - "github.com/stretchr/testify/assert" -) - -func TestReadingBlameOutput(t *testing.T) { - setting.AppDataPath = t.TempDir() - ctx, cancel := context.WithCancel(t.Context()) - defer cancel() - - t.Run("Without .git-blame-ignore-revs", func(t *testing.T) { - repo, err := OpenRepository(ctx, "./tests/repos/repo5_pulls") - assert.NoError(t, err) - defer repo.Close() - - commit, err := repo.GetCommit("f32b0a9dfd09a60f616f29158f772cedd89942d2") - assert.NoError(t, err) - - parts := []*BlamePart{ - { - Sha: "72866af952e98d02a73003501836074b286a78f6", - Lines: []string{ - "# test_repo", - "Test repository for testing migration from github to gitea", - }, - }, - { - Sha: "f32b0a9dfd09a60f616f29158f772cedd89942d2", - Lines: []string{"", "Do not make any changes to this repo it is used for unit testing"}, - PreviousSha: "72866af952e98d02a73003501836074b286a78f6", - PreviousPath: "README.md", - }, - } - - for _, bypass := range []bool{false, true} { - blameReader, err := CreateBlameReader(ctx, Sha1ObjectFormat, "./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{ - { - Sha: "af7486bd54cfc39eea97207ca666aa69c9d6df93", - Lines: []string{"line", "line"}, - }, - { - Sha: "45fb6cbc12f970b04eacd5cd4165edd11c8d7376", - Lines: []string{"changed line"}, - PreviousSha: "af7486bd54cfc39eea97207ca666aa69c9d6df93", - PreviousPath: "blame.txt", - }, - { - Sha: "af7486bd54cfc39eea97207ca666aa69c9d6df93", - Lines: []string{"line", "line", ""}, - }, - } - - cases := []struct { - CommitID string - UsesIgnoreRevs bool - Bypass bool - Parts []*BlamePart - }{ - { - CommitID: "544d8f7a3b15927cddf2299b4b562d6ebd71b6a7", - UsesIgnoreRevs: true, - Bypass: false, - Parts: []*BlamePart{ - { - Sha: "af7486bd54cfc39eea97207ca666aa69c9d6df93", - Lines: []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, - }, - } - - objectFormat, err := repo.GetObjectFormat() - assert.NoError(t, err) - for _, c := range cases { - commit, err := repo.GetCommit(c.CommitID) - assert.NoError(t, err) - - blameReader, err := CreateBlameReader(ctx, objectFormat, "./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/routers/web/repo/blame.go b/routers/web/repo/blame.go index 6a4618a3c7d48..0e95a9d023b77 100644 --- a/routers/web/repo/blame.go +++ b/routers/web/repo/blame.go @@ -17,6 +17,7 @@ import ( "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git/languagestats" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -99,7 +100,7 @@ func RefBlame(ctx *context.Context) { } type blameResult struct { - Parts []*git.BlamePart + Parts []*gitrepo.BlamePart UsesIgnoreRevs bool FaultyIgnoreRevsFile bool } @@ -107,7 +108,7 @@ type blameResult struct { func performBlame(ctx *context.Context, repo *repo_model.Repository, commit *git.Commit, file string, bypassBlameIgnore bool) (*blameResult, error) { objectFormat := ctx.Repo.GetObjectFormat() - blameReader, err := git.CreateBlameReader(ctx, objectFormat, repo.RepoPath(), commit, file, bypassBlameIgnore) + blameReader, err := gitrepo.CreateBlameReader(ctx, objectFormat, repo, commit, file, bypassBlameIgnore) if err != nil { return nil, err } @@ -123,7 +124,7 @@ func performBlame(ctx *context.Context, repo *repo_model.Repository, commit *git if len(r.Parts) == 0 && r.UsesIgnoreRevs { // try again without ignored revs - blameReader, err = git.CreateBlameReader(ctx, objectFormat, repo.RepoPath(), commit, file, true) + blameReader, err = gitrepo.CreateBlameReader(ctx, objectFormat, repo, commit, file, true) if err != nil { return nil, err } @@ -143,12 +144,12 @@ func performBlame(ctx *context.Context, repo *repo_model.Repository, commit *git return r, nil } -func fillBlameResult(br *git.BlameReader, r *blameResult) error { +func fillBlameResult(br *gitrepo.BlameReader, r *blameResult) error { r.UsesIgnoreRevs = br.UsesIgnoreRevs() - previousHelper := make(map[string]*git.BlamePart) + previousHelper := make(map[string]*gitrepo.BlamePart) - r.Parts = make([]*git.BlamePart, 0, 5) + r.Parts = make([]*gitrepo.BlamePart, 0, 5) for { blamePart, err := br.NextPart() if err != nil { @@ -173,7 +174,7 @@ func fillBlameResult(br *git.BlameReader, r *blameResult) error { return nil } -func processBlameParts(ctx *context.Context, blameParts []*git.BlamePart) map[string]*user_model.UserCommit { +func processBlameParts(ctx *context.Context, blameParts []*gitrepo.BlamePart) map[string]*user_model.UserCommit { // store commit data by SHA to look up avatar info etc commitNames := make(map[string]*user_model.UserCommit) // and as blameParts can reference the same commits multiple @@ -220,7 +221,7 @@ func processBlameParts(ctx *context.Context, blameParts []*git.BlamePart) map[st return commitNames } -func renderBlameFillFirstBlameRow(repoLink string, avatarUtils *templates.AvatarUtils, part *git.BlamePart, commit *user_model.UserCommit, br *blameRow) { +func renderBlameFillFirstBlameRow(repoLink string, avatarUtils *templates.AvatarUtils, part *gitrepo.BlamePart, commit *user_model.UserCommit, br *blameRow) { if commit.User != nil { br.Avatar = avatarUtils.Avatar(commit.User, 18) } else { @@ -234,7 +235,7 @@ func renderBlameFillFirstBlameRow(repoLink string, avatarUtils *templates.Avatar br.CommitSince = templates.TimeSince(commit.Author.When) } -func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames map[string]*user_model.UserCommit) { +func renderBlame(ctx *context.Context, blameParts []*gitrepo.BlamePart, commitNames map[string]*user_model.UserCommit) { language, err := languagestats.GetFileLanguage(ctx, ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath) if err != nil { log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)