Skip to content

Commit a603ff4

Browse files
authored
Merge branch 'main' into rb/disable-more-features-for-external-users
2 parents 3df7358 + 4696bcb commit a603ff4

File tree

224 files changed

+1375
-987
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

224 files changed

+1375
-987
lines changed

.eslintrc.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,10 @@ overrides:
5858
worker: true
5959
rules:
6060
no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, status, statusbar, stop, toolbar, top]
61-
- files: ["*.config.*"]
61+
- files: ["*.config.*", "**/*.d.ts"]
6262
rules:
6363
i/no-unused-modules: [0]
64-
- files: ["**/*.test.*", "web_src/js/test/setup.js"]
64+
- files: ["**/*.test.*", "web_src/js/test/setup.ts"]
6565
env:
6666
vitest-globals/env: true
6767
rules:
@@ -114,7 +114,7 @@ overrides:
114114
vitest/valid-describe-callback: [2]
115115
vitest/valid-expect: [2]
116116
vitest/valid-title: [2]
117-
- files: ["web_src/js/modules/fetch.js", "web_src/js/standalone/**/*"]
117+
- files: ["web_src/js/modules/fetch.ts", "web_src/js/standalone/**/*"]
118118
rules:
119119
no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement, SequenceExpression]
120120
- files: ["**/*.vue"]
@@ -467,7 +467,7 @@ rules:
467467
no-dupe-else-if: [2]
468468
no-dupe-keys: [2]
469469
no-duplicate-case: [2]
470-
no-duplicate-imports: [2]
470+
no-duplicate-imports: [0]
471471
no-else-return: [2]
472472
no-empty-character-class: [2]
473473
no-empty-function: [0]
@@ -619,7 +619,7 @@ rules:
619619
no-restricted-exports: [0]
620620
no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, location, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, self, status, statusbar, stop, toolbar, top, __dirname, __filename]
621621
no-restricted-imports: [0]
622-
no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement, SequenceExpression, {selector: "CallExpression[callee.name='fetch']", message: "use modules/fetch.js instead"}]
622+
no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement, SequenceExpression, {selector: "CallExpression[callee.name='fetch']", message: "use modules/fetch.ts instead"}]
623623
no-return-assign: [0]
624624
no-script-url: [2]
625625
no-self-assign: [2, {props: true}]

Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(FOMAN
144144
GO_DIRS := build cmd models modules routers services tests
145145
WEB_DIRS := web_src/js web_src/css
146146

147-
ESLINT_FILES := web_src/js tools *.js tests/e2e
147+
ESLINT_FILES := web_src/js tools *.js *.ts tests/e2e
148148
STYLELINT_FILES := web_src/css web_src/js/components/*.vue
149149
SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) docs/content templates options/locale/locale_en-US.ini .github $(filter-out CHANGELOG.md, $(wildcard *.go *.js *.md *.yml *.yaml *.toml))
150150
EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.ini
@@ -376,12 +376,12 @@ lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig
376376
.PHONY: lint-js
377377
lint-js: node_modules
378378
npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES)
379-
npx tsc
379+
# npx tsc
380380

381381
.PHONY: lint-js-fix
382382
lint-js-fix: node_modules
383383
npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) --fix
384-
npx tsc
384+
# npx tsc
385385

386386
.PHONY: lint-css
387387
lint-css: node_modules

models/git/protected_branch.go

Lines changed: 93 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ type ProtectedBranch struct {
4444
WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
4545
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
4646
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
47+
CanForcePush bool `xorm:"NOT NULL DEFAULT false"`
48+
EnableForcePushAllowlist bool `xorm:"NOT NULL DEFAULT false"`
49+
ForcePushAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
50+
ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
51+
ForcePushAllowlistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
4752
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"`
4853
StatusCheckContexts []string `xorm:"JSON TEXT"`
4954
EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"`
@@ -143,6 +148,33 @@ func (protectBranch *ProtectedBranch) CanUserPush(ctx context.Context, user *use
143148
return in
144149
}
145150

151+
// CanUserForcePush returns if some user could force push to this protected branch
152+
// Since force-push extends normal push, we also check if user has regular push access
153+
func (protectBranch *ProtectedBranch) CanUserForcePush(ctx context.Context, user *user_model.User) bool {
154+
if !protectBranch.CanForcePush {
155+
return false
156+
}
157+
158+
if !protectBranch.EnableForcePushAllowlist {
159+
return protectBranch.CanUserPush(ctx, user)
160+
}
161+
162+
if slices.Contains(protectBranch.ForcePushAllowlistUserIDs, user.ID) {
163+
return protectBranch.CanUserPush(ctx, user)
164+
}
165+
166+
if len(protectBranch.ForcePushAllowlistTeamIDs) == 0 {
167+
return false
168+
}
169+
170+
in, err := organization.IsUserInTeams(ctx, user.ID, protectBranch.ForcePushAllowlistTeamIDs)
171+
if err != nil {
172+
log.Error("IsUserInTeams: %v", err)
173+
return false
174+
}
175+
return in && protectBranch.CanUserPush(ctx, user)
176+
}
177+
146178
// IsUserMergeWhitelisted checks if some user is whitelisted to merge to this branch
147179
func IsUserMergeWhitelisted(ctx context.Context, protectBranch *ProtectedBranch, userID int64, permissionInRepo access_model.Permission) bool {
148180
if !protectBranch.EnableMergeWhitelist {
@@ -301,6 +333,9 @@ type WhitelistOptions struct {
301333
UserIDs []int64
302334
TeamIDs []int64
303335

336+
ForcePushUserIDs []int64
337+
ForcePushTeamIDs []int64
338+
304339
MergeUserIDs []int64
305340
MergeTeamIDs []int64
306341

@@ -328,6 +363,12 @@ func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, prote
328363
}
329364
protectBranch.WhitelistUserIDs = whitelist
330365

366+
whitelist, err = updateUserWhitelist(ctx, repo, protectBranch.ForcePushAllowlistUserIDs, opts.ForcePushUserIDs)
367+
if err != nil {
368+
return err
369+
}
370+
protectBranch.ForcePushAllowlistUserIDs = whitelist
371+
331372
whitelist, err = updateUserWhitelist(ctx, repo, protectBranch.MergeWhitelistUserIDs, opts.MergeUserIDs)
332373
if err != nil {
333374
return err
@@ -347,6 +388,12 @@ func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, prote
347388
}
348389
protectBranch.WhitelistTeamIDs = whitelist
349390

391+
whitelist, err = updateTeamWhitelist(ctx, repo, protectBranch.ForcePushAllowlistTeamIDs, opts.ForcePushTeamIDs)
392+
if err != nil {
393+
return err
394+
}
395+
protectBranch.ForcePushAllowlistTeamIDs = whitelist
396+
350397
whitelist, err = updateTeamWhitelist(ctx, repo, protectBranch.MergeWhitelistTeamIDs, opts.MergeTeamIDs)
351398
if err != nil {
352399
return err
@@ -468,43 +515,58 @@ func DeleteProtectedBranch(ctx context.Context, repo *repo_model.Repository, id
468515
return nil
469516
}
470517

471-
// RemoveUserIDFromProtectedBranch remove all user ids from protected branch options
472-
func RemoveUserIDFromProtectedBranch(ctx context.Context, p *ProtectedBranch, userID int64) error {
473-
lenIDs, lenApprovalIDs, lenMergeIDs := len(p.WhitelistUserIDs), len(p.ApprovalsWhitelistUserIDs), len(p.MergeWhitelistUserIDs)
474-
p.WhitelistUserIDs = util.SliceRemoveAll(p.WhitelistUserIDs, userID)
475-
p.ApprovalsWhitelistUserIDs = util.SliceRemoveAll(p.ApprovalsWhitelistUserIDs, userID)
476-
p.MergeWhitelistUserIDs = util.SliceRemoveAll(p.MergeWhitelistUserIDs, userID)
477-
478-
if lenIDs != len(p.WhitelistUserIDs) || lenApprovalIDs != len(p.ApprovalsWhitelistUserIDs) ||
479-
lenMergeIDs != len(p.MergeWhitelistUserIDs) {
480-
if _, err := db.GetEngine(ctx).ID(p.ID).Cols(
481-
"whitelist_user_i_ds",
482-
"merge_whitelist_user_i_ds",
483-
"approvals_whitelist_user_i_ds",
484-
).Update(p); err != nil {
518+
// removeIDsFromProtectedBranch is a helper function to remove IDs from protected branch options
519+
func removeIDsFromProtectedBranch(ctx context.Context, p *ProtectedBranch, userID, teamID int64, columnNames []string) error {
520+
lenUserIDs, lenForcePushIDs, lenApprovalIDs, lenMergeIDs := len(p.WhitelistUserIDs), len(p.ForcePushAllowlistUserIDs), len(p.ApprovalsWhitelistUserIDs), len(p.MergeWhitelistUserIDs)
521+
lenTeamIDs, lenForcePushTeamIDs, lenApprovalTeamIDs, lenMergeTeamIDs := len(p.WhitelistTeamIDs), len(p.ForcePushAllowlistTeamIDs), len(p.ApprovalsWhitelistTeamIDs), len(p.MergeWhitelistTeamIDs)
522+
523+
if userID > 0 {
524+
p.WhitelistUserIDs = util.SliceRemoveAll(p.WhitelistUserIDs, userID)
525+
p.ForcePushAllowlistUserIDs = util.SliceRemoveAll(p.ForcePushAllowlistUserIDs, userID)
526+
p.ApprovalsWhitelistUserIDs = util.SliceRemoveAll(p.ApprovalsWhitelistUserIDs, userID)
527+
p.MergeWhitelistUserIDs = util.SliceRemoveAll(p.MergeWhitelistUserIDs, userID)
528+
}
529+
530+
if teamID > 0 {
531+
p.WhitelistTeamIDs = util.SliceRemoveAll(p.WhitelistTeamIDs, teamID)
532+
p.ForcePushAllowlistTeamIDs = util.SliceRemoveAll(p.ForcePushAllowlistTeamIDs, teamID)
533+
p.ApprovalsWhitelistTeamIDs = util.SliceRemoveAll(p.ApprovalsWhitelistTeamIDs, teamID)
534+
p.MergeWhitelistTeamIDs = util.SliceRemoveAll(p.MergeWhitelistTeamIDs, teamID)
535+
}
536+
537+
if (lenUserIDs != len(p.WhitelistUserIDs) ||
538+
lenForcePushIDs != len(p.ForcePushAllowlistUserIDs) ||
539+
lenApprovalIDs != len(p.ApprovalsWhitelistUserIDs) ||
540+
lenMergeIDs != len(p.MergeWhitelistUserIDs)) ||
541+
(lenTeamIDs != len(p.WhitelistTeamIDs) ||
542+
lenForcePushTeamIDs != len(p.ForcePushAllowlistTeamIDs) ||
543+
lenApprovalTeamIDs != len(p.ApprovalsWhitelistTeamIDs) ||
544+
lenMergeTeamIDs != len(p.MergeWhitelistTeamIDs)) {
545+
if _, err := db.GetEngine(ctx).ID(p.ID).Cols(columnNames...).Update(p); err != nil {
485546
return fmt.Errorf("updateProtectedBranches: %v", err)
486547
}
487548
}
488549
return nil
489550
}
490551

491-
// RemoveTeamIDFromProtectedBranch remove all team ids from protected branch options
552+
// RemoveUserIDFromProtectedBranch removes all user ids from protected branch options
553+
func RemoveUserIDFromProtectedBranch(ctx context.Context, p *ProtectedBranch, userID int64) error {
554+
columnNames := []string{
555+
"whitelist_user_i_ds",
556+
"force_push_allowlist_user_i_ds",
557+
"merge_whitelist_user_i_ds",
558+
"approvals_whitelist_user_i_ds",
559+
}
560+
return removeIDsFromProtectedBranch(ctx, p, userID, 0, columnNames)
561+
}
562+
563+
// RemoveTeamIDFromProtectedBranch removes all team ids from protected branch options
492564
func RemoveTeamIDFromProtectedBranch(ctx context.Context, p *ProtectedBranch, teamID int64) error {
493-
lenIDs, lenApprovalIDs, lenMergeIDs := len(p.WhitelistTeamIDs), len(p.ApprovalsWhitelistTeamIDs), len(p.MergeWhitelistTeamIDs)
494-
p.WhitelistTeamIDs = util.SliceRemoveAll(p.WhitelistTeamIDs, teamID)
495-
p.ApprovalsWhitelistTeamIDs = util.SliceRemoveAll(p.ApprovalsWhitelistTeamIDs, teamID)
496-
p.MergeWhitelistTeamIDs = util.SliceRemoveAll(p.MergeWhitelistTeamIDs, teamID)
497-
498-
if lenIDs != len(p.WhitelistTeamIDs) ||
499-
lenApprovalIDs != len(p.ApprovalsWhitelistTeamIDs) ||
500-
lenMergeIDs != len(p.MergeWhitelistTeamIDs) {
501-
if _, err := db.GetEngine(ctx).ID(p.ID).Cols(
502-
"whitelist_team_i_ds",
503-
"merge_whitelist_team_i_ds",
504-
"approvals_whitelist_team_i_ds",
505-
).Update(p); err != nil {
506-
return fmt.Errorf("updateProtectedBranches: %v", err)
507-
}
565+
columnNames := []string{
566+
"whitelist_team_i_ds",
567+
"force_push_allowlist_team_i_ds",
568+
"merge_whitelist_team_i_ds",
569+
"approvals_whitelist_team_i_ds",
508570
}
509-
return nil
571+
return removeIDsFromProtectedBranch(ctx, p, 0, teamID, columnNames)
510572
}

models/migrations/migrations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,8 @@ var migrations = []Migration{
591591

592592
// v299 -> v300
593593
NewMigration("Add content version to issue and comment table", v1_23.AddContentVersionToIssueAndComment),
594+
// v300 -> v301
595+
NewMigration("Add force-push branch protection support", v1_23.AddForcePushBranchProtection),
594596
}
595597

596598
// GetCurrentDBVersion returns the current db version

models/migrations/v1_23/v300.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package v1_23 //nolint
5+
6+
import "xorm.io/xorm"
7+
8+
func AddForcePushBranchProtection(x *xorm.Engine) error {
9+
type ProtectedBranch struct {
10+
CanForcePush bool `xorm:"NOT NULL DEFAULT false"`
11+
EnableForcePushAllowlist bool `xorm:"NOT NULL DEFAULT false"`
12+
ForcePushAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
13+
ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
14+
ForcePushAllowlistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
15+
}
16+
return x.Sync(new(ProtectedBranch))
17+
}

models/user/user.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"context"
99
"encoding/hex"
1010
"fmt"
11+
"mime"
12+
"net/mail"
1113
"net/url"
1214
"path/filepath"
1315
"regexp"
@@ -413,6 +415,34 @@ func (u *User) DisplayName() string {
413415
return u.Name
414416
}
415417

418+
var emailToReplacer = strings.NewReplacer(
419+
"\n", "",
420+
"\r", "",
421+
"<", "",
422+
">", "",
423+
",", "",
424+
":", "",
425+
";", "",
426+
)
427+
428+
// EmailTo returns a string suitable to be put into a e-mail `To:` header.
429+
func (u *User) EmailTo() string {
430+
sanitizedDisplayName := emailToReplacer.Replace(u.DisplayName())
431+
432+
// should be an edge case but nice to have
433+
if sanitizedDisplayName == u.Email {
434+
return u.Email
435+
}
436+
437+
to := fmt.Sprintf("%s <%s>", sanitizedDisplayName, u.Email)
438+
add, err := mail.ParseAddress(to)
439+
if err != nil {
440+
return u.Email
441+
}
442+
443+
return fmt.Sprintf("%s <%s>", mime.QEncoding.Encode("utf-8", add.Name), add.Address)
444+
}
445+
416446
// GetDisplayName returns full name if it's not empty and DEFAULT_SHOW_FULL_NAME is set,
417447
// returns username otherwise.
418448
func (u *User) GetDisplayName() string {

models/user/user_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,29 @@ func Test_NormalizeUserFromEmail(t *testing.T) {
529529
}
530530
}
531531

532+
func TestEmailTo(t *testing.T) {
533+
testCases := []struct {
534+
fullName string
535+
mail string
536+
result string
537+
}{
538+
{"Awareness Hub", "[email protected]", "Awareness Hub <[email protected]>"},
539+
540+
{"Hi Its <Mee>", "[email protected]", "Hi Its Mee <[email protected]>"},
541+
{"Sinéad.O'Connor", "[email protected]", "=?utf-8?q?Sin=C3=A9ad.O'Connor?= <[email protected]>"},
542+
{"Æsir", "[email protected]", "=?utf-8?q?=C3=86sir?= <[email protected]>"},
543+
{"new😀user", "[email protected]", "=?utf-8?q?new=F0=9F=98=80user?= <[email protected]>"},
544+
{`"quoted"`, "[email protected]", "quoted <[email protected]>"},
545+
}
546+
547+
for _, testCase := range testCases {
548+
t.Run(testCase.result, func(t *testing.T) {
549+
testUser := &user_model.User{FullName: testCase.fullName, Email: testCase.mail}
550+
assert.EqualValues(t, testCase.result, testUser.EmailTo())
551+
})
552+
}
553+
}
554+
532555
func TestDisabledUserFeatures(t *testing.T) {
533556
assert.NoError(t, unittest.PrepareTestDatabase())
534557

modules/structs/repo_branch.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ type BranchProtection struct {
3030
PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
3131
PushWhitelistTeams []string `json:"push_whitelist_teams"`
3232
PushWhitelistDeployKeys bool `json:"push_whitelist_deploy_keys"`
33+
EnableForcePush bool `json:"enable_force_push"`
34+
EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"`
35+
ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"`
36+
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
37+
ForcePushAllowlistDeployKeys bool `json:"force_push_allowlist_deploy_keys"`
3338
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
3439
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
3540
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
@@ -63,6 +68,11 @@ type CreateBranchProtectionOption struct {
6368
PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
6469
PushWhitelistTeams []string `json:"push_whitelist_teams"`
6570
PushWhitelistDeployKeys bool `json:"push_whitelist_deploy_keys"`
71+
EnableForcePush bool `json:"enable_force_push"`
72+
EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"`
73+
ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"`
74+
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
75+
ForcePushAllowlistDeployKeys bool `json:"force_push_allowlist_deploy_keys"`
6676
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
6777
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
6878
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
@@ -89,6 +99,11 @@ type EditBranchProtectionOption struct {
8999
PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
90100
PushWhitelistTeams []string `json:"push_whitelist_teams"`
91101
PushWhitelistDeployKeys *bool `json:"push_whitelist_deploy_keys"`
102+
EnableForcePush *bool `json:"enable_force_push"`
103+
EnableForcePushAllowlist *bool `json:"enable_force_push_allowlist"`
104+
ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"`
105+
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
106+
ForcePushAllowlistDeployKeys *bool `json:"force_push_allowlist_deploy_keys"`
92107
EnableMergeWhitelist *bool `json:"enable_merge_whitelist"`
93108
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
94109
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`

0 commit comments

Comments
 (0)