Skip to content

Commit 58597cc

Browse files
GiteaBotbircniclaude
authored
fix: Allow direct commits for unprotected files with push restrictions (#37657) (#37756)
Backport #37657 by @bircni Fixes an issue where users could not commit changes on a file which is unprotected. Fixes #37655 Co-authored-by: Nicolas <bircni@icloud.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 86cc3e8 commit 58597cc

2 files changed

Lines changed: 46 additions & 0 deletions

File tree

services/context/repo.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,16 @@ func PrepareCommitFormOptions(ctx *Context, doer *user_model.User, targetRepo *r
173173
protectedBranch.Repo = targetRepo
174174
canPushWithProtection = protectedBranch.CanUserPush(ctx, doer)
175175
protectionRequireSigned = protectedBranch.RequireSignedCommits
176+
// If branch-wide push is restricted, allow direct commit when the
177+
// URL-derived tree path matches an unprotected file pattern. The
178+
// pre-receive hook re-checks every path the commit actually touches
179+
// (e.g. rename source and destination).
180+
if !canPushWithProtection && ctx.Repo.TreePath != "" && protectedBranch.UnprotectedFilePatterns != "" {
181+
globs := protectedBranch.GetUnprotectedFilePatterns()
182+
if protectedBranch.IsUnprotectedFile(globs, ctx.Repo.TreePath) {
183+
canPushWithProtection = true
184+
}
185+
}
176186
}
177187

178188
targetGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, targetRepo)

tests/integration/editor_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,42 @@ func testEditorProtectedBranch(t *testing.T) {
9898
resp := testEditorActionPostRequest(t, session, "/user2/repo1/_new/master/", map[string]string{"tree_path": "test-protected-branch.txt", "commit_choice": "direct"})
9999
assert.Equal(t, http.StatusBadRequest, resp.Code)
100100
assert.Equal(t, `Cannot commit to protected branch "master".`, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)
101+
102+
// Change "master" branch to mark files under "docs/" as unprotected
103+
req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
104+
"rule_name": "master",
105+
"protected_file_patterns": "",
106+
"unprotected_file_patterns": "docs/*.md",
107+
"enable_push": "true",
108+
})
109+
session.MakeRequest(t, req, http.StatusSeeOther)
110+
flashMsg = session.GetCookieFlashMessage()
111+
assert.Equal(t, `Branch protection for rule "master" has been updated.`, flashMsg.SuccessMsg)
112+
113+
resp = testEditorActionPostRequest(t, session, "/user2/repo1/_new/master/docs/new.md", map[string]string{"tree_path": "docs/new.md", "commit_choice": "direct"})
114+
assert.Equal(t, http.StatusOK, resp.Code)
115+
assert.Contains(t, resp.Body.String(), `"redirect":"/user2/repo1/src/branch/master/docs/new.md"`)
116+
117+
// Form's destination (renamed.md) is decided by the pre-receive hook, not the controller.
118+
resp = testEditorActionPostRequest(t, session, "/user2/repo1/_edit/master/docs/new.md", map[string]string{
119+
"content": "renamed via editor",
120+
"commit_choice": "direct",
121+
"tree_path": "docs/renamed.md",
122+
})
123+
assert.Equal(t, http.StatusOK, resp.Code)
124+
assert.Contains(t, resp.Body.String(), `"redirect":"/user2/repo1/src/branch/master/docs/renamed.md"`)
125+
126+
// Protected source path: controller rejects up-front regardless of unprotected destination.
127+
resp = testEditorActionPostRequest(t, session, "/user2/repo1/_edit/master/README.md", map[string]string{
128+
"commit_choice": "direct",
129+
"tree_path": "docs/from-readme.md",
130+
})
131+
assert.Equal(t, http.StatusBadRequest, resp.Code)
132+
assert.Equal(t, `Cannot commit to protected branch "master".`, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)
133+
134+
resp = testEditorActionPostRequest(t, session, "/user2/repo1/_delete/master/README.md", map[string]string{"commit_choice": "direct"})
135+
assert.Equal(t, http.StatusBadRequest, resp.Code)
136+
assert.Equal(t, `Cannot commit to protected branch "master".`, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)
101137
}
102138

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

0 commit comments

Comments
 (0)