Skip to content

Commit 9c09929

Browse files
author
Antonello Tartamo
committed
added api to delete, edit or get a specific branch protection
1 parent 70a93c6 commit 9c09929

File tree

6 files changed

+580
-9
lines changed

6 files changed

+580
-9
lines changed

models/git/protected_branch.go

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ var ErrBranchIsProtected = errors.New("branch is protected")
3030
// ProtectedBranch struct
3131
type ProtectedBranch struct {
3232
ID int64 `xorm:"pk autoincr"`
33-
RepoID int64 `xorm:"UNIQUE(s) DEFAULT 0"`
34-
OwnerID int64 `xorm:"UNIQUE(s) DEFAULT 0"`
33+
RepoID int64 `xorm:"INDEX DEFAULT 0"`
34+
OwnerID int64 `xorm:"INDEX DEFAULT 0"`
3535
Repo *repo_model.Repository `xorm:"-"`
36-
RuleName string `xorm:"'branch_name' UNIQUE(s)"` // a branch name or a glob match to branch name
36+
RuleName string `xorm:"'branch_name'"` // a branch name or a glob match to branch name
3737
Priority int64 `xorm:"NOT NULL DEFAULT 0"`
3838
globRule glob.Glob `xorm:"-"`
3939
isPlainName bool `xorm:"-"`
@@ -327,25 +327,51 @@ func GetOrgProtectedBranchRuleByName(ctx context.Context, ownerID int64, ruleNam
327327

328328
// GetProtectedBranchRuleByName getting protected branch rule by name
329329
func GetProtectedBranchRuleByName(ctx context.Context, repoID int64, ruleName string) (*ProtectedBranch, error) {
330+
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
331+
if err != nil {
332+
return nil, fmt.Errorf("GetRepositoryByID: %w", err)
333+
}
334+
if err := repo.LoadOwner(ctx); err != nil {
335+
return nil, fmt.Errorf("LoadOwner: %w", err)
336+
}
337+
if repo.Owner.IsOrganization() {
338+
if rel, exist, err := db.Get[ProtectedBranch](ctx, builder.Eq{"owner_id": repo.OwnerID, "branch_name": ruleName}); err != nil {
339+
return nil, err
340+
} else if exist {
341+
return rel, nil
342+
}
343+
}
330344
// branch_name is legacy name, it actually is rule name
331345
rel, exist, err := db.Get[ProtectedBranch](ctx, builder.Eq{"repo_id": repoID, "branch_name": ruleName})
332346
if err != nil {
333347
return nil, err
334-
} else if !exist {
335-
return nil, nil
336348
}
337-
return rel, nil
349+
return util.Iif(exist, rel, nil), nil
338350
}
339351

340352
// GetProtectedBranchRuleByID getting protected branch rule by rule ID
341353
func GetProtectedBranchRuleByID(ctx context.Context, repoID, ruleID int64) (*ProtectedBranch, error) {
354+
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
355+
if err != nil {
356+
return nil, fmt.Errorf("GetRepositoryByID: %w", err)
357+
}
358+
if err := repo.LoadOwner(ctx); err != nil {
359+
return nil, fmt.Errorf("LoadOwner: %w", err)
360+
}
361+
if repo.Owner.IsOrganization() {
362+
if rel, exist, err := db.Get[ProtectedBranch](ctx, builder.Eq{"owner_id": repo.OwnerID, "id": ruleID}); err != nil {
363+
return nil, err
364+
} else if exist {
365+
return rel, nil
366+
}
367+
}
368+
342369
rel, exist, err := db.Get[ProtectedBranch](ctx, builder.Eq{"repo_id": repoID, "id": ruleID})
343370
if err != nil {
344371
return nil, err
345-
} else if !exist {
346-
return nil, nil
347372
}
348-
return rel, nil
373+
374+
return util.Iif(exist, rel, nil), nil
349375
}
350376

351377
// WhitelistOptions represent all sorts of whitelists used for protected branches
@@ -673,6 +699,22 @@ func updateOrgTeamWhitelist(ctx context.Context, org *organization.Organization,
673699
return whitelist, nil
674700
}
675701

702+
// DeleteProtectedBranch removes ProtectedBranch relation between the user and repository.
703+
func DeleteOrgProtectedBranch(ctx context.Context, org *organization.Organization, id int64) (err error) {
704+
protectedBranch := &ProtectedBranch{
705+
OwnerID: org.ID,
706+
ID: id,
707+
}
708+
709+
if affected, err := db.GetEngine(ctx).Delete(protectedBranch); err != nil {
710+
return err
711+
} else if affected != 1 {
712+
return fmt.Errorf("delete protected branch ID(%v) failed", id)
713+
}
714+
715+
return nil
716+
}
717+
676718
// DeleteProtectedBranch removes ProtectedBranch relation between the user and repository.
677719
func DeleteProtectedBranch(ctx context.Context, repo *repo_model.Repository, id int64) (err error) {
678720
err = repo.MustNotBeArchived()

models/migrations/migrations.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ func prepareMigrationTasks() []*migration {
395395
newMigration(321, "Use LONGTEXT for some columns and fix review_state.updated_files column", v1_25.UseLongTextInSomeColumnsAndFixBugs),
396396
newMigration(322, "Extend comment tree_path length limit", v1_25.ExtendCommentTreePathLength),
397397
newMigration(323, "Add support for actions concurrency", v1_25.AddActionsConcurrency),
398+
newMigration(324, "Add owner_id to protected_branch", v1_25.AddOwnerIDToProtectedBranch),
398399
}
399400
return preparedMigrations
400401
}

models/migrations/v1_25/v324.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package v1_25
5+
6+
import (
7+
"code.gitea.io/gitea/modules/setting"
8+
9+
"xorm.io/xorm"
10+
)
11+
12+
func AddOwnerIDToProtectedBranch(x *xorm.Engine) error {
13+
type ProtectedBranch struct {
14+
ID int64 `xorm:"pk autoincr"`
15+
RepoID int64 `xorm:"INDEX DEFAULT 0"`
16+
OwnerID int64 `xorm:"INDEX DEFAULT 0"`
17+
}
18+
19+
if err := x.Sync(new(ProtectedBranch)); err != nil {
20+
return err
21+
}
22+
23+
db := x.NewSession()
24+
defer db.Close()
25+
26+
if err := db.Begin(); err != nil {
27+
return err
28+
}
29+
30+
// Drop old unique index if it exists, ignoring errors if it doesn't.
31+
if _, err := db.Exec("DROP INDEX `UQE_protected_branch_s`"); err != nil {
32+
return err
33+
//log.Warn("Could not drop index UQE_protected_branch_s: %v", err)
34+
}
35+
36+
// These partial indexes might not be supported on all database versions, but they are the correct approach.
37+
// We will assume modern database versions. The WHERE clause might need adjustment for MSSQL.
38+
if !setting.Database.Type.IsMSSQL() {
39+
if _, err := db.Exec("CREATE UNIQUE INDEX `UQE_protected_branch_repo_id_branch_name` ON `protected_branch` (`repo_id`, `branch_name`) WHERE `repo_id` != 0"); err != nil {
40+
return err
41+
}
42+
if _, err := db.Exec("CREATE UNIQUE INDEX `UQE_protected_branch_owner_id_branch_name` ON `protected_branch` (`owner_id`, `branch_name`) WHERE `owner_id` != 0"); err != nil {
43+
return err
44+
}
45+
}
46+
47+
return db.Commit()
48+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package v1_25
5+
6+
import (
7+
"testing"
8+
9+
"code.gitea.io/gitea/models/migrations/base"
10+
"code.gitea.io/gitea/modules/setting"
11+
12+
"github.com/stretchr/testify/assert"
13+
"xorm.io/xorm"
14+
"xorm.io/xorm/schemas"
15+
)
16+
17+
func prepareOldProtectedBranch(t *testing.T) (*xorm.Engine, func()) {
18+
type ProtectedBranch struct {
19+
ID int64 `xorm:"pk autoincr"`
20+
RepoID int64 `xorm:"UNIQUE(s)"`
21+
RuleName string `xorm:"'branch_name' UNIQUE(s)"`
22+
}
23+
24+
return base.PrepareTestEnv(t, 0, new(ProtectedBranch))
25+
}
26+
27+
func Test_AddOwnerIDToProtectedBranch(t *testing.T) {
28+
x, deferable := prepareOldProtectedBranch(t)
29+
defer deferable()
30+
if x == nil {
31+
return
32+
}
33+
34+
// The test fixtures will have already created the UQE_protected_branch_s index.
35+
// We can run the migration.
36+
assert.NoError(t, AddOwnerIDToProtectedBranch(x))
37+
38+
// Verify that the new column exists.
39+
type ProtectedBranch struct {
40+
ID int64 `xorm:"pk autoincr"`
41+
RepoID int64 `xorm:"INDEX DEFAULT 0"`
42+
OwnerID int64 `xorm:"INDEX DEFAULT 0"`
43+
RuleName string
44+
}
45+
46+
has, err := x.Dialect().IsColumnExist(x.DB(), t.Context(), "protected_branch", "owner_id")
47+
assert.NoError(t, err)
48+
assert.True(t, has, "owner_id column should exist")
49+
50+
// Skip index check for unsupported DBs
51+
if setting.Database.Type.IsMSSQL() || setting.Database.Type.IsSQLite3() {
52+
t.Log("Skipping index check for unsupported database")
53+
return
54+
}
55+
56+
table, err := x.TableInfo("protected_branch")
57+
assert.NoError(t, err)
58+
59+
// Check if the old index is gone
60+
_, exist := table.Indexes["UQE_protected_branch_s"]
61+
assert.False(t, exist, "old index UQE_protected_branch_s should not exist")
62+
63+
// Check if new indexes are created
64+
idx, exist := table.Indexes["UQE_protected_branch_repo_id_branch_name"]
65+
assert.True(t, exist, "new index UQE_protected_branch_repo_id_branch_name should exist")
66+
if exist {
67+
assert.Equal(t, schemas.UniqueType, idx.Type)
68+
assert.ElementsMatch(t, []string{"repo_id", "branch_name"}, idx.Cols)
69+
}
70+
71+
idx, exist = table.Indexes["UQE_protected_branch_owner_id_branch_name"]
72+
assert.True(t, exist, "new index UQE_protected_branch_owner_id_branch_name should exist")
73+
if exist {
74+
assert.Equal(t, schemas.UniqueType, idx.Type)
75+
assert.ElementsMatch(t, []string{"owner_id", "branch_name"}, idx.Cols)
76+
}
77+
78+
// Test repo-level unique constraint
79+
_, err = x.Insert(&ProtectedBranch{RepoID: 1, RuleName: "main"})
80+
assert.NoError(t, err)
81+
_, err = x.Insert(&ProtectedBranch{RepoID: 1, RuleName: "main"})
82+
assert.Error(t, err, "should fail to insert duplicate repo-level rule")
83+
84+
// Test org-level unique constraint
85+
_, err = x.Insert(&ProtectedBranch{OwnerID: 1, RuleName: "main"})
86+
assert.NoError(t, err)
87+
_, err = x.Insert(&ProtectedBranch{OwnerID: 1, RuleName: "main"})
88+
assert.Error(t, err, "should fail to insert duplicate org-level rule")
89+
90+
// Test that repo-level and org-level rules with the same name don't conflict
91+
_, err = x.Insert(&ProtectedBranch{RepoID: 2, RuleName: "develop"})
92+
assert.NoError(t, err)
93+
_, err = x.Insert(&ProtectedBranch{OwnerID: 2, RuleName: "develop"})
94+
assert.NoError(t, err)
95+
96+
// Test that rules with repo_id=0 or owner_id=0 don't conflict with partial indexes
97+
_, err = x.Insert(&ProtectedBranch{RepoID: 3, OwnerID: 0, RuleName: "feature-a"})
98+
assert.NoError(t, err)
99+
_, err = x.Insert(&ProtectedBranch{RepoID: 4, OwnerID: 0, RuleName: "feature-a"})
100+
assert.NoError(t, err)
101+
102+
_, err = x.Insert(&ProtectedBranch{RepoID: 0, OwnerID: 3, RuleName: "feature-b"})
103+
assert.NoError(t, err)
104+
_, err = x.Insert(&ProtectedBranch{RepoID: 0, OwnerID: 4, RuleName: "feature-b"})
105+
assert.NoError(t, err)
106+
}

routers/api/v1/api.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,6 +1187,11 @@ func Routes() *web.Router {
11871187
m.Group("/branch_protections", func() {
11881188
m.Get("", repo.ListOrgBranchProtections)
11891189
m.Post("", bind(api.CreateBranchProtectionOption{}), repo.CreateOrgBranchProtection)
1190+
m.Group("/{name}", func() {
1191+
m.Get("", repo.GetOrgBranchProtection)
1192+
m.Patch("", bind(api.EditBranchProtectionOption{}), repo.EditOrgBranchProtection)
1193+
m.Delete("", repo.DeleteOrgBranchProtection)
1194+
})
11901195
}, reqToken(), reqOrgOwnership())
11911196
}, orgAssignment(true), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
11921197

0 commit comments

Comments
 (0)