Skip to content

Commit

Permalink
Add priority to protected branch (#32286)
Browse files Browse the repository at this point in the history
## 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*
  • Loading branch information
6543 authored Nov 27, 2024
1 parent 3fc1bbe commit 846f618
Show file tree
Hide file tree
Showing 22 changed files with 454 additions and 13 deletions.
34 changes: 33 additions & 1 deletion models/git/protected_branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -413,21 +414,52 @@ 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)
}

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) {
Expand Down
7 changes: 7 additions & 0 deletions models/git/protected_branch_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
36 changes: 35 additions & 1 deletion models/git/protected_branch_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
78 changes: 78 additions & 0 deletions models/git/protected_branch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)
}
1 change: 1 addition & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
16 changes: 16 additions & 0 deletions models/migrations/v1_23/v310.go
Original file line number Diff line number Diff line change
@@ -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))
}
8 changes: 8 additions & 0 deletions modules/structs/repo_branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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"`
}
1 change: 1 addition & 0 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
56 changes: 52 additions & 4 deletions routers/api/v1/repo/branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}
3 changes: 3 additions & 0 deletions routers/api/v1/swagger/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ type swaggerParameterBodies struct {
// in:body
EditBranchProtectionOption api.EditBranchProtectionOption

// in:body
UpdateBranchProtectionPriories api.UpdateBranchProtectionPriories

// in:body
CreateOAuth2ApplicationOptions api.CreateOAuth2ApplicationOptions

Expand Down
10 changes: 10 additions & 0 deletions routers/web/repo/setting/protected_branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 846f618

Please sign in to comment.