Protected branches system (#339)

* Protected branches system

* Moved default branch to branches section (`:org/:reponame/settings/branches`).
* Initial support Protected Branch.
  - Admin does not restrict
  - Owner not to limit
  - To write permission restrictions

* reformat tmpl

* finished the UI and add/delete protected branch response

* remove unused comment

* indent all the template files and remove ru translations since we use crowdin

* fix the push bug
This commit is contained in:
Denis Denisov 2017-02-21 17:02:10 +02:00 committed by Lunny Xiao
parent fe5ff8e4b2
commit fd941db246
17 changed files with 606 additions and 49 deletions

View file

@ -42,10 +42,20 @@ func HTTP(ctx *context.Context) {
} else if service == "git-upload-pack" ||
strings.HasSuffix(ctx.Req.URL.Path, "git-upload-pack") {
isPull = true
} else if service == "git-upload-archive" ||
strings.HasSuffix(ctx.Req.URL.Path, "git-upload-archive") {
isPull = true
} else {
isPull = (ctx.Req.Method == "GET")
}
var accessMode models.AccessMode
if isPull {
accessMode = models.AccessModeRead
} else {
accessMode = models.AccessModeWrite
}
isWiki := false
if strings.HasSuffix(reponame, ".wiki") {
isWiki = true
@ -146,17 +156,12 @@ func HTTP(ctx *context.Context) {
}
if !isPublicPull {
var tp = models.AccessModeWrite
if isPull {
tp = models.AccessModeRead
}
has, err := models.HasAccess(authUser, repo, tp)
has, err := models.HasAccess(authUser, repo, accessMode)
if err != nil {
ctx.Handle(http.StatusInternalServerError, "HasAccess", err)
return
} else if !has {
if tp == models.AccessModeRead {
if accessMode == models.AccessModeRead {
has, err = models.HasAccess(authUser, repo, models.AccessModeWrite)
if err != nil {
ctx.Handle(http.StatusInternalServerError, "HasAccess2", err)
@ -232,9 +237,20 @@ func HTTP(ctx *context.Context) {
}
}
params := make(map[string]string)
if askAuth {
params[models.ProtectedBranchUserID] = fmt.Sprintf("%d", authUser.ID)
if err == nil {
params[models.ProtectedBranchAccessMode] = accessMode.String()
}
params[models.ProtectedBranchRepoID] = fmt.Sprintf("%d", repo.ID)
}
HTTPBackend(ctx, &serviceConfig{
UploadPack: true,
ReceivePack: true,
Params: params,
OnSucceed: callback,
})(ctx.Resp, ctx.Req.Request)
@ -244,6 +260,7 @@ func HTTP(ctx *context.Context) {
type serviceConfig struct {
UploadPack bool
ReceivePack bool
Params map[string]string
OnSucceed func(rpc string, input []byte)
}
@ -261,6 +278,42 @@ func (h *serviceHandler) setHeaderNoCache() {
h.w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
}
func (h *serviceHandler) getBranch(input []byte) string {
var lastLine int64
var branchName string
for {
head := input[lastLine : lastLine+2]
if head[0] == '0' && head[1] == '0' {
size, err := strconv.ParseInt(string(input[lastLine+2:lastLine+4]), 16, 32)
if err != nil {
log.Error(4, "%v", err)
return branchName
}
if size == 0 {
//fmt.Println(string(input[lastLine:]))
break
}
line := input[lastLine : lastLine+size]
idx := bytes.IndexRune(line, '\000')
if idx > -1 {
line = line[:idx]
}
fields := strings.Fields(string(line))
if len(fields) >= 3 {
refFullName := fields[2]
branchName = strings.TrimPrefix(refFullName, git.BranchPrefix)
}
lastLine = lastLine + size
} else {
break
}
}
return branchName
}
func (h *serviceHandler) setHeaderCacheForever() {
now := time.Now().Unix()
expires := now + 31536000
@ -358,13 +411,15 @@ func serviceRPC(h serviceHandler, service string) {
h.w.WriteHeader(http.StatusUnauthorized)
return
}
h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service))
var (
reqBody = h.r.Body
input []byte
br io.Reader
err error
reqBody = h.r.Body
input []byte
br io.Reader
err error
branchName string
)
// Handle GZIP.
@ -385,11 +440,31 @@ func serviceRPC(h serviceHandler, service string) {
return
}
branchName = h.getBranch(input)
br = bytes.NewReader(input)
} else {
br = reqBody
}
// check protected branch
repoID, _ := strconv.ParseInt(h.cfg.Params[models.ProtectedBranchRepoID], 10, 64)
accessMode := models.ParseAccessMode(h.cfg.Params[models.ProtectedBranchAccessMode])
// skip admin or owner AccessMode
if accessMode == models.AccessModeWrite {
protectBranch, err := models.GetProtectedBranchBy(repoID, branchName)
if err != nil {
log.GitLogger.Error(2, "fail to get protected branch information: %v", err)
h.w.WriteHeader(http.StatusInternalServerError)
return
}
if protectBranch != nil {
log.GitLogger.Error(2, "protected branches can not be pushed to")
h.w.WriteHeader(http.StatusForbidden)
return
}
}
cmd := exec.Command("git", service, "--stateless-rpc", h.dir)
cmd.Dir = h.dir
cmd.Stdout = h.w

View file

@ -21,6 +21,7 @@ import (
const (
tplSettingsOptions base.TplName = "repo/settings/options"
tplCollaboration base.TplName = "repo/settings/collaboration"
tplBranches base.TplName = "repo/settings/branches"
tplGithooks base.TplName = "repo/settings/githooks"
tplGithookEdit base.TplName = "repo/settings/githook_edit"
tplDeployKeys base.TplName = "repo/settings/deploy_keys"
@ -78,17 +79,6 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) {
// In case it's just a case change.
repo.Name = newRepoName
repo.LowerName = strings.ToLower(newRepoName)
if ctx.Repo.GitRepo.IsBranchExist(form.Branch) &&
repo.DefaultBranch != form.Branch {
repo.DefaultBranch = form.Branch
if err := ctx.Repo.GitRepo.SetDefaultBranch(form.Branch); err != nil {
if !git.IsErrUnsupportedVersion(err) {
ctx.Handle(500, "SetDefaultBranch", err)
return
}
}
}
repo.Description = form.Description
repo.Website = form.Website
@ -429,6 +419,142 @@ func DeleteCollaboration(ctx *context.Context) {
})
}
// ProtectedBranch render the page to protect the repository
func ProtectedBranch(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings")
ctx.Data["PageIsSettingsBranches"] = true
protectedBranches, err := ctx.Repo.Repository.GetProtectedBranches()
if err != nil {
ctx.Handle(500, "GetProtectedBranches", err)
return
}
ctx.Data["ProtectedBranches"] = protectedBranches
branches := ctx.Data["Branches"].([]string)
leftBranches := make([]string, 0, len(branches)-len(protectedBranches))
for _, b := range branches {
var protected bool
for _, pb := range protectedBranches {
if b == pb.BranchName {
protected = true
break
}
}
if !protected {
leftBranches = append(leftBranches, b)
}
}
ctx.Data["LeftBranches"] = leftBranches
ctx.HTML(200, tplBranches)
}
// ProtectedBranchPost response for protect for a branch of a repository
func ProtectedBranchPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings")
ctx.Data["PageIsSettingsBranches"] = true
repo := ctx.Repo.Repository
switch ctx.Query("action") {
case "default_branch":
if ctx.HasError() {
ctx.HTML(200, tplBranches)
return
}
branch := strings.ToLower(ctx.Query("branch"))
if ctx.Repo.GitRepo.IsBranchExist(branch) &&
repo.DefaultBranch != branch {
repo.DefaultBranch = branch
if err := ctx.Repo.GitRepo.SetDefaultBranch(branch); err != nil {
if !git.IsErrUnsupportedVersion(err) {
ctx.Handle(500, "SetDefaultBranch", err)
return
}
}
if err := repo.UpdateDefaultBranch(); err != nil {
ctx.Handle(500, "SetDefaultBranch", err)
return
}
}
log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
case "protected_branch":
if ctx.HasError() {
ctx.JSON(200, map[string]string{
"redirect": setting.AppSubURL + ctx.Req.URL.Path,
})
return
}
branchName := strings.ToLower(ctx.Query("branchName"))
if len(branchName) == 0 || !ctx.Repo.GitRepo.IsBranchExist(branchName) {
ctx.JSON(200, map[string]string{
"redirect": setting.AppSubURL + ctx.Req.URL.Path,
})
return
}
canPush := ctx.QueryBool("canPush")
if canPush {
if err := ctx.Repo.Repository.AddProtectedBranch(branchName, canPush); err != nil {
ctx.Flash.Error(ctx.Tr("repo.settings.add_protected_branch_failed", branchName))
ctx.JSON(200, map[string]string{
"status": "ok",
})
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.add_protected_branch_success", branchName))
ctx.JSON(200, map[string]string{
"redirect": setting.AppSubURL + ctx.Req.URL.Path,
})
} else {
if err := ctx.Repo.Repository.DeleteProtectedBranch(ctx.QueryInt64("id")); err != nil {
ctx.Flash.Error("DeleteProtectedBranch: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success", branchName))
}
ctx.JSON(200, map[string]interface{}{
"status": "ok",
})
}
default:
ctx.Handle(404, "", nil)
}
}
// ChangeProtectedBranch response for changing access of a protect branch
func ChangeProtectedBranch(ctx *context.Context) {
if err := ctx.Repo.Repository.ChangeProtectedBranch(
ctx.QueryInt64("id"),
ctx.QueryBool("canPush")); err != nil {
log.Error(4, "ChangeProtectedBranch: %v", err)
}
}
// DeleteProtectedBranch delete a protection for a branch of a repository
func DeleteProtectedBranch(ctx *context.Context) {
if err := ctx.Repo.Repository.DeleteProtectedBranch(ctx.QueryInt64("id")); err != nil {
ctx.Flash.Error("DeleteProtectedBranch: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success"))
}
ctx.JSON(200, map[string]interface{}{
"redirect": ctx.Repo.RepoLink + "/settings/branches",
})
}
// parseOwnerAndRepo get repos by owner
func parseOwnerAndRepo(ctx *context.Context) (*models.User, *models.Repository) {
owner, err := models.GetUserByName(ctx.Params(":username"))
if err != nil {