mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 19:06:18 +01: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