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 +}