diff --git a/modules/git/commit.go b/modules/git/commit.go index b09be25ba0..bc22d52b45 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -509,6 +509,62 @@ func GetCommitFileStatus(ctx context.Context, repoPath, commitID string) (*Commi 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. func GetFullCommitID(ctx context.Context, repoPath, shortID string) (string, error) { commitID, _, err := NewCommand(ctx, "rev-parse").AddDynamicArguments(shortID).RunStdString(&RunOpts{Dir: repoPath}) diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go index ac586fdf09..a095ac9844 100644 --- a/modules/git/commit_test.go +++ b/modules/git/commit_test.go @@ -278,3 +278,30 @@ func TestGetCommitFileStatusMerges(t *testing.T) { assert.Equal(t, commitFileStatus.Removed, expected.Removed) 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) + } +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 0e6a1cca99..9b334960cd 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1277,6 +1277,8 @@ commits.find = Search commits.search_all = All Branches commits.author = Author commits.message = Message +commits.browse_further = Browse further +commits.renamed_from = Renamed from %s commits.date = Date commits.older = Older commits.newer = Newer diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index a6eb7efeb0..fd47aa5ba5 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -239,6 +239,22 @@ func FileHistory(ctx *context.Context) { ctx.ServerError("CommitsByFileAndRange", err) 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["Username"] = ctx.Repo.Owner.Name diff --git a/templates/repo/commits.tmpl b/templates/repo/commits.tmpl index 42004c2610..b3d983029b 100644 --- a/templates/repo/commits.tmpl +++ b/templates/repo/commits.tmpl @@ -13,6 +13,11 @@ {{template "repo/commits_table" .}} + {{if .OldFilename}} +
+ {{.locale.Tr "repo.commits.renamed_from" .OldFilename}} ({{.locale.Tr "repo.commits.browse_further"}}) +
+ {{end}} {{template "base/footer" .}} diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/40/8bbd3bd1f96950f8cf2f98c479557f6b18817a b/tests/gitea-repositories-meta/user2/repo59.git/objects/40/8bbd3bd1f96950f8cf2f98c479557f6b18817a deleted file mode 100644 index 567284ef1c..0000000000 Binary files a/tests/gitea-repositories-meta/user2/repo59.git/objects/40/8bbd3bd1f96950f8cf2f98c479557f6b18817a and /dev/null differ diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/5d/5c87a90af64cc67f22d60a942d5efaef8bc96b b/tests/gitea-repositories-meta/user2/repo59.git/objects/5d/5c87a90af64cc67f22d60a942d5efaef8bc96b deleted file mode 100644 index f23960f4cc..0000000000 Binary files a/tests/gitea-repositories-meta/user2/repo59.git/objects/5d/5c87a90af64cc67f22d60a942d5efaef8bc96b and /dev/null differ diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/88/3e2970ed6937cbb63311e941adb97df0ae3a52 b/tests/gitea-repositories-meta/user2/repo59.git/objects/88/3e2970ed6937cbb63311e941adb97df0ae3a52 deleted file mode 100644 index 46cc9e3e5e..0000000000 Binary files a/tests/gitea-repositories-meta/user2/repo59.git/objects/88/3e2970ed6937cbb63311e941adb97df0ae3a52 and /dev/null differ diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/8c/ac7a8f434451410cc91ab9c04d07baff974ad8 b/tests/gitea-repositories-meta/user2/repo59.git/objects/8c/ac7a8f434451410cc91ab9c04d07baff974ad8 deleted file mode 100644 index 5a1e79326f..0000000000 Binary files a/tests/gitea-repositories-meta/user2/repo59.git/objects/8c/ac7a8f434451410cc91ab9c04d07baff974ad8 and /dev/null differ diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/a0/ccafed39086ef520be6886d9395eb2100d317e b/tests/gitea-repositories-meta/user2/repo59.git/objects/a0/ccafed39086ef520be6886d9395eb2100d317e deleted file mode 100644 index 3b71228a7e..0000000000 Binary files a/tests/gitea-repositories-meta/user2/repo59.git/objects/a0/ccafed39086ef520be6886d9395eb2100d317e and /dev/null differ diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/ab/e2a9ddfd7f542ff89bc13960a929dc8ca86c99 b/tests/gitea-repositories-meta/user2/repo59.git/objects/ab/e2a9ddfd7f542ff89bc13960a929dc8ca86c99 deleted file mode 100644 index dcbd3b3eb9..0000000000 Binary files a/tests/gitea-repositories-meta/user2/repo59.git/objects/ab/e2a9ddfd7f542ff89bc13960a929dc8ca86c99 and /dev/null differ diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/cd/879fb6cf5b7bbe0fbc3a0ef44c8695fde89a56 b/tests/gitea-repositories-meta/user2/repo59.git/objects/cd/879fb6cf5b7bbe0fbc3a0ef44c8695fde89a56 deleted file mode 100644 index cd9cea6797..0000000000 Binary files a/tests/gitea-repositories-meta/user2/repo59.git/objects/cd/879fb6cf5b7bbe0fbc3a0ef44c8695fde89a56 and /dev/null differ diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/d8/f53dfb33f6ccf4169c34970b5e747511c18beb b/tests/gitea-repositories-meta/user2/repo59.git/objects/d8/f53dfb33f6ccf4169c34970b5e747511c18beb deleted file mode 100644 index bea28c9f69..0000000000 Binary files a/tests/gitea-repositories-meta/user2/repo59.git/objects/d8/f53dfb33f6ccf4169c34970b5e747511c18beb and /dev/null differ diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/f3/c1ec36c0e7605be54e71f24035caa675b7ba41 b/tests/gitea-repositories-meta/user2/repo59.git/objects/f3/c1ec36c0e7605be54e71f24035caa675b7ba41 deleted file mode 100644 index b6803eb5a2..0000000000 Binary files a/tests/gitea-repositories-meta/user2/repo59.git/objects/f3/c1ec36c0e7605be54e71f24035caa675b7ba41 and /dev/null differ diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/info/commit-graph b/tests/gitea-repositories-meta/user2/repo59.git/objects/info/commit-graph new file mode 100644 index 0000000000..d151dc87e6 Binary files /dev/null and b/tests/gitea-repositories-meta/user2/repo59.git/objects/info/commit-graph differ diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/info/packs b/tests/gitea-repositories-meta/user2/repo59.git/objects/info/packs new file mode 100644 index 0000000000..0374746b5e --- /dev/null +++ b/tests/gitea-repositories-meta/user2/repo59.git/objects/info/packs @@ -0,0 +1,2 @@ +P pack-6dd3a6fe138f1d77e14c2e6b8e6c41e5ae242adf.pack + diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/pack/pack-6dd3a6fe138f1d77e14c2e6b8e6c41e5ae242adf.idx b/tests/gitea-repositories-meta/user2/repo59.git/objects/pack/pack-6dd3a6fe138f1d77e14c2e6b8e6c41e5ae242adf.idx new file mode 100644 index 0000000000..aaa9981cf5 Binary files /dev/null and b/tests/gitea-repositories-meta/user2/repo59.git/objects/pack/pack-6dd3a6fe138f1d77e14c2e6b8e6c41e5ae242adf.idx differ diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/pack/pack-6dd3a6fe138f1d77e14c2e6b8e6c41e5ae242adf.pack b/tests/gitea-repositories-meta/user2/repo59.git/objects/pack/pack-6dd3a6fe138f1d77e14c2e6b8e6c41e5ae242adf.pack new file mode 100644 index 0000000000..ddb8c16caf Binary files /dev/null and b/tests/gitea-repositories-meta/user2/repo59.git/objects/pack/pack-6dd3a6fe138f1d77e14c2e6b8e6c41e5ae242adf.pack differ diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/pack/pack-6dd3a6fe138f1d77e14c2e6b8e6c41e5ae242adf.rev b/tests/gitea-repositories-meta/user2/repo59.git/objects/pack/pack-6dd3a6fe138f1d77e14c2e6b8e6c41e5ae242adf.rev new file mode 100644 index 0000000000..81554dba74 Binary files /dev/null and b/tests/gitea-repositories-meta/user2/repo59.git/objects/pack/pack-6dd3a6fe138f1d77e14c2e6b8e6c41e5ae242adf.rev differ diff --git a/tests/gitea-repositories-meta/user2/repo59.git/packed-refs b/tests/gitea-repositories-meta/user2/repo59.git/packed-refs index 114c84d2aa..77fedbf67d 100644 --- a/tests/gitea-repositories-meta/user2/repo59.git/packed-refs +++ b/tests/gitea-repositories-meta/user2/repo59.git/packed-refs @@ -1,3 +1,4 @@ # 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 diff --git a/tests/gitea-repositories-meta/user2/repo59.git/refs/heads/cake-recipe b/tests/gitea-repositories-meta/user2/repo59.git/refs/heads/cake-recipe deleted file mode 100644 index 63bbea6692..0000000000 --- a/tests/gitea-repositories-meta/user2/repo59.git/refs/heads/cake-recipe +++ /dev/null @@ -1 +0,0 @@ -d8f53dfb33f6ccf4169c34970b5e747511c18beb diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go index d044174df1..2ac1632188 100644 --- a/tests/integration/repo_test.go +++ b/tests/integration/repo_test.go @@ -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) + }) +}