[GITEA] Detect file rename and show in history

- Add a indication to the file history if the file has been renamed,
this indication contains a link to browse the history of the file
further.
- Added unit testing.
- Added integration testing.
- Resolves https://codeberg.org/forgejo/forgejo/issues/1279

(cherry picked from commit 72c297521b)
(cherry picked from commit 283f964894)

Conflicts:
	options/locale/locale_en-US.ini
	https://codeberg.org/forgejo/forgejo/pulls/1550
(cherry picked from commit 7c30af7fde)
(cherry picked from commit f3be6eb269)
(cherry picked from commit 78e1755b94)
(cherry picked from commit 73799479e0)
(cherry picked from commit 938359b941)
This commit is contained in:
Gusted 2023-09-14 21:53:57 +02:00 committed by Earl Warren
parent 107a9b8233
commit b168a9c081
No known key found for this signature in database
GPG key ID: 0579CB2928A78A00
22 changed files with 140 additions and 2 deletions

View file

@ -509,6 +509,62 @@ func GetCommitFileStatus(ctx context.Context, repoPath, commitID string) (*Commi
return fileStatus, nil return fileStatus, nil
} }
func parseCommitRenames(renames *[][2]string, stdout io.Reader) {
rd := bufio.NewReader(stdout)
for {
// Skip (R || three digits || NULL byte)
_, err := rd.Discard(5)
if err != nil {
if err != io.EOF {
log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err)
}
return
}
oldFileName, err := rd.ReadString('\x00')
if err != nil {
if err != io.EOF {
log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err)
}
return
}
newFileName, err := rd.ReadString('\x00')
if err != nil {
if err != io.EOF {
log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err)
}
return
}
oldFileName = strings.TrimSuffix(oldFileName, "\x00")
newFileName = strings.TrimSuffix(newFileName, "\x00")
*renames = append(*renames, [2]string{oldFileName, newFileName})
}
}
// GetCommitFileRenames returns the renames that the commit contains.
func GetCommitFileRenames(ctx context.Context, repoPath, commitID string) ([][2]string, error) {
renames := [][2]string{}
stdout, w := io.Pipe()
done := make(chan struct{})
go func() {
parseCommitRenames(&renames, stdout)
close(done)
}()
stderr := new(bytes.Buffer)
err := NewCommand(ctx, "show", "--name-status", "--pretty=format:", "-z", "--diff-filter=R").AddDynamicArguments(commitID).Run(&RunOpts{
Dir: repoPath,
Stdout: w,
Stderr: stderr,
})
w.Close() // Close writer to exit parsing goroutine
if err != nil {
return nil, ConcatenateError(err, stderr.String())
}
<-done
return renames, nil
}
// GetFullCommitID returns full length (40) of commit ID by given short SHA in a repository. // GetFullCommitID returns full length (40) of commit ID by given short SHA in a repository.
func GetFullCommitID(ctx context.Context, repoPath, shortID string) (string, error) { func GetFullCommitID(ctx context.Context, repoPath, shortID string) (string, error) {
commitID, _, err := NewCommand(ctx, "rev-parse").AddDynamicArguments(shortID).RunStdString(&RunOpts{Dir: repoPath}) commitID, _, err := NewCommand(ctx, "rev-parse").AddDynamicArguments(shortID).RunStdString(&RunOpts{Dir: repoPath})

View file

@ -278,3 +278,30 @@ func TestGetCommitFileStatusMerges(t *testing.T) {
assert.Equal(t, commitFileStatus.Removed, expected.Removed) assert.Equal(t, commitFileStatus.Removed, expected.Removed)
assert.Equal(t, commitFileStatus.Modified, expected.Modified) assert.Equal(t, commitFileStatus.Modified, expected.Modified)
} }
func TestParseCommitRenames(t *testing.T) {
testcases := []struct {
output string
renames [][2]string
}{
{
output: "R090\x00renamed.txt\x00history.txt\x00",
renames: [][2]string{{"renamed.txt", "history.txt"}},
},
{
output: "R090\x00renamed.txt\x00history.txt\x00R000\x00corruptedstdouthere",
renames: [][2]string{{"renamed.txt", "history.txt"}},
},
{
output: "R100\x00renamed.txt\x00history.txt\x00R001\x00readme.md\x00README.md\x00",
renames: [][2]string{{"renamed.txt", "history.txt"}, {"readme.md", "README.md"}},
},
}
for _, testcase := range testcases {
renames := [][2]string{}
parseCommitRenames(&renames, strings.NewReader(testcase.output))
assert.Equal(t, testcase.renames, renames)
}
}

View file

@ -1280,6 +1280,8 @@ commits.find = Search
commits.search_all = All Branches commits.search_all = All Branches
commits.author = Author commits.author = Author
commits.message = Message commits.message = Message
commits.browse_further = Browse further
commits.renamed_from = Renamed from %s
commits.date = Date commits.date = Date
commits.older = Older commits.older = Older
commits.newer = Newer commits.newer = Newer

View file

@ -239,6 +239,22 @@ func FileHistory(ctx *context.Context) {
ctx.ServerError("CommitsByFileAndRange", err) ctx.ServerError("CommitsByFileAndRange", err)
return return
} }
oldestCommit := commits[len(commits)-1]
renamedFiles, err := git.GetCommitFileRenames(ctx, ctx.Repo.GitRepo.Path, oldestCommit.ID.String())
if err != nil {
ctx.ServerError("GetCommitFileRenames", err)
return
}
for _, renames := range renamedFiles {
if renames[1] == fileName {
ctx.Data["OldFilename"] = renames[0]
ctx.Data["OldFilenameHistory"] = fmt.Sprintf("%s/commits/commit/%s/%s", ctx.Repo.RepoLink, oldestCommit.ID.String(), renames[0])
break
}
}
ctx.Data["Commits"] = git_model.ConvertFromGitCommit(ctx, commits, ctx.Repo.Repository) ctx.Data["Commits"] = git_model.ConvertFromGitCommit(ctx, commits, ctx.Repo.Repository)
ctx.Data["Username"] = ctx.Repo.Owner.Name ctx.Data["Username"] = ctx.Repo.Owner.Name

View file

@ -13,6 +13,11 @@
</div> </div>
</div> </div>
{{template "repo/commits_table" .}} {{template "repo/commits_table" .}}
{{if .OldFilename}}
<div class="ui bottom attached header">
<span>{{.locale.Tr "repo.commits.renamed_from" .OldFilename}} (<a href="{{.OldFilenameHistory}}">{{.locale.Tr "repo.commits.browse_further"}}</a>)</span>
</div>
{{end}}
</div> </div>
</div> </div>
{{template "base/footer" .}} {{template "base/footer" .}}

View file

@ -0,0 +1,2 @@
P pack-6dd3a6fe138f1d77e14c2e6b8e6c41e5ae242adf.pack

View file

@ -1,3 +1,4 @@
# pack-refs with: peeled fully-peeled sorted # pack-refs with: peeled fully-peeled sorted
d8f53dfb33f6ccf4169c34970b5e747511c18beb refs/heads/master d8f53dfb33f6ccf4169c34970b5e747511c18beb refs/heads/cake-recipe
80b83c5c8220c3aa3906e081f202a2a7563ec879 refs/heads/master
d8f53dfb33f6ccf4169c34970b5e747511c18beb refs/tags/v1.0 d8f53dfb33f6ccf4169c34970b5e747511c18beb refs/tags/v1.0

View file

@ -1 +0,0 @@
d8f53dfb33f6ccf4169c34970b5e747511c18beb

View file

@ -690,3 +690,33 @@ func TestDangerZoneConfirmation(t *testing.T) {
}) })
}) })
} }
func TestRenamedFileHistory(t *testing.T) {
defer tests.PrepareTestEnv(t)()
t.Run("Renamed file", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", "/user2/repo59/commits/branch/master/license")
resp := MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
renameNotice := htmlDoc.doc.Find(".ui.bottom.attached.header")
assert.Equal(t, 1, renameNotice.Length())
assert.Contains(t, renameNotice.Text(), "Renamed from licnse (Browse further)")
oldFileHistoryLink, ok := renameNotice.Find("a").Attr("href")
assert.True(t, ok)
assert.Equal(t, "/user2/repo59/commits/commit/80b83c5c8220c3aa3906e081f202a2a7563ec879/licnse", oldFileHistoryLink)
})
t.Run("Non renamed file", func(t *testing.T) {
req := NewRequest(t, "GET", "/user2/repo59/commits/branch/master/README.md")
resp := MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
htmlDoc.AssertElement(t, ".ui.bottom.attached.header", false)
})
}