Skip to content

Commit 9ff4e1d

Browse files
davidsvantessonlunnyzeripath
authored
Add API branch protection endpoint (#9311)
* add API branch protection endpoint * lint * Change to use team names instead of ids. * Status codes. * fix * Fix * Add new branch protection options (BlockOnRejectedReviews, DismissStaleApprovals, RequireSignedCommits) * Do xorm query directly * fix xorm GetUserNamesByIDs * Add some tests * Improved GetTeamNamesByID * http status created for CreateBranchProtection * Correct status code in integration test Co-authored-by: Lunny Xiao <[email protected]> Co-authored-by: zeripath <[email protected]>
1 parent 908f895 commit 9ff4e1d

File tree

10 files changed

+1352
-28
lines changed

10 files changed

+1352
-28
lines changed

integrations/api_branch_test.go

+68
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,54 @@ func testAPIGetBranch(t *testing.T, branchName string, exists bool) {
3030
assert.EqualValues(t, branchName, branch.Name)
3131
}
3232

33+
func testAPIGetBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) {
34+
session := loginUser(t, "user2")
35+
token := getTokenForLoggedInUser(t, session)
36+
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/branch_protections/%s?token=%s", branchName, token)
37+
resp := session.MakeRequest(t, req, expectedHTTPStatus)
38+
39+
if resp.Code == 200 {
40+
var branchProtection api.BranchProtection
41+
DecodeJSON(t, resp, &branchProtection)
42+
assert.EqualValues(t, branchName, branchProtection.BranchName)
43+
}
44+
}
45+
46+
func testAPICreateBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) {
47+
session := loginUser(t, "user2")
48+
token := getTokenForLoggedInUser(t, session)
49+
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branch_protections?token="+token, &api.BranchProtection{
50+
BranchName: branchName,
51+
})
52+
resp := session.MakeRequest(t, req, expectedHTTPStatus)
53+
54+
if resp.Code == 201 {
55+
var branchProtection api.BranchProtection
56+
DecodeJSON(t, resp, &branchProtection)
57+
assert.EqualValues(t, branchName, branchProtection.BranchName)
58+
}
59+
}
60+
61+
func testAPIEditBranchProtection(t *testing.T, branchName string, body *api.BranchProtection, expectedHTTPStatus int) {
62+
session := loginUser(t, "user2")
63+
token := getTokenForLoggedInUser(t, session)
64+
req := NewRequestWithJSON(t, "PATCH", "/api/v1/repos/user2/repo1/branch_protections/"+branchName+"?token="+token, body)
65+
resp := session.MakeRequest(t, req, expectedHTTPStatus)
66+
67+
if resp.Code == 200 {
68+
var branchProtection api.BranchProtection
69+
DecodeJSON(t, resp, &branchProtection)
70+
assert.EqualValues(t, branchName, branchProtection.BranchName)
71+
}
72+
}
73+
74+
func testAPIDeleteBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) {
75+
session := loginUser(t, "user2")
76+
token := getTokenForLoggedInUser(t, session)
77+
req := NewRequestf(t, "DELETE", "/api/v1/repos/user2/repo1/branch_protections/%s?token=%s", branchName, token)
78+
session.MakeRequest(t, req, expectedHTTPStatus)
79+
}
80+
3381
func TestAPIGetBranch(t *testing.T) {
3482
for _, test := range []struct {
3583
BranchName string
@@ -43,3 +91,23 @@ func TestAPIGetBranch(t *testing.T) {
4391
testAPIGetBranch(t, test.BranchName, test.Exists)
4492
}
4593
}
94+
95+
func TestAPIBranchProtection(t *testing.T) {
96+
defer prepareTestEnv(t)()
97+
98+
// Branch protection only on branch that exist
99+
testAPICreateBranchProtection(t, "master/doesnotexist", http.StatusNotFound)
100+
// Get branch protection on branch that exist but not branch protection
101+
testAPIGetBranchProtection(t, "master", http.StatusNotFound)
102+
103+
testAPICreateBranchProtection(t, "master", http.StatusCreated)
104+
// Can only create once
105+
testAPICreateBranchProtection(t, "master", http.StatusForbidden)
106+
107+
testAPIGetBranchProtection(t, "master", http.StatusOK)
108+
testAPIEditBranchProtection(t, "master", &api.BranchProtection{
109+
EnablePush: true,
110+
}, http.StatusOK)
111+
112+
testAPIDeleteBranchProtection(t, "master", http.StatusNoContent)
113+
}

models/org_team.go

+33
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,23 @@ func GetTeam(orgID int64, name string) (*Team, error) {
553553
return getTeam(x, orgID, name)
554554
}
555555

556+
// GetTeamIDsByNames returns a slice of team ids corresponds to names.
557+
func GetTeamIDsByNames(orgID int64, names []string, ignoreNonExistent bool) ([]int64, error) {
558+
ids := make([]int64, 0, len(names))
559+
for _, name := range names {
560+
u, err := GetTeam(orgID, name)
561+
if err != nil {
562+
if ignoreNonExistent {
563+
continue
564+
} else {
565+
return nil, err
566+
}
567+
}
568+
ids = append(ids, u.ID)
569+
}
570+
return ids, nil
571+
}
572+
556573
// getOwnerTeam returns team by given team name and organization.
557574
func getOwnerTeam(e Engine, orgID int64) (*Team, error) {
558575
return getTeam(e, orgID, ownerTeamName)
@@ -574,6 +591,22 @@ func GetTeamByID(teamID int64) (*Team, error) {
574591
return getTeamByID(x, teamID)
575592
}
576593

594+
// GetTeamNamesByID returns team's lower name from a list of team ids.
595+
func GetTeamNamesByID(teamIDs []int64) ([]string, error) {
596+
if len(teamIDs) == 0 {
597+
return []string{}, nil
598+
}
599+
600+
var teamNames []string
601+
err := x.Table("team").
602+
Select("lower_name").
603+
In("id", teamIDs).
604+
Asc("name").
605+
Find(&teamNames)
606+
607+
return teamNames, err
608+
}
609+
577610
// UpdateTeam updates information of team.
578611
func UpdateTeam(t *Team, authChanged bool, includeAllChanged bool) (err error) {
579612
if len(t.Name) == 0 {

models/user.go

+11
Original file line numberDiff line numberDiff line change
@@ -1386,6 +1386,17 @@ func GetMaileableUsersByIDs(ids []int64) ([]*User, error) {
13861386
Find(&ous)
13871387
}
13881388

1389+
// GetUserNamesByIDs returns usernames for all resolved users from a list of Ids.
1390+
func GetUserNamesByIDs(ids []int64) ([]string, error) {
1391+
unames := make([]string, 0, len(ids))
1392+
err := x.In("id", ids).
1393+
Table("user").
1394+
Asc("name").
1395+
Cols("name").
1396+
Find(&unames)
1397+
return unames, err
1398+
}
1399+
13891400
// GetUsersByIDs returns all resolved users from a list of Ids.
13901401
func GetUsersByIDs(ids []int64) ([]*User, error) {
13911402
ous := make([]*User, 0, len(ids))

modules/convert/convert.go

+75-17
Original file line numberDiff line numberDiff line change
@@ -30,28 +30,86 @@ func ToEmail(email *models.EmailAddress) *api.Email {
3030
}
3131

3232
// ToBranch convert a git.Commit and git.Branch to an api.Branch
33-
func ToBranch(repo *models.Repository, b *git.Branch, c *git.Commit, bp *models.ProtectedBranch, user *models.User) *api.Branch {
33+
func ToBranch(repo *models.Repository, b *git.Branch, c *git.Commit, bp *models.ProtectedBranch, user *models.User, isRepoAdmin bool) *api.Branch {
3434
if bp == nil {
3535
return &api.Branch{
36-
Name: b.Name,
37-
Commit: ToCommit(repo, c),
38-
Protected: false,
39-
RequiredApprovals: 0,
40-
EnableStatusCheck: false,
41-
StatusCheckContexts: []string{},
42-
UserCanPush: true,
43-
UserCanMerge: true,
36+
Name: b.Name,
37+
Commit: ToCommit(repo, c),
38+
Protected: false,
39+
RequiredApprovals: 0,
40+
EnableStatusCheck: false,
41+
StatusCheckContexts: []string{},
42+
UserCanPush: true,
43+
UserCanMerge: true,
44+
EffectiveBranchProtectionName: "",
4445
}
4546
}
47+
branchProtectionName := ""
48+
if isRepoAdmin {
49+
branchProtectionName = bp.BranchName
50+
}
51+
4652
return &api.Branch{
47-
Name: b.Name,
48-
Commit: ToCommit(repo, c),
49-
Protected: true,
50-
RequiredApprovals: bp.RequiredApprovals,
51-
EnableStatusCheck: bp.EnableStatusCheck,
52-
StatusCheckContexts: bp.StatusCheckContexts,
53-
UserCanPush: bp.CanUserPush(user.ID),
54-
UserCanMerge: bp.IsUserMergeWhitelisted(user.ID),
53+
Name: b.Name,
54+
Commit: ToCommit(repo, c),
55+
Protected: true,
56+
RequiredApprovals: bp.RequiredApprovals,
57+
EnableStatusCheck: bp.EnableStatusCheck,
58+
StatusCheckContexts: bp.StatusCheckContexts,
59+
UserCanPush: bp.CanUserPush(user.ID),
60+
UserCanMerge: bp.IsUserMergeWhitelisted(user.ID),
61+
EffectiveBranchProtectionName: branchProtectionName,
62+
}
63+
}
64+
65+
// ToBranchProtection convert a ProtectedBranch to api.BranchProtection
66+
func ToBranchProtection(bp *models.ProtectedBranch) *api.BranchProtection {
67+
pushWhitelistUsernames, err := models.GetUserNamesByIDs(bp.WhitelistUserIDs)
68+
if err != nil {
69+
log.Error("GetUserNamesByIDs (WhitelistUserIDs): %v", err)
70+
}
71+
mergeWhitelistUsernames, err := models.GetUserNamesByIDs(bp.MergeWhitelistUserIDs)
72+
if err != nil {
73+
log.Error("GetUserNamesByIDs (MergeWhitelistUserIDs): %v", err)
74+
}
75+
approvalsWhitelistUsernames, err := models.GetUserNamesByIDs(bp.ApprovalsWhitelistUserIDs)
76+
if err != nil {
77+
log.Error("GetUserNamesByIDs (ApprovalsWhitelistUserIDs): %v", err)
78+
}
79+
pushWhitelistTeams, err := models.GetTeamNamesByID(bp.WhitelistTeamIDs)
80+
if err != nil {
81+
log.Error("GetTeamNamesByID (WhitelistTeamIDs): %v", err)
82+
}
83+
mergeWhitelistTeams, err := models.GetTeamNamesByID(bp.MergeWhitelistTeamIDs)
84+
if err != nil {
85+
log.Error("GetTeamNamesByID (MergeWhitelistTeamIDs): %v", err)
86+
}
87+
approvalsWhitelistTeams, err := models.GetTeamNamesByID(bp.ApprovalsWhitelistTeamIDs)
88+
if err != nil {
89+
log.Error("GetTeamNamesByID (ApprovalsWhitelistTeamIDs): %v", err)
90+
}
91+
92+
return &api.BranchProtection{
93+
BranchName: bp.BranchName,
94+
EnablePush: bp.CanPush,
95+
EnablePushWhitelist: bp.EnableWhitelist,
96+
PushWhitelistUsernames: pushWhitelistUsernames,
97+
PushWhitelistTeams: pushWhitelistTeams,
98+
PushWhitelistDeployKeys: bp.WhitelistDeployKeys,
99+
EnableMergeWhitelist: bp.EnableMergeWhitelist,
100+
MergeWhitelistUsernames: mergeWhitelistUsernames,
101+
MergeWhitelistTeams: mergeWhitelistTeams,
102+
EnableStatusCheck: bp.EnableStatusCheck,
103+
StatusCheckContexts: bp.StatusCheckContexts,
104+
RequiredApprovals: bp.RequiredApprovals,
105+
EnableApprovalsWhitelist: bp.EnableApprovalsWhitelist,
106+
ApprovalsWhitelistUsernames: approvalsWhitelistUsernames,
107+
ApprovalsWhitelistTeams: approvalsWhitelistTeams,
108+
BlockOnRejectedReviews: bp.BlockOnRejectedReviews,
109+
DismissStaleApprovals: bp.DismissStaleApprovals,
110+
RequireSignedCommits: bp.RequireSignedCommits,
111+
Created: bp.CreatedUnix.AsTime(),
112+
Updated: bp.UpdatedUnix.AsTime(),
55113
}
56114
}
57115

modules/structs/repo_branch.go

+82-8
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,88 @@
44

55
package structs
66

7+
import (
8+
"time"
9+
)
10+
711
// Branch represents a repository branch
812
type Branch struct {
9-
Name string `json:"name"`
10-
Commit *PayloadCommit `json:"commit"`
11-
Protected bool `json:"protected"`
12-
RequiredApprovals int64 `json:"required_approvals"`
13-
EnableStatusCheck bool `json:"enable_status_check"`
14-
StatusCheckContexts []string `json:"status_check_contexts"`
15-
UserCanPush bool `json:"user_can_push"`
16-
UserCanMerge bool `json:"user_can_merge"`
13+
Name string `json:"name"`
14+
Commit *PayloadCommit `json:"commit"`
15+
Protected bool `json:"protected"`
16+
RequiredApprovals int64 `json:"required_approvals"`
17+
EnableStatusCheck bool `json:"enable_status_check"`
18+
StatusCheckContexts []string `json:"status_check_contexts"`
19+
UserCanPush bool `json:"user_can_push"`
20+
UserCanMerge bool `json:"user_can_merge"`
21+
EffectiveBranchProtectionName string `json:"effective_branch_protection_name"`
22+
}
23+
24+
// BranchProtection represents a branch protection for a repository
25+
type BranchProtection struct {
26+
BranchName string `json:"branch_name"`
27+
EnablePush bool `json:"enable_push"`
28+
EnablePushWhitelist bool `json:"enable_push_whitelist"`
29+
PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
30+
PushWhitelistTeams []string `json:"push_whitelist_teams"`
31+
PushWhitelistDeployKeys bool `json:"push_whitelist_deploy_keys"`
32+
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
33+
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
34+
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
35+
EnableStatusCheck bool `json:"enable_status_check"`
36+
StatusCheckContexts []string `json:"status_check_contexts"`
37+
RequiredApprovals int64 `json:"required_approvals"`
38+
EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist"`
39+
ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"`
40+
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"`
41+
BlockOnRejectedReviews bool `json:"block_on_rejected_reviews"`
42+
DismissStaleApprovals bool `json:"dismiss_stale_approvals"`
43+
RequireSignedCommits bool `json:"require_signed_commits"`
44+
// swagger:strfmt date-time
45+
Created time.Time `json:"created_at"`
46+
// swagger:strfmt date-time
47+
Updated time.Time `json:"updated_at"`
48+
}
49+
50+
// CreateBranchProtectionOption options for creating a branch protection
51+
type CreateBranchProtectionOption struct {
52+
BranchName string `json:"branch_name"`
53+
EnablePush bool `json:"enable_push"`
54+
EnablePushWhitelist bool `json:"enable_push_whitelist"`
55+
PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
56+
PushWhitelistTeams []string `json:"push_whitelist_teams"`
57+
PushWhitelistDeployKeys bool `json:"push_whitelist_deploy_keys"`
58+
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
59+
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
60+
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
61+
EnableStatusCheck bool `json:"enable_status_check"`
62+
StatusCheckContexts []string `json:"status_check_contexts"`
63+
RequiredApprovals int64 `json:"required_approvals"`
64+
EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist"`
65+
ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"`
66+
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"`
67+
BlockOnRejectedReviews bool `json:"block_on_rejected_reviews"`
68+
DismissStaleApprovals bool `json:"dismiss_stale_approvals"`
69+
RequireSignedCommits bool `json:"require_signed_commits"`
70+
}
71+
72+
// EditBranchProtectionOption options for editing a branch protection
73+
type EditBranchProtectionOption struct {
74+
EnablePush *bool `json:"enable_push"`
75+
EnablePushWhitelist *bool `json:"enable_push_whitelist"`
76+
PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
77+
PushWhitelistTeams []string `json:"push_whitelist_teams"`
78+
PushWhitelistDeployKeys *bool `json:"push_whitelist_deploy_keys"`
79+
EnableMergeWhitelist *bool `json:"enable_merge_whitelist"`
80+
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
81+
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
82+
EnableStatusCheck *bool `json:"enable_status_check"`
83+
StatusCheckContexts []string `json:"status_check_contexts"`
84+
RequiredApprovals *int64 `json:"required_approvals"`
85+
EnableApprovalsWhitelist *bool `json:"enable_approvals_whitelist"`
86+
ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"`
87+
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"`
88+
BlockOnRejectedReviews *bool `json:"block_on_rejected_reviews"`
89+
DismissStaleApprovals *bool `json:"dismiss_stale_approvals"`
90+
RequireSignedCommits *bool `json:"require_signed_commits"`
1791
}

routers/api/v1/api.go

+9
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,15 @@ func RegisterRoutes(m *macaron.Macaron) {
656656
m.Get("", repo.ListBranches)
657657
m.Get("/*", context.RepoRefByType(context.RepoRefBranch), repo.GetBranch)
658658
}, reqRepoReader(models.UnitTypeCode))
659+
m.Group("/branch_protections", func() {
660+
m.Get("", repo.ListBranchProtections)
661+
m.Post("", bind(api.CreateBranchProtectionOption{}), repo.CreateBranchProtection)
662+
m.Group("/:name", func() {
663+
m.Get("", repo.GetBranchProtection)
664+
m.Patch("", bind(api.EditBranchProtectionOption{}), repo.EditBranchProtection)
665+
m.Delete("", repo.DeleteBranchProtection)
666+
})
667+
}, reqToken(), reqAdmin())
659668
m.Group("/tags", func() {
660669
m.Get("", repo.ListTags)
661670
}, reqRepoReader(models.UnitTypeCode), context.ReferencesGitRepo(true))

0 commit comments

Comments
 (0)