mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-21 09:06:46 +09:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
829b49c1ae | ||
|
|
58031cdc97 | ||
|
|
1eee73e9bd | ||
|
|
dc30660291 | ||
|
|
6bc0e8cd57 | ||
|
|
cfcbdf41e2 | ||
|
|
6195cda58f | ||
|
|
5251cd572f | ||
|
|
ce8912c8ff | ||
|
|
0e2a188d8e | ||
|
|
95d6b572fe | ||
|
|
c4e9d96162 | ||
|
|
96cb6cd16f | ||
|
|
02b18a0c8e | ||
|
|
1ffe46d2a4 | ||
|
|
26f537dea6 | ||
|
|
74e7e813c3 | ||
|
|
3f0aa7a01e | ||
|
|
7e1e32b42e | ||
|
|
63669003eb | ||
|
|
f5fd936cbb | ||
|
|
d87bbd3dd4 | ||
|
|
7a03ec3a4c | ||
|
|
0132cb82c2 | ||
|
|
33aaf0cfa3 | ||
|
|
6064554b8d | ||
|
|
d29364a4f6 | ||
|
|
998a06627e | ||
|
|
250d18d5f6 | ||
|
|
c26daf578f | ||
|
|
5371e788fe | ||
|
|
0001e58e6b | ||
|
|
79c4237cb2 | ||
|
|
20f73b2230 | ||
|
|
175bc89bf9 | ||
|
|
793e77b073 | ||
|
|
5018ae1b29 | ||
|
|
c60b13b2dd | ||
|
|
27bd022b34 |
No files matched your search
@@ -56,7 +56,7 @@ jobs:
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
steps:
|
||||
- uses: go-gitea/giteabot@f48c6a15e0d384f037aea19cc05ff5e9551096b9 # v1.1.0
|
||||
- uses: go-gitea/giteabot@8033358e118c0bd97f5e9902dc5d6633eadc6aeb # v1.2.1
|
||||
with:
|
||||
github_token: ${{ secrets.GITEABOT_TOKEN }}
|
||||
checks: ${{ github.event.inputs.checks || 'labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }}
|
||||
@@ -37,8 +37,15 @@ jobs:
|
||||
# retry snapcraft uploads which can be flaky
|
||||
- name: Publish snap
|
||||
run: |
|
||||
# part-gitea-pull.sh decides what is built, see the comments there:
|
||||
# * grade devel (main or a prerelease tag) goes to latest/edge
|
||||
# * grade stable (a new release tag) goes to latest/stable, and to latest/candidate which marks it as released
|
||||
channel=latest/edge
|
||||
if unsquashfs -cat "$SNAP" meta/snap.yaml | grep -qx 'grade: stable'; then
|
||||
channel=latest/stable,latest/candidate
|
||||
fi
|
||||
for attempt in 1 2 3 4 5; do
|
||||
snapcraft upload "$SNAP" --release latest/edge && exit 0
|
||||
snapcraft upload "$SNAP" --release "$channel" && exit 0
|
||||
echo "::warning::snap upload attempt $attempt failed, retrying in 15s"
|
||||
sleep 15
|
||||
done
|
||||
|
||||
@@ -76,6 +76,10 @@ jobs:
|
||||
run: |
|
||||
previous=$(git tag --list --sort=-v:refname | grep -xE 'v[0-9]+\.[0-9]+\.[0-9]+' | grep -A1 -xF "$GITHUB_REF_NAME" | tail -1) # highest stable version below this one
|
||||
echo "range=$previous..$GITHUB_SHA" >> "$GITHUB_OUTPUT"
|
||||
- name: skip reverts and reverted commits in changelog
|
||||
env:
|
||||
RANGE: ${{ steps.range.outputs.range }}
|
||||
run: ./tools/generate-cliffignore.sh "$RANGE" | sort -u > .cliffignore
|
||||
- uses: orhun/git-cliff-action@a9a95522b26fe6403f7bb24031f21fb573d0f5ff # v4.9.1
|
||||
with:
|
||||
args: --tag ${{ github.ref_name }} ${{ steps.range.outputs.range }}
|
||||
|
||||
@@ -6,7 +6,7 @@ SHASUM ?= shasum -a 256
|
||||
|
||||
AIR_PACKAGE ?= github.com/air-verse/air@v1.67.4 # renovate: datasource=go
|
||||
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v4/cmd/editorconfig-checker@v4.0.2 # renovate: datasource=go
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.2 # renovate: datasource=go
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.14.0 # renovate: datasource=go
|
||||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.17 # renovate: datasource=go
|
||||
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 # renovate: datasource=go
|
||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.36.6 # renovate: datasource=go
|
||||
|
||||
@@ -638,7 +638,8 @@ ENABLED = true
|
||||
;; * https://github.com/git-ecosystem/git-credential-manager
|
||||
;; * https://gitea.com/gitea/tea
|
||||
;; * Gitea App (the official Gitea mobile app)
|
||||
;DEFAULT_APPLICATIONS = git-credential-oauth, git-credential-manager, tea, gitea-app
|
||||
;; * https://gitea.com/gitea/gitea-vscode
|
||||
;DEFAULT_APPLICATIONS = git-credential-oauth, git-credential-manager, tea, gitea-app, gitea-vscode
|
||||
;;
|
||||
;; By default, OAuth2 applications can only use "http" and "https" as their redirect URI schemes.
|
||||
;; If you need to use other schemes (e.g. for desktop applications), you can specify them here as a comma-separated list.
|
||||
@@ -810,7 +811,6 @@ LEVEL = Info
|
||||
;[git.config]
|
||||
;diff.algorithm = histogram
|
||||
;core.logAllRefUpdates = true
|
||||
;gc.reflogExpire = 90
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -1098,8 +1098,9 @@ LEVEL = Info
|
||||
;; Force ssh:// clone url instead of scp-style uri when default SSH port is used
|
||||
;USE_COMPAT_SSH_URI = false
|
||||
;;
|
||||
;; Value for the "go get" request returns the repository url as https or ssh, default is https
|
||||
;GO_GET_CLONE_URL_PROTOCOL = https
|
||||
;; Scheme of the returned URL for the "go get" response.
|
||||
;; Default is "https" if DISABLE_HTTP_GIT=false or SSH is disabled, otherwise "ssh".
|
||||
;GO_GET_CLONE_URL_PROTOCOL =
|
||||
;;
|
||||
;; Close issues as long as a commit on any branch marks it as fixed
|
||||
;DEFAULT_CLOSE_ISSUES_VIA_COMMITS_IN_ANY_BRANCH = false
|
||||
|
||||
@@ -2,7 +2,7 @@ module gitea.dev
|
||||
|
||||
go 1.27
|
||||
|
||||
toolchain go1.27.1
|
||||
toolchain go1.27.2
|
||||
|
||||
require (
|
||||
connectrpc.com/connect v1.21.0
|
||||
@@ -94,11 +94,11 @@ require (
|
||||
github.com/yuin/goldmark v1.8.6
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
gitlab.com/gitlab-org/api/client-go/v3 v3.13.0
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6.0.20261013184558-04dd63131525
|
||||
golang.org/x/crypto v0.57.0
|
||||
golang.org/x/image v0.46.0
|
||||
golang.org/x/mod v0.41.0
|
||||
golang.org/x/net v0.59.0
|
||||
golang.org/x/net v0.60.0
|
||||
golang.org/x/oauth2 v0.37.0
|
||||
golang.org/x/sync v0.23.0
|
||||
golang.org/x/sys v0.48.0
|
||||
|
||||
@@ -672,8 +672,8 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6.0.20261013184558-04dd63131525 h1:FtzHH3s777Nx50YcPAcodAPm1mNakc228nMYbr3f+90=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6.0.20261013184558-04dd63131525/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
@@ -714,8 +714,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
|
||||
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
|
||||
golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
||||
+20
-11
@@ -853,25 +853,34 @@ func cancelReusableCaller(ctx context.Context, caller *ActionRunJob, force bool)
|
||||
}
|
||||
|
||||
// Cancel descendants deepest-first, then the caller: a caller's status is aggregated from its children,
|
||||
// so each child must reach its final state before its parent caller is re-aggregated.
|
||||
// so each child must be cancelled before its parent caller is re-read.
|
||||
// A child's ID always exceeds its parent's, so descending ID is a valid deepest-first order.
|
||||
descendants := CollectAllDescendantJobs(caller, attemptJobs)
|
||||
slices.SortFunc(descendants, func(a, b *ActionRunJob) int { return cmp.Compare(b.ID, a.ID) })
|
||||
callersWithChildren := make(container.Set[int64])
|
||||
for _, d := range descendants {
|
||||
callersWithChildren.Add(d.ParentJobID)
|
||||
}
|
||||
|
||||
for _, c := range descendants {
|
||||
cancelled, err := cancelOneJob(ctx, c, force)
|
||||
for _, job := range append(descendants, caller) {
|
||||
if !callersWithChildren.Contains(job.ID) {
|
||||
cancelled, err := cancelOneJob(ctx, job, force)
|
||||
if err != nil {
|
||||
return cancelledJobs, err
|
||||
}
|
||||
if cancelled != nil {
|
||||
cancelledJobs = append(cancelledJobs, cancelled)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// the job is a caller and its children's cascade already re-aggregated it
|
||||
reloaded, err := GetRunJobByRunAndID(ctx, job.RunID, job.ID)
|
||||
if err != nil {
|
||||
return cancelledJobs, err
|
||||
}
|
||||
if cancelled != nil {
|
||||
cancelledJobs = append(cancelledJobs, cancelled)
|
||||
if reloaded.Status != job.Status {
|
||||
cancelledJobs = append(cancelledJobs, reloaded)
|
||||
}
|
||||
}
|
||||
|
||||
if c, err := cancelOneJob(ctx, caller, force); err != nil {
|
||||
return cancelledJobs, err
|
||||
} else if c != nil {
|
||||
cancelledJobs = append(cancelledJobs, c)
|
||||
}
|
||||
return cancelledJobs, nil
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
runnerv1 "gitea.dev/actionslib/runner/v1"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/timeutil"
|
||||
@@ -397,3 +398,72 @@ func TestForceCancelJobs(t *testing.T) {
|
||||
assert.Equal(t, StatusCancelled, callerAfter.Status)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCancelJobs_CallerWaitsForCancellingChild(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
ctx := t.Context()
|
||||
|
||||
run := &ActionRun{
|
||||
Title: "caller-cancelling-child",
|
||||
RepoID: 4,
|
||||
Index: 9811,
|
||||
OwnerID: 1,
|
||||
WorkflowID: "test.yaml",
|
||||
TriggerUserID: 1,
|
||||
Ref: "refs/heads/master",
|
||||
CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0",
|
||||
Event: "push",
|
||||
TriggerEvent: "push",
|
||||
EventPayload: "{}",
|
||||
Status: StatusRunning,
|
||||
}
|
||||
require.NoError(t, db.Insert(ctx, run))
|
||||
attempt := &ActionRunAttempt{RepoID: run.RepoID, RunID: run.ID, Attempt: 1, TriggerUserID: 1, Status: StatusRunning}
|
||||
require.NoError(t, db.Insert(ctx, attempt))
|
||||
run.LatestAttemptID = attempt.ID
|
||||
require.NoError(t, UpdateRun(ctx, run, "latest_attempt_id"))
|
||||
|
||||
newJob := func(name string, parentID int64, isCaller bool) *ActionRunJob {
|
||||
job := &ActionRunJob{
|
||||
RunID: run.ID,
|
||||
RunAttemptID: attempt.ID,
|
||||
RepoID: run.RepoID,
|
||||
OwnerID: run.OwnerID,
|
||||
CommitSHA: run.CommitSHA,
|
||||
Name: name,
|
||||
JobID: name,
|
||||
Attempt: 1,
|
||||
Status: StatusRunning,
|
||||
ParentJobID: parentID,
|
||||
IsReusableCaller: isCaller,
|
||||
IsExpanded: isCaller,
|
||||
}
|
||||
require.NoError(t, db.Insert(ctx, job))
|
||||
return job
|
||||
}
|
||||
outer := newJob("outer", 0, true)
|
||||
inner := newJob("inner", outer.ID, true)
|
||||
child := newJob("child", inner.ID, false)
|
||||
|
||||
runner := &ActionRunner{UUID: "caller-cancelling-child", Name: "caller-cancelling-child", HasCancellingSupport: true}
|
||||
require.NoError(t, db.Insert(ctx, runner))
|
||||
task := &ActionTask{JobID: child.ID, Attempt: 1, RunnerID: runner.ID, Status: StatusRunning, Started: timeutil.TimeStampNow(), RepoID: run.RepoID, OwnerID: run.OwnerID, CommitSHA: run.CommitSHA}
|
||||
require.NoError(t, db.Insert(ctx, task))
|
||||
child.TaskID = task.ID
|
||||
_, err := UpdateRunJob(ctx, child, nil, "task_id")
|
||||
require.NoError(t, err)
|
||||
|
||||
cancelled, err := CancelJobs(ctx, []*ActionRunJob{outer}, false)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, cancelled, 3)
|
||||
for _, job := range []*ActionRunJob{outer, inner, child} {
|
||||
assert.Equal(t, StatusCancelling, unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: job.ID}).Status, job.Name)
|
||||
}
|
||||
|
||||
_, err = UpdateTaskByState(ctx, runner.ID, &runnerv1.TaskState{Id: task.ID, Result: runnerv1.Result_RESULT_CANCELLED})
|
||||
require.NoError(t, err)
|
||||
for _, job := range []*ActionRunJob{outer, inner, child} {
|
||||
assert.Equal(t, StatusCancelled, unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: job.ID}).Status, job.Name)
|
||||
}
|
||||
assert.Equal(t, StatusCancelled, unittest.AssertExistsAndLoadBean(t, &ActionRun{ID: run.ID}).Status)
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/util"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
@@ -110,7 +111,7 @@ func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) ([
|
||||
jobs, err := CancelPreviousJobs(
|
||||
ctx,
|
||||
repo.ID,
|
||||
repo.DefaultBranch,
|
||||
git.RefNameFromBranch(repo.DefaultBranch).String(),
|
||||
"",
|
||||
webhook_module.HookEventSchedule,
|
||||
)
|
||||
|
||||
@@ -236,13 +236,13 @@ func GetRunningTaskByToken(ctx context.Context, token string) (*ActionTask, erro
|
||||
|
||||
func makeTaskStepDisplayName(step *jobparser.Step, limit int) (name string) {
|
||||
if step.Name != "" {
|
||||
name = string(step.Name) // the step has an explicit name
|
||||
name = step.Name // the step has an explicit name
|
||||
} else {
|
||||
// for unnamed step, its "String()" method tries to get a display name by its "name", "uses",
|
||||
// "run" or "id" (last fallback), we add the "Run " prefix for unnamed steps for better display
|
||||
// for multi-line "run" scripts, only use the first line to match GitHub's behavior
|
||||
// https://github.com/actions/runner/blob/66800900843747f37591b077091dd2c8cf2c1796/src/Runner.Worker/Handlers/ScriptHandler.cs#L45-L58
|
||||
runStr, _, _ := strings.Cut(strings.TrimSpace(string(step.Run)), "\n")
|
||||
runStr, _, _ := strings.Cut(strings.TrimSpace(step.Run), "\n")
|
||||
name = "Run " + util.IfZero(strings.TrimSpace(runStr), step.String())
|
||||
}
|
||||
return util.EllipsisDisplayString(name, limit) // database column has a length limit
|
||||
|
||||
@@ -94,14 +94,14 @@ func TestMakeTaskStepDisplayName(t *testing.T) {
|
||||
{
|
||||
name: "very long name truncated",
|
||||
jobStep: &jobparser.Step{
|
||||
Name: jobparser.BlockSafeString(strings.Repeat("a", 300)),
|
||||
Name: strings.Repeat("a", 300),
|
||||
},
|
||||
expected: strings.Repeat("a", 252) + "…",
|
||||
},
|
||||
{
|
||||
name: "very long run truncated",
|
||||
jobStep: &jobparser.Step{
|
||||
Run: jobparser.BlockSafeString(strings.Repeat("a", 300)),
|
||||
Run: strings.Repeat("a", 300),
|
||||
},
|
||||
expected: "Run " + strings.Repeat("a", 248) + "…",
|
||||
},
|
||||
|
||||
@@ -199,7 +199,7 @@ func SearchPrincipalKey(ctx context.Context, principalKey string) (*PublicKey, e
|
||||
// In the future, the existing principal keys should be migrated to use the fingerprint ("principal:{name}") instead of the content
|
||||
key := new(PublicKey)
|
||||
has, err := db.GetEngine(ctx).
|
||||
Where("content = ?", principalKey).
|
||||
Where("content = ? AND type = ?", principalKey, KeyTypePrincipal).
|
||||
Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -12,11 +12,13 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/42wim/sshsig"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_SSHParsePublicKey(t *testing.T) {
|
||||
@@ -481,6 +483,20 @@ func TestPublicKeysAreExternallyManaged(t *testing.T) {
|
||||
assert.False(t, externals[0])
|
||||
}
|
||||
|
||||
func TestSearchPrincipalKeyIgnoresOtherKeyTypes(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
userKey := unittest.AssertExistsAndLoadBean(t, &PublicKey{ID: 1})
|
||||
|
||||
_, err := SearchPrincipalKey(t.Context(), userKey.Content)
|
||||
assert.True(t, IsErrKeyNotExist(err))
|
||||
|
||||
principal := &PublicKey{OwnerID: 1, Name: userKey.Content, Content: userKey.Content, Type: KeyTypePrincipal}
|
||||
require.NoError(t, db.Insert(t.Context(), principal))
|
||||
key, err := SearchPrincipalKey(t.Context(), userKey.Content)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, principal.ID, key.ID)
|
||||
}
|
||||
|
||||
// TestCheckPublicKeyStringOversized tests if oversized SSH2 public key strings are rejected before triggering costly operations.
|
||||
func TestCheckPublicKeyStringOversized(t *testing.T) {
|
||||
_, err := parseKeyString(strings.Repeat("a", maxKeyContentBytes+1))
|
||||
|
||||
@@ -44,9 +44,10 @@ func GetAuthTokenByID(ctx context.Context, id string) (*AuthToken, error) {
|
||||
return at, nil
|
||||
}
|
||||
|
||||
func UpdateAuthTokenByID(ctx context.Context, t *AuthToken) error {
|
||||
_, err := db.GetEngine(ctx).ID(t.ID).Cols("token_hash", "expires_unix").Update(t)
|
||||
return err
|
||||
func UpdateAuthTokenByID(ctx context.Context, t *AuthToken, oldHash string) (bool, error) {
|
||||
affected, err := db.GetEngine(ctx).ID(t.ID).Where("token_hash = ?", oldHash).
|
||||
Cols("token_hash", "expires_unix").Update(t)
|
||||
return affected == 1, err
|
||||
}
|
||||
|
||||
func DeleteAuthTokenByID(ctx context.Context, id string) error {
|
||||
|
||||
@@ -88,6 +88,16 @@ func BuiltinApplications() map[string]*BuiltinOAuth2Application {
|
||||
DisplayName: "Gitea App",
|
||||
RedirectURIs: []string{"com.gitea.app://oauth/callback"},
|
||||
}
|
||||
m["75a9ec3e-42b5-40cb-9144-9ea96ecd56a6"] = &BuiltinOAuth2Application{
|
||||
ConfigName: "gitea-vscode",
|
||||
DisplayName: "gitea-vscode",
|
||||
RedirectURIs: []string{
|
||||
"http://127.0.0.1",
|
||||
"https://127.0.0.1",
|
||||
"vscode://gitea.gitea-for-vscode/auth/callback",
|
||||
"vscode-insiders://gitea.gitea-for-vscode/auth/callback",
|
||||
},
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
|
||||
@@ -99,8 +99,8 @@ func (protectBranch *ProtectedBranch) loadGlob() {
|
||||
var err error
|
||||
protectBranch.globRule, err = glob.Compile(protectBranch.RuleName, '/')
|
||||
if err != nil {
|
||||
log.Warn("Invalid glob rule for ProtectedBranch[%d]: %s %v", protectBranch.ID, protectBranch.RuleName, err)
|
||||
protectBranch.globRule = glob.MustCompile(glob.QuoteMeta(protectBranch.RuleName), '/')
|
||||
log.Debug("Invalid glob rule for ProtectedBranch[%d]: %s %v", protectBranch.ID, protectBranch.RuleName, err)
|
||||
protectBranch.globRule, _ = glob.Compile(glob.QuoteMeta(protectBranch.RuleName), '/')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,9 @@ func (protectBranch *ProtectedBranch) Match(branchName string) bool {
|
||||
if protectBranch.isPlainName {
|
||||
return strings.EqualFold(protectBranch.RuleName, branchName)
|
||||
}
|
||||
|
||||
if protectBranch.globRule == nil {
|
||||
return false // in case of invalid glob rule, we don't match anything
|
||||
}
|
||||
return protectBranch.globRule.Match(branchName)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/glob"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
)
|
||||
|
||||
@@ -55,10 +56,17 @@ func FindRepoProtectedBranchRules(ctx context.Context, repoID int64) (ProtectedB
|
||||
// FindAllMatchedBranches find all matched branches
|
||||
func FindAllMatchedBranches(ctx context.Context, repoID int64, ruleName string) ([]string, error) {
|
||||
results := make([]string, 0, 10)
|
||||
rule, err := glob.Compile(ruleName)
|
||||
if err != nil {
|
||||
log.Debug("Failed to compile rule %s: %v", ruleName, err)
|
||||
return results, nil
|
||||
}
|
||||
|
||||
const pageSize = 100
|
||||
for page := 1; ; page++ {
|
||||
brancheNames, err := FindBranchNames(ctx, FindBranchOptions{
|
||||
branchNames, err := FindBranchNames(ctx, FindBranchOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
PageSize: 100,
|
||||
PageSize: pageSize,
|
||||
Page: page,
|
||||
},
|
||||
RepoID: repoID,
|
||||
@@ -67,14 +75,12 @@ func FindAllMatchedBranches(ctx context.Context, repoID int64, ruleName string)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rule := glob.MustCompile(ruleName)
|
||||
|
||||
for _, branch := range brancheNames {
|
||||
for _, branch := range branchNames {
|
||||
if rule.Match(branch) {
|
||||
results = append(results, branch)
|
||||
}
|
||||
}
|
||||
if len(brancheNames) < 100 {
|
||||
if len(branchNames) < pageSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ type Issue struct {
|
||||
PullRequest *PullRequest `xorm:"-"`
|
||||
NumComments int
|
||||
|
||||
// TODO: RemoveIssueRef: see "repo/issue/branch_selector_field.tmpl"
|
||||
// TODO: ISSUE-SIDEBAR-REF-SELECTOR: see "repo/issue/branch_selector_field.tmpl"
|
||||
Ref string
|
||||
|
||||
PinOrder int `xorm:"-"` // 0 means not loaded, -1 means loaded but not pinned
|
||||
|
||||
@@ -477,7 +477,7 @@ func applySubscribedCondition(sess db.Session, subscriberID int64) {
|
||||
Select("repo_id").
|
||||
From("watch").
|
||||
Where(builder.And(builder.Eq{"user_id": subscriberID},
|
||||
builder.In("mode", repo_model.WatchModeNormal, repo_model.WatchModeAuto))),
|
||||
builder.In("mode", repo_model.WatchModeNormal, repo_model.WatchModeAuto, repo_model.WatchModeCustom))),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
+14
-18
@@ -269,27 +269,23 @@ func UpdateLabel(ctx context.Context, l *Label) error {
|
||||
return updateLabelCols(ctx, l, "name", "description", "color", "exclusive", "exclusive_order", "archived_unix")
|
||||
}
|
||||
|
||||
// DeleteLabel delete a label
|
||||
func DeleteLabel(ctx context.Context, id, labelID int64) error {
|
||||
l, err := GetLabelByID(ctx, labelID)
|
||||
if err != nil {
|
||||
if IsErrLabelNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
// DeleteRepoLabel deletes a repository label.
|
||||
func DeleteRepoLabel(ctx context.Context, repoID, labelID int64) error {
|
||||
return deleteLabel(ctx, labelID, builder.Eq{"repo_id": repoID, "org_id": 0})
|
||||
}
|
||||
|
||||
// DeleteOrgLabel deletes an organization label.
|
||||
func DeleteOrgLabel(ctx context.Context, orgID, labelID int64) error {
|
||||
return deleteLabel(ctx, labelID, builder.Eq{"org_id": orgID, "repo_id": 0})
|
||||
}
|
||||
|
||||
func deleteLabel(ctx context.Context, labelID int64, ownerCond builder.Cond) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
if l.BelongsToOrg() && l.OrgID != id {
|
||||
return nil
|
||||
}
|
||||
if l.BelongsToRepo() && l.RepoID != id {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err = db.DeleteByID[Label](ctx, labelID); err != nil {
|
||||
deleted, err := db.GetEngine(ctx).ID(labelID).Where(ownerCond).Delete(new(Label))
|
||||
if err != nil || deleted == 0 {
|
||||
return err
|
||||
} else if _, err = db.GetEngine(ctx).
|
||||
}
|
||||
if _, err = db.GetEngine(ctx).
|
||||
Where("label_id = ?", labelID).
|
||||
Delete(new(IssueLabel)); err != nil {
|
||||
return err
|
||||
|
||||
@@ -320,13 +320,29 @@ func TestUpdateLabel(t *testing.T) {
|
||||
func TestDeleteLabel(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
|
||||
assert.NoError(t, issues_model.DeleteLabel(t.Context(), label.RepoID, label.ID))
|
||||
orgLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 3})
|
||||
repoLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 10, RepoID: orgLabel.OrgID})
|
||||
assert.NoError(t, issues_model.DeleteOrgLabel(t.Context(), repoLabel.RepoID, repoLabel.ID))
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: repoLabel.ID})
|
||||
|
||||
assert.NoError(t, issues_model.DeleteRepoLabel(t.Context(), orgLabel.OrgID, orgLabel.ID))
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: orgLabel.ID})
|
||||
|
||||
assert.NoError(t, issues_model.DeleteOrgLabel(t.Context(), unittest.NonexistentID, orgLabel.ID))
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: orgLabel.ID})
|
||||
assert.NoError(t, issues_model.DeleteOrgLabel(t.Context(), orgLabel.OrgID, orgLabel.ID))
|
||||
unittest.AssertNotExistsBean(t, &issues_model.Label{ID: orgLabel.ID})
|
||||
|
||||
assert.NoError(t, issues_model.DeleteRepoLabel(t.Context(), unittest.NonexistentID, label.ID))
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: label.ID})
|
||||
assert.NoError(t, issues_model.DeleteRepoLabel(t.Context(), label.RepoID, label.ID))
|
||||
unittest.AssertNotExistsBean(t, &issues_model.Label{ID: label.ID, RepoID: label.RepoID})
|
||||
|
||||
assert.NoError(t, issues_model.DeleteLabel(t.Context(), label.RepoID, label.ID))
|
||||
assert.NoError(t, issues_model.DeleteRepoLabel(t.Context(), label.RepoID, label.ID))
|
||||
unittest.AssertNotExistsBean(t, &issues_model.Label{ID: label.ID})
|
||||
|
||||
assert.NoError(t, issues_model.DeleteLabel(t.Context(), unittest.NonexistentID, unittest.NonexistentID))
|
||||
assert.NoError(t, issues_model.DeleteRepoLabel(t.Context(), unittest.NonexistentID, unittest.NonexistentID))
|
||||
assert.NoError(t, issues_model.DeleteOrgLabel(t.Context(), unittest.NonexistentID, unittest.NonexistentID))
|
||||
unittest.CheckConsistencyFor(t, &issues_model.Label{}, &repo_model.Repository{})
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/markup/markdown"
|
||||
"gitea.dev/modules/test"
|
||||
|
||||
_ "gitea.dev/modules/markup/orgmode"
|
||||
|
||||
@@ -80,6 +81,23 @@ func TestRepoFile(t *testing.T) {
|
||||
<video src="/user2/repo1/media/commit/1234/my%20dir/LINK">
|
||||
</video>`, rendered)
|
||||
})
|
||||
|
||||
t.Run("PictureSource", func(t *testing.T) {
|
||||
rctx := NewRenderContextRepoFile(t.Context(), repo1, RepoFileOptions{CurrentRefSubURL: "branch/main"}).
|
||||
WithMarkupType(markdown.MarkupName)
|
||||
rendered, err := testRenderString(rctx, `<picture><source media="(prefers-color-scheme: dark)" srcset="dark.svg"><img src="light.svg"></picture>`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.NormalizeHTMLSpaces(`
|
||||
<p>
|
||||
<a href="/user2/repo1/src/branch/main/light.svg" target="_blank" rel="nofollow noopener">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="/user2/repo1/media/branch/main/dark.svg"/>
|
||||
<img src="/user2/repo1/media/branch/main/light.svg"/>
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
`), test.NormalizeHTMLSpaces(rendered))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepoFileOrgMode(t *testing.T) {
|
||||
|
||||
@@ -133,6 +133,9 @@ func ExistAttachmentsByUUID(ctx context.Context, uuid string) (bool, error) {
|
||||
|
||||
// GetAttachmentsByIssueID returns all attachments of an issue.
|
||||
func GetAttachmentsByIssueID(ctx context.Context, issueID int64) ([]*Attachment, error) {
|
||||
if issueID == 0 { // unsaved issue, would match every issue-less attachment
|
||||
return []*Attachment{}, nil
|
||||
}
|
||||
attachments := make([]*Attachment, 0, 10)
|
||||
return attachments, db.GetEngine(ctx).Where("issue_id = ? AND comment_id = 0", issueID).Find(&attachments)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ func TestGetByCommentOrIssueID(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, attachments, 1)
|
||||
|
||||
attachments, err = repo_model.GetAttachmentsByIssueID(t.Context(), 0)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, attachments)
|
||||
|
||||
attachments, err = repo_model.GetAttachmentsByCommentID(t.Context(), 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, attachments, 2)
|
||||
|
||||
+26
-19
@@ -191,6 +191,10 @@ func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs
|
||||
}
|
||||
|
||||
for i := range attachments {
|
||||
if attachments[i].IssueID != 0 || attachments[i].CommentID != 0 || attachments[i].ReleaseID != 0 {
|
||||
return util.NewPermissionDeniedErrorf("attachment is already linked")
|
||||
}
|
||||
|
||||
if attachments[i].RepoID == 0 && attachments[i].CreatedUnix < LegacyAttachmentMissingRepoIDCutoff {
|
||||
attachments[i].RepoID = rel.RepoID
|
||||
if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Cols("repo_id").Update(attachments[i]); err != nil {
|
||||
@@ -201,10 +205,6 @@ func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs
|
||||
if attachments[i].RepoID != rel.RepoID {
|
||||
return util.NewPermissionDeniedErrorf("attachment belongs to different repository")
|
||||
}
|
||||
|
||||
if attachments[i].ReleaseID != 0 {
|
||||
return util.NewPermissionDeniedErrorf("release permission denied")
|
||||
}
|
||||
attachments[i].ReleaseID = releaseID
|
||||
// No assign value could be 0, so ignore AllCols().
|
||||
if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Cols("release_id").Update(attachments[i]); err != nil {
|
||||
@@ -357,21 +357,21 @@ func GetLatestReleaseByRepoID(ctx context.Context, repoID int64) (*Release, erro
|
||||
}
|
||||
|
||||
type releaseMetaSearch struct {
|
||||
ID []int64
|
||||
Rel []*Release
|
||||
IDs []int64
|
||||
Releases []*Release
|
||||
}
|
||||
|
||||
func (s releaseMetaSearch) Len() int {
|
||||
return len(s.ID)
|
||||
return len(s.IDs)
|
||||
}
|
||||
|
||||
func (s releaseMetaSearch) Swap(i, j int) {
|
||||
s.ID[i], s.ID[j] = s.ID[j], s.ID[i]
|
||||
s.Rel[i], s.Rel[j] = s.Rel[j], s.Rel[i]
|
||||
s.IDs[i], s.IDs[j] = s.IDs[j], s.IDs[i]
|
||||
s.Releases[i], s.Releases[j] = s.Releases[j], s.Releases[i]
|
||||
}
|
||||
|
||||
func (s releaseMetaSearch) Less(i, j int) bool {
|
||||
return s.ID[i] < s.ID[j]
|
||||
return s.IDs[i] < s.IDs[j]
|
||||
}
|
||||
|
||||
func hasDuplicateName(attaches []*Attachment) bool {
|
||||
@@ -396,19 +396,26 @@ func GetReleaseAttachments(ctx context.Context, rels ...*Release) (err error) {
|
||||
// then merge join them
|
||||
|
||||
// Sort
|
||||
sortedRels := releaseMetaSearch{ID: make([]int64, len(rels)), Rel: make([]*Release, len(rels))}
|
||||
sortedRels := releaseMetaSearch{IDs: make([]int64, 0, len(rels)), Releases: make([]*Release, 0, len(rels))}
|
||||
var attachments []*Attachment
|
||||
for index, element := range rels {
|
||||
element.Attachments = []*Attachment{}
|
||||
sortedRels.ID[index] = element.ID
|
||||
sortedRels.Rel[index] = element
|
||||
for _, rel := range rels {
|
||||
// if a provided "Release" has no ID, don't query it, otherwise, the "IN" query will return all other attachments for the repo
|
||||
if rel.ID == 0 {
|
||||
continue
|
||||
}
|
||||
rel.Attachments = []*Attachment{}
|
||||
sortedRels.IDs = append(sortedRels.IDs, rel.ID)
|
||||
sortedRels.Releases = append(sortedRels.Releases, rel)
|
||||
}
|
||||
if len(sortedRels.IDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
sort.Sort(sortedRels)
|
||||
|
||||
// Select attachments
|
||||
err = db.GetEngine(ctx).
|
||||
Asc("release_id", "name").
|
||||
In("release_id", sortedRels.ID).
|
||||
In("release_id", sortedRels.IDs).
|
||||
Find(&attachments)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -417,14 +424,14 @@ func GetReleaseAttachments(ctx context.Context, rels ...*Release) (err error) {
|
||||
// merge join
|
||||
currentIndex := 0
|
||||
for _, attachment := range attachments {
|
||||
for sortedRels.ID[currentIndex] < attachment.ReleaseID {
|
||||
for sortedRels.IDs[currentIndex] < attachment.ReleaseID {
|
||||
currentIndex++
|
||||
}
|
||||
sortedRels.Rel[currentIndex].Attachments = append(sortedRels.Rel[currentIndex].Attachments, attachment)
|
||||
sortedRels.Releases[currentIndex].Attachments = append(sortedRels.Releases[currentIndex].Attachments, attachment)
|
||||
}
|
||||
|
||||
// Makes URL's predictable
|
||||
for _, release := range rels {
|
||||
for _, release := range sortedRels.Releases {
|
||||
// If we have no Repo, we don't need to execute this loop
|
||||
if release.Repo == nil {
|
||||
continue
|
||||
|
||||
@@ -53,6 +53,18 @@ func TestAddReleaseAttachmentsRejectsDifferentRepo(t *testing.T) {
|
||||
assert.Zero(t, attach.ReleaseID, "attachment should not be linked to release on failure")
|
||||
}
|
||||
|
||||
func TestAddReleaseAttachmentsRejectsIssueAttachment(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
uuid := "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"
|
||||
err := AddReleaseAttachments(t.Context(), 1, []string{uuid})
|
||||
assert.ErrorIs(t, err, util.ErrPermissionDenied)
|
||||
|
||||
attach, err := GetAttachmentByUUID(t.Context(), uuid)
|
||||
assert.NoError(t, err)
|
||||
assert.Zero(t, attach.ReleaseID)
|
||||
}
|
||||
|
||||
func TestAddReleaseAttachmentsAllowsLegacyMissingRepoID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
|
||||
+10
-1
@@ -616,6 +616,15 @@ func ComposeHTTPSCloneURL(ctx context.Context, owner, repo string) string {
|
||||
|
||||
// ComposeSSHCloneURL returns SSH clone URL based on the given owner and repository name.
|
||||
func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) string {
|
||||
return composeSSHCloneURL(doer, ownerName, repoName, setting.Repository.UseCompatSSHURI)
|
||||
}
|
||||
|
||||
// ComposeSSHCloneURI is like ComposeSSHCloneURL but always returns the "ssh://" form, because "go get" rejects scp-style addresses
|
||||
func ComposeSSHCloneURI(doer *user_model.User, ownerName, repoName string) string {
|
||||
return composeSSHCloneURL(doer, ownerName, repoName, true)
|
||||
}
|
||||
|
||||
func composeSSHCloneURL(doer *user_model.User, ownerName, repoName string, useURI bool) string {
|
||||
sshUser := setting.SSH.User
|
||||
sshDomain := setting.SSH.Domain
|
||||
|
||||
@@ -642,7 +651,7 @@ func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) strin
|
||||
if ip := net.ParseIP(sshHost); ip != nil && ip.To4() == nil {
|
||||
sshHost = "[" + sshHost + "]" // for IPv6 address, wrap it with brackets
|
||||
}
|
||||
if setting.Repository.UseCompatSSHURI {
|
||||
if useURI {
|
||||
return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
|
||||
}
|
||||
return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
|
||||
|
||||
@@ -185,6 +185,8 @@ func TestComposeSSHCloneURL(t *testing.T) {
|
||||
assert.Equal(t, "git@domain:user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo"))
|
||||
setting.Repository.UseCompatSSHURI = true
|
||||
assert.Equal(t, "ssh://git@domain/user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo"))
|
||||
setting.Repository.UseCompatSSHURI = false
|
||||
assert.Equal(t, "ssh://git@domain/user/repo.git", ComposeSSHCloneURI(nil, "user", "repo"))
|
||||
// test SSH_DOMAIN while use non-standard SSH port
|
||||
setting.SSH.Port = 123
|
||||
setting.Repository.UseCompatSSHURI = false
|
||||
|
||||
+28
-7
@@ -20,9 +20,10 @@ type WatchMode int8
|
||||
const (
|
||||
WatchModeNone WatchMode = iota // 0 watch nothing unless mentioned
|
||||
|
||||
WatchModeNormal // 1 proactively watching (all or custom)
|
||||
WatchModeNormal // 1 proactively watching (all, or custom before WatchModeCustom)
|
||||
WatchModeDont // 2 ignore the repo
|
||||
WatchModeAuto // 3 automatically watching (from AutoWatchOnChanges)
|
||||
WatchModeCustom // 4 watching the events picked in the custom watch options
|
||||
)
|
||||
|
||||
// WatchType is the `watch` column gating one kind of notification
|
||||
@@ -79,7 +80,7 @@ func (w *Watch) IsWatching() bool {
|
||||
|
||||
// IsWatchingAll reports whether every event is enabled, which is the "all activity" mode
|
||||
func (w *Watch) IsWatchingAll() bool {
|
||||
return w.IncludePullRequests && w.IncludeIssues && w.IncludeReleases
|
||||
return w.Mode != WatchModeCustom && w.IncludePullRequests && w.IncludeIssues && w.IncludeReleases
|
||||
}
|
||||
|
||||
func (w *Watch) IsWatchingAny() bool {
|
||||
@@ -166,6 +167,9 @@ func WatchRepoAuto(ctx context.Context, doer *user_model.User, repo *Repository,
|
||||
if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) {
|
||||
return user_model.ErrBlockedUser
|
||||
}
|
||||
if watch.Mode == WatchModeCustom { // keep the events the user picked
|
||||
return nil
|
||||
}
|
||||
|
||||
return watchRepoByMode(ctx, watch, WatchModeNormal)
|
||||
}
|
||||
@@ -189,7 +193,7 @@ func WatchRepoWithOptions(ctx context.Context, doer *user_model.User, repo *Repo
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.Mode == WatchModeNormal {
|
||||
if opts.Mode == WatchModeNormal || opts.Mode == WatchModeCustom {
|
||||
_, err = db.GetEngine(ctx).Where("user_id=? AND repo_id=?", doer.ID, repo.ID).
|
||||
Cols("include_pull_requests", "include_issues", "include_releases").
|
||||
Update(&Watch{IncludePullRequests: opts.WatchPullRequests, IncludeIssues: opts.WatchIssues, IncludeReleases: opts.WatchReleases})
|
||||
@@ -247,6 +251,16 @@ func GetRepoIgnorersIDs(ctx context.Context, repoID int64) ([]int64, error) {
|
||||
// but avoids joining with `user` for performance reasons
|
||||
// User permissions must be verified elsewhere if required
|
||||
func GetRepoWatchersIDs(ctx context.Context, repoID int64, watchType WatchType) ([]int64, error) {
|
||||
return getRepoWatchersIDs(ctx, repoID, watchType, false)
|
||||
}
|
||||
|
||||
// GetRepoCustomWatchersIDs is GetRepoWatchersIDs limited to custom watches, which unlike
|
||||
// watching all activity are never set up automatically (e.g. by AutoWatchNewRepos)
|
||||
func GetRepoCustomWatchersIDs(ctx context.Context, repoID int64, watchType WatchType) ([]int64, error) {
|
||||
return getRepoWatchersIDs(ctx, repoID, watchType, true)
|
||||
}
|
||||
|
||||
func getRepoWatchersIDs(ctx context.Context, repoID int64, watchType WatchType, customOnly bool) ([]int64, error) {
|
||||
ids := make([]int64, 0, 64)
|
||||
var watchColName string
|
||||
switch watchType {
|
||||
@@ -259,12 +273,19 @@ func GetRepoWatchersIDs(ctx context.Context, repoID int64, watchType WatchType)
|
||||
default:
|
||||
panic("invalid WatchType")
|
||||
}
|
||||
return ids, db.GetEngine(ctx).Table("watch").
|
||||
sess := db.GetEngine(ctx).Table("watch").
|
||||
Where("watch.repo_id=?", repoID).
|
||||
And("watch.mode<>?", WatchModeDont).
|
||||
And(builder.Eq{watchColName: true}).
|
||||
Select("user_id").
|
||||
Find(&ids)
|
||||
And(builder.Eq{watchColName: true})
|
||||
if customOnly { // matches SelectedMode() == "custom"
|
||||
sess = sess.And(builder.Or(
|
||||
builder.Eq{"watch.mode": WatchModeCustom},
|
||||
builder.Eq{"watch.include_pull_requests": false},
|
||||
builder.Eq{"watch.include_issues": false},
|
||||
builder.Eq{"watch.include_releases": false},
|
||||
))
|
||||
}
|
||||
return ids, sess.Select("user_id").Find(&ids)
|
||||
}
|
||||
|
||||
// GetRepoWatchers returns range of users watching given repository.
|
||||
|
||||
@@ -145,7 +145,7 @@ func TestWatchOptions(t *testing.T) {
|
||||
// repo 1 is watched by users 1, 4, 9 and 11, all with every event enabled
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
assert.NoError(t, repo_model.WatchRepoWithOptions(t.Context(), user, repo, repo_model.WatchOptions{Mode: repo_model.WatchModeNormal, WatchPullRequests: true}))
|
||||
assert.NoError(t, repo_model.WatchRepoWithOptions(t.Context(), user, repo, repo_model.WatchOptions{Mode: repo_model.WatchModeNormal, WatchPullRequests: true})) // custom watches were stored like this before WatchModeCustom
|
||||
|
||||
for watchType, expected := range map[repo_model.WatchType][]int64{
|
||||
repo_model.WatchPullRequests: {1, 4, 9, 11},
|
||||
@@ -157,6 +157,20 @@ func TestWatchOptions(t *testing.T) {
|
||||
assert.ElementsMatch(t, expected, ids, watchType)
|
||||
}
|
||||
|
||||
// a custom watch picking every event differs from all activity, which can be set up automatically
|
||||
customUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
assert.NoError(t, repo_model.WatchRepoWithOptions(t.Context(), customUser, repo, repo_model.WatchOptions{Mode: repo_model.WatchModeCustom, WatchPullRequests: true, WatchIssues: true, WatchReleases: true}))
|
||||
assert.NoError(t, repo_model.WatchRepoAuto(t.Context(), customUser, repo, true)) // must keep the custom watch
|
||||
for watchType, expected := range map[repo_model.WatchType][]int64{
|
||||
repo_model.WatchPullRequests: {1, 4},
|
||||
repo_model.WatchIssues: {4},
|
||||
repo_model.WatchReleases: {4},
|
||||
} {
|
||||
ids, err := repo_model.GetRepoCustomWatchersIDs(t.Context(), repo.ID, watchType)
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, expected, ids, watchType)
|
||||
}
|
||||
|
||||
// the options of one user must not show up for another
|
||||
watches, err := repo_model.GetUserWatches(t.Context(), 4, []int64{repo.ID})
|
||||
assert.NoError(t, err)
|
||||
@@ -176,5 +190,6 @@ func TestWatchSelectedMode(t *testing.T) {
|
||||
assert.Equal(t, "participate", (&repo_model.Watch{Mode: repo_model.WatchModeNormal}).SelectedMode())
|
||||
assert.Equal(t, "ignore", (&repo_model.Watch{Mode: repo_model.WatchModeDont}).SelectedMode())
|
||||
assert.Equal(t, "custom", (&repo_model.Watch{Mode: repo_model.WatchModeNormal, IncludeIssues: true}).SelectedMode())
|
||||
assert.Equal(t, "custom", (&repo_model.Watch{Mode: repo_model.WatchModeCustom, IncludePullRequests: true, IncludeIssues: true, IncludeReleases: true}).SelectedMode())
|
||||
assert.Equal(t, "all", (&repo_model.Watch{Mode: repo_model.WatchModeAuto, IncludePullRequests: true, IncludeIssues: true, IncludeReleases: true}).SelectedMode())
|
||||
}
|
||||
@@ -151,16 +151,28 @@ func TestListEmails(t *testing.T) {
|
||||
|
||||
func TestEmailAddressValidate(t *testing.T) {
|
||||
cases := map[string]bool{
|
||||
"": false,
|
||||
"root@localhost": true,
|
||||
"user@[192.168.1.2]": true,
|
||||
"@a": false,
|
||||
"abc@gmail.com": true,
|
||||
"abc@gmail.com\n": false,
|
||||
"": false,
|
||||
"@a": false,
|
||||
|
||||
// "_" shouldn't appear in domain but can appear in hostname, since we can't stop site admins from doing so, just accept it
|
||||
"root@local_host": true,
|
||||
"root@localhost": true,
|
||||
"root@LOCALHOST": true,
|
||||
|
||||
"user@[192.168.1.2]": true,
|
||||
"user@[IPv6:FFff::1]": true,
|
||||
|
||||
"abc@gmail.com": true,
|
||||
"abc@gmail.com.": false,
|
||||
"abc@gmail.com-": false,
|
||||
"abc@gmail.com\n": false,
|
||||
"abc@gmail..com": false,
|
||||
"abc@gmail com": false,
|
||||
"abc@gmail*com": false,
|
||||
|
||||
"Foo <foo@bar.com>": false,
|
||||
"abc@gmail.com (x)": false,
|
||||
"jürgen@example.com": false,
|
||||
"a@foo_bar.com": false,
|
||||
"jürgen@example.com": false, // utf8 address is not supported yet
|
||||
}
|
||||
for tc, isValid := range cases {
|
||||
t.Run(tc, func(t *testing.T) {
|
||||
|
||||
+31
-2
@@ -1021,7 +1021,8 @@ func GetInactiveUsers(ctx context.Context, olderThan time.Duration) ([]*User, er
|
||||
Find(&users)
|
||||
}
|
||||
|
||||
// GetUserByID returns the user object by given ID if exists.
|
||||
// GetUserByID returns the user object by given ID if exists. TODO: for most cases, this function should be avoided.
|
||||
// Use correct GetXxxUser functions instead to handle user types & states correctly.
|
||||
func GetUserByID(ctx context.Context, id int64) (*User, error) {
|
||||
u := new(User)
|
||||
has, err := db.GetEngine(ctx).ID(id).Get(u)
|
||||
@@ -1033,7 +1034,8 @@ func GetUserByID(ctx context.Context, id int64) (*User, error) {
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// GetUserByIDs returns the user objects by given IDs if exists.
|
||||
// GetUserByIDs returns the user objects by given IDs if exists. TODO: for most cases, this function should be avoided.
|
||||
// Use correct GetXxxUser functions instead to handle user types & states correctly.
|
||||
func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, nil
|
||||
@@ -1046,6 +1048,13 @@ func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
|
||||
return users, err
|
||||
}
|
||||
|
||||
// Glossaries for XxxUser:
|
||||
// * Possible: The user may or may not exist in the database, but we can still return a valid user object (e.g. Ghost user) for the given ID.
|
||||
// * Individual: The user is a real person, not an organization or a bot.
|
||||
// * Doer: The user is the one performing an action
|
||||
// * Valid: The user is an active user and not prohibited from signing in
|
||||
// * System: Negative ID (e.g. Ghost, Actions, DeployKey, etc.) and not a real person, Ghost is not included if there is no "Possible" keyword
|
||||
|
||||
// GetPossibleUserByID returns the possible user and its ID. If the user doesn't exist, it returns Ghost user
|
||||
func GetPossibleUserByID(ctx context.Context, id int64) (_ int64, u *User, err error) {
|
||||
if id < 0 {
|
||||
@@ -1082,6 +1091,23 @@ func GetPossibleUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func GetValidDoerUserByID(ctx context.Context, id int64) (*User, error) {
|
||||
u, err := GetUserByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !u.IsIndividual() && !u.IsTypeBot() {
|
||||
return nil, util.NewNotExistErrorf("user %d is not a doer", id)
|
||||
}
|
||||
if !u.IsActive {
|
||||
return nil, util.NewPermissionDeniedErrorf("user %d is inactive", id)
|
||||
}
|
||||
if u.ProhibitLogin {
|
||||
return nil, util.NewPermissionDeniedErrorf("user %d is prohibited from login", id)
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func getUserByNameWithTypes(ctx context.Context, name string, types ...UserType) (*User, error) {
|
||||
u := &User{}
|
||||
sess := db.GetEngine(ctx).Where(builder.Eq{"lower_name": strings.ToLower(name)})
|
||||
@@ -1201,6 +1227,9 @@ func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, erro
|
||||
noReplyAddressSuffix := "@" + strings.ToLower(setting.Service.NoReplyAddress)
|
||||
for _, email := range emails {
|
||||
emailLower := strings.ToLower(email)
|
||||
if emailLower == "" {
|
||||
continue // a commit without an author email names no user
|
||||
}
|
||||
needCheckEmails.Add(emailLower)
|
||||
if localPart, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix); ok {
|
||||
name, id := parseLocalPartToNameID(localPart)
|
||||
|
||||
@@ -95,6 +95,12 @@ func TestUserEmails(t *testing.T) {
|
||||
setting.Service.NoReplyAddress = "example.com"
|
||||
testGetUserByEmail(t, "user1-2@example.COM", 1)
|
||||
})
|
||||
|
||||
t.Run("EmptyEmail", func(t *testing.T) {
|
||||
// A user whose email address is empty must not own commits that have no author email.
|
||||
require.NoError(t, db.Insert(t.Context(), &user_model.EmailAddress{UID: 2, Email: "", LowerEmail: "", IsActivated: true}))
|
||||
testGetUserByEmail(t, "", 0)
|
||||
})
|
||||
})
|
||||
t.Run("GetUserByEmail", func(t *testing.T) {
|
||||
testGetUserByEmail := func(t *testing.T, email string, uid int64) {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package jobparser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -65,44 +64,31 @@ func (w *SingleWorkflow) SetJob(id string, job *Job) error {
|
||||
m := map[string]*Job{
|
||||
id: job,
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
encoder := yaml.NewEncoder(&buf)
|
||||
encoder.SetIndent(2)
|
||||
if err := encoder.Encode(m); err != nil {
|
||||
out, err := marshalYAML(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encoder.Close()
|
||||
|
||||
node := yaml.Node{}
|
||||
if err := yaml.Unmarshal(buf.Bytes(), &node); err != nil {
|
||||
if err := yaml.Unmarshal(out, &node); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(node.Content) != 1 || node.Content[0].Kind != yaml.MappingNode {
|
||||
return fmt.Errorf("can not set job: %s", buf.String())
|
||||
return fmt.Errorf("can not set job: %s", out)
|
||||
}
|
||||
w.RawJobs = *node.Content[0]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *SingleWorkflow) Marshal() ([]byte, error) {
|
||||
// Encode with the same indentation SetJob uses (2). yaml.Marshal's default
|
||||
// indentation (4) makes the encoder emit multi-line block scalars (e.g. a
|
||||
// `run:` step that begins with blank lines) with a wrong explicit indentation
|
||||
// indicator (`run: |4`) that then fails to re-parse, which silently strands
|
||||
// the job during concurrency evaluation. Keeping both encoders at indent 2
|
||||
// makes the serialized single workflow round-trip.
|
||||
var buf bytes.Buffer
|
||||
enc := yaml.NewEncoder(&buf)
|
||||
enc.SetIndent(2)
|
||||
payload := *w
|
||||
payload.RunName = "" // already interpolated into the run title, a runner would parse it as a template again
|
||||
if err := enc.Encode(&payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := enc.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
return marshalYAML(&payload)
|
||||
}
|
||||
|
||||
// marshalYAML never wraps lines, go-yaml wraps more-indented lines of folded scalars until https://github.com/yaml/go-yaml/pull/441 is released.
|
||||
func marshalYAML(v any) ([]byte, error) {
|
||||
return yaml.Dump(v, yaml.WithV3Defaults(), yaml.WithIndent(2), yaml.WithLineWidth(-1))
|
||||
}
|
||||
|
||||
type Job struct {
|
||||
@@ -188,29 +174,18 @@ func (j *Job) DisplayName() string {
|
||||
return util.EllipsisDisplayString(unescapeExpressions(j.Name), 255)
|
||||
}
|
||||
|
||||
// BlockSafeString works around https://github.com/yaml/go-yaml/issues/399, quoting a value whose
|
||||
// leading newline would cost a literal block scalar its indentation indicator.
|
||||
type BlockSafeString string
|
||||
|
||||
func (s BlockSafeString) MarshalYAML() (any, error) {
|
||||
if !strings.HasPrefix(string(s), "\n") {
|
||||
return string(s), nil
|
||||
}
|
||||
return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Style: yaml.DoubleQuotedStyle, Value: string(s)}, nil
|
||||
}
|
||||
|
||||
type Step struct {
|
||||
ID string `yaml:"id,omitempty"`
|
||||
If yaml.Node `yaml:"if,omitempty"`
|
||||
Name BlockSafeString `yaml:"name,omitempty"`
|
||||
Uses string `yaml:"uses,omitempty"`
|
||||
Run BlockSafeString `yaml:"run,omitempty"`
|
||||
WorkingDirectory string `yaml:"working-directory,omitempty"`
|
||||
Shell string `yaml:"shell,omitempty"`
|
||||
Env yaml.Node `yaml:"env,omitempty"`
|
||||
With yaml.Node `yaml:"with,omitempty"`
|
||||
RawContinueOnError yaml.Node `yaml:"continue-on-error,omitempty"` // raw: the runner evaluates it with the steps context
|
||||
TimeoutMinutes string `yaml:"timeout-minutes,omitempty"`
|
||||
ID string `yaml:"id,omitempty"`
|
||||
If yaml.Node `yaml:"if,omitempty"`
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Uses string `yaml:"uses,omitempty"`
|
||||
Run string `yaml:"run,omitempty"`
|
||||
WorkingDirectory string `yaml:"working-directory,omitempty"`
|
||||
Shell string `yaml:"shell,omitempty"`
|
||||
Env yaml.Node `yaml:"env,omitempty"`
|
||||
With yaml.Node `yaml:"with,omitempty"`
|
||||
RawContinueOnError yaml.Node `yaml:"continue-on-error,omitempty"` // raw: the runner evaluates it with the steps context
|
||||
TimeoutMinutes string `yaml:"timeout-minutes,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalYAML canonicalizes booleans like continue-on-error
|
||||
@@ -232,9 +207,9 @@ func (s *Step) String() string {
|
||||
}
|
||||
return (&model.Step{
|
||||
ID: s.ID,
|
||||
Name: string(s.Name),
|
||||
Name: s.Name,
|
||||
Uses: s.Uses,
|
||||
Run: string(s.Run),
|
||||
Run: s.Run,
|
||||
}).String()
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
_, origJob := sws[0].Job()
|
||||
require.Len(t, origJob.Steps, 1)
|
||||
const wantRun = "\n\necho start\necho done\n"
|
||||
require.Equal(t, wantRun, string(origJob.Steps[0].Run))
|
||||
require.Equal(t, wantRun, origJob.Steps[0].Run)
|
||||
|
||||
payload, err := sws[0].Marshal()
|
||||
require.NoError(t, err)
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
// the round-trip must preserve the run block byte-for-byte
|
||||
_, gotJob := roundTripped[0].Job()
|
||||
require.Len(t, gotJob.Steps, 1)
|
||||
require.Equal(t, wantRun, string(gotJob.Steps[0].Run), "round-trip must preserve run content; got payload:\n%s", payload)
|
||||
require.Equal(t, wantRun, gotJob.Steps[0].Run, "round-trip must preserve run content; got payload:\n%s", payload)
|
||||
}
|
||||
|
||||
// Typing a step's continue-on-error as a bool used to reject the whole `jobs:` node.
|
||||
@@ -104,3 +104,37 @@ jobs:
|
||||
require.Equal(t, w, steps[i].RawContinueOnError, "step %d, payload:\n%s", i, payload)
|
||||
}
|
||||
}
|
||||
|
||||
// Folded (`>`) scalars in with/env used to gain blank lines and have spaces turned into newlines
|
||||
// on every Marshal -> Parse cycle.
|
||||
func TestSingleWorkflowRoundTripFoldedScalar(t *testing.T) {
|
||||
const wf = `name: demo
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: ./.github/actions/build
|
||||
with:
|
||||
cfg: >
|
||||
begin
|
||||
first "indented" line with path dir/Installer Scripts/some long path/another long path/some file name.xzy,
|
||||
second "indented" line
|
||||
end
|
||||
`
|
||||
const want = "begin\n first \"indented\" line with path dir/Installer Scripts/some long path/another long path/some file name.xzy,\n second \"indented\" line\nend\n"
|
||||
sws, err := Parse([]byte(wf))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, sws, 1)
|
||||
for range 3 {
|
||||
payload, err := sws[0].Marshal()
|
||||
require.NoError(t, err)
|
||||
sws, err = Parse(payload)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, sws, 1)
|
||||
}
|
||||
_, job := sws[0].Job()
|
||||
var with map[string]string
|
||||
require.NoError(t, job.Steps[0].With.Decode(&with))
|
||||
require.Equal(t, want, with["cfg"])
|
||||
}
|
||||
@@ -7,5 +7,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: extract_tag
|
||||
name: "\nExtract tag for variant"
|
||||
run: "\necho Test\n"
|
||||
name: |-
|
||||
|
||||
Extract tag for variant
|
||||
run: |
|
||||
|
||||
echo Test
|
||||
@@ -532,7 +532,7 @@ func matchIssuesEvent(issuePayload *api.IssuePayload, evt *jobparser.Event) bool
|
||||
}
|
||||
|
||||
for _, val := range vals {
|
||||
if slices.ContainsFunc(actions, glob.MustCompile(val, '/').Match) {
|
||||
if matchActivityType(val, actions...) {
|
||||
matchTimes++
|
||||
break
|
||||
}
|
||||
@@ -575,7 +575,7 @@ func matchPullRequestEvent(ctx context.Context, gitRepo *git.Repository, commit
|
||||
}
|
||||
log.Trace("matching pull_request %s with %v", action, vals)
|
||||
for _, val := range vals {
|
||||
if glob.MustCompile(val, '/').Match(string(action)) {
|
||||
if matchActivityType(val, string(action)) {
|
||||
activityTypeMatched = true
|
||||
matchTimes++
|
||||
break
|
||||
@@ -678,7 +678,7 @@ func matchIssueCommentEvent(issueCommentPayload *api.IssueCommentPayload, evt *j
|
||||
// NONE
|
||||
|
||||
for _, val := range vals {
|
||||
if glob.MustCompile(val, '/').Match(string(issueCommentPayload.Action)) {
|
||||
if matchActivityType(val, string(issueCommentPayload.Action)) {
|
||||
matchTimes++
|
||||
break
|
||||
}
|
||||
@@ -718,7 +718,7 @@ func matchPullRequestReviewEvent(prPayload *api.PullRequestPayload, evt *jobpars
|
||||
}
|
||||
|
||||
for _, val := range vals {
|
||||
if slices.ContainsFunc(actions, glob.MustCompile(val, '/').Match) {
|
||||
if matchActivityType(val, actions...) {
|
||||
matchTimes++
|
||||
break
|
||||
}
|
||||
@@ -758,7 +758,7 @@ func matchPullRequestReviewCommentEvent(prPayload *api.PullRequestPayload, evt *
|
||||
}
|
||||
|
||||
for _, val := range vals {
|
||||
if slices.ContainsFunc(actions, glob.MustCompile(val, '/').Match) {
|
||||
if matchActivityType(val, actions...) {
|
||||
matchTimes++
|
||||
break
|
||||
}
|
||||
@@ -795,7 +795,7 @@ func matchReleaseEvent(payload *api.ReleasePayload, evt *jobparser.Event) bool {
|
||||
action = "edited"
|
||||
}
|
||||
for _, val := range vals {
|
||||
if glob.MustCompile(val, '/').Match(string(action)) {
|
||||
if matchActivityType(val, string(action)) {
|
||||
matchTimes++
|
||||
break
|
||||
}
|
||||
@@ -832,7 +832,7 @@ func matchPackageEvent(payload *api.PackagePayload, evt *jobparser.Event) bool {
|
||||
action = "published"
|
||||
}
|
||||
for _, val := range vals {
|
||||
if glob.MustCompile(val, '/').Match(string(action)) {
|
||||
if matchActivityType(val, string(action)) {
|
||||
matchTimes++
|
||||
break
|
||||
}
|
||||
@@ -857,7 +857,7 @@ func matchWorkflowRunEvent(payload *api.WorkflowRunPayload, evt *jobparser.Event
|
||||
case "types":
|
||||
action := payload.Action
|
||||
for _, val := range vals {
|
||||
if glob.MustCompile(val, '/').Match(action) {
|
||||
if matchActivityType(val, action) {
|
||||
matchTimes++
|
||||
break
|
||||
}
|
||||
@@ -893,3 +893,13 @@ func matchWorkflowRunEvent(payload *api.WorkflowRunPayload, evt *jobparser.Event
|
||||
}
|
||||
return matchTimes == len(evt.Acts())
|
||||
}
|
||||
|
||||
// matchActivityType treats an invalid user-provided pattern as no match instead of panicking
|
||||
func matchActivityType(pattern string, actions ...string) bool {
|
||||
g, err := glob.Compile(pattern, '/')
|
||||
if err != nil {
|
||||
log.Debug("invalid activity type pattern %q: %v", pattern, err)
|
||||
return false
|
||||
}
|
||||
return slices.ContainsFunc(actions, g.Match)
|
||||
}
|
||||
@@ -205,6 +205,13 @@ func TestDetectMatched(t *testing.T) {
|
||||
yamlOn: "on:\n release:\n types: [published]",
|
||||
expected: detectMatched,
|
||||
},
|
||||
{
|
||||
desc: "HookEventRelease(release) doesn't match an invalid activity type pattern",
|
||||
triggedEvent: webhook_module.HookEventRelease,
|
||||
payload: &api.ReleasePayload{Action: api.HookReleasePublished},
|
||||
yamlOn: "on:\n release:\n types: [\"[\"]",
|
||||
expected: detectNotApplicable,
|
||||
},
|
||||
{
|
||||
desc: "HookEventPackage(package) `created` action doesn't match GithubEventRegistryPackage(registry_package) with `updated` activity type",
|
||||
triggedEvent: webhook_module.HookEventPackage,
|
||||
|
||||
@@ -102,11 +102,11 @@ func NewEmbeddedFS(data []byte) fs.ReadDirFS {
|
||||
efs := &embeddedFS{data: data, files: make(map[string]*embeddedFileInfo)}
|
||||
efs.meta = sync.OnceValue(func() *EmbeddedMeta {
|
||||
var meta EmbeddedMeta
|
||||
p := bytes.LastIndexByte(data, '\n')
|
||||
if p < 0 {
|
||||
_, metaJSON, ok := bytes.CutLast(data, []byte{'\n'})
|
||||
if !ok {
|
||||
return &meta
|
||||
}
|
||||
if err := json.Unmarshal(data[p+1:], &meta); err != nil {
|
||||
if err := json.Unmarshal(metaJSON, &meta); err != nil {
|
||||
panic("embedded file is not valid")
|
||||
}
|
||||
return &meta
|
||||
|
||||
@@ -32,6 +32,14 @@ func Init() {
|
||||
RPID: setting.AppDomain,
|
||||
RPOrigins: []string{appURL},
|
||||
AttestationPreference: protocol.PreferNoAttestation, // Gitea never verifies attestation
|
||||
// WebKit (Safari on iOS and macOS) returns an unsolicited "appid" client
|
||||
// extension output even when the relying party did not request it. Since the
|
||||
// webauthn library upgrade to v0.18, the default policy fails such ceremonies
|
||||
// ('Client returned the appid extension output which was not requested'), breaking
|
||||
// every passkey and security-key login from Safari on iOS. Ignore unsolicited
|
||||
// extension outputs, the same policy other relying parties adopted for the
|
||||
// same WebKit behavior.
|
||||
ExtensionsUnsolicitedOutputPolicy: protocol.UnsolicitedOutputPolicyIgnore,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/go-webauthn/webauthn/protocol"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -22,4 +23,5 @@ func TestInit(t *testing.T) {
|
||||
assert.Equal(t, setting.AppDomain, WebAuthn.Config.RPID)
|
||||
assert.Equal(t, setting.AppName, WebAuthn.Config.RPDisplayName)
|
||||
assert.Equal(t, rpOrigin, WebAuthn.Config.RPOrigins)
|
||||
assert.Equal(t, protocol.UnsolicitedOutputPolicyIgnore, WebAuthn.Config.ExtensionsUnsolicitedOutputPolicy)
|
||||
}
|
||||
+11
-15
@@ -125,6 +125,11 @@ func syncGitConfig(ctx context.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Gitea used to write "90", which git reads as a date in 1990
|
||||
if err := configUnsetAll(ctx, "gc.reflogExpire", "90"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Apply user's git config options last so they take precedence over builtin defaults
|
||||
for k, v := range setting.GitConfig.Options {
|
||||
if err = configSet(ctx, strings.ToLower(k), v); err != nil {
|
||||
@@ -162,7 +167,7 @@ func configSet(ctx context.Context, key, value string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, _, err = gitcmd.NewCommand("config", "--global").
|
||||
if _, _, err = gitcmd.NewCommand("config", "--global", "--replace-all").
|
||||
AddDynamicArguments(key, value).
|
||||
RunStdString(ctx); err != nil {
|
||||
return fmt.Errorf("failed to set git global config %s, err: %w", key, err)
|
||||
@@ -190,7 +195,7 @@ func configSetNonExist(ctx context.Context, key, value string) error {
|
||||
}
|
||||
|
||||
func configAddNonExist(ctx context.Context, key, value string) error {
|
||||
_, _, err := gitcmd.NewCommand("config", "--global", "--get").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(ctx)
|
||||
_, _, err := gitcmd.NewCommand("config", "--global", "--get").AddDynamicArguments(key, "^"+regexp.QuoteMeta(value)+"$").RunStdString(ctx)
|
||||
if err == nil {
|
||||
// already exist
|
||||
return nil
|
||||
@@ -207,18 +212,9 @@ func configAddNonExist(ctx context.Context, key, value string) error {
|
||||
}
|
||||
|
||||
func configUnsetAll(ctx context.Context, key, value string) error {
|
||||
_, _, err := gitcmd.NewCommand("config", "--global", "--get").AddDynamicArguments(key).RunStdString(ctx)
|
||||
if err == nil {
|
||||
// exist, need to remove
|
||||
_, _, err = gitcmd.NewCommand("config", "--global", "--unset-all").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unset git global config %s, err: %w", key, err)
|
||||
}
|
||||
return nil
|
||||
_, _, err := gitcmd.NewCommand("config", "--global", "--unset-all").AddDynamicArguments(key, "^"+regexp.QuoteMeta(value)+"$").RunStdString(ctx)
|
||||
if err != nil && !gitcmd.IsErrorExitCode(err, 5) { // 5: no such key or no matching value
|
||||
return fmt.Errorf("failed to unset git global config %s, err: %w", key, err)
|
||||
}
|
||||
if gitcmd.IsErrorExitCode(err, 1) {
|
||||
// not exist
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to get git config %s, err: %w", key, err)
|
||||
return nil
|
||||
}
|
||||
@@ -32,7 +32,9 @@ func TestGitConfig(t *testing.T) {
|
||||
assert.NoError(t, configSetNonExist(ctx, "test.key-a", "val-a-changed"))
|
||||
assert.False(t, gitConfigContains("key-a = val-a-changed"))
|
||||
|
||||
assert.NoError(t, configAddNonExist(ctx, "test.key-a", "val-a2"))
|
||||
assert.NoError(t, configSet(ctx, "test.key-a", "val-a-changed"))
|
||||
assert.NoError(t, configUnsetAll(ctx, "test.key-a", "val-a"))
|
||||
assert.True(t, gitConfigContains("key-a = val-a-changed"))
|
||||
|
||||
assert.NoError(t, configAddNonExist(ctx, "test.key-b", "val-b"))
|
||||
@@ -41,6 +43,8 @@ func TestGitConfig(t *testing.T) {
|
||||
assert.NoError(t, configAddNonExist(ctx, "test.key-b", "val-2b"))
|
||||
assert.True(t, gitConfigContains("key-b = val-b"))
|
||||
assert.True(t, gitConfigContains("key-b = val-2b"))
|
||||
assert.NoError(t, configAddNonExist(ctx, "test.key-b", "val"))
|
||||
assert.True(t, gitConfigContains("key-b = val\n"))
|
||||
|
||||
assert.NoError(t, configUnsetAll(ctx, "test.key-b", "val-b"))
|
||||
assert.False(t, gitConfigContains("key-b = val-b"))
|
||||
@@ -60,8 +64,10 @@ func TestSyncGitConfig(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.GitConfig)()
|
||||
|
||||
assert.Empty(t, setting.GitConfig.Options)
|
||||
assert.NoError(t, configSet(t.Context(), "gc.reflogExpire", "90"))
|
||||
assert.NoError(t, syncGitConfig(t.Context()))
|
||||
assert.True(t, gitConfigContains("commitGraph = true")) // builtin default config
|
||||
assert.False(t, gitConfigContains("reflogExpire"))
|
||||
|
||||
setting.GitConfig.Options["sync-test.cfg-key-a"] = "CfgValA"
|
||||
setting.GitConfig.Options["core.commitgraph"] = "false"
|
||||
|
||||
@@ -27,6 +27,7 @@ type FastImportFile struct {
|
||||
|
||||
type FastImportCommit struct {
|
||||
Ref string
|
||||
Parent string
|
||||
Message string
|
||||
Files []FastImportFile
|
||||
|
||||
@@ -72,6 +73,9 @@ func ForceFastImport(ctx context.Context, repo RepositoryFacade, commits []FastI
|
||||
|
||||
msg := util.IfZero(c.Message, fmt.Sprintf("test commit %d", i+1))
|
||||
_, _ = fmt.Fprintf(buf, "data %d\n%s\n", len(msg), msg)
|
||||
if c.Parent != "" {
|
||||
_, _ = fmt.Fprintf(buf, "from %s\n", c.Parent)
|
||||
}
|
||||
|
||||
for _, f := range c.Files {
|
||||
mode := util.IfZero(f.Mode, EntryModeBlob)
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
// merge base between commits.
|
||||
func FetchRemoteCommit(ctx context.Context, repo, remoteRepo RepositoryFacade, commitID string) error {
|
||||
return LockWriteAndDo(ctx, repo, func(ctx context.Context) error {
|
||||
return gitcmd.NewCommand("fetch", "--no-tags").
|
||||
return gitcmd.NewCommand("fetch", "--no-tags", "--no-write-commit-graph").
|
||||
AddDynamicArguments(gitrepo.RepoLocalPath(remoteRepo)).
|
||||
AddDynamicArguments(commitID).
|
||||
WithRepo(repo).Run(ctx)
|
||||
|
||||
+11
-7
@@ -56,13 +56,17 @@ func parseLsTreeLine(line []byte) (*LsTreeEntry, error) {
|
||||
return nil, fmt.Errorf("invalid ls-tree output (invalid object id): %q, err: %w", line, err)
|
||||
}
|
||||
|
||||
if len(entryName) > 0 && entryName[0] == '"' {
|
||||
entry.Name, err = strconv.Unquote(string(entryName))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid ls-tree output (invalid name): %q, err: %w", line, err)
|
||||
}
|
||||
} else {
|
||||
entry.Name = string(entryName)
|
||||
entry.Name, err = UnquoteDiffFilePath(string(entryName))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid ls-tree output (invalid name): %q, err: %w", line, err)
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func UnquoteDiffFilePath(path string) (string, error) {
|
||||
// ref: see the document of git config's "core.quotepath"
|
||||
if !strings.HasPrefix(path, `"`) {
|
||||
return path, nil
|
||||
}
|
||||
return strconv.Unquote(path)
|
||||
}
|
||||
@@ -415,8 +415,26 @@ func (repo *Repository) GetCommitsFromIDs(ctx context.Context, commitIDs []strin
|
||||
return commits
|
||||
}
|
||||
|
||||
// IsCommitInBranch check if the commit is on the branch
|
||||
// IsCommitInRepo checks if the commit exists in the repository.
|
||||
func IsCommitInRepo(ctx context.Context, repo gitrepo.RepositoryFacade, commitID string) (bool, error) {
|
||||
// "--quiet" makes it exit with 1 and no output if the commit doesn't exist
|
||||
_, _, err := gitcmd.NewCommand("rev-parse", "--verify", "--quiet").
|
||||
AddDynamicArguments(commitID + "^{commit}").
|
||||
WithRepo(repo).
|
||||
RunStdString(ctx)
|
||||
if gitcmd.IsErrorExitCode(err, 1) {
|
||||
return false, nil
|
||||
}
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
// IsCommitInBranch checks if the commit is on the branch.
|
||||
// It returns false (no error) if the commit doesn't exist or isn't on the branch.
|
||||
func IsCommitInBranch(ctx context.Context, repo gitrepo.RepositoryFacade, commitID, branch string) (r bool, err error) {
|
||||
if exist, err := IsCommitInRepo(ctx, repo, commitID); err != nil || !exist {
|
||||
return false, err
|
||||
}
|
||||
|
||||
stdout, _, err := gitcmd.NewCommand("branch", "--contains").
|
||||
AddDynamicArguments(commitID, branch).
|
||||
WithRepo(repo).
|
||||
@@ -424,7 +442,7 @@ func IsCommitInBranch(ctx context.Context, repo gitrepo.RepositoryFacade, commit
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return len(stdout) > 0, err
|
||||
return len(stdout) > 0, nil
|
||||
}
|
||||
|
||||
// GetCommitBranchStart returns the commit where the branch diverged
|
||||
|
||||
@@ -70,6 +70,20 @@ func TestGetCommitWithBadCommitID(t *testing.T) {
|
||||
assert.True(t, IsErrNotExist(err))
|
||||
}
|
||||
|
||||
func TestIsCommitInRepo(t *testing.T) {
|
||||
bareRepo1, err := OpenRepositoryLocal(t.Context(), filepath.Join(testReposDir, "repo1_bare"))
|
||||
assert.NoError(t, err)
|
||||
defer bareRepo1.Close()
|
||||
|
||||
exist, err := IsCommitInRepo(t.Context(), bareRepo1, "2839944139e0de9737a044f78b0e4b40d989a9e3")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exist)
|
||||
|
||||
exist, err = IsCommitInRepo(t.Context(), bareRepo1, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, exist)
|
||||
}
|
||||
|
||||
func TestIsCommitInBranch(t *testing.T) {
|
||||
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
|
||||
bareRepo1, err := OpenRepositoryLocal(t.Context(), bareRepo1Path)
|
||||
@@ -83,6 +97,10 @@ func TestIsCommitInBranch(t *testing.T) {
|
||||
result, err = IsCommitInBranch(t.Context(), bareRepo1, "2839944139e0de9737a044f78b0e4b40d989a9e3", "branch2")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, result)
|
||||
|
||||
result, err = IsCommitInBranch(t.Context(), bareRepo1, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "branch1")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, result)
|
||||
}
|
||||
|
||||
func TestRepository_CommitsBetween(t *testing.T) {
|
||||
|
||||
@@ -177,7 +177,7 @@ func initGlobCompiler(g *globCompiler, pattern string, separators []rune) (Glob,
|
||||
|
||||
regex, err := regexp.Compile(g.regexpPattern)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to compile regexp: %w", err)
|
||||
return nil, fmt.Errorf("failed to compile glob regexp: %w", err)
|
||||
}
|
||||
|
||||
g.regexp = regex
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package htmlutil
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SrcSetCandidate struct {
|
||||
SrcLink string
|
||||
SrcDesc []string
|
||||
}
|
||||
|
||||
const (
|
||||
asciiSpaces = " \t\n\r\f"
|
||||
asciiSpacesAndComma = " \t\n\r\f,"
|
||||
)
|
||||
|
||||
func ParseSrcset(srcset string) (candidates []SrcSetCandidate) {
|
||||
// ref: https://html.spec.whatwg.org/multipage/images.html#parse-a-srcset-attribute
|
||||
// It can't simply use "split" because comma can appear in URLs and descriptors can be parenthesized and contain commas.
|
||||
extractLink := func() (string, bool) {
|
||||
srcset = strings.TrimLeft(srcset, asciiSpacesAndComma)
|
||||
if srcset == "" {
|
||||
return "", false
|
||||
}
|
||||
end := strings.IndexAny(srcset, asciiSpaces)
|
||||
if end == -1 {
|
||||
end = len(srcset)
|
||||
}
|
||||
link := srcset[:end]
|
||||
srcset = srcset[end:]
|
||||
if strings.HasSuffix(link, ",") {
|
||||
return strings.TrimRight(link, ","), false
|
||||
}
|
||||
return link, true
|
||||
}
|
||||
extractDesc := func() (string, bool) {
|
||||
srcset = strings.TrimLeft(srcset, asciiSpaces)
|
||||
if srcset == "" {
|
||||
return "", false
|
||||
}
|
||||
inParentheses := false
|
||||
for i := range len(srcset) {
|
||||
c := srcset[i]
|
||||
if inParentheses {
|
||||
inParentheses = c != ')'
|
||||
continue
|
||||
}
|
||||
if c == '(' {
|
||||
inParentheses = true
|
||||
continue
|
||||
}
|
||||
if strings.IndexByte(asciiSpacesAndComma, c) != -1 {
|
||||
desc := srcset[:i]
|
||||
srcset = srcset[i+1:]
|
||||
return desc, c != ','
|
||||
}
|
||||
}
|
||||
desc := srcset
|
||||
srcset = ""
|
||||
return desc, false
|
||||
}
|
||||
|
||||
for {
|
||||
link, hasDesc := extractLink()
|
||||
if link == "" {
|
||||
break
|
||||
}
|
||||
var descriptors []string
|
||||
if hasDesc {
|
||||
for {
|
||||
desc, hasMoreDesc := extractDesc()
|
||||
if desc != "" {
|
||||
descriptors = append(descriptors, desc)
|
||||
}
|
||||
if !hasMoreDesc {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates = append(candidates, SrcSetCandidate{SrcLink: link, SrcDesc: descriptors})
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
|
||||
func EncodeSrcset(candidates []SrcSetCandidate) string {
|
||||
var sb strings.Builder
|
||||
for i, c := range candidates {
|
||||
if i != 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
link := c.SrcLink
|
||||
if prefix, ok := strings.CutSuffix(link, ","); ok {
|
||||
link = prefix + "%2C"
|
||||
}
|
||||
sb.WriteString(link)
|
||||
for _, d := range c.SrcDesc {
|
||||
sb.WriteByte(' ')
|
||||
sb.WriteString(d)
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package htmlutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSrcset(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
input string
|
||||
candidates []SrcSetCandidate
|
||||
encoded string
|
||||
}{
|
||||
{
|
||||
name: "bare URL",
|
||||
input: "image.png",
|
||||
candidates: []SrcSetCandidate{{SrcLink: "image.png"}},
|
||||
},
|
||||
{
|
||||
name: "bare URLs",
|
||||
input: "a.png, b.png , c.png,",
|
||||
candidates: []SrcSetCandidate{{SrcLink: "a.png"}, {SrcLink: "b.png"}, {SrcLink: "c.png"}},
|
||||
encoded: "a.png, b.png, c.png",
|
||||
},
|
||||
{
|
||||
name: "empty candidates",
|
||||
input: ", ,\ta.png 1x,, ,b.png 2x, ,",
|
||||
candidates: []SrcSetCandidate{{SrcLink: "a.png", SrcDesc: []string{"1x"}}, {SrcLink: "b.png", SrcDesc: []string{"2x"}}},
|
||||
encoded: "a.png 1x, b.png 2x",
|
||||
},
|
||||
{
|
||||
name: "trailing commas in URL",
|
||||
input: "a.png,, b.png 2x",
|
||||
candidates: []SrcSetCandidate{{SrcLink: "a.png"}, {SrcLink: "b.png", SrcDesc: []string{"2x"}}},
|
||||
encoded: "a.png, b.png 2x",
|
||||
},
|
||||
{
|
||||
name: "comma in URL",
|
||||
input: "comma,in,url.png",
|
||||
candidates: []SrcSetCandidate{{SrcLink: "comma,in,url.png"}},
|
||||
},
|
||||
{
|
||||
name: "embedded data URL",
|
||||
input: "data:image/png;base64,abcd 1x, image.png 2x",
|
||||
candidates: []SrcSetCandidate{{SrcLink: "data:image/png;base64,abcd", SrcDesc: []string{"1x"}}, {SrcLink: "image.png", SrcDesc: []string{"2x"}}},
|
||||
},
|
||||
{
|
||||
name: "parenthesized descriptor",
|
||||
input: "a.png (x,y) 1x, b.png 2x",
|
||||
candidates: []SrcSetCandidate{{SrcLink: "a.png", SrcDesc: []string{"(x,y)", "1x"}}, {SrcLink: "b.png", SrcDesc: []string{"2x"}}},
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
candidates := ParseSrcset(test.input)
|
||||
assert.Equal(t, test.candidates, candidates)
|
||||
encoded := EncodeSrcset(candidates)
|
||||
assert.Equal(t, util.IfZero(test.encoded, test.input), encoded)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("encode trailing commas", func(t *testing.T) {
|
||||
encoded := EncodeSrcset([]SrcSetCandidate{
|
||||
{SrcLink: "image,"},
|
||||
{SrcLink: "image,,", SrcDesc: []string{"2x"}},
|
||||
{SrcLink: "image.svg?variant=light,", SrcDesc: []string{"3x"}},
|
||||
})
|
||||
assert.Equal(t, "image%2C, image,%2C 2x, image.svg?variant=light%2C 3x", encoded)
|
||||
assert.Equal(t, []SrcSetCandidate{
|
||||
{SrcLink: "image%2C"},
|
||||
{SrcLink: "image,%2C", SrcDesc: []string{"2x"}},
|
||||
{SrcLink: "image.svg?variant=light%2C", SrcDesc: []string{"3x"}},
|
||||
}, ParseSrcset(encoded))
|
||||
})
|
||||
}
|
||||
@@ -8,12 +8,16 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
u, _ := url.Parse("file:///test")
|
||||
c := newClient(u, nil)
|
||||
assert.IsType(t, &FilesystemClient{}, c)
|
||||
require.IsType(t, &FilesystemClient{}, c)
|
||||
localPath, err := c.(*FilesystemClient).objectPath("invalid") //nolint:forcetypeassert // type is expected
|
||||
assert.Empty(t, localPath)
|
||||
assert.ErrorIs(t, err, ErrInvalidOIDFormat)
|
||||
|
||||
u, _ = url.Parse("https://test.com/lfs")
|
||||
c = newClient(u, nil)
|
||||
|
||||
@@ -18,7 +18,7 @@ type FilesystemClient struct {
|
||||
lfsDir string
|
||||
}
|
||||
|
||||
// BatchSize returns the preferred size of batchs to process
|
||||
// BatchSize returns the preferred size of batches to process
|
||||
func (c *FilesystemClient) BatchSize() int {
|
||||
return 1
|
||||
}
|
||||
@@ -29,8 +29,12 @@ func newFilesystemClient(endpoint *url.URL) *FilesystemClient {
|
||||
return &FilesystemClient{lfsDir}
|
||||
}
|
||||
|
||||
func (c *FilesystemClient) objectPath(oid string) string {
|
||||
return filepath.Join(c.lfsDir, oid[0:2], oid[2:4], oid)
|
||||
func (c *FilesystemClient) objectPath(oid string) (string, error) {
|
||||
if !isValidOid(oid) {
|
||||
return "", ErrInvalidOIDFormat
|
||||
}
|
||||
// HINT: LFS-INTERNAL-PATH-PATTERN
|
||||
return filepath.Join(c.lfsDir, oid[0:2], oid[2:4], oid), nil
|
||||
}
|
||||
|
||||
// Download reads the specific LFS object from the target path
|
||||
@@ -38,8 +42,10 @@ func (c *FilesystemClient) Download(ctx context.Context, objects []Pointer, call
|
||||
for _, object := range objects {
|
||||
p := Pointer{object.Oid, object.Size}
|
||||
|
||||
objectPath := c.objectPath(p.Oid)
|
||||
|
||||
objectPath, err := c.objectPath(p.Oid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.Open(objectPath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -57,8 +63,10 @@ func (c *FilesystemClient) Upload(ctx context.Context, objects []Pointer, callba
|
||||
for _, object := range objects {
|
||||
p := Pointer{object.Oid, object.Size}
|
||||
|
||||
objectPath := c.objectPath(p.Oid)
|
||||
|
||||
objectPath, err := c.objectPath(p.Oid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(objectPath), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+25
-11
@@ -10,9 +10,11 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
// spec: https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md
|
||||
@@ -47,8 +49,6 @@ func ReadPointer(reader io.Reader) (Pointer, error) {
|
||||
return ReadPointerFromBuffer(buf)
|
||||
}
|
||||
|
||||
var oidPattern = regexp.MustCompile(`^[a-f\d]{64}$`)
|
||||
|
||||
// ReadPointerFromBuffer will return a pointer if the provided byte slice is a pointer file or an error otherwise.
|
||||
func ReadPointerFromBuffer(buf []byte) (Pointer, error) {
|
||||
var p Pointer
|
||||
@@ -65,7 +65,7 @@ func ReadPointerFromBuffer(buf []byte) (Pointer, error) {
|
||||
|
||||
// spec says "key/value pairs MUST be sorted alphabetically in ascending order (version is exception and must be the first)"
|
||||
oid := strings.TrimPrefix(splitLines[1], MetaFileOidPrefix)
|
||||
if len(oid) != 64 || !oidPattern.MatchString(oid) {
|
||||
if !isValidOid(oid) {
|
||||
return p, ErrInvalidOIDFormat
|
||||
}
|
||||
size, err := strconv.ParseInt(strings.TrimPrefix(splitLines[2], "size "), 10, 64)
|
||||
@@ -79,13 +79,24 @@ func ReadPointerFromBuffer(buf []byte) (Pointer, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func isValidOid(oid string) bool {
|
||||
if len(oid) != 64 {
|
||||
return false
|
||||
}
|
||||
for i := range len(oid) {
|
||||
c := oid[i]
|
||||
valid := (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')
|
||||
if !valid {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// IsValid checks if the pointer has a valid structure.
|
||||
// It doesn't check if the pointed-to-content exists.
|
||||
func (p Pointer) IsValid() bool {
|
||||
if len(p.Oid) != 64 {
|
||||
return false
|
||||
}
|
||||
if !oidPattern.MatchString(p.Oid) {
|
||||
if !isValidOid(p.Oid) {
|
||||
return false
|
||||
}
|
||||
if p.Size < 0 {
|
||||
@@ -102,10 +113,13 @@ func (p Pointer) StringContent() string {
|
||||
|
||||
// RelativePath returns the relative storage path of the pointer
|
||||
func (p Pointer) RelativePath() string {
|
||||
if len(p.Oid) < 5 {
|
||||
return p.Oid
|
||||
if !isValidOid(p.Oid) {
|
||||
setting.PanicInDevOrTesting("invalid LFS OID: %s", p.Oid)
|
||||
return "invalid-lfs-oid-" + util.PathJoinRelX(p.Oid)
|
||||
}
|
||||
|
||||
// HINT: LFS-INTERNAL-PATH-PATTERN: this path pattern is only used by Gitea internally,
|
||||
// It isn't the same as the path pattern used by Git LFS, which is: <oid[0:2]>/<oid[2:4]>/<oid>
|
||||
// However, there are other parts of Gitea that rely on such path pattern.
|
||||
return path.Join(p.Oid[0:2], p.Oid[2:4], p.Oid[4:])
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -22,8 +25,10 @@ func TestRelativePath(t *testing.T) {
|
||||
expected := path.Join("4d", "7a", "214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393")
|
||||
assert.Equal(t, expected, p.RelativePath())
|
||||
|
||||
defer test.MockVariableValue(&setting.IsProd, true)()
|
||||
defer test.MockVariableValue(&setting.IsInTesting, false)()
|
||||
p2 := Pointer{Oid: "4d7a"}
|
||||
assert.Equal(t, "4d7a", p2.RelativePath())
|
||||
assert.Equal(t, "invalid-lfs-oid-4d7a", p2.RelativePath())
|
||||
}
|
||||
|
||||
func TestIsValid(t *testing.T) {
|
||||
|
||||
@@ -36,7 +36,7 @@ func b64encode(data []byte) string {
|
||||
func camoHandleLink(link string) string {
|
||||
if setting.Camo.Enabled {
|
||||
lnkURL, err := url.Parse(link)
|
||||
if err == nil && lnkURL.IsAbs() && !strings.HasPrefix(link, setting.AppURL) &&
|
||||
if err == nil && (lnkURL.Scheme == "http" || lnkURL.Scheme == "https") && !strings.HasPrefix(link, setting.AppURL) &&
|
||||
(setting.Camo.Always || lnkURL.Scheme != "https") {
|
||||
return CamoEncode(link)
|
||||
}
|
||||
|
||||
@@ -15,8 +15,11 @@ func TestCamoHandleLink(t *testing.T) {
|
||||
setting.AppURL = "https://gitea.com"
|
||||
// Test media proxy
|
||||
setting.Camo.Enabled = true
|
||||
setting.Camo.Always = false
|
||||
setting.Camo.ServerURL = "https://image.proxy"
|
||||
setting.Camo.HMACKey = "geheim"
|
||||
dataURL := "data:image/png;base64,i//V"
|
||||
assert.Equal(t, dataURL, camoHandleLink(dataURL))
|
||||
|
||||
assert.Equal(t,
|
||||
"https://gitea.com/img.jpg",
|
||||
@@ -29,6 +32,7 @@ func TestCamoHandleLink(t *testing.T) {
|
||||
camoHandleLink("http://testimages.org/img.jpg"))
|
||||
|
||||
setting.Camo.Always = true
|
||||
assert.Equal(t, dataURL, camoHandleLink(dataURL))
|
||||
assert.Equal(t,
|
||||
"https://gitea.com/img.jpg",
|
||||
camoHandleLink("https://gitea.com/img.jpg"))
|
||||
|
||||
@@ -355,6 +355,8 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod
|
||||
return visitNodeImg(ctx, node)
|
||||
} else if node.Data == "video" {
|
||||
return visitNodeVideo(ctx, node)
|
||||
} else if node.Data == "source" {
|
||||
return visitNodePictureSource(ctx, node)
|
||||
}
|
||||
|
||||
if node.Data == "a" {
|
||||
@@ -363,10 +365,11 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod
|
||||
// because the content there is not processable, for example: the content is a commit id or a full URL.
|
||||
procs = emojiProcessors
|
||||
}
|
||||
next := node.NextSibling
|
||||
for n := node.FirstChild; n != nil; {
|
||||
n = visitNode(ctx, procs, n)
|
||||
}
|
||||
return node.NextSibling
|
||||
return next
|
||||
}
|
||||
|
||||
// createKeyword() renders a highlighted version of an action keyword
|
||||
|
||||
+32
-10
@@ -6,6 +6,7 @@ package markup
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/htmlutil"
|
||||
"gitea.dev/modules/markup/common"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
@@ -127,6 +128,10 @@ func processNodeA(ctx *RenderContext, node *html.Node) {
|
||||
}
|
||||
}
|
||||
|
||||
func resolveMediaLinkWithCamo(ctx *RenderContext, link string) string {
|
||||
return camoHandleLink(ctx.RenderHelper.ResolveLink(link, LinkTypeMedia))
|
||||
}
|
||||
|
||||
func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) {
|
||||
next = img.NextSibling
|
||||
attrSrc, hasLazy := "", false
|
||||
@@ -152,19 +157,21 @@ func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) {
|
||||
cnt++
|
||||
}
|
||||
if isLinkable {
|
||||
linkNode := img
|
||||
if img.Parent.Data == "picture" {
|
||||
linkNode = img.Parent
|
||||
}
|
||||
wrapper := &html.Node{Type: html.ElementNode, Data: "a", Attr: []html.Attribute{
|
||||
{Key: "href", Val: ctx.RenderHelper.ResolveLink(imgSrcOrigin, LinkTypeDefault)},
|
||||
{Key: "target", Val: "_blank"},
|
||||
}}
|
||||
parent := img.Parent
|
||||
imgNext := img.NextSibling
|
||||
parent.RemoveChild(img)
|
||||
parent.InsertBefore(wrapper, imgNext)
|
||||
wrapper.AppendChild(img)
|
||||
parent := linkNode.Parent
|
||||
linkNext := linkNode.NextSibling
|
||||
parent.RemoveChild(linkNode)
|
||||
parent.InsertBefore(wrapper, linkNext)
|
||||
wrapper.AppendChild(linkNode)
|
||||
}
|
||||
|
||||
imgAttr.Val = ctx.RenderHelper.ResolveLink(imgSrcOrigin, LinkTypeMedia)
|
||||
imgAttr.Val = camoHandleLink(imgAttr.Val)
|
||||
imgAttr.Val = resolveMediaLinkWithCamo(ctx, imgSrcOrigin)
|
||||
img.Attr[i] = imgAttr
|
||||
}
|
||||
if !RenderBehaviorForTesting.DisableAdditionalAttributes && !hasLazy && !strings.HasPrefix(attrSrc, "data:") {
|
||||
@@ -179,9 +186,24 @@ func visitNodeVideo(ctx *RenderContext, node *html.Node) (next *html.Node) {
|
||||
if attr.Key != "src" {
|
||||
continue
|
||||
}
|
||||
attr.Val = ctx.RenderHelper.ResolveLink(attr.Val, LinkTypeMedia)
|
||||
attr.Val = camoHandleLink(attr.Val)
|
||||
attr.Val = resolveMediaLinkWithCamo(ctx, attr.Val)
|
||||
node.Attr[i] = attr
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
func visitNodePictureSource(ctx *RenderContext, node *html.Node) (next *html.Node) {
|
||||
next = node.NextSibling
|
||||
for attrIdx, attr := range node.Attr {
|
||||
if attr.Key != "srcset" {
|
||||
continue
|
||||
}
|
||||
srcsetCandidates := htmlutil.ParseSrcset(attr.Val)
|
||||
for i, candidate := range srcsetCandidates {
|
||||
srcsetCandidates[i].SrcLink = resolveMediaLinkWithCamo(ctx, candidate.SrcLink)
|
||||
}
|
||||
attr.Val = htmlutil.EncodeSrcset(srcsetCandidates)
|
||||
node.Attr[attrIdx] = attr
|
||||
}
|
||||
return next
|
||||
}
|
||||
@@ -42,7 +42,7 @@ func resolveLinkRelative(ctx context.Context, base, cur, link string, absolute b
|
||||
finalLink += "?" + linkURL.RawQuery
|
||||
}
|
||||
if linkURL.Fragment != "" {
|
||||
finalLink += "#" + linkURL.Fragment
|
||||
finalLink += "#" + linkURL.EscapedFragment()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestResolveLinkRelative(t *testing.T) {
|
||||
assert.Equal(t, "/a/c", resolveLinkRelative(ctx, "/a", "b", "/c", false))
|
||||
assert.Equal(t, "/a/c#id", resolveLinkRelative(ctx, "/a", "b", "/c#id", false))
|
||||
assert.Equal(t, "/a/%2f?k=/", resolveLinkRelative(ctx, "/a", "b", "/%2f/?k=/", false))
|
||||
assert.Equal(t, "/a/b/c?k=v#id", resolveLinkRelative(ctx, "/a", "b", "c/?k=v#id", false))
|
||||
assert.Equal(t, "/a/b/c?k=v#with%20space", resolveLinkRelative(ctx, "/a", "b", "c/?k=v#with%20space", false))
|
||||
assert.Equal(t, "%invalid", resolveLinkRelative(ctx, "/a", "b", "%invalid", false))
|
||||
assert.Equal(t, "http://localhost:3000/a", resolveLinkRelative(ctx, "/a", "", "", true))
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -343,7 +344,9 @@ func parseUploadPackage(upload *packageUpload) (*Package, error) {
|
||||
p.DistTags = append(p.DistTags, tag)
|
||||
}
|
||||
|
||||
p.Filename = strings.ToLower(fmt.Sprintf("%s-%s.tgz", name, p.Version))
|
||||
filename := fmt.Sprintf("%s-%s.tgz", name, p.Version)
|
||||
p.Filename = strings.ToLower(filename)
|
||||
p.Metadata.GiteaTarballPath = util.PathEscapeSegments(p.Name) + "/-/" + url.PathEscape(filename) // canonical URL clients derive from name and version, file lookups ignore case
|
||||
|
||||
attachment := upload.Attachments[meta.Name+"-"+meta.Version+".tgz"] // not the sigstore bundle of `npm publish --provenance`
|
||||
if attachment == nil && len(upload.Attachments) == 1 {
|
||||
|
||||
@@ -23,7 +23,7 @@ func TestParsePackage(t *testing.T) {
|
||||
packageScope := "@scope"
|
||||
packageName := "test-package"
|
||||
packageFullName := packageScope + "/" + packageName
|
||||
packageVersion := "1.0.1-pre"
|
||||
packageVersion := "1.0.1-Pre"
|
||||
packageTag := "latest"
|
||||
packageAuthor := "KN4CK3R"
|
||||
packageBin := "gitea"
|
||||
@@ -287,7 +287,8 @@ func TestParsePackage(t *testing.T) {
|
||||
assert.Equal(t, packageFullName, p.Name)
|
||||
assert.Equal(t, packageVersion, p.Version)
|
||||
assert.Equal(t, []string{packageTag}, p.DistTags)
|
||||
assert.Equal(t, fmt.Sprintf("%s-%s.tgz", strings.Split(packageFullName, "/")[1], packageVersion), p.Filename)
|
||||
assert.Equal(t, strings.ToLower(fmt.Sprintf("%s-%s.tgz", packageName, packageVersion)), p.Filename)
|
||||
assert.Equal(t, fmt.Sprintf("%s/-/%s-%s.tgz", packageFullName, packageName, packageVersion), p.Metadata.GiteaTarballPath)
|
||||
b, _ = base64.StdEncoding.DecodeString(data)
|
||||
assert.Equal(t, b, p.Data)
|
||||
assert.Equal(t, packageName, p.Metadata.Name)
|
||||
|
||||
@@ -34,4 +34,6 @@ type Metadata struct {
|
||||
Funding any `json:"funding,omitempty"`
|
||||
AcceptDependencies map[string]string `json:"accept_dependencies,omitempty"`
|
||||
Deprecated string `json:"deprecated,omitempty"`
|
||||
|
||||
GiteaTarballPath string `json:"gitea_tarball_path,omitempty"` // Gitea-internal, fixed at upload because lockfiles pin it
|
||||
}
|
||||
@@ -38,14 +38,14 @@ func ReloadTemplates(ctx context.Context) ResponseExtra {
|
||||
|
||||
// FlushOptions represents the options for the flush call
|
||||
type FlushOptions struct {
|
||||
Timeout time.Duration
|
||||
Timeout int64
|
||||
NonBlocking bool
|
||||
}
|
||||
|
||||
// FlushQueues calls the internal flush-queues function
|
||||
func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) ResponseExtra {
|
||||
reqURL := setting.LocalURL + "api/internal/manager/flush-queues"
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST", FlushOptions{Timeout: timeout, NonBlocking: nonBlocking})
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST", FlushOptions{Timeout: int64(timeout), NonBlocking: nonBlocking})
|
||||
if timeout > 0 {
|
||||
req.SetReadWriteTimeout(timeout + 10*time.Second)
|
||||
}
|
||||
|
||||
@@ -89,7 +89,6 @@ func loadGitFrom(rootCfg ConfigProvider) {
|
||||
GitConfig.Options = make(map[string]string)
|
||||
GitConfig.SetOption("diff.algorithm", "histogram")
|
||||
GitConfig.SetOption("core.logAllRefUpdates", "true")
|
||||
GitConfig.SetOption("gc.reflogExpire", "90")
|
||||
|
||||
secGitReflog := rootCfg.Section("git.reflog")
|
||||
if secGitReflog.HasKey("ENABLED") {
|
||||
@@ -97,8 +96,8 @@ func loadGitFrom(rootCfg ConfigProvider) {
|
||||
GitConfig.SetOption("core.logAllRefUpdates", secGitReflog.Key("ENABLED").In("true", []string{"true", "false"}))
|
||||
}
|
||||
if secGitReflog.HasKey("EXPIRATION") {
|
||||
deprecatedSetting(rootCfg, "git.reflog", "EXPIRATION", "git.config", "core.reflogExpire", "1.21")
|
||||
GitConfig.SetOption("gc.reflogExpire", secGitReflog.Key("EXPIRATION").String())
|
||||
deprecatedSetting(rootCfg, "git.reflog", "EXPIRATION", "git.config", "gc.reflogExpire", "1.21")
|
||||
GitConfig.SetOption("gc.reflogExpire", secGitReflog.Key("EXPIRATION").String()+".days")
|
||||
}
|
||||
|
||||
for _, key := range secGitConfig.Keys() {
|
||||
|
||||
@@ -38,8 +38,8 @@ diff.algorithm = other
|
||||
}
|
||||
|
||||
func TestGitReflog(t *testing.T) {
|
||||
defer test.MockVariableValue(&Git)
|
||||
defer test.MockVariableValue(&GitConfig)
|
||||
defer test.MockVariableValue(&Git)()
|
||||
defer test.MockVariableValue(&GitConfig)()
|
||||
|
||||
// default reflog config without legacy options
|
||||
cfg, err := NewConfigProviderFromData(``)
|
||||
@@ -47,7 +47,7 @@ func TestGitReflog(t *testing.T) {
|
||||
loadGitFrom(cfg)
|
||||
|
||||
assert.Equal(t, "true", GitConfig.GetOption("core.logAllRefUpdates"))
|
||||
assert.Equal(t, "90", GitConfig.GetOption("gc.reflogExpire"))
|
||||
assert.Empty(t, GitConfig.GetOption("gc.reflogExpire"))
|
||||
|
||||
// custom reflog config by legacy options
|
||||
cfg, err = NewConfigProviderFromData(`
|
||||
@@ -59,5 +59,5 @@ EXPIRATION = 123
|
||||
loadGitFrom(cfg)
|
||||
|
||||
assert.Equal(t, "false", GitConfig.GetOption("core.logAllRefUpdates"))
|
||||
assert.Equal(t, "123", GitConfig.GetOption("gc.reflogExpire"))
|
||||
assert.Equal(t, "123.days", GitConfig.GetOption("gc.reflogExpire"))
|
||||
}
|
||||
@@ -108,7 +108,7 @@ var OAuth2 = struct {
|
||||
JWTSigningAlgorithm: "RS256",
|
||||
JWTSigningPrivateKeyFile: "jwt/private.pem",
|
||||
MaxTokenLength: math.MaxInt16,
|
||||
DefaultApplications: []string{"git-credential-oauth", "git-credential-manager", "tea", "gitea-app"},
|
||||
DefaultApplications: []string{"git-credential-oauth", "git-credential-manager", "tea", "gitea-app", "gitea-vscode"},
|
||||
}
|
||||
|
||||
func loadOAuth2From(rootCfg ConfigProvider) {
|
||||
|
||||
@@ -62,7 +62,7 @@ func TestGetGeneralSigningSecretSave(t *testing.T) {
|
||||
func TestOauth2DefaultApplications(t *testing.T) {
|
||||
cfg, _ := NewConfigProviderFromData(``)
|
||||
loadOAuth2From(cfg)
|
||||
assert.Equal(t, []string{"git-credential-oauth", "git-credential-manager", "tea", "gitea-app"}, OAuth2.DefaultApplications)
|
||||
assert.Equal(t, []string{"git-credential-oauth", "git-credential-manager", "tea", "gitea-app", "gitea-vscode"}, OAuth2.DefaultApplications)
|
||||
|
||||
cfg, _ = NewConfigProviderFromData(`[oauth2]
|
||||
DEFAULT_APPLICATIONS = tea
|
||||
|
||||
@@ -303,7 +303,7 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("repository")
|
||||
Repository.DisableHTTPGit = sec.Key("DISABLE_HTTP_GIT").MustBool()
|
||||
Repository.UseCompatSSHURI = sec.Key("USE_COMPAT_SSH_URI").MustBool()
|
||||
Repository.GoGetCloneURLProtocol = sec.Key("GO_GET_CLONE_URL_PROTOCOL").MustString("https")
|
||||
Repository.GoGetCloneURLProtocol = sec.Key("GO_GET_CLONE_URL_PROTOCOL").String()
|
||||
// MAX_CREATION_LIMIT is a shortcut that sets the default for the two per-type limits below.
|
||||
// USER_/ORG_MAX_CREATION_LIMIT take precedence when explicitly set.
|
||||
Repository.MaxCreationLimit = sec.Key("MAX_CREATION_LIMIT").MustInt(-1) // FIXME: INI-MUST-SIDE-EFFECT
|
||||
|
||||
@@ -14,8 +14,6 @@ import (
|
||||
|
||||
"gitea.dev/modules/glob"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"golang.org/x/net/idna"
|
||||
)
|
||||
|
||||
type globalVarsStruct struct {
|
||||
@@ -24,6 +22,8 @@ type globalVarsStruct struct {
|
||||
invalidUsernamePattern *regexp.Regexp
|
||||
validBadgeSlugPattern *regexp.Regexp
|
||||
invalidBadgeSlugPattern *regexp.Regexp
|
||||
validEmailHostName *regexp.Regexp
|
||||
validEmailHostIP *regexp.Regexp
|
||||
}
|
||||
|
||||
var globalVars = sync.OnceValue(func() *globalVarsStruct {
|
||||
@@ -33,6 +33,8 @@ var globalVars = sync.OnceValue(func() *globalVarsStruct {
|
||||
invalidUsernamePattern: regexp.MustCompile(`[-._]{2,}|[-._]$`), // No consecutive or trailing non-alphanumeric chars
|
||||
validBadgeSlugPattern: regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*$`),
|
||||
invalidBadgeSlugPattern: regexp.MustCompile(`[-._]{2,}|[-._]$`),
|
||||
validEmailHostName: regexp.MustCompile(`^[a-zA-Z0-9][-.\w]*$`),
|
||||
validEmailHostIP: regexp.MustCompile(`(?i)^\[([0-9.]+|ipv6:[0-9a-f:.]+)\]$`),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -124,10 +126,12 @@ func IsEmailAddressValid(email string) bool {
|
||||
return false
|
||||
}
|
||||
_, domain, _ := strings.Cut(email, "@")
|
||||
if strings.HasPrefix(domain, "[") {
|
||||
// address like "foo@[192.168.1.2]"
|
||||
return true
|
||||
if !globalVars().validEmailHostName.MatchString(domain) && !globalVars().validEmailHostIP.MatchString(domain) {
|
||||
return false
|
||||
}
|
||||
_, err = idna.Registration.ToASCII(domain)
|
||||
return err == nil
|
||||
if strings.HasPrefix(domain, "-") || strings.HasSuffix(domain, "-") ||
|
||||
strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -135,8 +135,8 @@ func shortenFilename(filename, fallback string) string {
|
||||
if filename == "" {
|
||||
return fallback
|
||||
}
|
||||
if lastIndex := strings.LastIndexByte(filename, '/'); lastIndex >= 0 {
|
||||
if secondLastIndex := strings.LastIndexByte(filename[:lastIndex], '/'); secondLastIndex >= 0 {
|
||||
if dir, _, ok := strings.CutLast(filename, "/"); ok {
|
||||
if secondLastIndex := strings.LastIndexByte(dir, '/'); secondLastIndex >= 0 {
|
||||
return filename[secondLastIndex+1:]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -930,7 +930,7 @@
|
||||
"settings.delete_account_title": "Delete User Account",
|
||||
"settings.delete_account_desc": "Are you sure you want to permanently delete this user account?",
|
||||
"settings.email_notifications.enable": "Enable Email Notifications",
|
||||
"settings.email_notifications.onmention": "Only Email on Mention",
|
||||
"settings.email_notifications.onmention_and_custom": "Only Email on Mention and Custom Watches",
|
||||
"settings.email_notifications.disable": "Disable Email Notifications",
|
||||
"settings.email_notifications.submit": "Set Email Preference",
|
||||
"settings.email_notifications.andyourown": "And Your Own Notifications",
|
||||
|
||||
@@ -290,13 +290,22 @@ func (r *artifactV4Routes) findArtifactByName(ctx *ArtifactContext, runID int64,
|
||||
return &art, nil
|
||||
}
|
||||
|
||||
// maxProtobufBodySize caps the Twirp request body size accepted by parseProtobufBody.
|
||||
// These requests only ever carry a handful of small scalar fields (run IDs, artifact
|
||||
// names, etc.), so this is far above any legitimate payload while still bounding memory use.
|
||||
const maxProtobufBodySize = 1024 * 1024 // 1 MiB
|
||||
|
||||
func (r *artifactV4Routes) parseProtobufBody(ctx *ArtifactContext, req protoreflect.ProtoMessage) bool {
|
||||
body, err := io.ReadAll(ctx.Req.Body)
|
||||
body, err := io.ReadAll(io.LimitReader(ctx.Req.Body, maxProtobufBodySize+1))
|
||||
if err != nil {
|
||||
log.Error("Error decode request body: %v", err)
|
||||
ctx.HTTPError(http.StatusInternalServerError, "Error decode request body")
|
||||
return false
|
||||
}
|
||||
if len(body) > maxProtobufBodySize {
|
||||
ctx.HTTPError(http.StatusRequestEntityTooLarge, "request body too large")
|
||||
return false
|
||||
}
|
||||
err = protojson.Unmarshal(body, req)
|
||||
if err != nil {
|
||||
log.Error("Error decode request body: %v", err)
|
||||
@@ -646,9 +655,10 @@ func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) {
|
||||
Size: artifact.FileSize,
|
||||
}
|
||||
}
|
||||
for _, artifact := range table {
|
||||
if artifact != nil {
|
||||
list = append(list, artifact)
|
||||
for _, artifact := range artifacts {
|
||||
if item := table[artifact.ArtifactName]; item != nil {
|
||||
list = append(list, item)
|
||||
delete(table, artifact.ArtifactName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -414,7 +414,7 @@ func CommonRoutes() *web.Router {
|
||||
g.UseUnescapedPath()
|
||||
g.MatchPath("DELETE", packageId+"/-/<version>/<filename>/-rev/<revision>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
|
||||
g.MatchPath("DELETE", packageId+"/-/<filename>/-rev/<revision>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
|
||||
g.MatchPath("GET", packageId+"/-/<version>/<filename>", npm.DownloadPackageFileByName) // former tarball URL, still in lockfiles
|
||||
g.MatchPath("GET", packageId+"/-/<version>/<filename>", npm.DownloadPackageFileByName) // 1.27 and 28.0 URL, still advertised for older uploads
|
||||
g.MatchPath("GET", packageId+"/-/<filename>", npm.DownloadPackageFileByName)
|
||||
g.MatchPath("DELETE", packageId+"/-rev/<revision>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackage)
|
||||
g.MatchPath("PUT", packageId+"/-rev/<revision>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePreview)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"slices"
|
||||
"sort"
|
||||
"time"
|
||||
@@ -82,6 +83,11 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package
|
||||
|
||||
metadata := packages_model.DescriptorMetadata[*npm_module.Metadata](pd)
|
||||
|
||||
tarballPath := metadata.GiteaTarballPath
|
||||
if tarballPath == "" {
|
||||
tarballPath = fmt.Sprintf("%s/-/%s/%s", url.QueryEscape(pd.Package.Name), url.PathEscape(pd.Version.Version), url.PathEscape(pd.Files[0].File.LowerName)) // 1.27 URL, pinned by older lockfiles
|
||||
}
|
||||
|
||||
return &npm_module.PackageMetadataVersion{
|
||||
ID: fmt.Sprintf("%s@%s", pd.Package.Name, pd.Version.Version),
|
||||
Name: pd.Package.Name,
|
||||
@@ -113,7 +119,7 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package
|
||||
Dist: npm_module.PackageDistribution{
|
||||
Shasum: pd.Files[0].Blob.HashSHA1,
|
||||
Integrity: "sha512-" + base64.StdEncoding.EncodeToString(hashBytes),
|
||||
Tarball: fmt.Sprintf("%s/%s/-/%s", registryURL, pd.Package.Name, pd.Files[0].File.LowerName), // npmjs shape, which npm parses for allowScripts and yarn keeps registry-relative
|
||||
Tarball: registryURL + "/" + tarballPath,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,13 +19,13 @@ import (
|
||||
|
||||
func TestCreatePackageMetadataResponse(t *testing.T) {
|
||||
repository := npm_module.Repository{Type: "git", URL: "https://gitea.dev/alice/test.git"}
|
||||
descriptor := func(v string, publishedUnix int64, repo npm_module.Repository) *packages_model.PackageDescriptor {
|
||||
descriptor := func(v string, publishedUnix int64, repo npm_module.Repository, tarballPath string) *packages_model.PackageDescriptor {
|
||||
return &packages_model.PackageDescriptor{
|
||||
Package: &packages_model.Package{Name: "@scope/test"},
|
||||
Owner: &user_model.User{Name: "alice"},
|
||||
Version: &packages_model.PackageVersion{Version: v, CreatedUnix: timeutil.TimeStamp(publishedUnix)},
|
||||
SemVer: version.Must(version.NewVersion(v)),
|
||||
Metadata: &npm_module.Metadata{Readme: v, Keywords: []string{"gitea"}, Repository: repo},
|
||||
Metadata: &npm_module.Metadata{Readme: v, Keywords: []string{"gitea"}, Repository: repo, GiteaTarballPath: tarballPath},
|
||||
Files: []*packages_model.PackageFileDescriptor{{
|
||||
File: &packages_model.PackageFile{LowerName: "test-" + v + ".tgz"},
|
||||
Blob: &packages_model.PackageBlob{},
|
||||
@@ -34,9 +34,9 @@ func TestCreatePackageMetadataResponse(t *testing.T) {
|
||||
}
|
||||
|
||||
result := createPackageMetadataResponse("https://gitea.dev/api/packages/alice/npm", []*packages_model.PackageDescriptor{
|
||||
descriptor("1.1.0", 1000, npm_module.Repository{}),
|
||||
descriptor("2.0.0-rc.1", 1500, repository),
|
||||
descriptor("1.0.0", 2000, repository),
|
||||
descriptor("1.1.0", 1000, npm_module.Repository{}, "@scope/test/-/test-1.1.0.tgz"),
|
||||
descriptor("2.0.0-rc.1", 1500, repository, ""),
|
||||
descriptor("1.0.0", 2000, repository, ""),
|
||||
})
|
||||
|
||||
assert.Equal(t, map[string]time.Time{
|
||||
@@ -54,9 +54,13 @@ func TestCreatePackageMetadataResponse(t *testing.T) {
|
||||
assert.Equal(t, []string{"gitea"}, result.Versions["1.0.0"].Keywords)
|
||||
assert.Equal(t, []npm_module.User{{Name: "alice"}}, result.Versions["1.0.0"].Maintainers)
|
||||
assert.Equal(t,
|
||||
"https://gitea.dev/api/packages/alice/npm/@scope/test/-/test-1.0.0.tgz",
|
||||
"https://gitea.dev/api/packages/alice/npm/%40scope%2Ftest/-/1.0.0/test-1.0.0.tgz",
|
||||
result.Versions["1.0.0"].Dist.Tarball,
|
||||
)
|
||||
assert.Equal(t,
|
||||
"https://gitea.dev/api/packages/alice/npm/@scope/test/-/test-1.1.0.tgz",
|
||||
result.Versions["1.1.0"].Dist.Tarball,
|
||||
)
|
||||
assert.Equal(t, repository, result.Versions["1.0.0"].Repository)
|
||||
|
||||
withoutRepository, err := json.Marshal(result.Versions["1.1.0"])
|
||||
|
||||
@@ -1656,8 +1656,8 @@ func Routes() *web.Router {
|
||||
})
|
||||
m.Group("/{index}", func() {
|
||||
m.Combo("").Get(repo.GetIssue).
|
||||
Patch(reqToken(), bind(api.EditIssueOption{}), repo.EditIssue).
|
||||
Delete(reqToken(), reqAdmin(), context.ReferencesGitRepo(), repo.DeleteIssue)
|
||||
Patch(reqToken(), mustNotBeArchived, bind(api.EditIssueOption{}), repo.EditIssue).
|
||||
Delete(reqToken(), reqAdmin(), mustNotBeArchived, context.ReferencesGitRepo(), repo.DeleteIssue)
|
||||
m.Combo("/assignees").
|
||||
Post(reqToken(), mustNotBeArchived, bind(api.IssueAssigneesOption{}), repo.AddIssueAssignees).
|
||||
Delete(reqToken(), mustNotBeArchived, bind(api.IssueAssigneesOption{}), repo.DeleteIssueAssignees)
|
||||
@@ -1671,10 +1671,10 @@ func Routes() *web.Router {
|
||||
m.Get("/timeline", repo.ListIssueCommentsAndTimeline)
|
||||
m.Group("/labels", func() {
|
||||
m.Combo("").Get(repo.ListIssueLabels).
|
||||
Post(reqToken(), bind(api.IssueLabelsOption{}), repo.AddIssueLabels).
|
||||
Put(reqToken(), bind(api.IssueLabelsOption{}), repo.ReplaceIssueLabels).
|
||||
Delete(reqToken(), repo.ClearIssueLabels)
|
||||
m.Delete("/{id}", reqToken(), repo.DeleteIssueLabel)
|
||||
Post(reqToken(), mustNotBeArchived, bind(api.IssueLabelsOption{}), repo.AddIssueLabels).
|
||||
Put(reqToken(), mustNotBeArchived, bind(api.IssueLabelsOption{}), repo.ReplaceIssueLabels).
|
||||
Delete(reqToken(), mustNotBeArchived, repo.ClearIssueLabels)
|
||||
m.Delete("/{id}", reqToken(), mustNotBeArchived, repo.DeleteIssueLabel)
|
||||
})
|
||||
m.Group("/times", func() {
|
||||
m.Combo("").
|
||||
|
||||
@@ -249,7 +249,7 @@ func DeleteLabel(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if err := issues_model.DeleteLabel(ctx, ctx.Org.Organization.ID, ctx.PathParamInt64("id")); err != nil {
|
||||
if err := issues_model.DeleteOrgLabel(ctx, ctx.Org.Organization.ID, ctx.PathParamInt64("id")); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -694,6 +694,8 @@ func EditIssue(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "412":
|
||||
// "$ref": "#/responses/error"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
form := web.GetForm[*api.EditIssueOption](ctx)
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
@@ -897,6 +899,8 @@ func DeleteIssue(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
issues_model "gitea.dev/models/issues"
|
||||
"gitea.dev/models/perm"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
@@ -571,8 +572,7 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
|
||||
if !issue_service.CanSelfChangeOrAccessContent(ctx.Doer, ctx.Repo.Permission, comment.Issue, comment.PosterID, perm.AccessModeWrite) {
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
@@ -676,11 +676,11 @@ func deleteIssueComment(ctx *context.APIContext) {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
|
||||
if !issue_service.CanSelfChangeOrAccessContent(ctx.Doer, ctx.Repo.Permission, comment.Issue, comment.PosterID, perm.AccessModeWrite) {
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return
|
||||
} else if !comment.Type.HasContentSupport() {
|
||||
}
|
||||
if !comment.Type.HasContentSupport() {
|
||||
ctx.Status(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
issues_model "gitea.dev/models/issues"
|
||||
"gitea.dev/models/perm"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
@@ -344,21 +345,13 @@ func DeleteIssueCommentAttachment(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment {
|
||||
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
|
||||
comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
return nil
|
||||
}
|
||||
if err := comment.LoadIssue(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil
|
||||
}
|
||||
if comment.Issue == nil || comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.APIError(http.StatusNotFound, "no matching issue comment found")
|
||||
return nil
|
||||
}
|
||||
|
||||
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
||||
ctx.APIErrorNotFound()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -379,7 +372,7 @@ func getIssueCommentAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Att
|
||||
}
|
||||
|
||||
func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues_model.Comment) bool {
|
||||
canEditComment := ctx.IsSigned && (ctx.Doer.ID == comment.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin()) && ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull)
|
||||
canEditComment := issue_service.CanSelfChangeOrAccessContent(ctx.Doer, ctx.Repo.Permission, comment.Issue, comment.PosterID, perm.AccessModeWrite)
|
||||
if !canEditComment {
|
||||
ctx.APIError(http.StatusForbidden, "user should have permission to edit comment")
|
||||
return false
|
||||
|
||||
@@ -102,6 +102,8 @@ func AddIssueLabels(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
form := web.GetForm[*api.IssueLabelsOption](ctx)
|
||||
issue, labels, err := prepareForReplaceOrAdd(ctx, *form)
|
||||
@@ -162,6 +164,8 @@ func DeleteIssueLabel(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
@@ -232,6 +236,8 @@ func ReplaceIssueLabels(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
form := web.GetForm[*api.IssueLabelsOption](ctx)
|
||||
issue, labels, err := prepareForReplaceOrAdd(ctx, *form)
|
||||
if err != nil {
|
||||
@@ -283,6 +289,8 @@ func ClearIssueLabels(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
issues_model "gitea.dev/models/issues"
|
||||
"gitea.dev/models/perm"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
@@ -184,28 +185,12 @@ func DeleteIssueCommentReaction(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) {
|
||||
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
|
||||
comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = comment.LoadIssue(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull) {
|
||||
if !issue_service.CanSelfChangeOrAccessContent(ctx.Doer, ctx.Repo.Permission, comment.Issue, comment.PosterID, perm.AccessModeRead) {
|
||||
ctx.APIError(http.StatusForbidden, "no permission to change reaction")
|
||||
return
|
||||
}
|
||||
@@ -420,7 +405,7 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
|
||||
return
|
||||
}
|
||||
|
||||
if issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
|
||||
if !issue_service.CanSelfChangeOrAccessContent(ctx.Doer, ctx.Repo.Permission, issue, issue.PosterID, perm.AccessModeRead) {
|
||||
ctx.APIError(http.StatusForbidden, "no permission to change reaction")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -268,7 +268,7 @@ func DeleteLabel(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if err := issues_model.DeleteLabel(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")); err != nil {
|
||||
if err := issues_model.DeleteRepoLabel(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -717,7 +717,9 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
|
||||
|
||||
// Default branch only updated if changed and exist or the repository is empty
|
||||
updateRepoLicense := false
|
||||
oldDefaultBranch := ""
|
||||
if opts.DefaultBranch != nil && repo.DefaultBranch != *opts.DefaultBranch && (repo.IsEmpty || git.IsBranchExist(ctx, ctx.Repo.Repository, *opts.DefaultBranch)) {
|
||||
oldDefaultBranch = repo.DefaultBranch
|
||||
repo.DefaultBranch = *opts.DefaultBranch
|
||||
if !repo.IsEmpty {
|
||||
if err := git.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
|
||||
@@ -733,6 +735,10 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
|
||||
return err
|
||||
}
|
||||
|
||||
if oldDefaultBranch != "" {
|
||||
actions_service.SwitchSchedulesToDefaultBranch(ctx, repo, oldDefaultBranch)
|
||||
}
|
||||
|
||||
if visibilityChanged {
|
||||
audit.Record(ctx, audit_model.RepositoryVisibility, repo, "visibility", repo.IsPrivate)
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64, workflowID string)
|
||||
if actor := ctx.FormString("actor"); actor != "" {
|
||||
user, err := user_model.GetUserByName(ctx, actor)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
opts.TriggerUserID = user.ID
|
||||
|
||||
@@ -5,6 +5,7 @@ package private
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/graceful"
|
||||
@@ -34,7 +35,7 @@ func FlushQueues(ctx *context.PrivateContext) {
|
||||
// Save the hammer ctx here - as a new one is created each time you call this.
|
||||
baseCtx := graceful.GetManager().HammerContext()
|
||||
go func() {
|
||||
err := queue.GetManager().FlushAll(baseCtx, opts.Timeout)
|
||||
err := queue.GetManager().FlushAll(baseCtx, time.Duration(opts.Timeout))
|
||||
if err != nil {
|
||||
log.Error("Flushing request timed-out with error: %v", err)
|
||||
}
|
||||
@@ -44,7 +45,7 @@ func FlushQueues(ctx *context.PrivateContext) {
|
||||
})
|
||||
return
|
||||
}
|
||||
err := queue.GetManager().FlushAll(ctx, opts.Timeout)
|
||||
err := queue.GetManager().FlushAll(ctx, time.Duration(opts.Timeout))
|
||||
if err != nil {
|
||||
ctx.PrivateUserErrorf(http.StatusRequestTimeout, "%v", err)
|
||||
return
|
||||
|
||||
@@ -77,9 +77,9 @@ func prepareCommonAuthPageData(ctx *context.Context, opt CommonAuthOptions) {
|
||||
|
||||
// autoSignIn reads cookie and try to auto-login.
|
||||
func autoSignIn(ctx *context.Context) (bool, error) {
|
||||
isSucceed := false
|
||||
keepRememberCookie := false
|
||||
defer func() {
|
||||
if !isSucceed {
|
||||
if !keepRememberCookie {
|
||||
ctx.DeleteSiteCookie(setting.CookieRememberName)
|
||||
}
|
||||
}()
|
||||
@@ -112,14 +112,20 @@ func autoSignIn(ctx *context.Context) (bool, error) {
|
||||
return false, fmt.Errorf("HasTwoFactorOrWebAuthn: %w", err)
|
||||
}
|
||||
|
||||
isSucceed = true
|
||||
|
||||
nt, token, err := auth_service.RegenerateAuthToken(ctx, t)
|
||||
if errors.Is(err, auth_service.ErrAuthTokenRotationConflict) {
|
||||
keepRememberCookie = true // A late response must not clear the winner's cookie.
|
||||
return false, nil
|
||||
}
|
||||
if errors.Is(err, auth_service.ErrAuthTokenExpired) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
|
||||
keepRememberCookie = true
|
||||
|
||||
if err := regenerateSession(ctx, map[string]any{
|
||||
session.KeyUID: u.ID,
|
||||
|
||||
@@ -31,7 +31,6 @@ func OIDCWellKnown(ctx *context.Context) {
|
||||
"introspection_endpoint": oidcBaseUrl + "/login/oauth/introspect",
|
||||
"response_types_supported": []string{
|
||||
"code",
|
||||
"id_token",
|
||||
},
|
||||
"id_token_signing_alg_values_supported": []string{
|
||||
oauth2_provider.DefaultSigningKey.SigningMethod().Alg(),
|
||||
|
||||
@@ -69,13 +69,7 @@ func goGet(ctx *context.Context) {
|
||||
|
||||
goGetImport := context.ComposeGoGetImport(ctx, ownerName, trimmedRepoName)
|
||||
|
||||
var cloneURL string
|
||||
if setting.Repository.GoGetCloneURLProtocol == "ssh" {
|
||||
cloneURL = repo_model.ComposeSSHCloneURL(ctx.Doer, ownerName, repoName)
|
||||
} else {
|
||||
cloneURL = repo_model.ComposeHTTPSCloneURL(ctx, ownerName, repoName)
|
||||
}
|
||||
goImportContent := fmt.Sprintf("%s git %s", goGetImport, cloneURL /*CloneLink*/)
|
||||
goImportContent := fmt.Sprintf("%s git %s", goGetImport, context.ComposeGoGetCloneURL(ctx, ownerName, trimmedRepoName))
|
||||
goSourceContent := fmt.Sprintf("%s _ %s %s", goGetImport, prefix+"{/dir}" /*GoDocDirectory*/, prefix+"{/dir}/{file}#L{line}" /*GoDocFile*/)
|
||||
goGetCli := fmt.Sprintf("go get %s%s", insecure, goGetImport)
|
||||
|
||||
|
||||
@@ -38,15 +38,20 @@ func Home(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
home(ctx, false)
|
||||
orgHome(ctx, false)
|
||||
}
|
||||
|
||||
func Repositories(ctx *context.Context) {
|
||||
home(ctx, true)
|
||||
orgHome(ctx, true)
|
||||
}
|
||||
|
||||
func home(ctx *context.Context, viewRepositories bool) {
|
||||
func orgHome(ctx *context.Context, viewRepositories bool) {
|
||||
org := ctx.Org.Organization
|
||||
if org == nil {
|
||||
// some requests may reach here by "user" route patterns, e.g.: "/{owner}/-/repositories"
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["PageIsUserProfile"] = true
|
||||
ctx.Data["Title"] = org.DisplayName()
|
||||
|
||||
@@ -85,7 +85,7 @@ func UpdateLabel(ctx *context.Context) {
|
||||
|
||||
// DeleteLabel delete a label
|
||||
func DeleteLabel(ctx *context.Context) {
|
||||
if err := issues_model.DeleteLabel(ctx, ctx.Org.Organization.ID, ctx.FormInt64("id")); err != nil {
|
||||
if err := issues_model.DeleteOrgLabel(ctx, ctx.Org.Organization.ID, ctx.FormInt64("id")); err != nil {
|
||||
ctx.Flash.Error("DeleteLabel: " + err.Error())
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
|
||||
|
||||
@@ -853,7 +853,7 @@ func convertToViewModel(ctx context.Context, locale translation.Locale, cursors
|
||||
}
|
||||
|
||||
for _, cursor := range cursors {
|
||||
if !cursor.Expanded {
|
||||
if !cursor.Expanded || cursor.Step < 0 || cursor.Step >= len(steps) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -88,6 +88,13 @@ func TestConvertToViewModel(t *testing.T) {
|
||||
viewJobSteps, _, err := convertToViewModel(t.Context(), translation.MockLocale{}, nil, task)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("out of range cursor step", func(t *testing.T) {
|
||||
cursors := []LogCursor{{Step: -1, Expanded: true}, {Step: 3, Expanded: true}}
|
||||
_, logs, err := convertToViewModel(t.Context(), translation.MockLocale{}, cursors, task)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, logs)
|
||||
})
|
||||
|
||||
expectedViewJobs := []*ViewJobStep{
|
||||
{
|
||||
Summary: "Set up job",
|
||||
|
||||
+19
-38
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"gitea.dev/models/db"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
"gitea.dev/models/perm"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
project_model "gitea.dev/models/project"
|
||||
"gitea.dev/models/renderhelper"
|
||||
@@ -186,8 +187,8 @@ func GetActionIssue(ctx *context.Context) *issues_model.Issue {
|
||||
return nil
|
||||
}
|
||||
issue.Repo = ctx.Repo.Repository
|
||||
checkIssueRights(ctx, issue)
|
||||
if ctx.Written() {
|
||||
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
}
|
||||
if err = issue.LoadAttributes(ctx); err != nil {
|
||||
@@ -197,13 +198,6 @@ func GetActionIssue(ctx *context.Context) *issues_model.Issue {
|
||||
return issue
|
||||
}
|
||||
|
||||
func checkIssueRights(ctx *context.Context, issue *issues_model.Issue) {
|
||||
if issue.IsPull && !ctx.Repo.Permission.CanRead(unit.TypePullRequests) ||
|
||||
!issue.IsPull && !ctx.Repo.Permission.CanRead(unit.TypeIssues) {
|
||||
ctx.NotFound(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func getActionIssues(ctx *context.Context) issues_model.IssueList {
|
||||
commaSeparatedIssueIDs := ctx.FormString("issue_ids")
|
||||
if len(commaSeparatedIssueIDs) == 0 {
|
||||
@@ -481,39 +475,26 @@ func UpdateIssueAssignee(ctx *context.Context) {
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
func canSelfChangeOrAccessContent(ctx *context.Context, refIssue *issues_model.Issue, contentPosterID int64, accessMode perm.AccessMode) bool {
|
||||
if !issue_service.CanSelfChangeOrAccessContent(ctx.Doer, ctx.Repo.Permission, refIssue, contentPosterID, accessMode) {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ChangeIssueReaction create a reaction for issue
|
||||
func ChangeIssueReaction(ctx *context.Context) {
|
||||
form := web.GetForm[*forms.ReactionForm](ctx)
|
||||
issue := GetActionIssue(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull)) {
|
||||
if log.IsTrace() {
|
||||
if ctx.IsSigned {
|
||||
issueType := "issues"
|
||||
if issue.IsPull {
|
||||
issueType = "pulls"
|
||||
}
|
||||
log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
|
||||
"User in Repo has Permissions: %-+v",
|
||||
ctx.Doer,
|
||||
issue.PosterID,
|
||||
issueType,
|
||||
ctx.Repo.Repository,
|
||||
ctx.Repo.Permission)
|
||||
} else {
|
||||
log.Trace("Permission Denied: Not logged in")
|
||||
}
|
||||
}
|
||||
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.ServerError("ChangeIssueReaction", errors.New(ctx.GetErrMsg()))
|
||||
ctx.HTTPError(http.StatusBadRequest, ctx.GetErrMsg())
|
||||
return
|
||||
}
|
||||
issue := GetActionIssue(ctx)
|
||||
if issue == nil {
|
||||
return
|
||||
}
|
||||
if !canSelfChangeOrAccessContent(ctx, issue, issue.PosterID, perm.AccessModeRead) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
git_model "gitea.dev/models/git"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
"gitea.dev/models/perm"
|
||||
"gitea.dev/models/renderhelper"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git"
|
||||
@@ -173,31 +174,38 @@ func NewComment(ctx *context.Context) {
|
||||
ctx.JSONRedirect(redirect)
|
||||
}
|
||||
|
||||
func getRepoIssueComment(ctx *context.Context, commentID int64) *issues_model.Comment {
|
||||
comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, commentID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetCommentWithRepoID", err)
|
||||
return nil
|
||||
}
|
||||
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
}
|
||||
return comment
|
||||
}
|
||||
|
||||
func getRepoIssueCommentForSelfWriteOrAccess(ctx *context.Context, commentID int64, accessMode perm.AccessMode) *issues_model.Comment {
|
||||
comment := getRepoIssueComment(ctx, commentID)
|
||||
if comment == nil {
|
||||
return nil
|
||||
}
|
||||
if !canSelfChangeOrAccessContent(ctx, comment.Issue, comment.PosterID, accessMode) {
|
||||
return nil
|
||||
}
|
||||
if !comment.Type.HasContentSupport() {
|
||||
ctx.HTTPError(http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
return comment
|
||||
}
|
||||
|
||||
// UpdateCommentContent change comment of issue's content
|
||||
func UpdateCommentContent(ctx *context.Context) {
|
||||
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := comment.LoadIssue(ctx); err != nil {
|
||||
ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.NotFound(issues_model.ErrCommentNotExist{})
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if !comment.Type.HasContentSupport() {
|
||||
ctx.HTTPError(http.StatusNoContent)
|
||||
comment := getRepoIssueCommentForSelfWriteOrAccess(ctx, ctx.PathParamInt64("id"), perm.AccessModeWrite)
|
||||
if comment == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -213,7 +221,7 @@ func UpdateCommentContent(ctx *context.Context) {
|
||||
oldContent := comment.Content
|
||||
comment.Content = newContent
|
||||
|
||||
if err = issue_service.UpdateComment(ctx, comment, contentVersion, ctx.Doer, oldContent); err != nil {
|
||||
if err := issue_service.UpdateComment(ctx, comment, contentVersion, ctx.Doer, oldContent); err != nil {
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
|
||||
} else if errors.Is(err, issues_model.ErrCommentAlreadyChanged) {
|
||||
@@ -243,6 +251,7 @@ func UpdateCommentContent(ctx *context.Context) {
|
||||
rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{
|
||||
FootnoteContextID: strconv.FormatInt(comment.ID, 10),
|
||||
})
|
||||
var err error
|
||||
renderedContent, err = markdown.RenderString(rctx, comment.Content)
|
||||
if err != nil {
|
||||
ctx.ServerError("RenderString", err)
|
||||
@@ -259,31 +268,12 @@ func UpdateCommentContent(ctx *context.Context) {
|
||||
|
||||
// DeleteComment delete comment of issue
|
||||
func DeleteComment(ctx *context.Context) {
|
||||
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
|
||||
comment := getRepoIssueCommentForSelfWriteOrAccess(ctx, ctx.PathParamInt64("id"), perm.AccessModeWrite)
|
||||
if comment == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := comment.LoadIssue(ctx); err != nil {
|
||||
ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.NotFound(issues_model.ErrCommentNotExist{})
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
} else if !comment.Type.HasContentSupport() {
|
||||
ctx.HTTPError(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
if err = issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil {
|
||||
if err := issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil {
|
||||
ctx.ServerError("DeleteComment", err)
|
||||
return
|
||||
}
|
||||
@@ -293,51 +283,12 @@ func DeleteComment(ctx *context.Context) {
|
||||
|
||||
// ChangeCommentReaction create a reaction for comment
|
||||
func ChangeCommentReaction(ctx *context.Context) {
|
||||
comment := getRepoIssueCommentForSelfWriteOrAccess(ctx, ctx.PathParamInt64("id"), perm.AccessModeRead)
|
||||
if comment == nil {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm[*forms.ReactionForm](ctx)
|
||||
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := comment.LoadIssue(ctx); err != nil {
|
||||
ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.NotFound(issues_model.ErrCommentNotExist{})
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull)) {
|
||||
if log.IsTrace() {
|
||||
if ctx.IsSigned {
|
||||
issueType := "issues"
|
||||
if comment.Issue.IsPull {
|
||||
issueType = "pulls"
|
||||
}
|
||||
log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
|
||||
"User in Repo has Permissions: %-+v",
|
||||
ctx.Doer,
|
||||
comment.Issue.PosterID,
|
||||
issueType,
|
||||
ctx.Repo.Repository,
|
||||
ctx.Repo.Permission)
|
||||
} else {
|
||||
log.Trace("Permission Denied: Not logged in")
|
||||
}
|
||||
}
|
||||
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if !comment.Type.HasContentSupport() {
|
||||
ctx.HTTPError(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
switch ctx.PathParam("action") {
|
||||
case "react":
|
||||
reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Content)
|
||||
@@ -365,7 +316,7 @@ func ChangeCommentReaction(ctx *context.Context) {
|
||||
|
||||
// Reload new reactions
|
||||
comment.Reactions = nil
|
||||
if err = comment.LoadReactions(ctx, ctx.Repo.Repository); err != nil {
|
||||
if err := comment.LoadReactions(ctx, ctx.Repo.Repository); err != nil {
|
||||
log.Info("comment.LoadReactions: %s", err)
|
||||
break
|
||||
}
|
||||
@@ -399,24 +350,8 @@ func ChangeCommentReaction(ctx *context.Context) {
|
||||
|
||||
// GetCommentAttachments returns attachments for the comment
|
||||
func GetCommentAttachments(ctx *context.Context) {
|
||||
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := comment.LoadIssue(ctx); err != nil {
|
||||
ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.NotFound(issues_model.ErrCommentNotExist{})
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
||||
ctx.NotFound(issues_model.ErrCommentNotExist{})
|
||||
comment := getRepoIssueComment(ctx, ctx.PathParamInt64("id"))
|
||||
if comment == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ func UpdateLabel(ctx *context.Context) {
|
||||
|
||||
// DeleteLabel delete a label
|
||||
func DeleteLabel(ctx *context.Context) {
|
||||
if err := issues_model.DeleteLabel(ctx, ctx.Repo.Repository.ID, ctx.FormInt64("id")); err != nil {
|
||||
if err := issues_model.DeleteRepoLabel(ctx, ctx.Repo.Repository.ID, ctx.FormInt64("id")); err != nil {
|
||||
ctx.Flash.Error("DeleteLabel: " + err.Error())
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"net/http"
|
||||
|
||||
issues_model "gitea.dev/models/issues"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
@@ -19,29 +18,7 @@ const (
|
||||
// IssueWatch sets issue watching
|
||||
func IssueWatch(ctx *context.Context) {
|
||||
issue := GetActionIssue(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull)) {
|
||||
if log.IsTrace() {
|
||||
if ctx.IsSigned {
|
||||
issueType := "issues"
|
||||
if issue.IsPull {
|
||||
issueType = "pulls"
|
||||
}
|
||||
log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
|
||||
"User in Repo has Permissions: %-+v",
|
||||
ctx.Doer,
|
||||
issue.PosterID,
|
||||
issueType,
|
||||
ctx.Repo.Repository,
|
||||
ctx.Repo.Permission)
|
||||
} else {
|
||||
log.Trace("Permission Denied: Not logged in")
|
||||
}
|
||||
}
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
if issue == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Loaded 100 of 167 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user