Skip to content
4 changes: 2 additions & 2 deletions routers/web/repo/editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func prepareEditorPageFormOptions(ctx *context.Context, editorAction string) *co
return nil
}

commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName)
commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName, ctx.Repo.TreePath)
if err != nil {
ctx.ServerError("PrepareCommitFormOptions", err)
return nil
Expand Down Expand Up @@ -121,7 +121,7 @@ func prepareEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *co
commonForm := form.GetCommitCommonForm()
commonForm.TreePath = files_service.CleanGitTreePath(commonForm.TreePath)

commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName)
commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName, commonForm.TreePath)
if err != nil {
ctx.ServerError("PrepareCommitFormOptions", err)
return nil
Expand Down
10 changes: 9 additions & 1 deletion services/context/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ type CommitFormOptions struct {
CanCreateBasePullRequest bool
}

func PrepareCommitFormOptions(ctx *Context, doer *user_model.User, targetRepo *repo_model.Repository, doerRepoPerm access_model.Permission, refName git.RefName) (*CommitFormOptions, error) {
func PrepareCommitFormOptions(ctx *Context, doer *user_model.User, targetRepo *repo_model.Repository, doerRepoPerm access_model.Permission, refName git.RefName, treePath string) (*CommitFormOptions, error) {
if !refName.IsBranch() {
// it shouldn't happen because middleware already checks
return nil, util.NewInvalidArgumentErrorf("ref %q is not a branch", refName)
Expand Down Expand Up @@ -173,6 +173,14 @@ func PrepareCommitFormOptions(ctx *Context, doer *user_model.User, targetRepo *r
protectedBranch.Repo = targetRepo
canPushWithProtection = protectedBranch.CanUserPush(ctx, doer)
protectionRequireSigned = protectedBranch.RequireSignedCommits
// If the user can't push branch-wide but the specific file being edited
// matches an unprotected file pattern, direct commit is still allowed.
if !canPushWithProtection && treePath != "" {
globUnprotected := protectedBranch.GetUnprotectedFilePatterns()
if protectedBranch.IsUnprotectedFile(globUnprotected, treePath) {
canPushWithProtection = true
}
}
}

targetGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, targetRepo)
Expand Down
21 changes: 21 additions & 0 deletions tests/integration/editor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,27 @@ func testEditorProtectedBranch(t *testing.T) {
resp := testEditorActionPostRequest(t, session, "/user2/repo1/_new/master/", map[string]string{"tree_path": "test-protected-branch.txt", "commit_choice": "direct"})
assert.Equal(t, http.StatusBadRequest, resp.Code)
assert.Equal(t, `Cannot commit to protected branch "master".`, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)

// Change "master" branch to mark files under "docs/" as unprotected
req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
"rule_name": "master",
"protected_file_patterns": "",
"unprotected_file_patterns": "docs/*.md",
"enable_push": "true",
})
session.MakeRequest(t, req, http.StatusSeeOther)
flashMsg = session.GetCookieFlashMessage()
assert.Equal(t, `Branch protection for rule "master" has been updated.`, flashMsg.SuccessMsg)

// Try to commit a non-matching file to the "master" branch and it should fail
resp = testEditorActionPostRequest(t, session, "/user2/repo1/_new/master/", map[string]string{"tree_path": "test-protected-branch.txt", "commit_choice": "direct"})
assert.Equal(t, http.StatusBadRequest, resp.Code)
assert.Equal(t, `Cannot commit to protected branch "master".`, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)

// Try to commit a file matching the unprotected pattern and it should succeed
resp = testEditorActionPostRequest(t, session, "/user2/repo1/_new/master/", map[string]string{"tree_path": "docs/new.md", "commit_choice": "direct"})
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), `"redirect":"/user2/repo1/src/branch/master/docs/new.md"`)
}

func testEditorActionPostRequest(t *testing.T, session *TestSession, requestPath string, params map[string]string) *httptest.ResponseRecorder {
Expand Down
Loading