mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 13:51:35 +01:00 
			
		
		
		
	Add priority to protected branch (#32286)
## Solves Currently for rules to re-order them you have to alter the creation date. so you basicly have to delete and recreate them in the right order. This is more than just inconvinient ... ## Solution Add a new col for prioritization ## Demo WebUI Video https://github.com/user-attachments/assets/92182a31-9705-4ac5-b6e3-9bb74108cbd1 --- *Sponsored by Kithara Software GmbH*
This commit is contained in:
		
							parent
							
								
									3fc1bbe971
								
							
						
					
					
						commit
						846f618716
					
				| @ -34,6 +34,7 @@ type ProtectedBranch struct { | ||||
| 	RepoID                        int64                  `xorm:"UNIQUE(s)"` | ||||
| 	Repo                          *repo_model.Repository `xorm:"-"` | ||||
| 	RuleName                      string                 `xorm:"'branch_name' UNIQUE(s)"` // a branch name or a glob match to branch name | ||||
| 	Priority                      int64                  `xorm:"NOT NULL DEFAULT 0"` | ||||
| 	globRule                      glob.Glob              `xorm:"-"` | ||||
| 	isPlainName                   bool                   `xorm:"-"` | ||||
| 	CanPush                       bool                   `xorm:"NOT NULL DEFAULT false"` | ||||
| @ -413,14 +414,27 @@ func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, prote | ||||
| 	} | ||||
| 	protectBranch.ApprovalsWhitelistTeamIDs = whitelist | ||||
| 
 | ||||
| 	// Make sure protectBranch.ID is not 0 for whitelists | ||||
| 	// Looks like it's a new rule | ||||
| 	if protectBranch.ID == 0 { | ||||
| 		// as it's a new rule and if priority was not set, we need to calc it. | ||||
| 		if protectBranch.Priority == 0 { | ||||
| 			var lowestPrio int64 | ||||
| 			// because of mssql we can not use builder or save xorm syntax, so raw sql it is | ||||
| 			if _, err := db.GetEngine(ctx).SQL(`SELECT MAX(priority) FROM protected_branch WHERE repo_id = ?`, protectBranch.RepoID). | ||||
| 				Get(&lowestPrio); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			log.Trace("Create new ProtectedBranch at repo[%d] and detect current lowest priority '%d'", protectBranch.RepoID, lowestPrio) | ||||
| 			protectBranch.Priority = lowestPrio + 1 | ||||
| 		} | ||||
| 
 | ||||
| 		if _, err = db.GetEngine(ctx).Insert(protectBranch); err != nil { | ||||
| 			return fmt.Errorf("Insert: %v", err) | ||||
| 		} | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	// update the rule | ||||
| 	if _, err = db.GetEngine(ctx).ID(protectBranch.ID).AllCols().Update(protectBranch); err != nil { | ||||
| 		return fmt.Errorf("Update: %v", err) | ||||
| 	} | ||||
| @ -428,6 +442,24 @@ func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, prote | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func UpdateProtectBranchPriorities(ctx context.Context, repo *repo_model.Repository, ids []int64) error { | ||||
| 	prio := int64(1) | ||||
| 	return db.WithTx(ctx, func(ctx context.Context) error { | ||||
| 		for _, id := range ids { | ||||
| 			if _, err := db.GetEngine(ctx). | ||||
| 				ID(id).Where("repo_id = ?", repo.ID). | ||||
| 				Cols("priority"). | ||||
| 				Update(&ProtectedBranch{ | ||||
| 					Priority: prio, | ||||
| 				}); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			prio++ | ||||
| 		} | ||||
| 		return nil | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // updateApprovalWhitelist checks whether the user whitelist changed and returns a whitelist with | ||||
| // the users from newWhitelist which have explicit read or write access to the repo. | ||||
| func updateApprovalWhitelist(ctx context.Context, repo *repo_model.Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) { | ||||
|  | ||||
| @ -28,6 +28,13 @@ func (rules ProtectedBranchRules) sort() { | ||||
| 	sort.Slice(rules, func(i, j int) bool { | ||||
| 		rules[i].loadGlob() | ||||
| 		rules[j].loadGlob() | ||||
| 
 | ||||
| 		// if priority differ, use that to sort | ||||
| 		if rules[i].Priority != rules[j].Priority { | ||||
| 			return rules[i].Priority < rules[j].Priority | ||||
| 		} | ||||
| 
 | ||||
| 		// now we sort the old way | ||||
| 		if rules[i].isPlainName != rules[j].isPlainName { | ||||
| 			return rules[i].isPlainName // plain name comes first, so plain name means "less" | ||||
| 		} | ||||
|  | ||||
| @ -75,7 +75,7 @@ func TestBranchRuleMatchPriority(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestBranchRuleSort(t *testing.T) { | ||||
| func TestBranchRuleSortLegacy(t *testing.T) { | ||||
| 	in := []*ProtectedBranch{{ | ||||
| 		RuleName:    "b", | ||||
| 		CreatedUnix: 1, | ||||
| @ -103,3 +103,37 @@ func TestBranchRuleSort(t *testing.T) { | ||||
| 	} | ||||
| 	assert.Equal(t, expect, got) | ||||
| } | ||||
| 
 | ||||
| func TestBranchRuleSortPriority(t *testing.T) { | ||||
| 	in := []*ProtectedBranch{{ | ||||
| 		RuleName:    "b", | ||||
| 		CreatedUnix: 1, | ||||
| 		Priority:    4, | ||||
| 	}, { | ||||
| 		RuleName:    "b/*", | ||||
| 		CreatedUnix: 3, | ||||
| 		Priority:    2, | ||||
| 	}, { | ||||
| 		RuleName:    "a/*", | ||||
| 		CreatedUnix: 2, | ||||
| 		Priority:    1, | ||||
| 	}, { | ||||
| 		RuleName:    "c", | ||||
| 		CreatedUnix: 0, | ||||
| 		Priority:    0, | ||||
| 	}, { | ||||
| 		RuleName:    "a", | ||||
| 		CreatedUnix: 4, | ||||
| 		Priority:    3, | ||||
| 	}} | ||||
| 	expect := []string{"c", "a/*", "b/*", "a", "b"} | ||||
| 
 | ||||
| 	pbr := ProtectedBranchRules(in) | ||||
| 	pbr.sort() | ||||
| 
 | ||||
| 	var got []string | ||||
| 	for i := range pbr { | ||||
| 		got = append(got, pbr[i].RuleName) | ||||
| 	} | ||||
| 	assert.Equal(t, expect, got) | ||||
| } | ||||
|  | ||||
| @ -7,6 +7,10 @@ import ( | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	"code.gitea.io/gitea/models/unittest" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| 
 | ||||
| @ -76,3 +80,77 @@ func TestBranchRuleMatch(t *testing.T) { | ||||
| 		) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestUpdateProtectBranchPriorities(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 
 | ||||
| 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) | ||||
| 
 | ||||
| 	// Create some test protected branches with initial priorities | ||||
| 	protectedBranches := []*ProtectedBranch{ | ||||
| 		{ | ||||
| 			RepoID:   repo.ID, | ||||
| 			RuleName: "master", | ||||
| 			Priority: 1, | ||||
| 		}, | ||||
| 		{ | ||||
| 			RepoID:   repo.ID, | ||||
| 			RuleName: "develop", | ||||
| 			Priority: 2, | ||||
| 		}, | ||||
| 		{ | ||||
| 			RepoID:   repo.ID, | ||||
| 			RuleName: "feature/*", | ||||
| 			Priority: 3, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, pb := range protectedBranches { | ||||
| 		_, err := db.GetEngine(db.DefaultContext).Insert(pb) | ||||
| 		assert.NoError(t, err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Test updating priorities | ||||
| 	newPriorities := []int64{protectedBranches[2].ID, protectedBranches[0].ID, protectedBranches[1].ID} | ||||
| 	err := UpdateProtectBranchPriorities(db.DefaultContext, repo, newPriorities) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// Verify new priorities | ||||
| 	pbs, err := FindRepoProtectedBranchRules(db.DefaultContext, repo.ID) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	expectedPriorities := map[string]int64{ | ||||
| 		"feature/*": 1, | ||||
| 		"master":    2, | ||||
| 		"develop":   3, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, pb := range pbs { | ||||
| 		assert.Equal(t, expectedPriorities[pb.RuleName], pb.Priority) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestNewProtectBranchPriority(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) | ||||
| 
 | ||||
| 	err := UpdateProtectBranch(db.DefaultContext, repo, &ProtectedBranch{ | ||||
| 		RepoID:   repo.ID, | ||||
| 		RuleName: "branch-1", | ||||
| 		Priority: 1, | ||||
| 	}, WhitelistOptions{}) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	newPB := &ProtectedBranch{ | ||||
| 		RepoID:   repo.ID, | ||||
| 		RuleName: "branch-2", | ||||
| 		// Priority intentionally omitted | ||||
| 	} | ||||
| 
 | ||||
| 	err = UpdateProtectBranch(db.DefaultContext, repo, newPB, WhitelistOptions{}) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	savedPB2, err := GetFirstMatchProtectedBranchRule(db.DefaultContext, repo.ID, "branch-2") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, int64(2), savedPB2.Priority) | ||||
| } | ||||
|  | ||||
| @ -367,6 +367,7 @@ func prepareMigrationTasks() []*migration { | ||||
| 		newMigration(307, "Fix milestone deadline_unix when there is no due date", v1_23.FixMilestoneNoDueDate), | ||||
| 		newMigration(308, "Add index(user_id, is_deleted) for action table", v1_23.AddNewIndexForUserDashboard), | ||||
| 		newMigration(309, "Improve Notification table indices", v1_23.ImproveNotificationTableIndices), | ||||
| 		newMigration(310, "Add Priority to ProtectedBranch", v1_23.AddPriorityToProtectedBranch), | ||||
| 	} | ||||
| 	return preparedMigrations | ||||
| } | ||||
|  | ||||
							
								
								
									
										16
									
								
								models/migrations/v1_23/v310.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								models/migrations/v1_23/v310.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| // Copyright 2024 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package v1_23 //nolint | ||||
| 
 | ||||
| import ( | ||||
| 	"xorm.io/xorm" | ||||
| ) | ||||
| 
 | ||||
| func AddPriorityToProtectedBranch(x *xorm.Engine) error { | ||||
| 	type ProtectedBranch struct { | ||||
| 		Priority int64 `xorm:"NOT NULL DEFAULT 0"` | ||||
| 	} | ||||
| 
 | ||||
| 	return x.Sync(new(ProtectedBranch)) | ||||
| } | ||||
| @ -25,6 +25,7 @@ type BranchProtection struct { | ||||
| 	// Deprecated: true | ||||
| 	BranchName                    string   `json:"branch_name"` | ||||
| 	RuleName                      string   `json:"rule_name"` | ||||
| 	Priority                      int64    `json:"priority"` | ||||
| 	EnablePush                    bool     `json:"enable_push"` | ||||
| 	EnablePushWhitelist           bool     `json:"enable_push_whitelist"` | ||||
| 	PushWhitelistUsernames        []string `json:"push_whitelist_usernames"` | ||||
| @ -64,6 +65,7 @@ type CreateBranchProtectionOption struct { | ||||
| 	// Deprecated: true | ||||
| 	BranchName                    string   `json:"branch_name"` | ||||
| 	RuleName                      string   `json:"rule_name"` | ||||
| 	Priority                      int64    `json:"priority"` | ||||
| 	EnablePush                    bool     `json:"enable_push"` | ||||
| 	EnablePushWhitelist           bool     `json:"enable_push_whitelist"` | ||||
| 	PushWhitelistUsernames        []string `json:"push_whitelist_usernames"` | ||||
| @ -96,6 +98,7 @@ type CreateBranchProtectionOption struct { | ||||
| 
 | ||||
| // EditBranchProtectionOption options for editing a branch protection | ||||
| type EditBranchProtectionOption struct { | ||||
| 	Priority                      *int64   `json:"priority"` | ||||
| 	EnablePush                    *bool    `json:"enable_push"` | ||||
| 	EnablePushWhitelist           *bool    `json:"enable_push_whitelist"` | ||||
| 	PushWhitelistUsernames        []string `json:"push_whitelist_usernames"` | ||||
| @ -125,3 +128,8 @@ type EditBranchProtectionOption struct { | ||||
| 	UnprotectedFilePatterns       *string  `json:"unprotected_file_patterns"` | ||||
| 	BlockAdminMergeOverride       *bool    `json:"block_admin_merge_override"` | ||||
| } | ||||
| 
 | ||||
| // UpdateBranchProtectionPriories a list to update the branch protection rule priorities | ||||
| type UpdateBranchProtectionPriories struct { | ||||
| 	IDs []int64 `json:"ids"` | ||||
| } | ||||
|  | ||||
| @ -1204,6 +1204,7 @@ func Routes() *web.Router { | ||||
| 						m.Patch("", bind(api.EditBranchProtectionOption{}), mustNotBeArchived, repo.EditBranchProtection) | ||||
| 						m.Delete("", repo.DeleteBranchProtection) | ||||
| 					}) | ||||
| 					m.Post("/priority", bind(api.UpdateBranchProtectionPriories{}), mustNotBeArchived, repo.UpdateBranchProtectionPriories) | ||||
| 				}, reqToken(), reqAdmin()) | ||||
| 				m.Group("/tags", func() { | ||||
| 					m.Get("", repo.ListTags) | ||||
|  | ||||
| @ -618,6 +618,7 @@ func CreateBranchProtection(ctx *context.APIContext) { | ||||
| 	protectBranch = &git_model.ProtectedBranch{ | ||||
| 		RepoID:                        ctx.Repo.Repository.ID, | ||||
| 		RuleName:                      ruleName, | ||||
| 		Priority:                      form.Priority, | ||||
| 		CanPush:                       form.EnablePush, | ||||
| 		EnableWhitelist:               form.EnablePush && form.EnablePushWhitelist, | ||||
| 		WhitelistDeployKeys:           form.EnablePush && form.EnablePushWhitelist && form.PushWhitelistDeployKeys, | ||||
| @ -640,7 +641,7 @@ func CreateBranchProtection(ctx *context.APIContext) { | ||||
| 		BlockAdminMergeOverride:       form.BlockAdminMergeOverride, | ||||
| 	} | ||||
| 
 | ||||
| 	err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{ | ||||
| 	if err := git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{ | ||||
| 		UserIDs:          whitelistUsers, | ||||
| 		TeamIDs:          whitelistTeams, | ||||
| 		ForcePushUserIDs: forcePushAllowlistUsers, | ||||
| @ -649,14 +650,13 @@ func CreateBranchProtection(ctx *context.APIContext) { | ||||
| 		MergeTeamIDs:     mergeWhitelistTeams, | ||||
| 		ApprovalsUserIDs: approvalsWhitelistUsers, | ||||
| 		ApprovalsTeamIDs: approvalsWhitelistTeams, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 	}); err != nil { | ||||
| 		ctx.Error(http.StatusInternalServerError, "UpdateProtectBranch", err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if isBranchExist { | ||||
| 		if err = pull_service.CheckPRsForBaseBranch(ctx, ctx.Repo.Repository, ruleName); err != nil { | ||||
| 		if err := pull_service.CheckPRsForBaseBranch(ctx, ctx.Repo.Repository, ruleName); err != nil { | ||||
| 			ctx.Error(http.StatusInternalServerError, "CheckPRsForBaseBranch", err) | ||||
| 			return | ||||
| 		} | ||||
| @ -796,6 +796,10 @@ func EditBranchProtection(ctx *context.APIContext) { | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if form.Priority != nil { | ||||
| 		protectBranch.Priority = *form.Priority | ||||
| 	} | ||||
| 
 | ||||
| 	if form.EnableMergeWhitelist != nil { | ||||
| 		protectBranch.EnableMergeWhitelist = *form.EnableMergeWhitelist | ||||
| 	} | ||||
| @ -1080,3 +1084,47 @@ func DeleteBranchProtection(ctx *context.APIContext) { | ||||
| 
 | ||||
| 	ctx.Status(http.StatusNoContent) | ||||
| } | ||||
| 
 | ||||
| // UpdateBranchProtectionPriories updates the priorities of branch protections for a repo | ||||
| func UpdateBranchProtectionPriories(ctx *context.APIContext) { | ||||
| 	// swagger:operation POST /repos/{owner}/{repo}/branch_protections/priority repository repoUpdateBranchProtectionPriories | ||||
| 	// --- | ||||
| 	// summary: Update the priorities of branch protections for a repository. | ||||
| 	// consumes: | ||||
| 	// - application/json | ||||
| 	// 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: body | ||||
| 	//   in: body | ||||
| 	//   schema: | ||||
| 	//     "$ref": "#/definitions/UpdateBranchProtectionPriories" | ||||
| 	// responses: | ||||
| 	//   "204": | ||||
| 	//     "$ref": "#/responses/empty" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	//   "422": | ||||
| 	//     "$ref": "#/responses/validationError" | ||||
| 	//   "423": | ||||
| 	//     "$ref": "#/responses/repoArchivedError" | ||||
| 	form := web.GetForm(ctx).(*api.UpdateBranchProtectionPriories) | ||||
| 	repo := ctx.Repo.Repository | ||||
| 
 | ||||
| 	if err := git_model.UpdateProtectBranchPriorities(ctx, repo, form.IDs); err != nil { | ||||
| 		ctx.Error(http.StatusInternalServerError, "UpdateProtectBranchPriorities", err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.Status(http.StatusNoContent) | ||||
| } | ||||
|  | ||||
| @ -146,6 +146,9 @@ type swaggerParameterBodies struct { | ||||
| 	// in:body | ||||
| 	EditBranchProtectionOption api.EditBranchProtectionOption | ||||
| 
 | ||||
| 	// in:body | ||||
| 	UpdateBranchProtectionPriories api.UpdateBranchProtectionPriories | ||||
| 
 | ||||
| 	// in:body | ||||
| 	CreateOAuth2ApplicationOptions api.CreateOAuth2ApplicationOptions | ||||
| 
 | ||||
|  | ||||
| @ -322,6 +322,16 @@ func DeleteProtectedBranchRulePost(ctx *context.Context) { | ||||
| 	ctx.JSONRedirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink)) | ||||
| } | ||||
| 
 | ||||
| func UpdateBranchProtectionPriories(ctx *context.Context) { | ||||
| 	form := web.GetForm(ctx).(*forms.ProtectBranchPriorityForm) | ||||
| 	repo := ctx.Repo.Repository | ||||
| 
 | ||||
| 	if err := git_model.UpdateProtectBranchPriorities(ctx, repo, form.IDs); err != nil { | ||||
| 		ctx.ServerError("UpdateProtectBranchPriorities", err) | ||||
| 		return | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // RenameBranchPost responses for rename a branch | ||||
| func RenameBranchPost(ctx *context.Context) { | ||||
| 	form := web.GetForm(ctx).(*forms.RenameBranchForm) | ||||
|  | ||||
| @ -1081,6 +1081,7 @@ func registerRoutes(m *web.Router) { | ||||
| 			m.Combo("/edit").Get(repo_setting.SettingsProtectedBranch). | ||||
| 				Post(web.Bind(forms.ProtectBranchForm{}), context.RepoMustNotBeArchived(), repo_setting.SettingsProtectedBranchPost) | ||||
| 			m.Post("/{id}/delete", repo_setting.DeleteProtectedBranchRulePost) | ||||
| 			m.Post("/priority", web.Bind(forms.ProtectBranchPriorityForm{}), context.RepoMustNotBeArchived(), repo_setting.UpdateBranchProtectionPriories) | ||||
| 		}) | ||||
| 
 | ||||
| 		m.Group("/tags", func() { | ||||
|  | ||||
| @ -158,6 +158,7 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo | ||||
| 	return &api.BranchProtection{ | ||||
| 		BranchName:                    branchName, | ||||
| 		RuleName:                      bp.RuleName, | ||||
| 		Priority:                      bp.Priority, | ||||
| 		EnablePush:                    bp.CanPush, | ||||
| 		EnablePushWhitelist:           bp.EnableWhitelist, | ||||
| 		PushWhitelistUsernames:        pushWhitelistUsernames, | ||||
|  | ||||
| @ -228,6 +228,10 @@ func (f *ProtectBranchForm) Validate(req *http.Request, errs binding.Errors) bin | ||||
| 	return middleware.Validate(errs, ctx.Data, f, ctx.Locale) | ||||
| } | ||||
| 
 | ||||
| type ProtectBranchPriorityForm struct { | ||||
| 	IDs []int64 | ||||
| } | ||||
| 
 | ||||
| //  __      __      ___.   .__                   __ | ||||
| // /  \    /  \ ____\_ |__ |  |__   ____   ____ |  | __ | ||||
| // \   \/\/   // __ \| __ \|  |  \ /  _ \ /  _ \|  |/ / | ||||
|  | ||||
| @ -37,9 +37,12 @@ | ||||
| 			</h4> | ||||
| 
 | ||||
| 			<div class="ui attached segment"> | ||||
| 				<div class="flex-list"> | ||||
| 				<div class="flex-list" id="protected-branches-list" data-update-priority-url="{{$.Repository.Link}}/settings/branches/priority"> | ||||
| 					{{range .ProtectedBranches}} | ||||
| 						<div class="flex-item tw-items-center"> | ||||
| 						<div class="flex-item tw-items-center item" data-id="{{.ID}}" > | ||||
| 							<div class="drag-handle tw-cursor-grab"> | ||||
| 								{{svg "octicon-grabber" 16}} | ||||
| 							</div> | ||||
| 							<div class="flex-item-main"> | ||||
| 								<div class="flex-item-title"> | ||||
| 									<div class="ui basic primary label">{{.RuleName}}</div> | ||||
|  | ||||
							
								
								
									
										82
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										82
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							| @ -4666,6 +4666,58 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/repos/{owner}/{repo}/branch_protections/priority": { | ||||
|       "post": { | ||||
|         "consumes": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "repository" | ||||
|         ], | ||||
|         "summary": "Update the priorities of branch protections for a repository.", | ||||
|         "operationId": "repoUpdateBranchProtectionPriories", | ||||
|         "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 | ||||
|           }, | ||||
|           { | ||||
|             "name": "body", | ||||
|             "in": "body", | ||||
|             "schema": { | ||||
|               "$ref": "#/definitions/UpdateBranchProtectionPriories" | ||||
|             } | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "204": { | ||||
|             "$ref": "#/responses/empty" | ||||
|           }, | ||||
|           "404": { | ||||
|             "$ref": "#/responses/notFound" | ||||
|           }, | ||||
|           "422": { | ||||
|             "$ref": "#/responses/validationError" | ||||
|           }, | ||||
|           "423": { | ||||
|             "$ref": "#/responses/repoArchivedError" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/repos/{owner}/{repo}/branch_protections/{name}": { | ||||
|       "get": { | ||||
|         "produces": [ | ||||
| @ -18874,6 +18926,11 @@ | ||||
|           }, | ||||
|           "x-go-name": "MergeWhitelistUsernames" | ||||
|         }, | ||||
|         "priority": { | ||||
|           "type": "integer", | ||||
|           "format": "int64", | ||||
|           "x-go-name": "Priority" | ||||
|         }, | ||||
|         "protected_file_patterns": { | ||||
|           "type": "string", | ||||
|           "x-go-name": "ProtectedFilePatterns" | ||||
| @ -19568,6 +19625,11 @@ | ||||
|           }, | ||||
|           "x-go-name": "MergeWhitelistUsernames" | ||||
|         }, | ||||
|         "priority": { | ||||
|           "type": "integer", | ||||
|           "format": "int64", | ||||
|           "x-go-name": "Priority" | ||||
|         }, | ||||
|         "protected_file_patterns": { | ||||
|           "type": "string", | ||||
|           "x-go-name": "ProtectedFilePatterns" | ||||
| @ -20800,6 +20862,11 @@ | ||||
|           }, | ||||
|           "x-go-name": "MergeWhitelistUsernames" | ||||
|         }, | ||||
|         "priority": { | ||||
|           "type": "integer", | ||||
|           "format": "int64", | ||||
|           "x-go-name": "Priority" | ||||
|         }, | ||||
|         "protected_file_patterns": { | ||||
|           "type": "string", | ||||
|           "x-go-name": "ProtectedFilePatterns" | ||||
| @ -24886,6 +24953,21 @@ | ||||
|       }, | ||||
|       "x-go-package": "code.gitea.io/gitea/modules/structs" | ||||
|     }, | ||||
|     "UpdateBranchProtectionPriories": { | ||||
|       "description": "UpdateBranchProtectionPriories a list to update the branch protection rule priorities", | ||||
|       "type": "object", | ||||
|       "properties": { | ||||
|         "ids": { | ||||
|           "type": "array", | ||||
|           "items": { | ||||
|             "type": "integer", | ||||
|             "format": "int64" | ||||
|           }, | ||||
|           "x-go-name": "IDs" | ||||
|         } | ||||
|       }, | ||||
|       "x-go-package": "code.gitea.io/gitea/modules/structs" | ||||
|     }, | ||||
|     "UpdateFileOptions": { | ||||
|       "description": "UpdateFileOptions options for updating files\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)", | ||||
|       "type": "object", | ||||
|  | ||||
| @ -49,7 +49,7 @@ func testAPIGetBranchProtection(t *testing.T, branchName string, expectedHTTPSta | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func testAPICreateBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) { | ||||
| func testAPICreateBranchProtection(t *testing.T, branchName string, expectedPriority, expectedHTTPStatus int) { | ||||
| 	token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository) | ||||
| 	req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branch_protections", &api.BranchProtection{ | ||||
| 		RuleName: branchName, | ||||
| @ -60,6 +60,7 @@ func testAPICreateBranchProtection(t *testing.T, branchName string, expectedHTTP | ||||
| 		var branchProtection api.BranchProtection | ||||
| 		DecodeJSON(t, resp, &branchProtection) | ||||
| 		assert.EqualValues(t, branchName, branchProtection.RuleName) | ||||
| 		assert.EqualValues(t, expectedPriority, branchProtection.Priority) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| @ -189,13 +190,13 @@ func TestAPIBranchProtection(t *testing.T) { | ||||
| 	defer tests.PrepareTestEnv(t)() | ||||
| 
 | ||||
| 	// Branch protection  on branch that not exist | ||||
| 	testAPICreateBranchProtection(t, "master/doesnotexist", http.StatusCreated) | ||||
| 	testAPICreateBranchProtection(t, "master/doesnotexist", 1, http.StatusCreated) | ||||
| 	// Get branch protection on branch that exist but not branch protection | ||||
| 	testAPIGetBranchProtection(t, "master", http.StatusNotFound) | ||||
| 
 | ||||
| 	testAPICreateBranchProtection(t, "master", http.StatusCreated) | ||||
| 	testAPICreateBranchProtection(t, "master", 2, http.StatusCreated) | ||||
| 	// Can only create once | ||||
| 	testAPICreateBranchProtection(t, "master", http.StatusForbidden) | ||||
| 	testAPICreateBranchProtection(t, "master", 0, http.StatusForbidden) | ||||
| 
 | ||||
| 	// Can't delete a protected branch | ||||
| 	testAPIDeleteBranch(t, "master", http.StatusForbidden) | ||||
|  | ||||
| @ -196,7 +196,11 @@ async function initIssuePinSort() { | ||||
| 
 | ||||
|   createSortable(pinDiv, { | ||||
|     group: 'shared', | ||||
|     onEnd: pinMoveEnd, // eslint-disable-line @typescript-eslint/no-misused-promises
 | ||||
|     onEnd: (e) => { | ||||
|       (async () => { | ||||
|         await pinMoveEnd(e); | ||||
|       })(); | ||||
|     }, | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										71
									
								
								web_src/js/features/repo-settings-branches.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								web_src/js/features/repo-settings-branches.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,71 @@ | ||||
| import {beforeEach, describe, expect, test, vi} from 'vitest'; | ||||
| import {initRepoBranchesSettings} from './repo-settings-branches.ts'; | ||||
| import {POST} from '../modules/fetch.ts'; | ||||
| import {createSortable} from '../modules/sortable.ts'; | ||||
| 
 | ||||
| vi.mock('../modules/fetch.ts', () => ({ | ||||
|   POST: vi.fn(), | ||||
| })); | ||||
| 
 | ||||
| vi.mock('../modules/sortable.ts', () => ({ | ||||
|   createSortable: vi.fn(), | ||||
| })); | ||||
| 
 | ||||
| describe('Repository Branch Settings', () => { | ||||
|   beforeEach(() => { | ||||
|     document.body.innerHTML = ` | ||||
|       <div id="protected-branches-list" data-update-priority-url="some/repo/branches/priority"> | ||||
|         <div class="flex-item tw-items-center item" data-id="1" > | ||||
|           <div class="drag-handle"></div> | ||||
|         </div> | ||||
|         <div class="flex-item tw-items-center item" data-id="2" > | ||||
|           <div class="drag-handle"></div> | ||||
|         </div> | ||||
|         <div class="flex-item tw-items-center item" data-id="3" > | ||||
|           <div class="drag-handle"></div> | ||||
|         </div> | ||||
|       </div> | ||||
|     `;
 | ||||
| 
 | ||||
|     vi.clearAllMocks(); | ||||
|   }); | ||||
| 
 | ||||
|   test('should initialize sortable for protected branches list', () => { | ||||
|     initRepoBranchesSettings(); | ||||
| 
 | ||||
|     expect(createSortable).toHaveBeenCalledWith( | ||||
|       document.querySelector('#protected-branches-list'), | ||||
|       expect.objectContaining({ | ||||
|         handle: '.drag-handle', | ||||
|         animation: 150, | ||||
|       }), | ||||
|     ); | ||||
|   }); | ||||
| 
 | ||||
|   test('should not initialize if protected branches list is not present', () => { | ||||
|     document.body.innerHTML = ''; | ||||
| 
 | ||||
|     initRepoBranchesSettings(); | ||||
| 
 | ||||
|     expect(createSortable).not.toHaveBeenCalled(); | ||||
|   }); | ||||
| 
 | ||||
|   test('should post new order after sorting', async () => { | ||||
|     vi.mocked(POST).mockResolvedValue({ok: true} as Response); | ||||
| 
 | ||||
|     // Mock createSortable to capture and execute the onEnd callback
 | ||||
|     vi.mocked(createSortable).mockImplementation((_el, options) => { | ||||
|       options.onEnd(); | ||||
|       return {destroy: vi.fn()}; | ||||
|     }); | ||||
| 
 | ||||
|     initRepoBranchesSettings(); | ||||
| 
 | ||||
|     expect(POST).toHaveBeenCalledWith( | ||||
|       'some/repo/branches/priority', | ||||
|       expect.objectContaining({ | ||||
|         data: {ids: [1, 2, 3]}, | ||||
|       }), | ||||
|     ); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										32
									
								
								web_src/js/features/repo-settings-branches.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								web_src/js/features/repo-settings-branches.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | ||||
| import {createSortable} from '../modules/sortable.ts'; | ||||
| import {POST} from '../modules/fetch.ts'; | ||||
| import {showErrorToast} from '../modules/toast.ts'; | ||||
| import {queryElemChildren} from '../utils/dom.ts'; | ||||
| 
 | ||||
| export function initRepoBranchesSettings() { | ||||
|   const protectedBranchesList = document.querySelector('#protected-branches-list'); | ||||
|   if (!protectedBranchesList) return; | ||||
| 
 | ||||
|   createSortable(protectedBranchesList, { | ||||
|     handle: '.drag-handle', | ||||
|     animation: 150, | ||||
| 
 | ||||
|     onEnd: () => { | ||||
|       (async () => { | ||||
|         const itemElems = queryElemChildren(protectedBranchesList, '.item[data-id]'); | ||||
|         const itemIds = Array.from(itemElems, (el) => parseInt(el.getAttribute('data-id'))); | ||||
| 
 | ||||
|         try { | ||||
|           await POST(protectedBranchesList.getAttribute('data-update-priority-url'), { | ||||
|             data: { | ||||
|               ids: itemIds, | ||||
|             }, | ||||
|           }); | ||||
|         } catch (err) { | ||||
|           const errorMessage = String(err); | ||||
|           showErrorToast(`Failed to update branch protection rule priority:, error: ${errorMessage}`); | ||||
|         } | ||||
|       })(); | ||||
|     }, | ||||
|   }); | ||||
| } | ||||
| @ -3,6 +3,7 @@ import {minimatch} from 'minimatch'; | ||||
| import {createMonaco} from './codeeditor.ts'; | ||||
| import {onInputDebounce, queryElems, toggleElem} from '../utils/dom.ts'; | ||||
| import {POST} from '../modules/fetch.ts'; | ||||
| import {initRepoBranchesSettings} from './repo-settings-branches.ts'; | ||||
| 
 | ||||
| const {appSubUrl, csrfToken} = window.config; | ||||
| 
 | ||||
| @ -154,4 +155,5 @@ export function initRepoSettings() { | ||||
|   initRepoSettingsCollaboration(); | ||||
|   initRepoSettingsSearchTeamBox(); | ||||
|   initRepoSettingsGitHook(); | ||||
|   initRepoBranchesSettings(); | ||||
| } | ||||
|  | ||||
| @ -34,6 +34,7 @@ import octiconGitCommit from '../../public/assets/img/svg/octicon-git-commit.svg | ||||
| import octiconGitMerge from '../../public/assets/img/svg/octicon-git-merge.svg'; | ||||
| import octiconGitPullRequest from '../../public/assets/img/svg/octicon-git-pull-request.svg'; | ||||
| import octiconGitPullRequestDraft from '../../public/assets/img/svg/octicon-git-pull-request-draft.svg'; | ||||
| import octiconGrabber from '../../public/assets/img/svg/octicon-grabber.svg'; | ||||
| import octiconHeading from '../../public/assets/img/svg/octicon-heading.svg'; | ||||
| import octiconHorizontalRule from '../../public/assets/img/svg/octicon-horizontal-rule.svg'; | ||||
| import octiconImage from '../../public/assets/img/svg/octicon-image.svg'; | ||||
| @ -107,6 +108,7 @@ const svgs = { | ||||
|   'octicon-git-merge': octiconGitMerge, | ||||
|   'octicon-git-pull-request': octiconGitPullRequest, | ||||
|   'octicon-git-pull-request-draft': octiconGitPullRequestDraft, | ||||
|   'octicon-grabber': octiconGrabber, | ||||
|   'octicon-heading': octiconHeading, | ||||
|   'octicon-horizontal-rule': octiconHorizontalRule, | ||||
|   'octicon-image': octiconImage, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user