diff --git a/routers/common/compare.go b/routers/common/compare.go
index 4d1cc2f0d8908..736f73db0e0e0 100644
--- a/routers/common/compare.go
+++ b/routers/common/compare.go
@@ -18,4 +18,5 @@ type CompareInfo struct {
BaseBranch string
HeadBranch string
DirectComparison bool
+ RawDiffType git.RawDiffType
}
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index 8b99dd95da109..7fb99925b9dab 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -221,13 +221,9 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
// base<-head: master...head:feature
// same repo: master...feature
- var (
- isSameRepo bool
- infoPath string
- err error
- )
+ var isSameRepo bool
- infoPath = ctx.PathParam("*")
+ infoPath := ctx.PathParam("*")
var infos []string
if infoPath == "" {
infos = []string{baseRepo.DefaultBranch, baseRepo.DefaultBranch}
@@ -247,15 +243,17 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
ci.BaseBranch = infos[0]
ctx.Data["BaseBranch"] = ci.BaseBranch
- // If there is no head repository, it means compare between same repository.
+ var err error
+
+ // If there is no head repository, it means compare between the same repository.
headInfos := strings.Split(infos[1], ":")
- if len(headInfos) == 1 {
+ if len(headInfos) == 1 { // {:headBranch} case, guaranteed baseRepo is headRepo
isSameRepo = true
ci.HeadUser = ctx.Repo.Owner
- ci.HeadBranch = headInfos[0]
- } else if len(headInfos) == 2 {
+ ci.HeadBranch, ci.RawDiffType = parseRefForRawDiff(ctx, baseRepo, headInfos[0])
+ } else if len(headInfos) == 2 { // {:headOwner}:{:headBranch} or {:headOwner}/{:headRepoName}:{:headBranch} case
headInfosSplit := strings.Split(headInfos[0], "/")
- if len(headInfosSplit) == 1 {
+ if len(headInfosSplit) == 1 { // {:headOwner}:{:headBranch} case, guaranteed baseRepo.Name is headRepo.Name
ci.HeadUser, err = user_model.GetUserByName(ctx, headInfos[0])
if err != nil {
if user_model.IsErrUserNotExist(err) {
@@ -265,12 +263,23 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
}
return nil
}
- ci.HeadBranch = headInfos[1]
+
+ headRepo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ci.HeadUser.Name, baseRepo.Name)
+ if err != nil {
+ if repo_model.IsErrRepoNotExist(err) {
+ ctx.NotFound(nil)
+ } else {
+ ctx.ServerError("GetRepositoryByOwnerAndName", err)
+ }
+ return nil
+ }
+ ci.HeadBranch, ci.RawDiffType = parseRefForRawDiff(ctx, headRepo, headInfos[1])
+
isSameRepo = ci.HeadUser.ID == ctx.Repo.Owner.ID
- if isSameRepo {
+ if isSameRepo { // not a fork
ci.HeadRepo = baseRepo
}
- } else {
+ } else { // {:headOwner}/{:headRepoName}:{:headBranch} case, across forks
ci.HeadRepo, err = repo_model.GetRepositoryByOwnerAndName(ctx, headInfosSplit[0], headInfosSplit[1])
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
@@ -288,7 +297,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
}
return nil
}
- ci.HeadBranch = headInfos[1]
+ ci.HeadBranch, ci.RawDiffType = parseRefForRawDiff(ctx, ci.HeadRepo, headInfos[1])
ci.HeadUser = ci.HeadRepo.Owner
isSameRepo = ci.HeadRepo.ID == ctx.Repo.Repository.ID
}
@@ -729,6 +738,7 @@ func CompareDiff(ctx *context.Context) {
return
}
+ ctx.Data["PageIsCompareDiff"] = true
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
ctx.Data["DirectComparison"] = ci.DirectComparison
ctx.Data["OtherCompareSeparator"] = ".."
@@ -743,6 +753,15 @@ func CompareDiff(ctx *context.Context) {
return
}
+ if ci.RawDiffType != "" {
+ err := git.GetRepoRawDiffForFile(ci.HeadGitRepo, ci.BaseBranch, ci.HeadBranch, ci.RawDiffType, "", ctx.Resp)
+ if err != nil {
+ ctx.ServerError("GetRepoRawDiffForFile", err)
+ return
+ }
+ return
+ }
+
baseTags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("GetTagNamesByRepoID", err)
@@ -984,3 +1003,20 @@ func getExcerptLines(commit *git.Commit, filePath string, idxLeft, idxRight, chu
}
return diffLines, nil
}
+
+func parseRefForRawDiff(ctx *context.Context, refRepo *repo_model.Repository, refShortName string) (string, git.RawDiffType) {
+ if !strings.HasSuffix(refShortName, ".diff") && !strings.HasSuffix(refShortName, ".patch") {
+ return refShortName, ""
+ }
+
+ if gitrepo.IsBranchExist(ctx, refRepo, refShortName) || gitrepo.IsTagExist(ctx, refRepo, refShortName) {
+ return refShortName, ""
+ }
+
+ if s, ok := strings.CutSuffix(refShortName, ".diff"); ok {
+ return s, git.RawDiffNormal
+ } else if s, ok = strings.CutSuffix(refShortName, ".patch"); ok {
+ return s, git.RawDiffPatch
+ }
+ return refShortName, ""
+}
diff --git a/templates/repo/diff/options_dropdown.tmpl b/templates/repo/diff/options_dropdown.tmpl
index 8d08e7ad46021..03519165db6dc 100644
--- a/templates/repo/diff/options_dropdown.tmpl
+++ b/templates/repo/diff/options_dropdown.tmpl
@@ -10,6 +10,9 @@
{{else if .Commit.ID.String}}
{{ctx.Locale.Tr "repo.diff.download_patch"}}
{{ctx.Locale.Tr "repo.diff.download_diff"}}
+ {{else if $.PageIsCompareDiff}}
+ {{ctx.Locale.Tr "repo.diff.download_patch"}}
+ {{ctx.Locale.Tr "repo.diff.download_diff"}}
{{end}}
{{ctx.Locale.Tr "repo.pulls.expand_files"}}
{{ctx.Locale.Tr "repo.pulls.collapse_files"}}
diff --git a/tests/integration/compare_test.go b/tests/integration/compare_test.go
index 0648777fede0e..08e0f285563cd 100644
--- a/tests/integration/compare_test.go
+++ b/tests/integration/compare_test.go
@@ -9,10 +9,13 @@ import (
"net/url"
"strings"
"testing"
+ "time"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
+ git_module "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/test"
repo_service "code.gitea.io/gitea/services/repository"
"code.gitea.io/gitea/tests"
@@ -158,3 +161,212 @@ func TestCompareCodeExpand(t *testing.T) {
}
})
}
+
+func TestCompareRawDiffNormal(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ repo, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user1, user1, repo_service.CreateRepoOptions{
+ Name: "test_raw_diff",
+ Readme: "Default",
+ AutoInit: true,
+ DefaultBranch: "main",
+ }, true)
+ assert.NoError(t, err)
+ session := loginUser(t, user1.Name)
+
+ r, _ := gitrepo.OpenRepository(db.DefaultContext, repo)
+
+ oldRef, _ := r.GetBranchCommit(repo.DefaultBranch)
+ oldBlobRef, _ := revParse(r, oldRef.ID.String(), "README.md")
+
+ testEditFile(t, session, user1.Name, repo.Name, "main", "README.md", strings.Repeat("a\n", 2))
+
+ newRef, _ := r.GetBranchCommit(repo.DefaultBranch)
+ newBlobRef, _ := revParse(r, newRef.ID.String(), "README.md")
+
+ req := NewRequest(t, "GET", fmt.Sprintf("/user1/test_raw_diff/compare/%s...%s.diff", oldRef.ID.String(), newRef.ID.String()))
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ expected := fmt.Sprintf(`diff --git a/README.md b/README.md
+index %s..%s 100644
+--- a/README.md
++++ b/README.md
+@@ -1,2 +1,2 @@
+-# test_raw_diff
+-
++a
++a
+`, oldBlobRef[:7], newBlobRef[:7])
+ assert.Equal(t, expected, resp.Body.String())
+ })
+}
+
+func TestCompareRawDiffPatch(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ repo, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user1, user1, repo_service.CreateRepoOptions{
+ Name: "test_raw_diff",
+ Readme: "Default",
+ AutoInit: true,
+ DefaultBranch: "main",
+ }, true)
+ assert.NoError(t, err)
+ session := loginUser(t, user1.Name)
+
+ r, _ := gitrepo.OpenRepository(db.DefaultContext, repo)
+
+ // Get the old commit and blob reference
+ oldRef, _ := r.GetBranchCommit(repo.DefaultBranch)
+ oldBlobRef, _ := revParse(r, oldRef.ID.String(), "README.md")
+
+ resp := testEditFile(t, session, user1.Name, repo.Name, "main", "README.md", strings.Repeat("a\n", 2))
+
+ newRef, _ := r.GetBranchCommit(repo.DefaultBranch)
+ newBlobRef, _ := revParse(r, newRef.ID.String(), "README.md")
+
+ // Get the last modified time from the response header
+ respTs, _ := time.Parse(time.RFC1123, resp.Result().Header.Get("Last-Modified"))
+ respTs = respTs.In(time.Local)
+
+ // Format the timestamp to match the expected format in the patch
+ customFormat := "Mon, 2 Jan 2006 15:04:05 -0700"
+ respTsStr := respTs.Format(customFormat)
+
+ req := NewRequest(t, "GET", fmt.Sprintf("/user1/test_raw_diff/compare/%s...%s.patch", oldRef.ID.String(), newRef.ID.String()))
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ expected := fmt.Sprintf(`From %s Mon Sep 17 00:00:00 2001
+From: User One
+Date: %s
+Subject: [PATCH] Update README.md
+
+---
+ README.md | 4 ++--
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/README.md b/README.md
+index %s..%s 100644
+--- a/README.md
++++ b/README.md
+@@ -1,2 +1,2 @@
+-# test_raw_diff
+-
++a
++a
+`, newRef.ID.String(), respTsStr, oldBlobRef[:7], newBlobRef[:7])
+ assert.Equal(t, expected, resp.Body.String())
+ })
+}
+
+func TestCompareRawDiffNormalSameOwnerDifferentRepo(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ repo, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user1, user1, repo_service.CreateRepoOptions{
+ Name: "test_raw_diff",
+ Readme: "Default",
+ AutoInit: true,
+ DefaultBranch: "main",
+ }, true)
+ assert.NoError(t, err)
+ session := loginUser(t, user1.Name)
+
+ headRepo, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user1, user1, repo_service.CreateRepoOptions{
+ Name: "test_raw_diff_head",
+ Readme: "Default",
+ AutoInit: true,
+ DefaultBranch: "main",
+ }, true)
+ assert.NoError(t, err)
+
+ r, _ := gitrepo.OpenRepository(db.DefaultContext, repo)
+ hr, _ := gitrepo.OpenRepository(db.DefaultContext, headRepo)
+
+ oldRef, _ := r.GetBranchCommit(repo.DefaultBranch)
+ oldBlobRef, _ := revParse(r, oldRef.ID.String(), "README.md")
+
+ testEditFile(t, session, user1.Name, headRepo.Name, "main", "README.md", strings.Repeat("a\n", 2))
+
+ newRef, _ := hr.GetBranchCommit(headRepo.DefaultBranch)
+ newBlobRef, _ := revParse(hr, newRef.ID.String(), "README.md")
+
+ req := NewRequest(t, "GET", fmt.Sprintf("/user1/test_raw_diff/compare/%s...%s/%s:%s.diff", oldRef.ID.String(), user1.LowerName, headRepo.LowerName, newRef.ID.String()))
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ expected := fmt.Sprintf(`diff --git a/README.md b/README.md
+index %s..%s 100644
+--- a/README.md
++++ b/README.md
+@@ -1,2 +1,2 @@
+-# test_raw_diff
+-
++a
++a
+`, oldBlobRef[:7], newBlobRef[:7])
+ assert.Equal(t, expected, resp.Body.String())
+ })
+}
+
+func TestCompareRawDiffNormalAcrossForks(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ repo, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user1, user1, repo_service.CreateRepoOptions{
+ Name: "test_raw_diff",
+ Readme: "Default",
+ AutoInit: true,
+ DefaultBranch: "main",
+ }, true)
+ assert.NoError(t, err)
+
+ headRepo, err := repo_service.ForkRepository(db.DefaultContext, user2, user2, repo_service.ForkRepoOptions{
+ BaseRepo: repo,
+ Name: repo.Name,
+ Description: repo.Description,
+ SingleBranch: "",
+ })
+ assert.NoError(t, err)
+
+ session := loginUser(t, user2.Name)
+
+ r, _ := gitrepo.OpenRepository(db.DefaultContext, repo)
+ hr, _ := gitrepo.OpenRepository(db.DefaultContext, headRepo)
+
+ oldRef, _ := r.GetBranchCommit(repo.DefaultBranch)
+ oldBlobRef, _ := revParse(r, oldRef.ID.String(), "README.md")
+
+ testEditFile(t, session, user2.Name, headRepo.Name, "main", "README.md", strings.Repeat("a\n", 2))
+
+ newRef, _ := hr.GetBranchCommit(headRepo.DefaultBranch)
+ newBlobRef, _ := revParse(hr, newRef.ID.String(), "README.md")
+
+ session = loginUser(t, user1.Name)
+
+ req := NewRequest(t, "GET", fmt.Sprintf("/user1/test_raw_diff/compare/%s...%s:%s.diff", oldRef.ID.String(), user2.LowerName, newRef.ID.String()))
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ expected := fmt.Sprintf(`diff --git a/README.md b/README.md
+index %s..%s 100644
+--- a/README.md
++++ b/README.md
+@@ -1,2 +1,2 @@
+-# test_raw_diff
+-
++a
++a
+`, oldBlobRef[:7], newBlobRef[:7])
+ assert.Equal(t, expected, resp.Body.String())
+ })
+}
+
+// helper function to use rev-parse
+// revParse resolves a revision reference to other git-related objects
+func revParse(repo *git_module.Repository, ref, file string) (string, error) {
+ stdout, _, err := git_module.NewCommand("rev-parse").
+ AddDynamicArguments(ref+":"+file).
+ RunStdString(repo.Ctx, &git_module.RunOpts{Dir: repo.Path})
+ if err != nil {
+ return "", err
+ }
+ return strings.TrimSpace(stdout), nil
+}