diff --git a/models/git/protected_branch.go b/models/git/protected_branch.go index 511f7563cf52d..e530ecde7737e 100644 --- a/models/git/protected_branch.go +++ b/models/git/protected_branch.go @@ -30,9 +30,10 @@ var ErrBranchIsProtected = errors.New("branch is protected") // ProtectedBranch struct type ProtectedBranch struct { ID int64 `xorm:"pk autoincr"` - RepoID int64 `xorm:"UNIQUE(s)"` + RepoID int64 `xorm:"INDEX DEFAULT 0"` + OwnerID int64 `xorm:"INDEX DEFAULT 0"` Repo *repo_model.Repository `xorm:"-"` - RuleName string `xorm:"'branch_name' UNIQUE(s)"` // a branch name or a glob match to branch name + RuleName string `xorm:"'branch_name'"` // a branch name or a glob match to branch name Priority int64 `xorm:"NOT NULL DEFAULT 0"` globRule glob.Glob `xorm:"-"` isPlainName bool `xorm:"-"` @@ -313,9 +314,9 @@ func (protectBranch *ProtectedBranch) IsUnprotectedFile(patterns []glob.Glob, pa } // GetProtectedBranchRuleByName getting protected branch rule by name -func GetProtectedBranchRuleByName(ctx context.Context, repoID int64, ruleName string) (*ProtectedBranch, error) { +func GetOrgProtectedBranchRuleByName(ctx context.Context, ownerID int64, ruleName string) (*ProtectedBranch, error) { // branch_name is legacy name, it actually is rule name - rel, exist, err := db.Get[ProtectedBranch](ctx, builder.Eq{"repo_id": repoID, "branch_name": ruleName}) + rel, exist, err := db.Get[ProtectedBranch](ctx, builder.Eq{"owner_id": ownerID, "branch_name": ruleName}) if err != nil { return nil, err } else if !exist { @@ -324,15 +325,53 @@ func GetProtectedBranchRuleByName(ctx context.Context, repoID int64, ruleName st return rel, nil } +// GetProtectedBranchRuleByName getting protected branch rule by name +func GetProtectedBranchRuleByName(ctx context.Context, repoID int64, ruleName string) (*ProtectedBranch, error) { + repo, err := repo_model.GetRepositoryByID(ctx, repoID) + if err != nil { + return nil, fmt.Errorf("GetRepositoryByID: %w", err) + } + if err := repo.LoadOwner(ctx); err != nil { + return nil, fmt.Errorf("LoadOwner: %w", err) + } + if repo.Owner.IsOrganization() { + if rel, exist, err := db.Get[ProtectedBranch](ctx, builder.Eq{"owner_id": repo.OwnerID, "branch_name": ruleName}); err != nil { + return nil, err + } else if exist { + return rel, nil + } + } + // branch_name is legacy name, it actually is rule name + rel, exist, err := db.Get[ProtectedBranch](ctx, builder.Eq{"repo_id": repoID, "branch_name": ruleName}) + if err != nil { + return nil, err + } + return util.Iif(exist, rel, nil), nil +} + // GetProtectedBranchRuleByID getting protected branch rule by rule ID func GetProtectedBranchRuleByID(ctx context.Context, repoID, ruleID int64) (*ProtectedBranch, error) { + repo, err := repo_model.GetRepositoryByID(ctx, repoID) + if err != nil { + return nil, fmt.Errorf("GetRepositoryByID: %w", err) + } + if err := repo.LoadOwner(ctx); err != nil { + return nil, fmt.Errorf("LoadOwner: %w", err) + } + if repo.Owner.IsOrganization() { + if rel, exist, err := db.Get[ProtectedBranch](ctx, builder.Eq{"owner_id": repo.OwnerID, "id": ruleID}); err != nil { + return nil, err + } else if exist { + return rel, nil + } + } + rel, exist, err := db.Get[ProtectedBranch](ctx, builder.Eq{"repo_id": repoID, "id": ruleID}) if err != nil { return nil, err - } else if !exist { - return nil, nil } - return rel, nil + + return util.Iif(exist, rel, nil), nil } // WhitelistOptions represent all sorts of whitelists used for protected branches @@ -441,6 +480,70 @@ func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, prote return nil } +// UpdateOrgProtectBranch saves branch protection options for an organization. +// If ID is 0, it creates a new record. Otherwise, updates existing record. +// This function also performs check if whitelist user and team's IDs have been changed +// to avoid unnecessary whitelist delete and regenerate. +func UpdateOrgProtectBranch(ctx context.Context, org *organization.Organization, protectBranch *ProtectedBranch, opts WhitelistOptions) (err error) { + whitelist, err := updateOrgUserWhitelist(ctx, org, protectBranch.WhitelistUserIDs, opts.UserIDs) + if err != nil { + return err + } + protectBranch.WhitelistUserIDs = whitelist + + whitelist, err = updateOrgUserWhitelist(ctx, org, protectBranch.ForcePushAllowlistUserIDs, opts.ForcePushUserIDs) + if err != nil { + return err + } + protectBranch.ForcePushAllowlistUserIDs = whitelist + + whitelist, err = updateOrgUserWhitelist(ctx, org, protectBranch.MergeWhitelistUserIDs, opts.MergeUserIDs) + if err != nil { + return err + } + protectBranch.MergeWhitelistUserIDs = whitelist + + whitelist, err = updateOrgApprovalWhitelist(ctx, org, protectBranch.ApprovalsWhitelistUserIDs, opts.ApprovalsUserIDs) + if err != nil { + return err + } + protectBranch.ApprovalsWhitelistUserIDs = whitelist + + whitelist, err = updateOrgTeamWhitelist(ctx, org, protectBranch.WhitelistTeamIDs, opts.TeamIDs) + if err != nil { + return err + } + protectBranch.WhitelistTeamIDs = whitelist + + whitelist, err = updateOrgTeamWhitelist(ctx, org, protectBranch.ForcePushAllowlistTeamIDs, opts.ForcePushTeamIDs) + if err != nil { + return err + } + protectBranch.ForcePushAllowlistTeamIDs = whitelist + + whitelist, err = updateOrgTeamWhitelist(ctx, org, protectBranch.MergeWhitelistTeamIDs, opts.MergeTeamIDs) + if err != nil { + return err + } + protectBranch.MergeWhitelistTeamIDs = whitelist + + whitelist, err = updateOrgTeamWhitelist(ctx, org, protectBranch.ApprovalsWhitelistTeamIDs, opts.ApprovalsTeamIDs) + if err != nil { + return err + } + protectBranch.ApprovalsWhitelistTeamIDs = whitelist + + if protectBranch.ID == 0 { + _, err = db.GetEngine(ctx).Insert(protectBranch) + return err + } + + 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 { @@ -480,6 +583,27 @@ func updateApprovalWhitelist(ctx context.Context, repo *repo_model.Repository, c return whitelist, err } +// updateOrgApprovalWhitelist checks whether the user whitelist changed and returns a whitelist with +// the users from newWhitelist which are members of the org. +func updateOrgApprovalWhitelist(ctx context.Context, org *organization.Organization, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) { + hasUsersChanged := !util.SliceSortedEqual(currentWhitelist, newWhitelist) + if !hasUsersChanged { + return currentWhitelist, nil + } + + whitelist = make([]int64, 0, len(newWhitelist)) + for _, userID := range newWhitelist { + isMember, err := organization.IsOrganizationMember(ctx, org.ID, userID) + if err != nil { + return nil, err + } + if isMember { + whitelist = append(whitelist, userID) + } + } + return whitelist, nil +} + // updateUserWhitelist checks whether the user whitelist changed and returns a whitelist with // the users from newWhitelist which have write access to the repo. func updateUserWhitelist(ctx context.Context, repo *repo_model.Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) { @@ -532,6 +656,65 @@ func updateTeamWhitelist(ctx context.Context, repo *repo_model.Repository, curre return whitelist, err } +// updateOrgUserWhitelist checks whether the user whitelist changed and returns a whitelist with +// the users from newWhitelist which are members of the org. +func updateOrgUserWhitelist(ctx context.Context, org *organization.Organization, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) { + hasUsersChanged := !util.SliceSortedEqual(currentWhitelist, newWhitelist) + if !hasUsersChanged { + return currentWhitelist, nil + } + + whitelist = make([]int64, 0, len(newWhitelist)) + for _, userID := range newWhitelist { + isMember, err := organization.IsOrganizationMember(ctx, org.ID, userID) + if err != nil { + return nil, err + } + if isMember { + whitelist = append(whitelist, userID) + } + } + return whitelist, nil +} + +// updateOrgTeamWhitelist checks whether the team whitelist changed and returns a whitelist with +// the teams from newWhitelist which belong to the org. +func updateOrgTeamWhitelist(ctx context.Context, org *organization.Organization, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) { + hasTeamsChanged := !util.SliceSortedEqual(currentWhitelist, newWhitelist) + if !hasTeamsChanged { + return currentWhitelist, nil + } + + teams, err := organization.GetTeamsByIDs(ctx, newWhitelist) + if err != nil { + return nil, fmt.Errorf("GetTeamsByIDs [team_ids: %d]: %v", newWhitelist, err) + } + + whitelist = make([]int64, 0, len(teams)) + for _, team := range teams { + if team.OrgID == org.ID { + whitelist = append(whitelist, team.ID) + } + } + return whitelist, nil +} + +// DeleteProtectedBranch removes ProtectedBranch relation between the user and repository. +func DeleteOrgProtectedBranch(ctx context.Context, org *organization.Organization, id int64) (err error) { + protectedBranch := &ProtectedBranch{ + OwnerID: org.ID, + ID: id, + } + + if affected, err := db.GetEngine(ctx).Delete(protectedBranch); err != nil { + return err + } else if affected != 1 { + return fmt.Errorf("delete protected branch ID(%v) failed", id) + } + + return nil +} + // DeleteProtectedBranch removes ProtectedBranch relation between the user and repository. func DeleteProtectedBranch(ctx context.Context, repo *repo_model.Repository, id int64) (err error) { err = repo.MustNotBeArchived() diff --git a/models/git/protected_branch_list.go b/models/git/protected_branch_list.go index 6b282835a4687..fd1732c754593 100644 --- a/models/git/protected_branch_list.go +++ b/models/git/protected_branch_list.go @@ -7,6 +7,8 @@ import ( "context" "sort" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/glob" "code.gitea.io/gitea/modules/optional" @@ -41,6 +43,18 @@ func (rules ProtectedBranchRules) sort() { }) } +// FindOrgProtectedBranchRules load all repository's protected rules +func FindOrgProtectedBranchRules(ctx context.Context, ownerID int64) (ProtectedBranchRules, error) { + var rules ProtectedBranchRules + err := db.GetEngine(ctx).Where("owner_id = ?", ownerID).Asc("created_unix").Find(&rules) + if err != nil { + return nil, err + } + + rules.sort() // to make non-glob rules have higher priority, and for same glob/non-glob rules, first created rules have higher priority + return rules, nil +} + // FindRepoProtectedBranchRules load all repository's protected rules func FindRepoProtectedBranchRules(ctx context.Context, repoID int64) (ProtectedBranchRules, error) { var rules ProtectedBranchRules @@ -48,6 +62,18 @@ func FindRepoProtectedBranchRules(ctx context.Context, repoID int64) (ProtectedB if err != nil { return nil, err } + + // if no repo-level rules matched, try to find owner-level rules + repo, err := repo_model.GetRepositoryByID(ctx, repoID) + if err != nil { + return nil, err + } + + err = db.GetEngine(ctx).Where("owner_id = ?", repo.OwnerID).Asc("created_unix").Find(&rules) + if err != nil { + return nil, err + } + rules.sort() // to make non-glob rules have higher priority, and for same glob/non-glob rules, first created rules have higher priority return rules, nil } diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 8fb10e84cf68e..b6202350f01b0 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -395,6 +395,7 @@ func prepareMigrationTasks() []*migration { newMigration(321, "Use LONGTEXT for some columns and fix review_state.updated_files column", v1_25.UseLongTextInSomeColumnsAndFixBugs), newMigration(322, "Extend comment tree_path length limit", v1_25.ExtendCommentTreePathLength), newMigration(323, "Add support for actions concurrency", v1_25.AddActionsConcurrency), + newMigration(324, "Add owner_id to protected_branch", v1_25.AddOwnerIDToProtectedBranch), } return preparedMigrations } diff --git a/models/migrations/v1_25/v324.go b/models/migrations/v1_25/v324.go new file mode 100644 index 0000000000000..7228ce0cfdbe8 --- /dev/null +++ b/models/migrations/v1_25/v324.go @@ -0,0 +1,48 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_25 + +import ( + "code.gitea.io/gitea/modules/setting" + + "xorm.io/xorm" +) + +func AddOwnerIDToProtectedBranch(x *xorm.Engine) error { + type ProtectedBranch struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX DEFAULT 0"` + OwnerID int64 `xorm:"INDEX DEFAULT 0"` + } + + if err := x.Sync(new(ProtectedBranch)); err != nil { + return err + } + + db := x.NewSession() + defer db.Close() + + if err := db.Begin(); err != nil { + return err + } + + // Drop old unique index if it exists, ignoring errors if it doesn't. + if _, err := db.Exec("DROP INDEX `UQE_protected_branch_s`"); err != nil { + return err + //log.Warn("Could not drop index UQE_protected_branch_s: %v", err) + } + + // These partial indexes might not be supported on all database versions, but they are the correct approach. + // We will assume modern database versions. The WHERE clause might need adjustment for MSSQL. + if !setting.Database.Type.IsMSSQL() { + 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 { + return err + } + 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 { + return err + } + } + + return db.Commit() +} diff --git a/models/migrations/v1_25/v324_test.go b/models/migrations/v1_25/v324_test.go new file mode 100644 index 0000000000000..a000692430d86 --- /dev/null +++ b/models/migrations/v1_25/v324_test.go @@ -0,0 +1,106 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_25 + +import ( + "testing" + + "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/modules/setting" + + "github.com/stretchr/testify/assert" + "xorm.io/xorm" + "xorm.io/xorm/schemas" +) + +func prepareOldProtectedBranch(t *testing.T) (*xorm.Engine, func()) { + type ProtectedBranch struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"UNIQUE(s)"` + RuleName string `xorm:"'branch_name' UNIQUE(s)"` + } + + return base.PrepareTestEnv(t, 0, new(ProtectedBranch)) +} + +func Test_AddOwnerIDToProtectedBranch(t *testing.T) { + x, deferable := prepareOldProtectedBranch(t) + defer deferable() + if x == nil { + return + } + + // The test fixtures will have already created the UQE_protected_branch_s index. + // We can run the migration. + assert.NoError(t, AddOwnerIDToProtectedBranch(x)) + + // Verify that the new column exists. + type ProtectedBranch struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX DEFAULT 0"` + OwnerID int64 `xorm:"INDEX DEFAULT 0"` + RuleName string + } + + has, err := x.Dialect().IsColumnExist(x.DB(), t.Context(), "protected_branch", "owner_id") + assert.NoError(t, err) + assert.True(t, has, "owner_id column should exist") + + // Skip index check for unsupported DBs + if setting.Database.Type.IsMSSQL() || setting.Database.Type.IsSQLite3() { + t.Log("Skipping index check for unsupported database") + return + } + + table, err := x.TableInfo("protected_branch") + assert.NoError(t, err) + + // Check if the old index is gone + _, exist := table.Indexes["UQE_protected_branch_s"] + assert.False(t, exist, "old index UQE_protected_branch_s should not exist") + + // Check if new indexes are created + idx, exist := table.Indexes["UQE_protected_branch_repo_id_branch_name"] + assert.True(t, exist, "new index UQE_protected_branch_repo_id_branch_name should exist") + if exist { + assert.Equal(t, schemas.UniqueType, idx.Type) + assert.ElementsMatch(t, []string{"repo_id", "branch_name"}, idx.Cols) + } + + idx, exist = table.Indexes["UQE_protected_branch_owner_id_branch_name"] + assert.True(t, exist, "new index UQE_protected_branch_owner_id_branch_name should exist") + if exist { + assert.Equal(t, schemas.UniqueType, idx.Type) + assert.ElementsMatch(t, []string{"owner_id", "branch_name"}, idx.Cols) + } + + // Test repo-level unique constraint + _, err = x.Insert(&ProtectedBranch{RepoID: 1, RuleName: "main"}) + assert.NoError(t, err) + _, err = x.Insert(&ProtectedBranch{RepoID: 1, RuleName: "main"}) + assert.Error(t, err, "should fail to insert duplicate repo-level rule") + + // Test org-level unique constraint + _, err = x.Insert(&ProtectedBranch{OwnerID: 1, RuleName: "main"}) + assert.NoError(t, err) + _, err = x.Insert(&ProtectedBranch{OwnerID: 1, RuleName: "main"}) + assert.Error(t, err, "should fail to insert duplicate org-level rule") + + // Test that repo-level and org-level rules with the same name don't conflict + _, err = x.Insert(&ProtectedBranch{RepoID: 2, RuleName: "develop"}) + assert.NoError(t, err) + _, err = x.Insert(&ProtectedBranch{OwnerID: 2, RuleName: "develop"}) + assert.NoError(t, err) + + // Test that rules with repo_id=0 or owner_id=0 don't conflict with partial indexes + _, err = x.Insert(&ProtectedBranch{RepoID: 3, OwnerID: 0, RuleName: "feature-a"}) + assert.NoError(t, err) + _, err = x.Insert(&ProtectedBranch{RepoID: 4, OwnerID: 0, RuleName: "feature-a"}) + assert.NoError(t, err) + + _, err = x.Insert(&ProtectedBranch{RepoID: 0, OwnerID: 3, RuleName: "feature-b"}) + assert.NoError(t, err) + _, err = x.Insert(&ProtectedBranch{RepoID: 0, OwnerID: 4, RuleName: "feature-b"}) + assert.NoError(t, err) +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 5f52ee8a4323d..518a40954d1b2 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1632,6 +1632,15 @@ func Routes() *web.Router { m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create) m.Get("/orgs", org.GetAll, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization)) m.Group("/orgs/{org}", func() { + m.Group("/branch_protections", func() { + m.Get("", repo.ListOrgBranchProtections) + m.Post("", bind(api.CreateBranchProtectionOption{}), repo.CreateOrgBranchProtection) + m.Group("/{name}", func() { + m.Get("", repo.GetOrgBranchProtection) + m.Patch("", bind(api.EditBranchProtectionOption{}), repo.EditOrgBranchProtection) + m.Delete("", repo.DeleteOrgBranchProtection) + }) + }, reqToken(), reqOrgOwnership()) m.Combo("").Get(org.Get). Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit). Delete(reqToken(), reqOrgOwnership(), org.Delete) diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 65fac45aa11f2..e0092db36b512 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -458,6 +458,45 @@ func RenameBranch(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } +// GetOrgBranchProtection gets a branch protection +func GetOrgBranchProtection(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/branch_protections/{name} organization orgGetBranchProtection + // --- + // summary: Get a specific branch protection for the organization + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: organization + // type: string + // required: true + // - name: name + // in: path + // description: name of protected branch + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/BranchProtection" + // "404": + // "$ref": "#/responses/notFound" + + org := ctx.Org.Organization + bpName := ctx.PathParam("name") + bp, err := git_model.GetOrgProtectedBranchRuleByName(ctx, org.ID, bpName) + if err != nil { + ctx.APIErrorInternal(err) + return + } + if bp == nil { + ctx.APIErrorNotFound() + return + } + + ctx.JSON(http.StatusOK, convert.ToBranchProtectionFromOrg(ctx, bp, org)) +} + // GetBranchProtection gets a branch protection func GetBranchProtection(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/branch_protections/{name} repository repoGetBranchProtection @@ -502,6 +541,43 @@ func GetBranchProtection(ctx *context.APIContext) { ctx.JSON(http.StatusOK, convert.ToBranchProtection(ctx, bp, repo)) } +// ListOrgBranchProtections list branch protections for an organization +func ListOrgBranchProtections(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/branch_protections organization orgListBranchProtections + // --- + // summary: List branch protections for an organization + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: organization + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/BranchProtectionList" + + org := ctx.Org.Organization + + if org == nil { + ctx.APIErrorNotFound() + return + } + + bps, err := git_model.FindOrgProtectedBranchRules(ctx, org.ID) + if err != nil { + ctx.APIErrorInternal(err) + return + } + apiBps := make([]*api.BranchProtection, len(bps)) + for i := range bps { + apiBps[i] = convert.ToBranchProtectionFromOrg(ctx, bps[i], org) + } + + ctx.JSON(http.StatusOK, apiBps) +} + // ListBranchProtections list branch protections for a repo func ListBranchProtections(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/branch_protections repository repoListBranchProtection @@ -538,6 +614,191 @@ func ListBranchProtections(ctx *context.APIContext) { ctx.JSON(http.StatusOK, apiBps) } +// CreateBranchProtection creates a branch protection for a repo +func CreateOrgBranchProtection(ctx *context.APIContext) { + // swagger:operation POST /orgs/{org}/branch_protections organization orgCreateBranchProtection + // --- + // summary: Create a branch protection for an organization + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: organization + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateBranchProtectionOption" + // responses: + // "201": + // "$ref": "#/responses/BranchProtection" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + // "423": + // "$ref": "#/responses/repoArchivedError" + + form := web.GetForm(ctx).(*api.CreateBranchProtectionOption) + org := ctx.Org.Organization + + ruleName := form.RuleName + if ruleName == "" { + ruleName = form.BranchName //nolint:staticcheck // deprecated field + } + if len(ruleName) == 0 { + ctx.APIError(http.StatusBadRequest, "both rule_name and branch_name are empty") + return + } + + protectBranch, err := git_model.GetOrgProtectedBranchRuleByName(ctx, org.ID, ruleName) + if err != nil { + ctx.APIErrorInternal(err) + return + } else if protectBranch != nil { + ctx.APIError(http.StatusForbidden, "Branch protection already exist") + return + } + + var requiredApprovals int64 + if form.RequiredApprovals > 0 { + requiredApprovals = form.RequiredApprovals + } + + whitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false) + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) + return + } + ctx.APIErrorInternal(err) + return + } + forcePushAllowlistUsers, err := user_model.GetUserIDsByNames(ctx, form.ForcePushAllowlistUsernames, false) + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) + return + } + ctx.APIErrorInternal(err) + return + } + mergeWhitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.MergeWhitelistUsernames, false) + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) + return + } + ctx.APIErrorInternal(err) + return + } + approvalsWhitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.ApprovalsWhitelistUsernames, false) + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) + return + } + ctx.APIErrorInternal(err) + return + } + + whitelistTeams, err := organization.GetTeamIDsByNames(ctx, org.ID, form.PushWhitelistTeams, false) + if err != nil { + if organization.IsErrTeamNotExist(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) + return + } + ctx.APIErrorInternal(err) + return + } + forcePushAllowlistTeams, err := organization.GetTeamIDsByNames(ctx, org.ID, form.ForcePushAllowlistTeams, false) + if err != nil { + if organization.IsErrTeamNotExist(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) + return + } + ctx.APIErrorInternal(err) + return + } + mergeWhitelistTeams, err := organization.GetTeamIDsByNames(ctx, org.ID, form.MergeWhitelistTeams, false) + if err != nil { + if organization.IsErrTeamNotExist(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) + return + } + ctx.APIErrorInternal(err) + return + } + approvalsWhitelistTeams, err := organization.GetTeamIDsByNames(ctx, org.ID, form.ApprovalsWhitelistTeams, false) + if err != nil { + if organization.IsErrTeamNotExist(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) + return + } + ctx.APIErrorInternal(err) + return + } + + protectBranch = &git_model.ProtectedBranch{ + OwnerID: org.ID, + RuleName: ruleName, + Priority: form.Priority, + CanPush: form.EnablePush, + EnableWhitelist: form.EnablePush && form.EnablePushWhitelist, + WhitelistDeployKeys: form.EnablePush && form.EnablePushWhitelist && form.PushWhitelistDeployKeys, + CanForcePush: form.EnablePush && form.EnableForcePush, + EnableForcePushAllowlist: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist, + ForcePushAllowlistDeployKeys: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist && form.ForcePushAllowlistDeployKeys, + EnableMergeWhitelist: form.EnableMergeWhitelist, + EnableStatusCheck: form.EnableStatusCheck, + StatusCheckContexts: form.StatusCheckContexts, + EnableApprovalsWhitelist: form.EnableApprovalsWhitelist, + RequiredApprovals: requiredApprovals, + BlockOnRejectedReviews: form.BlockOnRejectedReviews, + BlockOnOfficialReviewRequests: form.BlockOnOfficialReviewRequests, + DismissStaleApprovals: form.DismissStaleApprovals, + IgnoreStaleApprovals: form.IgnoreStaleApprovals, + RequireSignedCommits: form.RequireSignedCommits, + ProtectedFilePatterns: form.ProtectedFilePatterns, + UnprotectedFilePatterns: form.UnprotectedFilePatterns, + BlockOnOutdatedBranch: form.BlockOnOutdatedBranch, + BlockAdminMergeOverride: form.BlockAdminMergeOverride, + } + + if err := pull_service.CreateOrUpdateProtectedBranch(ctx, nil, protectBranch, git_model.WhitelistOptions{ + UserIDs: whitelistUsers, + TeamIDs: whitelistTeams, + ForcePushUserIDs: forcePushAllowlistUsers, + ForcePushTeamIDs: forcePushAllowlistTeams, + MergeUserIDs: mergeWhitelistUsers, + MergeTeamIDs: mergeWhitelistTeams, + ApprovalsUserIDs: approvalsWhitelistUsers, + ApprovalsTeamIDs: approvalsWhitelistTeams, + }); err != nil { + ctx.APIErrorInternal(err) + return + } + + // Reload from db to get all whitelists + bp, err := git_model.GetOrgProtectedBranchRuleByName(ctx, org.ID, ruleName) + if err != nil { + ctx.APIErrorInternal(err) + return + } + if bp == nil || bp.OwnerID != org.ID { + ctx.APIErrorInternal(errors.New("created branch protection not found")) + return + } + + ctx.JSON(http.StatusCreated, convert.ToBranchProtectionFromOrg(ctx, bp, org)) +} + // CreateBranchProtection creates a branch protection for a repo func CreateBranchProtection(ctx *context.APIContext) { // swagger:operation POST /repos/{owner}/{repo}/branch_protections repository repoCreateBranchProtection @@ -731,23 +992,18 @@ func CreateBranchProtection(ctx *context.APIContext) { } // EditBranchProtection edits a branch protection for a repo -func EditBranchProtection(ctx *context.APIContext) { - // swagger:operation PATCH /repos/{owner}/{repo}/branch_protections/{name} repository repoEditBranchProtection +func EditOrgBranchProtection(ctx *context.APIContext) { + // swagger:operation PATCH /orgs/{org}/branch_protections/{name} organization orgEditBranchProtection // --- - // summary: Edit a branch protections for a repository. Only fields that are set will be changed + // summary: Edit a branch protection for an organization. Only fields that are set will be changed // consumes: // - application/json // produces: // - application/json // parameters: - // - name: owner + // - name: org // in: path - // description: owner of the repo - // type: string - // required: true - // - name: repo - // in: path - // description: name of the repo + // description: organization // type: string // required: true // - name: name @@ -769,14 +1025,14 @@ func EditBranchProtection(ctx *context.APIContext) { // "423": // "$ref": "#/responses/repoArchivedError" form := web.GetForm(ctx).(*api.EditBranchProtectionOption) - repo := ctx.Repo.Repository + org := ctx.Org.Organization bpName := ctx.PathParam("name") - protectBranch, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName) + protectBranch, err := git_model.GetOrgProtectedBranchRuleByName(ctx, org.ID, bpName) if err != nil { ctx.APIErrorInternal(err) return } - if protectBranch == nil || protectBranch.RepoID != repo.ID { + if protectBranch == nil { ctx.APIErrorNotFound() return } @@ -938,26 +1194,317 @@ func EditBranchProtection(ctx *context.APIContext) { } var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64 - if repo.Owner.IsOrganization() { - if form.PushWhitelistTeams != nil { - whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.PushWhitelistTeams, false) - if err != nil { - if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) - return - } - ctx.APIErrorInternal(err) + + if form.PushWhitelistTeams != nil { + whitelistTeams, err = organization.GetTeamIDsByNames(ctx, org.ID, form.PushWhitelistTeams, false) + if err != nil { + if organization.IsErrTeamNotExist(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) return } - } else { - whitelistTeams = protectBranch.WhitelistTeamIDs + ctx.APIErrorInternal(err) + return } - if form.ForcePushAllowlistTeams != nil { - forcePushAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ForcePushAllowlistTeams, false) - if err != nil { - if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) - return + } else { + whitelistTeams = protectBranch.WhitelistTeamIDs + } + if form.ForcePushAllowlistTeams != nil { + forcePushAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, org.ID, form.ForcePushAllowlistTeams, false) + if err != nil { + if organization.IsErrTeamNotExist(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) + return + } + ctx.APIErrorInternal(err) + return + } + } else { + forcePushAllowlistTeams = protectBranch.ForcePushAllowlistTeamIDs + } + if form.MergeWhitelistTeams != nil { + mergeWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, org.ID, form.MergeWhitelistTeams, false) + if err != nil { + if organization.IsErrTeamNotExist(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) + return + } + ctx.APIErrorInternal(err) + return + } + } else { + mergeWhitelistTeams = protectBranch.MergeWhitelistTeamIDs + } + if form.ApprovalsWhitelistTeams != nil { + approvalsWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, org.ID, form.ApprovalsWhitelistTeams, false) + if err != nil { + if organization.IsErrTeamNotExist(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) + return + } + ctx.APIErrorInternal(err) + return + } + } else { + approvalsWhitelistTeams = protectBranch.ApprovalsWhitelistTeamIDs + } + + err = git_model.UpdateOrgProtectBranch(ctx, org, protectBranch, git_model.WhitelistOptions{ + UserIDs: whitelistUsers, + TeamIDs: whitelistTeams, + ForcePushUserIDs: forcePushAllowlistUsers, + ForcePushTeamIDs: forcePushAllowlistTeams, + MergeUserIDs: mergeWhitelistUsers, + MergeTeamIDs: mergeWhitelistTeams, + ApprovalsUserIDs: approvalsWhitelistUsers, + ApprovalsTeamIDs: approvalsWhitelistTeams, + }) + if err != nil { + ctx.APIErrorInternal(err) + return + } + + // Reload from db to ensure get all whitelists + bp, err := git_model.GetOrgProtectedBranchRuleByName(ctx, org.ID, bpName) + if err != nil { + ctx.APIErrorInternal(err) + return + } + if bp == nil { + ctx.APIErrorInternal(err) + return + } + + ctx.JSON(http.StatusOK, convert.ToBranchProtectionFromOrg(ctx, bp, org)) +} + +// EditBranchProtection edits a branch protection for a repo +func EditBranchProtection(ctx *context.APIContext) { + // swagger:operation PATCH /repos/{owner}/{repo}/branch_protections/{name} repository repoEditBranchProtection + // --- + // summary: Edit a branch protections for a repository. Only fields that are set will be changed + // 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: name + // in: path + // description: name of protected branch + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/EditBranchProtectionOption" + // responses: + // "200": + // "$ref": "#/responses/BranchProtection" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + // "423": + // "$ref": "#/responses/repoArchivedError" + form := web.GetForm(ctx).(*api.EditBranchProtectionOption) + repo := ctx.Repo.Repository + bpName := ctx.PathParam("name") + protectBranch, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName) + if err != nil { + ctx.APIErrorInternal(err) + return + } + if protectBranch == nil || protectBranch.RepoID != repo.ID { + ctx.APIErrorNotFound() + return + } + + if form.EnablePush != nil { + if !*form.EnablePush { + protectBranch.CanPush = false + protectBranch.EnableWhitelist = false + protectBranch.WhitelistDeployKeys = false + } else { + protectBranch.CanPush = true + if form.EnablePushWhitelist != nil { + if !*form.EnablePushWhitelist { + protectBranch.EnableWhitelist = false + protectBranch.WhitelistDeployKeys = false + } else { + protectBranch.EnableWhitelist = true + if form.PushWhitelistDeployKeys != nil { + protectBranch.WhitelistDeployKeys = *form.PushWhitelistDeployKeys + } + } + } + } + } + + if form.EnableForcePush != nil { + if !*form.EnableForcePush { + protectBranch.CanForcePush = false + protectBranch.EnableForcePushAllowlist = false + protectBranch.ForcePushAllowlistDeployKeys = false + } else { + protectBranch.CanForcePush = true + if form.EnableForcePushAllowlist != nil { + if !*form.EnableForcePushAllowlist { + protectBranch.EnableForcePushAllowlist = false + protectBranch.ForcePushAllowlistDeployKeys = false + } else { + protectBranch.EnableForcePushAllowlist = true + if form.ForcePushAllowlistDeployKeys != nil { + protectBranch.ForcePushAllowlistDeployKeys = *form.ForcePushAllowlistDeployKeys + } + } + } + } + } + + if form.Priority != nil { + protectBranch.Priority = *form.Priority + } + + if form.EnableMergeWhitelist != nil { + protectBranch.EnableMergeWhitelist = *form.EnableMergeWhitelist + } + + if form.EnableStatusCheck != nil { + protectBranch.EnableStatusCheck = *form.EnableStatusCheck + } + + if form.StatusCheckContexts != nil { + protectBranch.StatusCheckContexts = form.StatusCheckContexts + } + + if form.RequiredApprovals != nil && *form.RequiredApprovals >= 0 { + protectBranch.RequiredApprovals = *form.RequiredApprovals + } + + if form.EnableApprovalsWhitelist != nil { + protectBranch.EnableApprovalsWhitelist = *form.EnableApprovalsWhitelist + } + + if form.BlockOnRejectedReviews != nil { + protectBranch.BlockOnRejectedReviews = *form.BlockOnRejectedReviews + } + + if form.BlockOnOfficialReviewRequests != nil { + protectBranch.BlockOnOfficialReviewRequests = *form.BlockOnOfficialReviewRequests + } + + if form.DismissStaleApprovals != nil { + protectBranch.DismissStaleApprovals = *form.DismissStaleApprovals + } + + if form.IgnoreStaleApprovals != nil { + protectBranch.IgnoreStaleApprovals = *form.IgnoreStaleApprovals + } + + if form.RequireSignedCommits != nil { + protectBranch.RequireSignedCommits = *form.RequireSignedCommits + } + + if form.ProtectedFilePatterns != nil { + protectBranch.ProtectedFilePatterns = *form.ProtectedFilePatterns + } + + if form.UnprotectedFilePatterns != nil { + protectBranch.UnprotectedFilePatterns = *form.UnprotectedFilePatterns + } + + if form.BlockOnOutdatedBranch != nil { + protectBranch.BlockOnOutdatedBranch = *form.BlockOnOutdatedBranch + } + + if form.BlockAdminMergeOverride != nil { + protectBranch.BlockAdminMergeOverride = *form.BlockAdminMergeOverride + } + + var whitelistUsers, forcePushAllowlistUsers, mergeWhitelistUsers, approvalsWhitelistUsers []int64 + if form.PushWhitelistUsernames != nil { + whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false) + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) + return + } + ctx.APIErrorInternal(err) + return + } + } else { + whitelistUsers = protectBranch.WhitelistUserIDs + } + if form.ForcePushAllowlistDeployKeys != nil { + forcePushAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.ForcePushAllowlistUsernames, false) + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) + return + } + ctx.APIErrorInternal(err) + return + } + } else { + forcePushAllowlistUsers = protectBranch.ForcePushAllowlistUserIDs + } + if form.MergeWhitelistUsernames != nil { + mergeWhitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.MergeWhitelistUsernames, false) + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) + return + } + ctx.APIErrorInternal(err) + return + } + } else { + mergeWhitelistUsers = protectBranch.MergeWhitelistUserIDs + } + if form.ApprovalsWhitelistUsernames != nil { + approvalsWhitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.ApprovalsWhitelistUsernames, false) + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) + return + } + ctx.APIErrorInternal(err) + return + } + } else { + approvalsWhitelistUsers = protectBranch.ApprovalsWhitelistUserIDs + } + + var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64 + if repo.Owner.IsOrganization() { + if form.PushWhitelistTeams != nil { + whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.PushWhitelistTeams, false) + if err != nil { + if organization.IsErrTeamNotExist(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) + return + } + ctx.APIErrorInternal(err) + return + } + } else { + whitelistTeams = protectBranch.WhitelistTeamIDs + } + if form.ForcePushAllowlistTeams != nil { + forcePushAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ForcePushAllowlistTeams, false) + if err != nil { + if organization.IsErrTeamNotExist(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) + return } ctx.APIErrorInternal(err) return @@ -1059,6 +1606,50 @@ func EditBranchProtection(ctx *context.APIContext) { ctx.JSON(http.StatusOK, convert.ToBranchProtection(ctx, bp, repo)) } +// DeleteOrgBranchProtection deletes a branch protection for a repo +func DeleteOrgBranchProtection(ctx *context.APIContext) { + // swagger:operation DELETE /orgs/{org}/branch_protections/{name} organization orgDeleteBranchProtection + // --- + // summary: Delete a specific branch protection for the organization + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: organization + // type: string + // required: true + // - name: name + // in: path + // description: name of protected branch + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + + org := ctx.Org.Organization + bpName := ctx.PathParam("name") + bp, err := git_model.GetOrgProtectedBranchRuleByName(ctx, org.ID, bpName) + if err != nil { + ctx.APIErrorInternal(err) + return + } + if bp == nil { + ctx.APIErrorNotFound() + return + } + + if err := git_model.DeleteOrgProtectedBranch(ctx, org, bp.ID); err != nil { + ctx.APIErrorInternal(err) + return + } + + ctx.Status(http.StatusNoContent) +} + // DeleteBranchProtection deletes a branch protection for a repo func DeleteBranchProtection(ctx *context.APIContext) { // swagger:operation DELETE /repos/{owner}/{repo}/branch_protections/{name} repository repoDeleteBranchProtection diff --git a/services/convert/convert.go b/services/convert/convert.go index 0de38221409bb..0d71c2a534895 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -137,6 +137,74 @@ func getWhitelistEntities[T *user_model.User | *organization.Team](entities []T, return whitelistNames } +// ToBranchProtection convert a ProtectedBranch to api.BranchProtection +func ToBranchProtectionFromOrg(ctx context.Context, bp *git_model.ProtectedBranch, org *organization.Organization) *api.BranchProtection { + // org, err := organization.GetOrgByID(ctx, bp.OwnerID) + // if err != nil { + // log.Error("GetOrgByID: %v", err) + // } + + readers, _, err := org.GetMembers(ctx, nil) + if err != nil { + log.Error("org.GetMembers: %v", err) + } + teamReaders, err := organization.FindOrgTeams(ctx, bp.OwnerID) + if err != nil { + log.Error("FindOrgTeams: %v", err) + } + + pushWhitelistUsernames := getWhitelistEntities(readers, bp.WhitelistUserIDs) + forcePushAllowlistUsernames := getWhitelistEntities(readers, bp.ForcePushAllowlistUserIDs) + mergeWhitelistUsernames := getWhitelistEntities(readers, bp.MergeWhitelistUserIDs) + approvalsWhitelistUsernames := getWhitelistEntities(readers, bp.ApprovalsWhitelistUserIDs) + + pushWhitelistTeams := getWhitelistEntities(teamReaders, bp.WhitelistTeamIDs) + forcePushAllowlistTeams := getWhitelistEntities(teamReaders, bp.ForcePushAllowlistTeamIDs) + mergeWhitelistTeams := getWhitelistEntities(teamReaders, bp.MergeWhitelistTeamIDs) + approvalsWhitelistTeams := getWhitelistEntities(teamReaders, bp.ApprovalsWhitelistTeamIDs) + + branchName := "" + if !git_model.IsRuleNameSpecial(bp.RuleName) { + branchName = bp.RuleName + } + + return &api.BranchProtection{ + BranchName: branchName, + RuleName: bp.RuleName, + Priority: bp.Priority, + EnablePush: bp.CanPush, + EnablePushWhitelist: bp.EnableWhitelist, + PushWhitelistUsernames: pushWhitelistUsernames, + PushWhitelistTeams: pushWhitelistTeams, + PushWhitelistDeployKeys: bp.WhitelistDeployKeys, + EnableForcePush: bp.CanForcePush, + EnableForcePushAllowlist: bp.EnableForcePushAllowlist, + ForcePushAllowlistUsernames: forcePushAllowlistUsernames, + ForcePushAllowlistTeams: forcePushAllowlistTeams, + ForcePushAllowlistDeployKeys: bp.ForcePushAllowlistDeployKeys, + EnableMergeWhitelist: bp.EnableMergeWhitelist, + MergeWhitelistUsernames: mergeWhitelistUsernames, + MergeWhitelistTeams: mergeWhitelistTeams, + EnableStatusCheck: bp.EnableStatusCheck, + StatusCheckContexts: bp.StatusCheckContexts, + RequiredApprovals: bp.RequiredApprovals, + EnableApprovalsWhitelist: bp.EnableApprovalsWhitelist, + ApprovalsWhitelistUsernames: approvalsWhitelistUsernames, + ApprovalsWhitelistTeams: approvalsWhitelistTeams, + BlockOnRejectedReviews: bp.BlockOnRejectedReviews, + BlockOnOfficialReviewRequests: bp.BlockOnOfficialReviewRequests, + BlockOnOutdatedBranch: bp.BlockOnOutdatedBranch, + DismissStaleApprovals: bp.DismissStaleApprovals, + IgnoreStaleApprovals: bp.IgnoreStaleApprovals, + RequireSignedCommits: bp.RequireSignedCommits, + ProtectedFilePatterns: bp.ProtectedFilePatterns, + UnprotectedFilePatterns: bp.UnprotectedFilePatterns, + BlockAdminMergeOverride: bp.BlockAdminMergeOverride, + Created: bp.CreatedUnix.AsTime(), + Updated: bp.UpdatedUnix.AsTime(), + } +} + // ToBranchProtection convert a ProtectedBranch to api.BranchProtection func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo *repo_model.Repository) *api.BranchProtection { readers, err := access_model.GetRepoReaders(ctx, repo) diff --git a/services/pull/protected_branch.go b/services/pull/protected_branch.go index 181bd32f44357..7878d9c2561d1 100644 --- a/services/pull/protected_branch.go +++ b/services/pull/protected_branch.go @@ -7,6 +7,7 @@ import ( "context" git_model "code.gitea.io/gitea/models/git" + "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/gitrepo" ) @@ -14,33 +15,43 @@ import ( func CreateOrUpdateProtectedBranch(ctx context.Context, repo *repo_model.Repository, protectBranch *git_model.ProtectedBranch, whitelistOptions git_model.WhitelistOptions, ) error { - err := git_model.UpdateProtectBranch(ctx, repo, protectBranch, whitelistOptions) + var err error + if repo != nil { + err = git_model.UpdateProtectBranch(ctx, repo, protectBranch, whitelistOptions) + } else { + org, err := organization.GetOrgByID(ctx, protectBranch.OwnerID) + if err == nil { + err = git_model.UpdateOrgProtectBranch(ctx, org, protectBranch, whitelistOptions) + } + } if err != nil { return err } - isPlainRule := !git_model.IsRuleNameSpecial(protectBranch.RuleName) - var isBranchExist bool - if isPlainRule { - // TODO: read the database directly to check if the branch exists - isBranchExist = gitrepo.IsBranchExist(ctx, repo, protectBranch.RuleName) - } - - if isBranchExist { - if err := CheckPRsForBaseBranch(ctx, repo, protectBranch.RuleName); err != nil { - return err + if repo != nil { + isPlainRule := !git_model.IsRuleNameSpecial(protectBranch.RuleName) + var isBranchExist bool + if isPlainRule { + // TODO: read the database directly to check if the branch exists + isBranchExist = gitrepo.IsBranchExist(ctx, repo, protectBranch.RuleName) } - } else { - if !isPlainRule { - // FIXME: since we only need to recheck files protected rules, we could improve this - matchedBranches, err := git_model.FindAllMatchedBranches(ctx, repo.ID, protectBranch.RuleName) - if err != nil { + + if isBranchExist { + if err := CheckPRsForBaseBranch(ctx, repo, protectBranch.RuleName); err != nil { return err } - for _, branchName := range matchedBranches { - if err = CheckPRsForBaseBranch(ctx, repo, branchName); err != nil { + } else { + if !isPlainRule { + // FIXME: since we only need to recheck files protected rules, we could improve this + matchedBranches, err := git_model.FindAllMatchedBranches(ctx, repo.ID, protectBranch.RuleName) + if err != nil { return err } + for _, branchName := range matchedBranches { + if err = CheckPRsForBaseBranch(ctx, repo, branchName); err != nil { + return err + } + } } } } diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 325f0b78a0564..b120671e3179d 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -2806,6 +2806,198 @@ } } }, + "/orgs/{org}/branch_protections": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "List branch protections for an organization", + "operationId": "orgListBranchProtections", + "parameters": [ + { + "type": "string", + "description": "organization", + "name": "org", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/BranchProtectionList" + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Create a branch protection for an organization", + "operationId": "orgCreateBranchProtection", + "parameters": [ + { + "type": "string", + "description": "organization", + "name": "org", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/CreateBranchProtectionOption" + } + } + ], + "responses": { + "201": { + "$ref": "#/responses/BranchProtection" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + } + }, + "/orgs/{org}/branch_protections/{name}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Get a specific branch protection for the organization", + "operationId": "orgGetBranchProtection", + "parameters": [ + { + "type": "string", + "description": "organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of protected branch", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/BranchProtection" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Delete a specific branch protection for the organization", + "operationId": "orgDeleteBranchProtection", + "parameters": [ + { + "type": "string", + "description": "organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of protected branch", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "patch": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Edit a branch protection for an organization. Only fields that are set will be changed", + "operationId": "orgEditBranchProtection", + "parameters": [ + { + "type": "string", + "description": "organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of protected branch", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/EditBranchProtectionOption" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/BranchProtection" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + } + }, "/orgs/{org}/hooks": { "get": { "produces": [