mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-04 01:34:27 +00:00 
			
		
		
		
	feat(api): implement branch/commit comparison API (#30349)
- Add new `Compare` struct to represent comparison between two commits - Introduce new API endpoint `/compare/*` to get commit comparison information - Create new file `repo_compare.go` with the `Compare` struct definition - Add new file `compare.go` in `routers/api/v1/repo` to handle comparison logic - Add new file `compare.go` in `routers/common` to define `CompareInfo` struct - Refactor `ParseCompareInfo` function to use `common.CompareInfo` struct - Update Swagger documentation to include the new API endpoint for commit comparison - Remove duplicate `CompareInfo` struct from `routers/web/repo/compare.go` - Adjust base path in Swagger template to be relative (`/api/v1`) GitHub API https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#compare-two-commits --------- Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
		
							
								
								
									
										10
									
								
								modules/structs/repo_compare.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								modules/structs/repo_compare.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
// Copyright 2024 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package structs
 | 
			
		||||
 | 
			
		||||
// Compare represents a comparison between two commits.
 | 
			
		||||
type Compare struct {
 | 
			
		||||
	TotalCommits int       `json:"total_commits"` // Total number of commits in the comparison.
 | 
			
		||||
	Commits      []*Commit `json:"commits"`       // List of commits in the comparison.
 | 
			
		||||
}
 | 
			
		||||
@@ -1066,6 +1066,8 @@ func Routes() *web.Route {
 | 
			
		||||
			m.Post("/migrate", reqToken(), bind(api.MigrateRepoOptions{}), repo.Migrate)
 | 
			
		||||
 | 
			
		||||
			m.Group("/{username}/{reponame}", func() {
 | 
			
		||||
				m.Get("/compare/*", reqRepoReader(unit.TypeCode), repo.CompareDiff)
 | 
			
		||||
 | 
			
		||||
				m.Combo("").Get(reqAnyRepoReader(), repo.Get).
 | 
			
		||||
					Delete(reqToken(), reqOwner(), repo.Delete).
 | 
			
		||||
					Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										99
									
								
								routers/api/v1/repo/compare.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								routers/api/v1/repo/compare.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,99 @@
 | 
			
		||||
// Copyright 2024 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package repo
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/gitrepo"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/services/context"
 | 
			
		||||
	"code.gitea.io/gitea/services/convert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CompareDiff compare two branches or commits
 | 
			
		||||
func CompareDiff(ctx *context.APIContext) {
 | 
			
		||||
	// swagger:operation GET /repos/{owner}/{repo}/compare/{basehead} Get commit comparison information
 | 
			
		||||
	// ---
 | 
			
		||||
	// summary: Get commit comparison information
 | 
			
		||||
	// produces:
 | 
			
		||||
	// - application/json
 | 
			
		||||
	// parameters:
 | 
			
		||||
	// - name: owner
 | 
			
		||||
	//   in: path
 | 
			
		||||
	//   description: owner of the repo
 | 
			
		||||
	//   type: string
 | 
			
		||||
	//   required: true
 | 
			
		||||
	// - name: repo
 | 
			
		||||
	//   in: path
 | 
			
		||||
	//   description: name of the repo
 | 
			
		||||
	//   type: string
 | 
			
		||||
	//   required: true
 | 
			
		||||
	// - name: basehead
 | 
			
		||||
	//   in: path
 | 
			
		||||
	//   description: compare two branches or commits
 | 
			
		||||
	//   type: string
 | 
			
		||||
	//   required: true
 | 
			
		||||
	// responses:
 | 
			
		||||
	//   "200":
 | 
			
		||||
	//     "$ref": "#/responses/Compare"
 | 
			
		||||
	//   "404":
 | 
			
		||||
	//     "$ref": "#/responses/notFound"
 | 
			
		||||
 | 
			
		||||
	if ctx.Repo.GitRepo == nil {
 | 
			
		||||
		gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		ctx.Repo.GitRepo = gitRepo
 | 
			
		||||
		defer gitRepo.Close()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	infoPath := ctx.Params("*")
 | 
			
		||||
	infos := []string{ctx.Repo.Repository.DefaultBranch, ctx.Repo.Repository.DefaultBranch}
 | 
			
		||||
	if infoPath != "" {
 | 
			
		||||
		infos = strings.SplitN(infoPath, "...", 2)
 | 
			
		||||
		if len(infos) != 2 {
 | 
			
		||||
			if infos = strings.SplitN(infoPath, "..", 2); len(infos) != 2 {
 | 
			
		||||
				infos = []string{ctx.Repo.Repository.DefaultBranch, infoPath}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, _, headGitRepo, ci, _, _ := parseCompareInfo(ctx, api.CreatePullRequestOption{
 | 
			
		||||
		Base: infos[0],
 | 
			
		||||
		Head: infos[1],
 | 
			
		||||
	})
 | 
			
		||||
	if ctx.Written() {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer headGitRepo.Close()
 | 
			
		||||
 | 
			
		||||
	verification := ctx.FormString("verification") == "" || ctx.FormBool("verification")
 | 
			
		||||
	files := ctx.FormString("files") == "" || ctx.FormBool("files")
 | 
			
		||||
 | 
			
		||||
	apiCommits := make([]*api.Commit, 0, len(ci.Commits))
 | 
			
		||||
	userCache := make(map[string]*user_model.User)
 | 
			
		||||
	for i := 0; i < len(ci.Commits); i++ {
 | 
			
		||||
		apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, ci.Commits[i], userCache,
 | 
			
		||||
			convert.ToCommitOptions{
 | 
			
		||||
				Stat:         true,
 | 
			
		||||
				Verification: verification,
 | 
			
		||||
				Files:        files,
 | 
			
		||||
			})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			ctx.ServerError("toCommit", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		apiCommits = append(apiCommits, apiCommit)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.JSON(http.StatusOK, &api.Compare{
 | 
			
		||||
		TotalCommits: len(ci.Commits),
 | 
			
		||||
		Commits:      apiCommits,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -414,3 +414,9 @@ type swaggerRepoNewIssuePinsAllowed struct {
 | 
			
		||||
	// in:body
 | 
			
		||||
	Body api.NewIssuePinsAllowed `json:"body"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// swagger:response Compare
 | 
			
		||||
type swaggerCompare struct {
 | 
			
		||||
	// in:body
 | 
			
		||||
	Body api.Compare `json:"body"`
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								routers/common/compare.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								routers/common/compare.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
// Copyright 2024 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package common
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CompareInfo represents the collected results from ParseCompareInfo
 | 
			
		||||
type CompareInfo struct {
 | 
			
		||||
	HeadUser         *user_model.User
 | 
			
		||||
	HeadRepo         *repo_model.Repository
 | 
			
		||||
	HeadGitRepo      *git.Repository
 | 
			
		||||
	CompareInfo      *git.CompareInfo
 | 
			
		||||
	BaseBranch       string
 | 
			
		||||
	HeadBranch       string
 | 
			
		||||
	DirectComparison bool
 | 
			
		||||
}
 | 
			
		||||
@@ -35,6 +35,7 @@ import (
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/typesniffer"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/routers/common"
 | 
			
		||||
	"code.gitea.io/gitea/services/context"
 | 
			
		||||
	"code.gitea.io/gitea/services/context/upload"
 | 
			
		||||
	"code.gitea.io/gitea/services/gitdiff"
 | 
			
		||||
@@ -185,21 +186,10 @@ func setCsvCompareContext(ctx *context.Context) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CompareInfo represents the collected results from ParseCompareInfo
 | 
			
		||||
type CompareInfo struct {
 | 
			
		||||
	HeadUser         *user_model.User
 | 
			
		||||
	HeadRepo         *repo_model.Repository
 | 
			
		||||
	HeadGitRepo      *git.Repository
 | 
			
		||||
	CompareInfo      *git.CompareInfo
 | 
			
		||||
	BaseBranch       string
 | 
			
		||||
	HeadBranch       string
 | 
			
		||||
	DirectComparison bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ParseCompareInfo parse compare info between two commit for preparing comparing references
 | 
			
		||||
func ParseCompareInfo(ctx *context.Context) *CompareInfo {
 | 
			
		||||
func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
 | 
			
		||||
	baseRepo := ctx.Repo.Repository
 | 
			
		||||
	ci := &CompareInfo{}
 | 
			
		||||
	ci := &common.CompareInfo{}
 | 
			
		||||
 | 
			
		||||
	fileOnly := ctx.FormBool("file-only")
 | 
			
		||||
 | 
			
		||||
@@ -576,7 +566,7 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo {
 | 
			
		||||
// PrepareCompareDiff renders compare diff page
 | 
			
		||||
func PrepareCompareDiff(
 | 
			
		||||
	ctx *context.Context,
 | 
			
		||||
	ci *CompareInfo,
 | 
			
		||||
	ci *common.CompareInfo,
 | 
			
		||||
	whitespaceBehavior git.TrustedCmdArgs,
 | 
			
		||||
) bool {
 | 
			
		||||
	var (
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										70
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										70
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							@@ -5340,6 +5340,51 @@
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "/repos/{owner}/{repo}/compare/{basehead}": {
 | 
			
		||||
      "get": {
 | 
			
		||||
        "produces": [
 | 
			
		||||
          "application/json"
 | 
			
		||||
        ],
 | 
			
		||||
        "tags": [
 | 
			
		||||
          "Get",
 | 
			
		||||
          "commit",
 | 
			
		||||
          "comparison"
 | 
			
		||||
        ],
 | 
			
		||||
        "summary": "Get commit comparison information",
 | 
			
		||||
        "operationId": "information",
 | 
			
		||||
        "parameters": [
 | 
			
		||||
          {
 | 
			
		||||
            "type": "string",
 | 
			
		||||
            "description": "owner of the repo",
 | 
			
		||||
            "name": "owner",
 | 
			
		||||
            "in": "path",
 | 
			
		||||
            "required": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "type": "string",
 | 
			
		||||
            "description": "name of the repo",
 | 
			
		||||
            "name": "repo",
 | 
			
		||||
            "in": "path",
 | 
			
		||||
            "required": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "type": "string",
 | 
			
		||||
            "description": "compare two branches or commits",
 | 
			
		||||
            "name": "basehead",
 | 
			
		||||
            "in": "path",
 | 
			
		||||
            "required": true
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "responses": {
 | 
			
		||||
          "200": {
 | 
			
		||||
            "$ref": "#/responses/Compare"
 | 
			
		||||
          },
 | 
			
		||||
          "404": {
 | 
			
		||||
            "$ref": "#/responses/notFound"
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "/repos/{owner}/{repo}/contents": {
 | 
			
		||||
      "get": {
 | 
			
		||||
        "produces": [
 | 
			
		||||
@@ -18717,6 +18762,25 @@
 | 
			
		||||
      },
 | 
			
		||||
      "x-go-package": "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
    },
 | 
			
		||||
    "Compare": {
 | 
			
		||||
      "type": "object",
 | 
			
		||||
      "title": "Compare represents a comparison between two commits.",
 | 
			
		||||
      "properties": {
 | 
			
		||||
        "commits": {
 | 
			
		||||
          "type": "array",
 | 
			
		||||
          "items": {
 | 
			
		||||
            "$ref": "#/definitions/Commit"
 | 
			
		||||
          },
 | 
			
		||||
          "x-go-name": "Commits"
 | 
			
		||||
        },
 | 
			
		||||
        "total_commits": {
 | 
			
		||||
          "type": "integer",
 | 
			
		||||
          "format": "int64",
 | 
			
		||||
          "x-go-name": "TotalCommits"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      "x-go-package": "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
    },
 | 
			
		||||
    "ContentsResponse": {
 | 
			
		||||
      "description": "ContentsResponse contains information about a repo's entry's (dir, file, symlink, submodule) metadata and content",
 | 
			
		||||
      "type": "object",
 | 
			
		||||
@@ -24678,6 +24742,12 @@
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "Compare": {
 | 
			
		||||
      "description": "",
 | 
			
		||||
      "schema": {
 | 
			
		||||
        "$ref": "#/definitions/Compare"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "ContentsListResponse": {
 | 
			
		||||
      "description": "ContentsListResponse",
 | 
			
		||||
      "schema": {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										38
									
								
								tests/integration/api_repo_compare_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								tests/integration/api_repo_compare_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
// Copyright 2024 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package integration
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	auth_model "code.gitea.io/gitea/models/auth"
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/tests"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestAPICompareBranches(t *testing.T) {
 | 
			
		||||
	defer tests.PrepareTestEnv(t)()
 | 
			
		||||
 | 
			
		||||
	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 | 
			
		||||
	// Login as User2.
 | 
			
		||||
	session := loginUser(t, user.Name)
 | 
			
		||||
	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 | 
			
		||||
 | 
			
		||||
	repoName := "repo20"
 | 
			
		||||
 | 
			
		||||
	req := NewRequestf(t, "GET", "/api/v1/repos/user2/%s/compare/add-csv...remove-files-b", repoName).
 | 
			
		||||
		AddTokenAuth(token)
 | 
			
		||||
	resp := MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
 | 
			
		||||
	var apiResp *api.Compare
 | 
			
		||||
	DecodeJSON(t, resp, &apiResp)
 | 
			
		||||
 | 
			
		||||
	assert.Equal(t, 2, apiResp.TotalCommits)
 | 
			
		||||
	assert.Len(t, apiResp.Commits, 2)
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user