mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-18 22:36:43 +09:00
Compare commits
38
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
pull-requests: write
|
||||||
statuses: write
|
statuses: write
|
||||||
steps:
|
steps:
|
||||||
- uses: go-gitea/giteabot@f48c6a15e0d384f037aea19cc05ff5e9551096b9 # v1.1.0
|
- uses: go-gitea/giteabot@8033358e118c0bd97f5e9902dc5d6633eadc6aeb # v1.2.1
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITEABOT_TOKEN }}
|
github_token: ${{ secrets.GITEABOT_TOKEN }}
|
||||||
checks: ${{ github.event.inputs.checks || 'labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }}
|
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
|
# retry snapcraft uploads which can be flaky
|
||||||
- name: Publish snap
|
- name: Publish snap
|
||||||
run: |
|
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
|
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"
|
echo "::warning::snap upload attempt $attempt failed, retrying in 15s"
|
||||||
sleep 15
|
sleep 15
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ jobs:
|
|||||||
run: |
|
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
|
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"
|
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
|
- uses: orhun/git-cliff-action@a9a95522b26fe6403f7bb24031f21fb573d0f5ff # v4.9.1
|
||||||
with:
|
with:
|
||||||
args: --tag ${{ github.ref_name }} ${{ steps.range.outputs.range }}
|
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
|
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
|
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
|
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
|
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
|
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://github.com/git-ecosystem/git-credential-manager
|
||||||
;; * https://gitea.com/gitea/tea
|
;; * https://gitea.com/gitea/tea
|
||||||
;; * Gitea App (the official Gitea mobile app)
|
;; * 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.
|
;; 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.
|
;; 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]
|
;[git.config]
|
||||||
;diff.algorithm = histogram
|
;diff.algorithm = histogram
|
||||||
;core.logAllRefUpdates = true
|
;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
|
;; Force ssh:// clone url instead of scp-style uri when default SSH port is used
|
||||||
;USE_COMPAT_SSH_URI = false
|
;USE_COMPAT_SSH_URI = false
|
||||||
;;
|
;;
|
||||||
;; Value for the "go get" request returns the repository url as https or ssh, default is https
|
;; Scheme of the returned URL for the "go get" response.
|
||||||
;GO_GET_CLONE_URL_PROTOCOL = https
|
;; 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
|
;; Close issues as long as a commit on any branch marks it as fixed
|
||||||
;DEFAULT_CLOSE_ISSUES_VIA_COMMITS_IN_ANY_BRANCH = false
|
;DEFAULT_CLOSE_ISSUES_VIA_COMMITS_IN_ANY_BRANCH = false
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ module gitea.dev
|
|||||||
|
|
||||||
go 1.27
|
go 1.27
|
||||||
|
|
||||||
toolchain go1.27.1
|
toolchain go1.27.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
connectrpc.com/connect v1.21.0
|
connectrpc.com/connect v1.21.0
|
||||||
@@ -94,11 +94,11 @@ require (
|
|||||||
github.com/yuin/goldmark v1.8.6
|
github.com/yuin/goldmark v1.8.6
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||||
gitlab.com/gitlab-org/api/client-go/v3 v3.13.0
|
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/crypto v0.57.0
|
||||||
golang.org/x/image v0.46.0
|
golang.org/x/image v0.46.0
|
||||||
golang.org/x/mod v0.41.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/oauth2 v0.37.0
|
||||||
golang.org/x/sync v0.23.0
|
golang.org/x/sync v0.23.0
|
||||||
golang.org/x/sys v0.48.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/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 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
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.0.20261013184558-04dd63131525 h1:FtzHH3s777Nx50YcPAcodAPm1mNakc228nMYbr3f+90=
|
||||||
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/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||||
go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
|
go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
|
||||||
go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0=
|
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=
|
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.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
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.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
|
||||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
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 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
|
||||||
golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58=
|
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=
|
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,
|
// 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.
|
// A child's ID always exceeds its parent's, so descending ID is a valid deepest-first order.
|
||||||
descendants := CollectAllDescendantJobs(caller, attemptJobs)
|
descendants := CollectAllDescendantJobs(caller, attemptJobs)
|
||||||
slices.SortFunc(descendants, func(a, b *ActionRunJob) int { return cmp.Compare(b.ID, a.ID) })
|
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 {
|
for _, job := range append(descendants, caller) {
|
||||||
cancelled, err := cancelOneJob(ctx, c, force)
|
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 {
|
if err != nil {
|
||||||
return cancelledJobs, err
|
return cancelledJobs, err
|
||||||
}
|
}
|
||||||
if cancelled != nil {
|
if reloaded.Status != job.Status {
|
||||||
cancelledJobs = append(cancelledJobs, cancelled)
|
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
|
return cancelledJobs, nil
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
runnerv1 "gitea.dev/actionslib/runner/v1"
|
||||||
"gitea.dev/models/db"
|
"gitea.dev/models/db"
|
||||||
"gitea.dev/models/unittest"
|
"gitea.dev/models/unittest"
|
||||||
"gitea.dev/modules/timeutil"
|
"gitea.dev/modules/timeutil"
|
||||||
@@ -397,3 +398,72 @@ func TestForceCancelJobs(t *testing.T) {
|
|||||||
assert.Equal(t, StatusCancelled, callerAfter.Status)
|
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"
|
"gitea.dev/models/db"
|
||||||
repo_model "gitea.dev/models/repo"
|
repo_model "gitea.dev/models/repo"
|
||||||
user_model "gitea.dev/models/user"
|
user_model "gitea.dev/models/user"
|
||||||
|
"gitea.dev/modules/git"
|
||||||
"gitea.dev/modules/timeutil"
|
"gitea.dev/modules/timeutil"
|
||||||
"gitea.dev/modules/util"
|
"gitea.dev/modules/util"
|
||||||
webhook_module "gitea.dev/modules/webhook"
|
webhook_module "gitea.dev/modules/webhook"
|
||||||
@@ -110,7 +111,7 @@ func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) ([
|
|||||||
jobs, err := CancelPreviousJobs(
|
jobs, err := CancelPreviousJobs(
|
||||||
ctx,
|
ctx,
|
||||||
repo.ID,
|
repo.ID,
|
||||||
repo.DefaultBranch,
|
git.RefNameFromBranch(repo.DefaultBranch).String(),
|
||||||
"",
|
"",
|
||||||
webhook_module.HookEventSchedule,
|
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) {
|
func makeTaskStepDisplayName(step *jobparser.Step, limit int) (name string) {
|
||||||
if step.Name != "" {
|
if step.Name != "" {
|
||||||
name = string(step.Name) // the step has an explicit name
|
name = step.Name // the step has an explicit name
|
||||||
} else {
|
} else {
|
||||||
// for unnamed step, its "String()" method tries to get a display name by its "name", "uses",
|
// 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
|
// "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
|
// 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
|
// 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())
|
name = "Run " + util.IfZero(strings.TrimSpace(runStr), step.String())
|
||||||
}
|
}
|
||||||
return util.EllipsisDisplayString(name, limit) // database column has a length limit
|
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",
|
name: "very long name truncated",
|
||||||
jobStep: &jobparser.Step{
|
jobStep: &jobparser.Step{
|
||||||
Name: jobparser.BlockSafeString(strings.Repeat("a", 300)),
|
Name: strings.Repeat("a", 300),
|
||||||
},
|
},
|
||||||
expected: strings.Repeat("a", 252) + "…",
|
expected: strings.Repeat("a", 252) + "…",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "very long run truncated",
|
name: "very long run truncated",
|
||||||
jobStep: &jobparser.Step{
|
jobStep: &jobparser.Step{
|
||||||
Run: jobparser.BlockSafeString(strings.Repeat("a", 300)),
|
Run: strings.Repeat("a", 300),
|
||||||
},
|
},
|
||||||
expected: "Run " + strings.Repeat("a", 248) + "…",
|
expected: "Run " + strings.Repeat("a", 248) + "…",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -44,9 +44,10 @@ func GetAuthTokenByID(ctx context.Context, id string) (*AuthToken, error) {
|
|||||||
return at, nil
|
return at, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateAuthTokenByID(ctx context.Context, t *AuthToken) error {
|
func UpdateAuthTokenByID(ctx context.Context, t *AuthToken, oldHash string) (bool, error) {
|
||||||
_, err := db.GetEngine(ctx).ID(t.ID).Cols("token_hash", "expires_unix").Update(t)
|
affected, err := db.GetEngine(ctx).ID(t.ID).Where("token_hash = ?", oldHash).
|
||||||
return err
|
Cols("token_hash", "expires_unix").Update(t)
|
||||||
|
return affected == 1, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteAuthTokenByID(ctx context.Context, id string) error {
|
func DeleteAuthTokenByID(ctx context.Context, id string) error {
|
||||||
|
|||||||
@@ -88,6 +88,16 @@ func BuiltinApplications() map[string]*BuiltinOAuth2Application {
|
|||||||
DisplayName: "Gitea App",
|
DisplayName: "Gitea App",
|
||||||
RedirectURIs: []string{"com.gitea.app://oauth/callback"},
|
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
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,8 +99,8 @@ func (protectBranch *ProtectedBranch) loadGlob() {
|
|||||||
var err error
|
var err error
|
||||||
protectBranch.globRule, err = glob.Compile(protectBranch.RuleName, '/')
|
protectBranch.globRule, err = glob.Compile(protectBranch.RuleName, '/')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Invalid glob rule for ProtectedBranch[%d]: %s %v", protectBranch.ID, protectBranch.RuleName, err)
|
log.Debug("Invalid glob rule for ProtectedBranch[%d]: %s %v", protectBranch.ID, protectBranch.RuleName, err)
|
||||||
protectBranch.globRule = glob.MustCompile(glob.QuoteMeta(protectBranch.RuleName), '/')
|
protectBranch.globRule, _ = glob.Compile(glob.QuoteMeta(protectBranch.RuleName), '/')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +110,9 @@ func (protectBranch *ProtectedBranch) Match(branchName string) bool {
|
|||||||
if protectBranch.isPlainName {
|
if protectBranch.isPlainName {
|
||||||
return strings.EqualFold(protectBranch.RuleName, branchName)
|
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)
|
return protectBranch.globRule.Match(branchName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"gitea.dev/models/db"
|
"gitea.dev/models/db"
|
||||||
"gitea.dev/modules/glob"
|
"gitea.dev/modules/glob"
|
||||||
|
"gitea.dev/modules/log"
|
||||||
"gitea.dev/modules/optional"
|
"gitea.dev/modules/optional"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,10 +56,17 @@ func FindRepoProtectedBranchRules(ctx context.Context, repoID int64) (ProtectedB
|
|||||||
// FindAllMatchedBranches find all matched branches
|
// FindAllMatchedBranches find all matched branches
|
||||||
func FindAllMatchedBranches(ctx context.Context, repoID int64, ruleName string) ([]string, error) {
|
func FindAllMatchedBranches(ctx context.Context, repoID int64, ruleName string) ([]string, error) {
|
||||||
results := make([]string, 0, 10)
|
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++ {
|
for page := 1; ; page++ {
|
||||||
brancheNames, err := FindBranchNames(ctx, FindBranchOptions{
|
branchNames, err := FindBranchNames(ctx, FindBranchOptions{
|
||||||
ListOptions: db.ListOptions{
|
ListOptions: db.ListOptions{
|
||||||
PageSize: 100,
|
PageSize: pageSize,
|
||||||
Page: page,
|
Page: page,
|
||||||
},
|
},
|
||||||
RepoID: repoID,
|
RepoID: repoID,
|
||||||
@@ -67,14 +75,12 @@ func FindAllMatchedBranches(ctx context.Context, repoID int64, ruleName string)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
rule := glob.MustCompile(ruleName)
|
for _, branch := range branchNames {
|
||||||
|
|
||||||
for _, branch := range brancheNames {
|
|
||||||
if rule.Match(branch) {
|
if rule.Match(branch) {
|
||||||
results = append(results, branch)
|
results = append(results, branch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(brancheNames) < 100 {
|
if len(branchNames) < pageSize {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ type Issue struct {
|
|||||||
PullRequest *PullRequest `xorm:"-"`
|
PullRequest *PullRequest `xorm:"-"`
|
||||||
NumComments int
|
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
|
Ref string
|
||||||
|
|
||||||
PinOrder int `xorm:"-"` // 0 means not loaded, -1 means loaded but not pinned
|
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").
|
Select("repo_id").
|
||||||
From("watch").
|
From("watch").
|
||||||
Where(builder.And(builder.Eq{"user_id": subscriberID},
|
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")
|
return updateLabelCols(ctx, l, "name", "description", "color", "exclusive", "exclusive_order", "archived_unix")
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteLabel delete a label
|
// DeleteRepoLabel deletes a repository label.
|
||||||
func DeleteLabel(ctx context.Context, id, labelID int64) error {
|
func DeleteRepoLabel(ctx context.Context, repoID, labelID int64) error {
|
||||||
l, err := GetLabelByID(ctx, labelID)
|
return deleteLabel(ctx, labelID, builder.Eq{"repo_id": repoID, "org_id": 0})
|
||||||
if err != nil {
|
}
|
||||||
if IsErrLabelNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 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 {
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
if l.BelongsToOrg() && l.OrgID != id {
|
deleted, err := db.GetEngine(ctx).ID(labelID).Where(ownerCond).Delete(new(Label))
|
||||||
return nil
|
if err != nil || deleted == 0 {
|
||||||
}
|
|
||||||
if l.BelongsToRepo() && l.RepoID != id {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = db.DeleteByID[Label](ctx, labelID); err != nil {
|
|
||||||
return err
|
return err
|
||||||
} else if _, err = db.GetEngine(ctx).
|
}
|
||||||
|
if _, err = db.GetEngine(ctx).
|
||||||
Where("label_id = ?", labelID).
|
Where("label_id = ?", labelID).
|
||||||
Delete(new(IssueLabel)); err != nil {
|
Delete(new(IssueLabel)); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -320,13 +320,29 @@ func TestUpdateLabel(t *testing.T) {
|
|||||||
func TestDeleteLabel(t *testing.T) {
|
func TestDeleteLabel(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
|
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})
|
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})
|
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{})
|
unittest.CheckConsistencyFor(t, &issues_model.Label{}, &repo_model.Repository{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
repo_model "gitea.dev/models/repo"
|
repo_model "gitea.dev/models/repo"
|
||||||
"gitea.dev/models/unittest"
|
"gitea.dev/models/unittest"
|
||||||
"gitea.dev/modules/markup/markdown"
|
"gitea.dev/modules/markup/markdown"
|
||||||
|
"gitea.dev/modules/test"
|
||||||
|
|
||||||
_ "gitea.dev/modules/markup/orgmode"
|
_ "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 src="/user2/repo1/media/commit/1234/my%20dir/LINK">
|
||||||
</video>`, rendered)
|
</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) {
|
func TestRepoFileOrgMode(t *testing.T) {
|
||||||
|
|||||||
+22
-15
@@ -357,21 +357,21 @@ func GetLatestReleaseByRepoID(ctx context.Context, repoID int64) (*Release, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
type releaseMetaSearch struct {
|
type releaseMetaSearch struct {
|
||||||
ID []int64
|
IDs []int64
|
||||||
Rel []*Release
|
Releases []*Release
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s releaseMetaSearch) Len() int {
|
func (s releaseMetaSearch) Len() int {
|
||||||
return len(s.ID)
|
return len(s.IDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s releaseMetaSearch) Swap(i, j int) {
|
func (s releaseMetaSearch) Swap(i, j int) {
|
||||||
s.ID[i], s.ID[j] = s.ID[j], s.ID[i]
|
s.IDs[i], s.IDs[j] = s.IDs[j], s.IDs[i]
|
||||||
s.Rel[i], s.Rel[j] = s.Rel[j], s.Rel[i]
|
s.Releases[i], s.Releases[j] = s.Releases[j], s.Releases[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s releaseMetaSearch) Less(i, j int) bool {
|
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 {
|
func hasDuplicateName(attaches []*Attachment) bool {
|
||||||
@@ -396,19 +396,26 @@ func GetReleaseAttachments(ctx context.Context, rels ...*Release) (err error) {
|
|||||||
// then merge join them
|
// then merge join them
|
||||||
|
|
||||||
// Sort
|
// 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
|
var attachments []*Attachment
|
||||||
for index, element := range rels {
|
for _, rel := range rels {
|
||||||
element.Attachments = []*Attachment{}
|
// if a provided "Release" has no ID, don't query it, otherwise, the "IN" query will return all other attachments for the repo
|
||||||
sortedRels.ID[index] = element.ID
|
if rel.ID == 0 {
|
||||||
sortedRels.Rel[index] = element
|
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)
|
sort.Sort(sortedRels)
|
||||||
|
|
||||||
// Select attachments
|
// Select attachments
|
||||||
err = db.GetEngine(ctx).
|
err = db.GetEngine(ctx).
|
||||||
Asc("release_id", "name").
|
Asc("release_id", "name").
|
||||||
In("release_id", sortedRels.ID).
|
In("release_id", sortedRels.IDs).
|
||||||
Find(&attachments)
|
Find(&attachments)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -417,14 +424,14 @@ func GetReleaseAttachments(ctx context.Context, rels ...*Release) (err error) {
|
|||||||
// merge join
|
// merge join
|
||||||
currentIndex := 0
|
currentIndex := 0
|
||||||
for _, attachment := range attachments {
|
for _, attachment := range attachments {
|
||||||
for sortedRels.ID[currentIndex] < attachment.ReleaseID {
|
for sortedRels.IDs[currentIndex] < attachment.ReleaseID {
|
||||||
currentIndex++
|
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
|
// 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 we have no Repo, we don't need to execute this loop
|
||||||
if release.Repo == nil {
|
if release.Repo == nil {
|
||||||
continue
|
continue
|
||||||
|
|||||||
+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.
|
// ComposeSSHCloneURL returns SSH clone URL based on the given owner and repository name.
|
||||||
func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) string {
|
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
|
sshUser := setting.SSH.User
|
||||||
sshDomain := setting.SSH.Domain
|
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 {
|
if ip := net.ParseIP(sshHost); ip != nil && ip.To4() == nil {
|
||||||
sshHost = "[" + sshHost + "]" // for IPv6 address, wrap it with brackets
|
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("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))
|
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"))
|
assert.Equal(t, "git@domain:user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo"))
|
||||||
setting.Repository.UseCompatSSHURI = true
|
setting.Repository.UseCompatSSHURI = true
|
||||||
assert.Equal(t, "ssh://git@domain/user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo"))
|
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
|
// test SSH_DOMAIN while use non-standard SSH port
|
||||||
setting.SSH.Port = 123
|
setting.SSH.Port = 123
|
||||||
setting.Repository.UseCompatSSHURI = false
|
setting.Repository.UseCompatSSHURI = false
|
||||||
|
|||||||
+28
-7
@@ -20,9 +20,10 @@ type WatchMode int8
|
|||||||
const (
|
const (
|
||||||
WatchModeNone WatchMode = iota // 0 watch nothing unless mentioned
|
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
|
WatchModeDont // 2 ignore the repo
|
||||||
WatchModeAuto // 3 automatically watching (from AutoWatchOnChanges)
|
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
|
// 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
|
// IsWatchingAll reports whether every event is enabled, which is the "all activity" mode
|
||||||
func (w *Watch) IsWatchingAll() bool {
|
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 {
|
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) {
|
if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) {
|
||||||
return user_model.ErrBlockedUser
|
return user_model.ErrBlockedUser
|
||||||
}
|
}
|
||||||
|
if watch.Mode == WatchModeCustom { // keep the events the user picked
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return watchRepoByMode(ctx, watch, WatchModeNormal)
|
return watchRepoByMode(ctx, watch, WatchModeNormal)
|
||||||
}
|
}
|
||||||
@@ -189,7 +193,7 @@ func WatchRepoWithOptions(ctx context.Context, doer *user_model.User, repo *Repo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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).
|
_, err = db.GetEngine(ctx).Where("user_id=? AND repo_id=?", doer.ID, repo.ID).
|
||||||
Cols("include_pull_requests", "include_issues", "include_releases").
|
Cols("include_pull_requests", "include_issues", "include_releases").
|
||||||
Update(&Watch{IncludePullRequests: opts.WatchPullRequests, IncludeIssues: opts.WatchIssues, IncludeReleases: opts.WatchReleases})
|
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
|
// but avoids joining with `user` for performance reasons
|
||||||
// User permissions must be verified elsewhere if required
|
// User permissions must be verified elsewhere if required
|
||||||
func GetRepoWatchersIDs(ctx context.Context, repoID int64, watchType WatchType) ([]int64, error) {
|
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)
|
ids := make([]int64, 0, 64)
|
||||||
var watchColName string
|
var watchColName string
|
||||||
switch watchType {
|
switch watchType {
|
||||||
@@ -259,12 +273,19 @@ func GetRepoWatchersIDs(ctx context.Context, repoID int64, watchType WatchType)
|
|||||||
default:
|
default:
|
||||||
panic("invalid WatchType")
|
panic("invalid WatchType")
|
||||||
}
|
}
|
||||||
return ids, db.GetEngine(ctx).Table("watch").
|
sess := db.GetEngine(ctx).Table("watch").
|
||||||
Where("watch.repo_id=?", repoID).
|
Where("watch.repo_id=?", repoID).
|
||||||
And("watch.mode<>?", WatchModeDont).
|
And("watch.mode<>?", WatchModeDont).
|
||||||
And(builder.Eq{watchColName: true}).
|
And(builder.Eq{watchColName: true})
|
||||||
Select("user_id").
|
if customOnly { // matches SelectedMode() == "custom"
|
||||||
Find(&ids)
|
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.
|
// 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 1 is watched by users 1, 4, 9 and 11, all with every event enabled
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{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{
|
for watchType, expected := range map[repo_model.WatchType][]int64{
|
||||||
repo_model.WatchPullRequests: {1, 4, 9, 11},
|
repo_model.WatchPullRequests: {1, 4, 9, 11},
|
||||||
@@ -157,6 +157,20 @@ func TestWatchOptions(t *testing.T) {
|
|||||||
assert.ElementsMatch(t, expected, ids, watchType)
|
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
|
// the options of one user must not show up for another
|
||||||
watches, err := repo_model.GetUserWatches(t.Context(), 4, []int64{repo.ID})
|
watches, err := repo_model.GetUserWatches(t.Context(), 4, []int64{repo.ID})
|
||||||
assert.NoError(t, err)
|
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, "participate", (&repo_model.Watch{Mode: repo_model.WatchModeNormal}).SelectedMode())
|
||||||
assert.Equal(t, "ignore", (&repo_model.Watch{Mode: repo_model.WatchModeDont}).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.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())
|
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) {
|
func TestEmailAddressValidate(t *testing.T) {
|
||||||
cases := map[string]bool{
|
cases := map[string]bool{
|
||||||
"": false,
|
"": false,
|
||||||
"root@localhost": true,
|
"@a": false,
|
||||||
"user@[192.168.1.2]": true,
|
|
||||||
"@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
|
||||||
"abc@gmail.com": true,
|
"root@local_host": true,
|
||||||
"abc@gmail.com\n": false,
|
"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,
|
"Foo <foo@bar.com>": false,
|
||||||
"abc@gmail.com (x)": false,
|
"abc@gmail.com (x)": false,
|
||||||
"jürgen@example.com": false,
|
"jürgen@example.com": false, // utf8 address is not supported yet
|
||||||
"a@foo_bar.com": false,
|
|
||||||
}
|
}
|
||||||
for tc, isValid := range cases {
|
for tc, isValid := range cases {
|
||||||
t.Run(tc, func(t *testing.T) {
|
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)
|
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) {
|
func GetUserByID(ctx context.Context, id int64) (*User, error) {
|
||||||
u := new(User)
|
u := new(User)
|
||||||
has, err := db.GetEngine(ctx).ID(id).Get(u)
|
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
|
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) {
|
func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
|
||||||
if len(ids) == 0 {
|
if len(ids) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -1046,6 +1048,13 @@ func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
|
|||||||
return users, err
|
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
|
// 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) {
|
func GetPossibleUserByID(ctx context.Context, id int64) (_ int64, u *User, err error) {
|
||||||
if id < 0 {
|
if id < 0 {
|
||||||
@@ -1082,6 +1091,23 @@ func GetPossibleUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
|
|||||||
return users, nil
|
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) {
|
func getUserByNameWithTypes(ctx context.Context, name string, types ...UserType) (*User, error) {
|
||||||
u := &User{}
|
u := &User{}
|
||||||
sess := db.GetEngine(ctx).Where(builder.Eq{"lower_name": strings.ToLower(name)})
|
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)
|
noReplyAddressSuffix := "@" + strings.ToLower(setting.Service.NoReplyAddress)
|
||||||
for _, email := range emails {
|
for _, email := range emails {
|
||||||
emailLower := strings.ToLower(email)
|
emailLower := strings.ToLower(email)
|
||||||
|
if emailLower == "" {
|
||||||
|
continue // a commit without an author email names no user
|
||||||
|
}
|
||||||
needCheckEmails.Add(emailLower)
|
needCheckEmails.Add(emailLower)
|
||||||
if localPart, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix); ok {
|
if localPart, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix); ok {
|
||||||
name, id := parseLocalPartToNameID(localPart)
|
name, id := parseLocalPartToNameID(localPart)
|
||||||
|
|||||||
@@ -95,6 +95,12 @@ func TestUserEmails(t *testing.T) {
|
|||||||
setting.Service.NoReplyAddress = "example.com"
|
setting.Service.NoReplyAddress = "example.com"
|
||||||
testGetUserByEmail(t, "user1-2@example.COM", 1)
|
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) {
|
t.Run("GetUserByEmail", func(t *testing.T) {
|
||||||
testGetUserByEmail := func(t *testing.T, email string, uid int64) {
|
testGetUserByEmail := func(t *testing.T, email string, uid int64) {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
package jobparser
|
package jobparser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -65,44 +64,31 @@ func (w *SingleWorkflow) SetJob(id string, job *Job) error {
|
|||||||
m := map[string]*Job{
|
m := map[string]*Job{
|
||||||
id: job,
|
id: job,
|
||||||
}
|
}
|
||||||
var buf bytes.Buffer
|
out, err := marshalYAML(m)
|
||||||
encoder := yaml.NewEncoder(&buf)
|
if err != nil {
|
||||||
encoder.SetIndent(2)
|
|
||||||
if err := encoder.Encode(m); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
encoder.Close()
|
|
||||||
|
|
||||||
node := yaml.Node{}
|
node := yaml.Node{}
|
||||||
if err := yaml.Unmarshal(buf.Bytes(), &node); err != nil {
|
if err := yaml.Unmarshal(out, &node); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(node.Content) != 1 || node.Content[0].Kind != yaml.MappingNode {
|
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]
|
w.RawJobs = *node.Content[0]
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *SingleWorkflow) Marshal() ([]byte, error) {
|
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 := *w
|
||||||
payload.RunName = "" // already interpolated into the run title, a runner would parse it as a template again
|
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 marshalYAML(&payload)
|
||||||
return nil, err
|
}
|
||||||
}
|
|
||||||
if err := enc.Close(); err != nil {
|
// marshalYAML never wraps lines, go-yaml wraps more-indented lines of folded scalars until https://github.com/yaml/go-yaml/pull/441 is released.
|
||||||
return nil, err
|
func marshalYAML(v any) ([]byte, error) {
|
||||||
}
|
return yaml.Dump(v, yaml.WithV3Defaults(), yaml.WithIndent(2), yaml.WithLineWidth(-1))
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Job struct {
|
type Job struct {
|
||||||
@@ -188,29 +174,18 @@ func (j *Job) DisplayName() string {
|
|||||||
return util.EllipsisDisplayString(unescapeExpressions(j.Name), 255)
|
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 {
|
type Step struct {
|
||||||
ID string `yaml:"id,omitempty"`
|
ID string `yaml:"id,omitempty"`
|
||||||
If yaml.Node `yaml:"if,omitempty"`
|
If yaml.Node `yaml:"if,omitempty"`
|
||||||
Name BlockSafeString `yaml:"name,omitempty"`
|
Name string `yaml:"name,omitempty"`
|
||||||
Uses string `yaml:"uses,omitempty"`
|
Uses string `yaml:"uses,omitempty"`
|
||||||
Run BlockSafeString `yaml:"run,omitempty"`
|
Run string `yaml:"run,omitempty"`
|
||||||
WorkingDirectory string `yaml:"working-directory,omitempty"`
|
WorkingDirectory string `yaml:"working-directory,omitempty"`
|
||||||
Shell string `yaml:"shell,omitempty"`
|
Shell string `yaml:"shell,omitempty"`
|
||||||
Env yaml.Node `yaml:"env,omitempty"`
|
Env yaml.Node `yaml:"env,omitempty"`
|
||||||
With yaml.Node `yaml:"with,omitempty"`
|
With yaml.Node `yaml:"with,omitempty"`
|
||||||
RawContinueOnError yaml.Node `yaml:"continue-on-error,omitempty"` // raw: the runner evaluates it with the steps context
|
RawContinueOnError yaml.Node `yaml:"continue-on-error,omitempty"` // raw: the runner evaluates it with the steps context
|
||||||
TimeoutMinutes string `yaml:"timeout-minutes,omitempty"`
|
TimeoutMinutes string `yaml:"timeout-minutes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYAML canonicalizes booleans like continue-on-error
|
// UnmarshalYAML canonicalizes booleans like continue-on-error
|
||||||
@@ -232,9 +207,9 @@ func (s *Step) String() string {
|
|||||||
}
|
}
|
||||||
return (&model.Step{
|
return (&model.Step{
|
||||||
ID: s.ID,
|
ID: s.ID,
|
||||||
Name: string(s.Name),
|
Name: s.Name,
|
||||||
Uses: s.Uses,
|
Uses: s.Uses,
|
||||||
Run: string(s.Run),
|
Run: s.Run,
|
||||||
}).String()
|
}).String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
_, origJob := sws[0].Job()
|
_, origJob := sws[0].Job()
|
||||||
require.Len(t, origJob.Steps, 1)
|
require.Len(t, origJob.Steps, 1)
|
||||||
const wantRun = "\n\necho start\necho done\n"
|
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()
|
payload, err := sws[0].Marshal()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
// the round-trip must preserve the run block byte-for-byte
|
// the round-trip must preserve the run block byte-for-byte
|
||||||
_, gotJob := roundTripped[0].Job()
|
_, gotJob := roundTripped[0].Job()
|
||||||
require.Len(t, gotJob.Steps, 1)
|
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.
|
// 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)
|
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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- id: extract_tag
|
- id: extract_tag
|
||||||
name: "\nExtract tag for variant"
|
name: |-
|
||||||
run: "\necho Test\n"
|
|
||||||
|
Extract tag for variant
|
||||||
|
run: |
|
||||||
|
|
||||||
|
echo Test
|
||||||
@@ -532,7 +532,7 @@ func matchIssuesEvent(issuePayload *api.IssuePayload, evt *jobparser.Event) bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, val := range vals {
|
for _, val := range vals {
|
||||||
if slices.ContainsFunc(actions, glob.MustCompile(val, '/').Match) {
|
if matchActivityType(val, actions...) {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -575,7 +575,7 @@ func matchPullRequestEvent(ctx context.Context, gitRepo *git.Repository, commit
|
|||||||
}
|
}
|
||||||
log.Trace("matching pull_request %s with %v", action, vals)
|
log.Trace("matching pull_request %s with %v", action, vals)
|
||||||
for _, val := range vals {
|
for _, val := range vals {
|
||||||
if glob.MustCompile(val, '/').Match(string(action)) {
|
if matchActivityType(val, string(action)) {
|
||||||
activityTypeMatched = true
|
activityTypeMatched = true
|
||||||
matchTimes++
|
matchTimes++
|
||||||
break
|
break
|
||||||
@@ -678,7 +678,7 @@ func matchIssueCommentEvent(issueCommentPayload *api.IssueCommentPayload, evt *j
|
|||||||
// NONE
|
// NONE
|
||||||
|
|
||||||
for _, val := range vals {
|
for _, val := range vals {
|
||||||
if glob.MustCompile(val, '/').Match(string(issueCommentPayload.Action)) {
|
if matchActivityType(val, string(issueCommentPayload.Action)) {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -718,7 +718,7 @@ func matchPullRequestReviewEvent(prPayload *api.PullRequestPayload, evt *jobpars
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, val := range vals {
|
for _, val := range vals {
|
||||||
if slices.ContainsFunc(actions, glob.MustCompile(val, '/').Match) {
|
if matchActivityType(val, actions...) {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -758,7 +758,7 @@ func matchPullRequestReviewCommentEvent(prPayload *api.PullRequestPayload, evt *
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, val := range vals {
|
for _, val := range vals {
|
||||||
if slices.ContainsFunc(actions, glob.MustCompile(val, '/').Match) {
|
if matchActivityType(val, actions...) {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -795,7 +795,7 @@ func matchReleaseEvent(payload *api.ReleasePayload, evt *jobparser.Event) bool {
|
|||||||
action = "edited"
|
action = "edited"
|
||||||
}
|
}
|
||||||
for _, val := range vals {
|
for _, val := range vals {
|
||||||
if glob.MustCompile(val, '/').Match(string(action)) {
|
if matchActivityType(val, string(action)) {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -832,7 +832,7 @@ func matchPackageEvent(payload *api.PackagePayload, evt *jobparser.Event) bool {
|
|||||||
action = "published"
|
action = "published"
|
||||||
}
|
}
|
||||||
for _, val := range vals {
|
for _, val := range vals {
|
||||||
if glob.MustCompile(val, '/').Match(string(action)) {
|
if matchActivityType(val, string(action)) {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -857,7 +857,7 @@ func matchWorkflowRunEvent(payload *api.WorkflowRunPayload, evt *jobparser.Event
|
|||||||
case "types":
|
case "types":
|
||||||
action := payload.Action
|
action := payload.Action
|
||||||
for _, val := range vals {
|
for _, val := range vals {
|
||||||
if glob.MustCompile(val, '/').Match(action) {
|
if matchActivityType(val, action) {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -893,3 +893,13 @@ func matchWorkflowRunEvent(payload *api.WorkflowRunPayload, evt *jobparser.Event
|
|||||||
}
|
}
|
||||||
return matchTimes == len(evt.Acts())
|
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]",
|
yamlOn: "on:\n release:\n types: [published]",
|
||||||
expected: detectMatched,
|
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",
|
desc: "HookEventPackage(package) `created` action doesn't match GithubEventRegistryPackage(registry_package) with `updated` activity type",
|
||||||
triggedEvent: webhook_module.HookEventPackage,
|
triggedEvent: webhook_module.HookEventPackage,
|
||||||
|
|||||||
@@ -102,11 +102,11 @@ func NewEmbeddedFS(data []byte) fs.ReadDirFS {
|
|||||||
efs := &embeddedFS{data: data, files: make(map[string]*embeddedFileInfo)}
|
efs := &embeddedFS{data: data, files: make(map[string]*embeddedFileInfo)}
|
||||||
efs.meta = sync.OnceValue(func() *EmbeddedMeta {
|
efs.meta = sync.OnceValue(func() *EmbeddedMeta {
|
||||||
var meta EmbeddedMeta
|
var meta EmbeddedMeta
|
||||||
p := bytes.LastIndexByte(data, '\n')
|
_, metaJSON, ok := bytes.CutLast(data, []byte{'\n'})
|
||||||
if p < 0 {
|
if !ok {
|
||||||
return &meta
|
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")
|
panic("embedded file is not valid")
|
||||||
}
|
}
|
||||||
return &meta
|
return &meta
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ func Init() {
|
|||||||
RPID: setting.AppDomain,
|
RPID: setting.AppDomain,
|
||||||
RPOrigins: []string{appURL},
|
RPOrigins: []string{appURL},
|
||||||
AttestationPreference: protocol.PreferNoAttestation, // Gitea never verifies attestation
|
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"
|
"gitea.dev/modules/setting"
|
||||||
|
|
||||||
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
"github.com/stretchr/testify/assert"
|
"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.AppDomain, WebAuthn.Config.RPID)
|
||||||
assert.Equal(t, setting.AppName, WebAuthn.Config.RPDisplayName)
|
assert.Equal(t, setting.AppName, WebAuthn.Config.RPDisplayName)
|
||||||
assert.Equal(t, rpOrigin, WebAuthn.Config.RPOrigins)
|
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
|
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
|
// Apply user's git config options last so they take precedence over builtin defaults
|
||||||
for k, v := range setting.GitConfig.Options {
|
for k, v := range setting.GitConfig.Options {
|
||||||
if err = configSet(ctx, strings.ToLower(k), v); err != nil {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, _, err = gitcmd.NewCommand("config", "--global").
|
if _, _, err = gitcmd.NewCommand("config", "--global", "--replace-all").
|
||||||
AddDynamicArguments(key, value).
|
AddDynamicArguments(key, value).
|
||||||
RunStdString(ctx); err != nil {
|
RunStdString(ctx); err != nil {
|
||||||
return fmt.Errorf("failed to set git global config %s, err: %w", key, err)
|
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 {
|
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 {
|
if err == nil {
|
||||||
// already exist
|
// already exist
|
||||||
return nil
|
return nil
|
||||||
@@ -207,18 +212,9 @@ func configAddNonExist(ctx context.Context, key, value string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func configUnsetAll(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)
|
_, _, err := gitcmd.NewCommand("config", "--global", "--unset-all").AddDynamicArguments(key, "^"+regexp.QuoteMeta(value)+"$").RunStdString(ctx)
|
||||||
if err == nil {
|
if err != nil && !gitcmd.IsErrorExitCode(err, 5) { // 5: no such key or no matching value
|
||||||
// exist, need to remove
|
return fmt.Errorf("failed to unset git global config %s, err: %w", key, err)
|
||||||
_, _, 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
|
|
||||||
}
|
}
|
||||||
if gitcmd.IsErrorExitCode(err, 1) {
|
return nil
|
||||||
// not exist
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("failed to get git config %s, err: %w", key, err)
|
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,9 @@ func TestGitConfig(t *testing.T) {
|
|||||||
assert.NoError(t, configSetNonExist(ctx, "test.key-a", "val-a-changed"))
|
assert.NoError(t, configSetNonExist(ctx, "test.key-a", "val-a-changed"))
|
||||||
assert.False(t, gitConfigContains("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, 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.True(t, gitConfigContains("key-a = val-a-changed"))
|
||||||
|
|
||||||
assert.NoError(t, configAddNonExist(ctx, "test.key-b", "val-b"))
|
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.NoError(t, configAddNonExist(ctx, "test.key-b", "val-2b"))
|
||||||
assert.True(t, gitConfigContains("key-b = val-b"))
|
assert.True(t, gitConfigContains("key-b = val-b"))
|
||||||
assert.True(t, gitConfigContains("key-b = val-2b"))
|
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.NoError(t, configUnsetAll(ctx, "test.key-b", "val-b"))
|
||||||
assert.False(t, gitConfigContains("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)()
|
defer test.MockVariableValue(&setting.GitConfig)()
|
||||||
|
|
||||||
assert.Empty(t, setting.GitConfig.Options)
|
assert.Empty(t, setting.GitConfig.Options)
|
||||||
|
assert.NoError(t, configSet(t.Context(), "gc.reflogExpire", "90"))
|
||||||
assert.NoError(t, syncGitConfig(t.Context()))
|
assert.NoError(t, syncGitConfig(t.Context()))
|
||||||
assert.True(t, gitConfigContains("commitGraph = true")) // builtin default config
|
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["sync-test.cfg-key-a"] = "CfgValA"
|
||||||
setting.GitConfig.Options["core.commitgraph"] = "false"
|
setting.GitConfig.Options["core.commitgraph"] = "false"
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type FastImportFile struct {
|
|||||||
|
|
||||||
type FastImportCommit struct {
|
type FastImportCommit struct {
|
||||||
Ref string
|
Ref string
|
||||||
|
Parent string
|
||||||
Message string
|
Message string
|
||||||
Files []FastImportFile
|
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))
|
msg := util.IfZero(c.Message, fmt.Sprintf("test commit %d", i+1))
|
||||||
_, _ = fmt.Fprintf(buf, "data %d\n%s\n", len(msg), msg)
|
_, _ = 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 {
|
for _, f := range c.Files {
|
||||||
mode := util.IfZero(f.Mode, EntryModeBlob)
|
mode := util.IfZero(f.Mode, EntryModeBlob)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import (
|
|||||||
// merge base between commits.
|
// merge base between commits.
|
||||||
func FetchRemoteCommit(ctx context.Context, repo, remoteRepo RepositoryFacade, commitID string) error {
|
func FetchRemoteCommit(ctx context.Context, repo, remoteRepo RepositoryFacade, commitID string) error {
|
||||||
return LockWriteAndDo(ctx, repo, func(ctx context.Context) 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(gitrepo.RepoLocalPath(remoteRepo)).
|
||||||
AddDynamicArguments(commitID).
|
AddDynamicArguments(commitID).
|
||||||
WithRepo(repo).Run(ctx)
|
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)
|
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 = UnquoteDiffFilePath(string(entryName))
|
||||||
entry.Name, err = strconv.Unquote(string(entryName))
|
if err != nil {
|
||||||
if err != nil {
|
return nil, fmt.Errorf("invalid ls-tree output (invalid name): %q, err: %w", line, err)
|
||||||
return nil, fmt.Errorf("invalid ls-tree output (invalid name): %q, err: %w", line, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
entry.Name = string(entryName)
|
|
||||||
}
|
}
|
||||||
return entry, nil
|
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
|
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) {
|
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").
|
stdout, _, err := gitcmd.NewCommand("branch", "--contains").
|
||||||
AddDynamicArguments(commitID, branch).
|
AddDynamicArguments(commitID, branch).
|
||||||
WithRepo(repo).
|
WithRepo(repo).
|
||||||
@@ -424,7 +442,7 @@ func IsCommitInBranch(ctx context.Context, repo gitrepo.RepositoryFacade, commit
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return len(stdout) > 0, err
|
return len(stdout) > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCommitBranchStart returns the commit where the branch diverged
|
// GetCommitBranchStart returns the commit where the branch diverged
|
||||||
|
|||||||
@@ -70,6 +70,20 @@ func TestGetCommitWithBadCommitID(t *testing.T) {
|
|||||||
assert.True(t, IsErrNotExist(err))
|
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) {
|
func TestIsCommitInBranch(t *testing.T) {
|
||||||
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
|
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
|
||||||
bareRepo1, err := OpenRepositoryLocal(t.Context(), bareRepo1Path)
|
bareRepo1, err := OpenRepositoryLocal(t.Context(), bareRepo1Path)
|
||||||
@@ -83,6 +97,10 @@ func TestIsCommitInBranch(t *testing.T) {
|
|||||||
result, err = IsCommitInBranch(t.Context(), bareRepo1, "2839944139e0de9737a044f78b0e4b40d989a9e3", "branch2")
|
result, err = IsCommitInBranch(t.Context(), bareRepo1, "2839944139e0de9737a044f78b0e4b40d989a9e3", "branch2")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.False(t, result)
|
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) {
|
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)
|
regex, err := regexp.Compile(g.regexpPattern)
|
||||||
if err != nil {
|
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
|
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"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewClient(t *testing.T) {
|
func TestNewClient(t *testing.T) {
|
||||||
u, _ := url.Parse("file:///test")
|
u, _ := url.Parse("file:///test")
|
||||||
c := newClient(u, nil)
|
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")
|
u, _ = url.Parse("https://test.com/lfs")
|
||||||
c = newClient(u, nil)
|
c = newClient(u, nil)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type FilesystemClient struct {
|
|||||||
lfsDir string
|
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 {
|
func (c *FilesystemClient) BatchSize() int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
@@ -29,8 +29,12 @@ func newFilesystemClient(endpoint *url.URL) *FilesystemClient {
|
|||||||
return &FilesystemClient{lfsDir}
|
return &FilesystemClient{lfsDir}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *FilesystemClient) objectPath(oid string) string {
|
func (c *FilesystemClient) objectPath(oid string) (string, error) {
|
||||||
return filepath.Join(c.lfsDir, oid[0:2], oid[2:4], oid)
|
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
|
// 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 {
|
for _, object := range objects {
|
||||||
p := Pointer{object.Oid, object.Size}
|
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)
|
f, err := os.Open(objectPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -57,8 +63,10 @@ func (c *FilesystemClient) Upload(ctx context.Context, objects []Pointer, callba
|
|||||||
for _, object := range objects {
|
for _, object := range objects {
|
||||||
p := Pointer{object.Oid, object.Size}
|
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 {
|
if err := os.MkdirAll(filepath.Dir(objectPath), os.ModePerm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-11
@@ -10,9 +10,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"gitea.dev/modules/setting"
|
||||||
|
"gitea.dev/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// spec: https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md
|
// 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)
|
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.
|
// ReadPointerFromBuffer will return a pointer if the provided byte slice is a pointer file or an error otherwise.
|
||||||
func ReadPointerFromBuffer(buf []byte) (Pointer, error) {
|
func ReadPointerFromBuffer(buf []byte) (Pointer, error) {
|
||||||
var p Pointer
|
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)"
|
// 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)
|
oid := strings.TrimPrefix(splitLines[1], MetaFileOidPrefix)
|
||||||
if len(oid) != 64 || !oidPattern.MatchString(oid) {
|
if !isValidOid(oid) {
|
||||||
return p, ErrInvalidOIDFormat
|
return p, ErrInvalidOIDFormat
|
||||||
}
|
}
|
||||||
size, err := strconv.ParseInt(strings.TrimPrefix(splitLines[2], "size "), 10, 64)
|
size, err := strconv.ParseInt(strings.TrimPrefix(splitLines[2], "size "), 10, 64)
|
||||||
@@ -79,13 +79,24 @@ func ReadPointerFromBuffer(buf []byte) (Pointer, error) {
|
|||||||
return p, nil
|
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.
|
// IsValid checks if the pointer has a valid structure.
|
||||||
// It doesn't check if the pointed-to-content exists.
|
// It doesn't check if the pointed-to-content exists.
|
||||||
func (p Pointer) IsValid() bool {
|
func (p Pointer) IsValid() bool {
|
||||||
if len(p.Oid) != 64 {
|
if !isValidOid(p.Oid) {
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !oidPattern.MatchString(p.Oid) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if p.Size < 0 {
|
if p.Size < 0 {
|
||||||
@@ -102,10 +113,13 @@ func (p Pointer) StringContent() string {
|
|||||||
|
|
||||||
// RelativePath returns the relative storage path of the pointer
|
// RelativePath returns the relative storage path of the pointer
|
||||||
func (p Pointer) RelativePath() string {
|
func (p Pointer) RelativePath() string {
|
||||||
if len(p.Oid) < 5 {
|
if !isValidOid(p.Oid) {
|
||||||
return 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:])
|
return path.Join(p.Oid[0:2], p.Oid[2:4], p.Oid[4:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dev/modules/setting"
|
||||||
|
"gitea.dev/modules/test"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,8 +25,10 @@ func TestRelativePath(t *testing.T) {
|
|||||||
expected := path.Join("4d", "7a", "214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393")
|
expected := path.Join("4d", "7a", "214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393")
|
||||||
assert.Equal(t, expected, p.RelativePath())
|
assert.Equal(t, expected, p.RelativePath())
|
||||||
|
|
||||||
|
defer test.MockVariableValue(&setting.IsProd, true)()
|
||||||
|
defer test.MockVariableValue(&setting.IsInTesting, false)()
|
||||||
p2 := Pointer{Oid: "4d7a"}
|
p2 := Pointer{Oid: "4d7a"}
|
||||||
assert.Equal(t, "4d7a", p2.RelativePath())
|
assert.Equal(t, "invalid-lfs-oid-4d7a", p2.RelativePath())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsValid(t *testing.T) {
|
func TestIsValid(t *testing.T) {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func b64encode(data []byte) string {
|
|||||||
func camoHandleLink(link string) string {
|
func camoHandleLink(link string) string {
|
||||||
if setting.Camo.Enabled {
|
if setting.Camo.Enabled {
|
||||||
lnkURL, err := url.Parse(link)
|
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") {
|
(setting.Camo.Always || lnkURL.Scheme != "https") {
|
||||||
return CamoEncode(link)
|
return CamoEncode(link)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,11 @@ func TestCamoHandleLink(t *testing.T) {
|
|||||||
setting.AppURL = "https://gitea.com"
|
setting.AppURL = "https://gitea.com"
|
||||||
// Test media proxy
|
// Test media proxy
|
||||||
setting.Camo.Enabled = true
|
setting.Camo.Enabled = true
|
||||||
|
setting.Camo.Always = false
|
||||||
setting.Camo.ServerURL = "https://image.proxy"
|
setting.Camo.ServerURL = "https://image.proxy"
|
||||||
setting.Camo.HMACKey = "geheim"
|
setting.Camo.HMACKey = "geheim"
|
||||||
|
dataURL := "data:image/png;base64,i//V"
|
||||||
|
assert.Equal(t, dataURL, camoHandleLink(dataURL))
|
||||||
|
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
"https://gitea.com/img.jpg",
|
"https://gitea.com/img.jpg",
|
||||||
@@ -29,6 +32,7 @@ func TestCamoHandleLink(t *testing.T) {
|
|||||||
camoHandleLink("http://testimages.org/img.jpg"))
|
camoHandleLink("http://testimages.org/img.jpg"))
|
||||||
|
|
||||||
setting.Camo.Always = true
|
setting.Camo.Always = true
|
||||||
|
assert.Equal(t, dataURL, camoHandleLink(dataURL))
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
"https://gitea.com/img.jpg",
|
"https://gitea.com/img.jpg",
|
||||||
camoHandleLink("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)
|
return visitNodeImg(ctx, node)
|
||||||
} else if node.Data == "video" {
|
} else if node.Data == "video" {
|
||||||
return visitNodeVideo(ctx, node)
|
return visitNodeVideo(ctx, node)
|
||||||
|
} else if node.Data == "source" {
|
||||||
|
return visitNodePictureSource(ctx, node)
|
||||||
}
|
}
|
||||||
|
|
||||||
if node.Data == "a" {
|
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.
|
// because the content there is not processable, for example: the content is a commit id or a full URL.
|
||||||
procs = emojiProcessors
|
procs = emojiProcessors
|
||||||
}
|
}
|
||||||
|
next := node.NextSibling
|
||||||
for n := node.FirstChild; n != nil; {
|
for n := node.FirstChild; n != nil; {
|
||||||
n = visitNode(ctx, procs, n)
|
n = visitNode(ctx, procs, n)
|
||||||
}
|
}
|
||||||
return node.NextSibling
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
// createKeyword() renders a highlighted version of an action keyword
|
// createKeyword() renders a highlighted version of an action keyword
|
||||||
|
|||||||
+32
-10
@@ -6,6 +6,7 @@ package markup
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"gitea.dev/modules/htmlutil"
|
||||||
"gitea.dev/modules/markup/common"
|
"gitea.dev/modules/markup/common"
|
||||||
|
|
||||||
"golang.org/x/net/html"
|
"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) {
|
func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) {
|
||||||
next = img.NextSibling
|
next = img.NextSibling
|
||||||
attrSrc, hasLazy := "", false
|
attrSrc, hasLazy := "", false
|
||||||
@@ -152,19 +157,21 @@ func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) {
|
|||||||
cnt++
|
cnt++
|
||||||
}
|
}
|
||||||
if isLinkable {
|
if isLinkable {
|
||||||
|
linkNode := img
|
||||||
|
if img.Parent.Data == "picture" {
|
||||||
|
linkNode = img.Parent
|
||||||
|
}
|
||||||
wrapper := &html.Node{Type: html.ElementNode, Data: "a", Attr: []html.Attribute{
|
wrapper := &html.Node{Type: html.ElementNode, Data: "a", Attr: []html.Attribute{
|
||||||
{Key: "href", Val: ctx.RenderHelper.ResolveLink(imgSrcOrigin, LinkTypeDefault)},
|
{Key: "href", Val: ctx.RenderHelper.ResolveLink(imgSrcOrigin, LinkTypeDefault)},
|
||||||
{Key: "target", Val: "_blank"},
|
{Key: "target", Val: "_blank"},
|
||||||
}}
|
}}
|
||||||
parent := img.Parent
|
parent := linkNode.Parent
|
||||||
imgNext := img.NextSibling
|
linkNext := linkNode.NextSibling
|
||||||
parent.RemoveChild(img)
|
parent.RemoveChild(linkNode)
|
||||||
parent.InsertBefore(wrapper, imgNext)
|
parent.InsertBefore(wrapper, linkNext)
|
||||||
wrapper.AppendChild(img)
|
wrapper.AppendChild(linkNode)
|
||||||
}
|
}
|
||||||
|
imgAttr.Val = resolveMediaLinkWithCamo(ctx, imgSrcOrigin)
|
||||||
imgAttr.Val = ctx.RenderHelper.ResolveLink(imgSrcOrigin, LinkTypeMedia)
|
|
||||||
imgAttr.Val = camoHandleLink(imgAttr.Val)
|
|
||||||
img.Attr[i] = imgAttr
|
img.Attr[i] = imgAttr
|
||||||
}
|
}
|
||||||
if !RenderBehaviorForTesting.DisableAdditionalAttributes && !hasLazy && !strings.HasPrefix(attrSrc, "data:") {
|
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" {
|
if attr.Key != "src" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
attr.Val = ctx.RenderHelper.ResolveLink(attr.Val, LinkTypeMedia)
|
attr.Val = resolveMediaLinkWithCamo(ctx, attr.Val)
|
||||||
attr.Val = camoHandleLink(attr.Val)
|
|
||||||
node.Attr[i] = attr
|
node.Attr[i] = attr
|
||||||
}
|
}
|
||||||
return next
|
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
|
finalLink += "?" + linkURL.RawQuery
|
||||||
}
|
}
|
||||||
if linkURL.Fragment != "" {
|
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", resolveLinkRelative(ctx, "/a", "b", "/c", false))
|
||||||
assert.Equal(t, "/a/c#id", resolveLinkRelative(ctx, "/a", "b", "/c#id", 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/%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, "%invalid", resolveLinkRelative(ctx, "/a", "b", "%invalid", false))
|
||||||
assert.Equal(t, "http://localhost:3000/a", resolveLinkRelative(ctx, "/a", "", "", true))
|
assert.Equal(t, "http://localhost:3000/a", resolveLinkRelative(ctx, "/a", "", "", true))
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -343,7 +344,9 @@ func parseUploadPackage(upload *packageUpload) (*Package, error) {
|
|||||||
p.DistTags = append(p.DistTags, tag)
|
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`
|
attachment := upload.Attachments[meta.Name+"-"+meta.Version+".tgz"] // not the sigstore bundle of `npm publish --provenance`
|
||||||
if attachment == nil && len(upload.Attachments) == 1 {
|
if attachment == nil && len(upload.Attachments) == 1 {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func TestParsePackage(t *testing.T) {
|
|||||||
packageScope := "@scope"
|
packageScope := "@scope"
|
||||||
packageName := "test-package"
|
packageName := "test-package"
|
||||||
packageFullName := packageScope + "/" + packageName
|
packageFullName := packageScope + "/" + packageName
|
||||||
packageVersion := "1.0.1-pre"
|
packageVersion := "1.0.1-Pre"
|
||||||
packageTag := "latest"
|
packageTag := "latest"
|
||||||
packageAuthor := "KN4CK3R"
|
packageAuthor := "KN4CK3R"
|
||||||
packageBin := "gitea"
|
packageBin := "gitea"
|
||||||
@@ -287,7 +287,8 @@ func TestParsePackage(t *testing.T) {
|
|||||||
assert.Equal(t, packageFullName, p.Name)
|
assert.Equal(t, packageFullName, p.Name)
|
||||||
assert.Equal(t, packageVersion, p.Version)
|
assert.Equal(t, packageVersion, p.Version)
|
||||||
assert.Equal(t, []string{packageTag}, p.DistTags)
|
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)
|
b, _ = base64.StdEncoding.DecodeString(data)
|
||||||
assert.Equal(t, b, p.Data)
|
assert.Equal(t, b, p.Data)
|
||||||
assert.Equal(t, packageName, p.Metadata.Name)
|
assert.Equal(t, packageName, p.Metadata.Name)
|
||||||
|
|||||||
@@ -34,4 +34,6 @@ type Metadata struct {
|
|||||||
Funding any `json:"funding,omitempty"`
|
Funding any `json:"funding,omitempty"`
|
||||||
AcceptDependencies map[string]string `json:"accept_dependencies,omitempty"`
|
AcceptDependencies map[string]string `json:"accept_dependencies,omitempty"`
|
||||||
Deprecated string `json:"deprecated,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
|
// FlushOptions represents the options for the flush call
|
||||||
type FlushOptions struct {
|
type FlushOptions struct {
|
||||||
Timeout time.Duration
|
Timeout int64
|
||||||
NonBlocking bool
|
NonBlocking bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// FlushQueues calls the internal flush-queues function
|
// FlushQueues calls the internal flush-queues function
|
||||||
func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) ResponseExtra {
|
func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) ResponseExtra {
|
||||||
reqURL := setting.LocalURL + "api/internal/manager/flush-queues"
|
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 {
|
if timeout > 0 {
|
||||||
req.SetReadWriteTimeout(timeout + 10*time.Second)
|
req.SetReadWriteTimeout(timeout + 10*time.Second)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ func loadGitFrom(rootCfg ConfigProvider) {
|
|||||||
GitConfig.Options = make(map[string]string)
|
GitConfig.Options = make(map[string]string)
|
||||||
GitConfig.SetOption("diff.algorithm", "histogram")
|
GitConfig.SetOption("diff.algorithm", "histogram")
|
||||||
GitConfig.SetOption("core.logAllRefUpdates", "true")
|
GitConfig.SetOption("core.logAllRefUpdates", "true")
|
||||||
GitConfig.SetOption("gc.reflogExpire", "90")
|
|
||||||
|
|
||||||
secGitReflog := rootCfg.Section("git.reflog")
|
secGitReflog := rootCfg.Section("git.reflog")
|
||||||
if secGitReflog.HasKey("ENABLED") {
|
if secGitReflog.HasKey("ENABLED") {
|
||||||
@@ -97,8 +96,8 @@ func loadGitFrom(rootCfg ConfigProvider) {
|
|||||||
GitConfig.SetOption("core.logAllRefUpdates", secGitReflog.Key("ENABLED").In("true", []string{"true", "false"}))
|
GitConfig.SetOption("core.logAllRefUpdates", secGitReflog.Key("ENABLED").In("true", []string{"true", "false"}))
|
||||||
}
|
}
|
||||||
if secGitReflog.HasKey("EXPIRATION") {
|
if secGitReflog.HasKey("EXPIRATION") {
|
||||||
deprecatedSetting(rootCfg, "git.reflog", "EXPIRATION", "git.config", "core.reflogExpire", "1.21")
|
deprecatedSetting(rootCfg, "git.reflog", "EXPIRATION", "git.config", "gc.reflogExpire", "1.21")
|
||||||
GitConfig.SetOption("gc.reflogExpire", secGitReflog.Key("EXPIRATION").String())
|
GitConfig.SetOption("gc.reflogExpire", secGitReflog.Key("EXPIRATION").String()+".days")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, key := range secGitConfig.Keys() {
|
for _, key := range secGitConfig.Keys() {
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ diff.algorithm = other
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGitReflog(t *testing.T) {
|
func TestGitReflog(t *testing.T) {
|
||||||
defer test.MockVariableValue(&Git)
|
defer test.MockVariableValue(&Git)()
|
||||||
defer test.MockVariableValue(&GitConfig)
|
defer test.MockVariableValue(&GitConfig)()
|
||||||
|
|
||||||
// default reflog config without legacy options
|
// default reflog config without legacy options
|
||||||
cfg, err := NewConfigProviderFromData(``)
|
cfg, err := NewConfigProviderFromData(``)
|
||||||
@@ -47,7 +47,7 @@ func TestGitReflog(t *testing.T) {
|
|||||||
loadGitFrom(cfg)
|
loadGitFrom(cfg)
|
||||||
|
|
||||||
assert.Equal(t, "true", GitConfig.GetOption("core.logAllRefUpdates"))
|
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
|
// custom reflog config by legacy options
|
||||||
cfg, err = NewConfigProviderFromData(`
|
cfg, err = NewConfigProviderFromData(`
|
||||||
@@ -59,5 +59,5 @@ EXPIRATION = 123
|
|||||||
loadGitFrom(cfg)
|
loadGitFrom(cfg)
|
||||||
|
|
||||||
assert.Equal(t, "false", GitConfig.GetOption("core.logAllRefUpdates"))
|
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",
|
JWTSigningAlgorithm: "RS256",
|
||||||
JWTSigningPrivateKeyFile: "jwt/private.pem",
|
JWTSigningPrivateKeyFile: "jwt/private.pem",
|
||||||
MaxTokenLength: math.MaxInt16,
|
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) {
|
func loadOAuth2From(rootCfg ConfigProvider) {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ func TestGetGeneralSigningSecretSave(t *testing.T) {
|
|||||||
func TestOauth2DefaultApplications(t *testing.T) {
|
func TestOauth2DefaultApplications(t *testing.T) {
|
||||||
cfg, _ := NewConfigProviderFromData(``)
|
cfg, _ := NewConfigProviderFromData(``)
|
||||||
loadOAuth2From(cfg)
|
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]
|
cfg, _ = NewConfigProviderFromData(`[oauth2]
|
||||||
DEFAULT_APPLICATIONS = tea
|
DEFAULT_APPLICATIONS = tea
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
|
|||||||
sec := rootCfg.Section("repository")
|
sec := rootCfg.Section("repository")
|
||||||
Repository.DisableHTTPGit = sec.Key("DISABLE_HTTP_GIT").MustBool()
|
Repository.DisableHTTPGit = sec.Key("DISABLE_HTTP_GIT").MustBool()
|
||||||
Repository.UseCompatSSHURI = sec.Key("USE_COMPAT_SSH_URI").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.
|
// 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.
|
// USER_/ORG_MAX_CREATION_LIMIT take precedence when explicitly set.
|
||||||
Repository.MaxCreationLimit = sec.Key("MAX_CREATION_LIMIT").MustInt(-1) // FIXME: INI-MUST-SIDE-EFFECT
|
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/glob"
|
||||||
"gitea.dev/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
|
|
||||||
"golang.org/x/net/idna"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type globalVarsStruct struct {
|
type globalVarsStruct struct {
|
||||||
@@ -24,6 +22,8 @@ type globalVarsStruct struct {
|
|||||||
invalidUsernamePattern *regexp.Regexp
|
invalidUsernamePattern *regexp.Regexp
|
||||||
validBadgeSlugPattern *regexp.Regexp
|
validBadgeSlugPattern *regexp.Regexp
|
||||||
invalidBadgeSlugPattern *regexp.Regexp
|
invalidBadgeSlugPattern *regexp.Regexp
|
||||||
|
validEmailHostName *regexp.Regexp
|
||||||
|
validEmailHostIP *regexp.Regexp
|
||||||
}
|
}
|
||||||
|
|
||||||
var globalVars = sync.OnceValue(func() *globalVarsStruct {
|
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
|
invalidUsernamePattern: regexp.MustCompile(`[-._]{2,}|[-._]$`), // No consecutive or trailing non-alphanumeric chars
|
||||||
validBadgeSlugPattern: regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*$`),
|
validBadgeSlugPattern: regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*$`),
|
||||||
invalidBadgeSlugPattern: regexp.MustCompile(`[-._]{2,}|[-._]$`),
|
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
|
return false
|
||||||
}
|
}
|
||||||
_, domain, _ := strings.Cut(email, "@")
|
_, domain, _ := strings.Cut(email, "@")
|
||||||
if strings.HasPrefix(domain, "[") {
|
if !globalVars().validEmailHostName.MatchString(domain) && !globalVars().validEmailHostIP.MatchString(domain) {
|
||||||
// address like "foo@[192.168.1.2]"
|
return false
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
_, err = idna.Registration.ToASCII(domain)
|
if strings.HasPrefix(domain, "-") || strings.HasSuffix(domain, "-") ||
|
||||||
return err == nil
|
strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
@@ -135,8 +135,8 @@ func shortenFilename(filename, fallback string) string {
|
|||||||
if filename == "" {
|
if filename == "" {
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
if lastIndex := strings.LastIndexByte(filename, '/'); lastIndex >= 0 {
|
if dir, _, ok := strings.CutLast(filename, "/"); ok {
|
||||||
if secondLastIndex := strings.LastIndexByte(filename[:lastIndex], '/'); secondLastIndex >= 0 {
|
if secondLastIndex := strings.LastIndexByte(dir, '/'); secondLastIndex >= 0 {
|
||||||
return filename[secondLastIndex+1:]
|
return filename[secondLastIndex+1:]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -930,7 +930,7 @@
|
|||||||
"settings.delete_account_title": "Delete User Account",
|
"settings.delete_account_title": "Delete User Account",
|
||||||
"settings.delete_account_desc": "Are you sure you want to permanently delete this 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.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.disable": "Disable Email Notifications",
|
||||||
"settings.email_notifications.submit": "Set Email Preference",
|
"settings.email_notifications.submit": "Set Email Preference",
|
||||||
"settings.email_notifications.andyourown": "And Your Own Notifications",
|
"settings.email_notifications.andyourown": "And Your Own Notifications",
|
||||||
|
|||||||
@@ -290,13 +290,22 @@ func (r *artifactV4Routes) findArtifactByName(ctx *ArtifactContext, runID int64,
|
|||||||
return &art, nil
|
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 {
|
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 {
|
if err != nil {
|
||||||
log.Error("Error decode request body: %v", err)
|
log.Error("Error decode request body: %v", err)
|
||||||
ctx.HTTPError(http.StatusInternalServerError, "Error decode request body")
|
ctx.HTTPError(http.StatusInternalServerError, "Error decode request body")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if len(body) > maxProtobufBodySize {
|
||||||
|
ctx.HTTPError(http.StatusRequestEntityTooLarge, "request body too large")
|
||||||
|
return false
|
||||||
|
}
|
||||||
err = protojson.Unmarshal(body, req)
|
err = protojson.Unmarshal(body, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error decode request body: %v", err)
|
log.Error("Error decode request body: %v", err)
|
||||||
@@ -646,9 +655,10 @@ func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) {
|
|||||||
Size: artifact.FileSize,
|
Size: artifact.FileSize,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, artifact := range table {
|
for _, artifact := range artifacts {
|
||||||
if artifact != nil {
|
if item := table[artifact.ArtifactName]; item != nil {
|
||||||
list = append(list, artifact)
|
list = append(list, item)
|
||||||
|
delete(table, artifact.ArtifactName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -414,7 +414,7 @@ func CommonRoutes() *web.Router {
|
|||||||
g.UseUnescapedPath()
|
g.UseUnescapedPath()
|
||||||
g.MatchPath("DELETE", packageId+"/-/<version>/<filename>/-rev/<revision>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
|
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("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("GET", packageId+"/-/<filename>", npm.DownloadPackageFileByName)
|
||||||
g.MatchPath("DELETE", packageId+"/-rev/<revision>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackage)
|
g.MatchPath("DELETE", packageId+"/-rev/<revision>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackage)
|
||||||
g.MatchPath("PUT", packageId+"/-rev/<revision>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePreview)
|
g.MatchPath("PUT", packageId+"/-rev/<revision>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePreview)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
@@ -82,6 +83,11 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package
|
|||||||
|
|
||||||
metadata := packages_model.DescriptorMetadata[*npm_module.Metadata](pd)
|
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{
|
return &npm_module.PackageMetadataVersion{
|
||||||
ID: fmt.Sprintf("%s@%s", pd.Package.Name, pd.Version.Version),
|
ID: fmt.Sprintf("%s@%s", pd.Package.Name, pd.Version.Version),
|
||||||
Name: pd.Package.Name,
|
Name: pd.Package.Name,
|
||||||
@@ -113,7 +119,7 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package
|
|||||||
Dist: npm_module.PackageDistribution{
|
Dist: npm_module.PackageDistribution{
|
||||||
Shasum: pd.Files[0].Blob.HashSHA1,
|
Shasum: pd.Files[0].Blob.HashSHA1,
|
||||||
Integrity: "sha512-" + base64.StdEncoding.EncodeToString(hashBytes),
|
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) {
|
func TestCreatePackageMetadataResponse(t *testing.T) {
|
||||||
repository := npm_module.Repository{Type: "git", URL: "https://gitea.dev/alice/test.git"}
|
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{
|
return &packages_model.PackageDescriptor{
|
||||||
Package: &packages_model.Package{Name: "@scope/test"},
|
Package: &packages_model.Package{Name: "@scope/test"},
|
||||||
Owner: &user_model.User{Name: "alice"},
|
Owner: &user_model.User{Name: "alice"},
|
||||||
Version: &packages_model.PackageVersion{Version: v, CreatedUnix: timeutil.TimeStamp(publishedUnix)},
|
Version: &packages_model.PackageVersion{Version: v, CreatedUnix: timeutil.TimeStamp(publishedUnix)},
|
||||||
SemVer: version.Must(version.NewVersion(v)),
|
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{{
|
Files: []*packages_model.PackageFileDescriptor{{
|
||||||
File: &packages_model.PackageFile{LowerName: "test-" + v + ".tgz"},
|
File: &packages_model.PackageFile{LowerName: "test-" + v + ".tgz"},
|
||||||
Blob: &packages_model.PackageBlob{},
|
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{
|
result := createPackageMetadataResponse("https://gitea.dev/api/packages/alice/npm", []*packages_model.PackageDescriptor{
|
||||||
descriptor("1.1.0", 1000, npm_module.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("2.0.0-rc.1", 1500, repository, ""),
|
||||||
descriptor("1.0.0", 2000, repository),
|
descriptor("1.0.0", 2000, repository, ""),
|
||||||
})
|
})
|
||||||
|
|
||||||
assert.Equal(t, map[string]time.Time{
|
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, []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, []npm_module.User{{Name: "alice"}}, result.Versions["1.0.0"].Maintainers)
|
||||||
assert.Equal(t,
|
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,
|
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)
|
assert.Equal(t, repository, result.Versions["1.0.0"].Repository)
|
||||||
|
|
||||||
withoutRepository, err := json.Marshal(result.Versions["1.1.0"])
|
withoutRepository, err := json.Marshal(result.Versions["1.1.0"])
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ func DeleteLabel(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$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)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ func DeleteLabel(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$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)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
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
|
// Default branch only updated if changed and exist or the repository is empty
|
||||||
updateRepoLicense := false
|
updateRepoLicense := false
|
||||||
|
oldDefaultBranch := ""
|
||||||
if opts.DefaultBranch != nil && repo.DefaultBranch != *opts.DefaultBranch && (repo.IsEmpty || git.IsBranchExist(ctx, ctx.Repo.Repository, *opts.DefaultBranch)) {
|
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
|
repo.DefaultBranch = *opts.DefaultBranch
|
||||||
if !repo.IsEmpty {
|
if !repo.IsEmpty {
|
||||||
if err := git.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if oldDefaultBranch != "" {
|
||||||
|
actions_service.SwitchSchedulesToDefaultBranch(ctx, repo, oldDefaultBranch)
|
||||||
|
}
|
||||||
|
|
||||||
if visibilityChanged {
|
if visibilityChanged {
|
||||||
audit.Record(ctx, audit_model.RepositoryVisibility, repo, "visibility", repo.IsPrivate)
|
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 != "" {
|
if actor := ctx.FormString("actor"); actor != "" {
|
||||||
user, err := user_model.GetUserByName(ctx, actor)
|
user, err := user_model.GetUserByName(ctx, actor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorAuto(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
opts.TriggerUserID = user.ID
|
opts.TriggerUserID = user.ID
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package private
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitea.dev/models/db"
|
"gitea.dev/models/db"
|
||||||
"gitea.dev/modules/graceful"
|
"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.
|
// Save the hammer ctx here - as a new one is created each time you call this.
|
||||||
baseCtx := graceful.GetManager().HammerContext()
|
baseCtx := graceful.GetManager().HammerContext()
|
||||||
go func() {
|
go func() {
|
||||||
err := queue.GetManager().FlushAll(baseCtx, opts.Timeout)
|
err := queue.GetManager().FlushAll(baseCtx, time.Duration(opts.Timeout))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Flushing request timed-out with error: %v", err)
|
log.Error("Flushing request timed-out with error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -44,7 +45,7 @@ func FlushQueues(ctx *context.PrivateContext) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := queue.GetManager().FlushAll(ctx, opts.Timeout)
|
err := queue.GetManager().FlushAll(ctx, time.Duration(opts.Timeout))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.PrivateUserErrorf(http.StatusRequestTimeout, "%v", err)
|
ctx.PrivateUserErrorf(http.StatusRequestTimeout, "%v", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -77,9 +77,9 @@ func prepareCommonAuthPageData(ctx *context.Context, opt CommonAuthOptions) {
|
|||||||
|
|
||||||
// autoSignIn reads cookie and try to auto-login.
|
// autoSignIn reads cookie and try to auto-login.
|
||||||
func autoSignIn(ctx *context.Context) (bool, error) {
|
func autoSignIn(ctx *context.Context) (bool, error) {
|
||||||
isSucceed := false
|
keepRememberCookie := false
|
||||||
defer func() {
|
defer func() {
|
||||||
if !isSucceed {
|
if !keepRememberCookie {
|
||||||
ctx.DeleteSiteCookie(setting.CookieRememberName)
|
ctx.DeleteSiteCookie(setting.CookieRememberName)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -112,14 +112,20 @@ func autoSignIn(ctx *context.Context) (bool, error) {
|
|||||||
return false, fmt.Errorf("HasTwoFactorOrWebAuthn: %w", err)
|
return false, fmt.Errorf("HasTwoFactorOrWebAuthn: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
isSucceed = true
|
|
||||||
|
|
||||||
nt, token, err := auth_service.RegenerateAuthToken(ctx, t)
|
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 {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
|
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
|
||||||
|
keepRememberCookie = true
|
||||||
|
|
||||||
if err := regenerateSession(ctx, map[string]any{
|
if err := regenerateSession(ctx, map[string]any{
|
||||||
session.KeyUID: u.ID,
|
session.KeyUID: u.ID,
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ func OIDCWellKnown(ctx *context.Context) {
|
|||||||
"introspection_endpoint": oidcBaseUrl + "/login/oauth/introspect",
|
"introspection_endpoint": oidcBaseUrl + "/login/oauth/introspect",
|
||||||
"response_types_supported": []string{
|
"response_types_supported": []string{
|
||||||
"code",
|
"code",
|
||||||
"id_token",
|
|
||||||
},
|
},
|
||||||
"id_token_signing_alg_values_supported": []string{
|
"id_token_signing_alg_values_supported": []string{
|
||||||
oauth2_provider.DefaultSigningKey.SigningMethod().Alg(),
|
oauth2_provider.DefaultSigningKey.SigningMethod().Alg(),
|
||||||
|
|||||||
@@ -69,13 +69,7 @@ func goGet(ctx *context.Context) {
|
|||||||
|
|
||||||
goGetImport := context.ComposeGoGetImport(ctx, ownerName, trimmedRepoName)
|
goGetImport := context.ComposeGoGetImport(ctx, ownerName, trimmedRepoName)
|
||||||
|
|
||||||
var cloneURL string
|
goImportContent := fmt.Sprintf("%s git %s", goGetImport, context.ComposeGoGetCloneURL(ctx, ownerName, trimmedRepoName))
|
||||||
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*/)
|
|
||||||
goSourceContent := fmt.Sprintf("%s _ %s %s", goGetImport, prefix+"{/dir}" /*GoDocDirectory*/, prefix+"{/dir}/{file}#L{line}" /*GoDocFile*/)
|
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)
|
goGetCli := fmt.Sprintf("go get %s%s", insecure, goGetImport)
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ func UpdateLabel(ctx *context.Context) {
|
|||||||
|
|
||||||
// DeleteLabel delete a label
|
// DeleteLabel delete a label
|
||||||
func DeleteLabel(ctx *context.Context) {
|
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())
|
ctx.Flash.Error("DeleteLabel: " + err.Error())
|
||||||
} else {
|
} else {
|
||||||
ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
|
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 {
|
for _, cursor := range cursors {
|
||||||
if !cursor.Expanded {
|
if !cursor.Expanded || cursor.Step < 0 || cursor.Step >= len(steps) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,13 @@ func TestConvertToViewModel(t *testing.T) {
|
|||||||
viewJobSteps, _, err := convertToViewModel(t.Context(), translation.MockLocale{}, nil, task)
|
viewJobSteps, _, err := convertToViewModel(t.Context(), translation.MockLocale{}, nil, task)
|
||||||
require.NoError(t, err)
|
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{
|
expectedViewJobs := []*ViewJobStep{
|
||||||
{
|
{
|
||||||
Summary: "Set up job",
|
Summary: "Set up job",
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ func UpdateLabel(ctx *context.Context) {
|
|||||||
|
|
||||||
// DeleteLabel delete a label
|
// DeleteLabel delete a label
|
||||||
func DeleteLabel(ctx *context.Context) {
|
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())
|
ctx.Flash.Error("DeleteLabel: " + err.Error())
|
||||||
} else {
|
} else {
|
||||||
ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
|
ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func ActionWatch(ctx *context.Context) {
|
|||||||
// ActionWatchOptions watches the repository with a custom selection of events
|
// ActionWatchOptions watches the repository with a custom selection of events
|
||||||
func ActionWatchOptions(ctx *context.Context) {
|
func ActionWatchOptions(ctx *context.Context) {
|
||||||
opts := repo_model.WatchOptions{ // clearing every event is allowed, it leaves the participating state
|
opts := repo_model.WatchOptions{ // clearing every event is allowed, it leaves the participating state
|
||||||
Mode: repo_model.WatchModeNormal,
|
Mode: repo_model.WatchModeCustom,
|
||||||
WatchPullRequests: ctx.FormBool("pull_requests"),
|
WatchPullRequests: ctx.FormBool("pull_requests"),
|
||||||
WatchIssues: ctx.FormBool("issues"),
|
WatchIssues: ctx.FormBool("issues"),
|
||||||
WatchReleases: ctx.FormBool("releases"),
|
WatchReleases: ctx.FormBool("releases"),
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ package security
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
audit_model "gitea.dev/models/audit"
|
audit_model "gitea.dev/models/audit"
|
||||||
user_model "gitea.dev/models/user"
|
user_model "gitea.dev/models/user"
|
||||||
"gitea.dev/modules/auth/openid"
|
"gitea.dev/modules/auth/openid"
|
||||||
|
"gitea.dev/modules/base"
|
||||||
"gitea.dev/modules/log"
|
"gitea.dev/modules/log"
|
||||||
"gitea.dev/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
"gitea.dev/modules/util"
|
"gitea.dev/modules/util"
|
||||||
@@ -19,6 +22,8 @@ import (
|
|||||||
"gitea.dev/services/forms"
|
"gitea.dev/services/forms"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const openIDAssociationCodeMinutes = 10
|
||||||
|
|
||||||
// OpenIDPost response for change user's openid
|
// OpenIDPost response for change user's openid
|
||||||
func OpenIDPost(ctx *context.Context) {
|
func OpenIDPost(ctx *context.Context) {
|
||||||
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
|
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
|
||||||
@@ -70,7 +75,8 @@ func OpenIDPost(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectTo := setting.AppURL + "user/settings/security"
|
state := base.CreateTimeLimitCode(fmt.Sprintf("openid_association:%d", ctx.Doer.ID), openIDAssociationCodeMinutes, time.Now(), nil)
|
||||||
|
redirectTo := setting.AppURL + "user/settings/security?state=" + state
|
||||||
url, err := openid.RedirectURL(id, redirectTo, setting.AppURL)
|
url, err := openid.RedirectURL(id, redirectTo, setting.AppURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
loadSecurityData(ctx)
|
loadSecurityData(ctx)
|
||||||
@@ -82,6 +88,11 @@ func OpenIDPost(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func settingsOpenIDVerify(ctx *context.Context) {
|
func settingsOpenIDVerify(ctx *context.Context) {
|
||||||
|
if !base.VerifyTimeLimitCode(time.Now(), fmt.Sprintf("openid_association:%d", ctx.Doer.ID), openIDAssociationCodeMinutes, ctx.FormString("state")) {
|
||||||
|
ctx.HTTPError(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
log.Trace("Incoming call to: " + ctx.Req.URL.String())
|
log.Trace("Incoming call to: " + ctx.Req.URL.String())
|
||||||
|
|
||||||
fullURL := setting.AppURL + ctx.Req.URL.String()[1:]
|
fullURL := setting.AppURL + ctx.Req.URL.String()[1:]
|
||||||
|
|||||||
@@ -4,15 +4,103 @@
|
|||||||
package security
|
package security
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitea.dev/models/unittest"
|
"gitea.dev/models/unittest"
|
||||||
|
user_model "gitea.dev/models/user"
|
||||||
|
"gitea.dev/modules/base"
|
||||||
|
"gitea.dev/modules/setting"
|
||||||
|
"gitea.dev/modules/test"
|
||||||
|
"gitea.dev/modules/web"
|
||||||
"gitea.dev/services/contexttest"
|
"gitea.dev/services/contexttest"
|
||||||
|
"gitea.dev/services/forms"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestOpenIDAssociation(t *testing.T) {
|
||||||
|
unittest.PrepareTestEnv(t)
|
||||||
|
defer test.MockVariableValue(&setting.AppURL, "https://gitea.example/")()
|
||||||
|
defer test.MockVariableValue(&setting.AppSubURL, "")()
|
||||||
|
defer test.MockVariableValue(&setting.Security.AllowedHostList, "127.0.0.1:*")()
|
||||||
|
|
||||||
|
provider := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
assert.Equal(t, "check_authentication", r.FormValue("openid.mode"))
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
fmt.Fprint(w, "ns:http://specs.openid.net/auth/2.0\nis_valid:true\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
fmt.Fprintf(w, `<html><head><link rel="openid2.provider" href="http://%s/provider"></head></html>`, r.Host)
|
||||||
|
}))
|
||||||
|
defer provider.Close()
|
||||||
|
identity := provider.URL + "/identity"
|
||||||
|
ctx, resp := contexttest.MockContext(t, "POST /user/settings/security/openid")
|
||||||
|
contexttest.LoadUser(t, ctx, 2)
|
||||||
|
web.SetForm(ctx, &forms.AddOpenIDForm{Openid: identity})
|
||||||
|
OpenIDPost(ctx)
|
||||||
|
require.Equal(t, http.StatusSeeOther, resp.Code)
|
||||||
|
redirect, err := url.Parse(resp.Header().Get("Location"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
callback, err := url.Parse(redirect.Query().Get("openid.return_to"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
state := callback.Query().Get("state")
|
||||||
|
require.NotEmpty(t, state)
|
||||||
|
|
||||||
|
params := url.Values{
|
||||||
|
"openid.ns": {"http://specs.openid.net/auth/2.0"},
|
||||||
|
"openid.mode": {"id_res"},
|
||||||
|
"openid.op_endpoint": {provider.URL + "/provider"},
|
||||||
|
"openid.claimed_id": {identity},
|
||||||
|
"openid.identity": {identity},
|
||||||
|
"openid.return_to": {callback.String()},
|
||||||
|
"openid.response_nonce": {time.Now().UTC().Format("2006-01-02T15:04:05Z") + "association"},
|
||||||
|
"openid.assoc_handle": {"test-association"},
|
||||||
|
"openid.signed": {"op_endpoint,claimed_id,identity,return_to,response_nonce,assoc_handle"},
|
||||||
|
"openid.sig": {"test-signature"},
|
||||||
|
}
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
uid int64
|
||||||
|
state string
|
||||||
|
}{
|
||||||
|
{"unsolicited", 2, ""},
|
||||||
|
{"wrong state", 2, "wrong"},
|
||||||
|
{"other user", 1, state},
|
||||||
|
{"expired", 2, base.CreateTimeLimitCode("openid_association:2", openIDAssociationCodeMinutes, time.Now().Add(-time.Hour), nil)},
|
||||||
|
{"other purpose", 2, base.CreateTimeLimitCode("reset_password:2", openIDAssociationCodeMinutes, time.Now(), nil)},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
params.Set("state", tc.state)
|
||||||
|
ctx, resp := contexttest.MockContext(t, "GET /user/settings/security?"+params.Encode())
|
||||||
|
contexttest.LoadUser(t, ctx, tc.uid)
|
||||||
|
Security(ctx)
|
||||||
|
assert.Equal(t, http.StatusForbidden, resp.Code)
|
||||||
|
unittest.AssertNotExistsBean(t, &user_model.UserOpenID{URI: identity})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
params.Set("state", state)
|
||||||
|
ctx, resp = contexttest.MockContext(t, "GET /user/settings/security?"+params.Encode())
|
||||||
|
contexttest.LoadUser(t, ctx, 2)
|
||||||
|
Security(ctx)
|
||||||
|
require.Equal(t, http.StatusSeeOther, resp.Code)
|
||||||
|
unittest.AssertExistsAndLoadBean(t, &user_model.UserOpenID{UID: 2, URI: identity})
|
||||||
|
|
||||||
|
ctx, resp = contexttest.MockContext(t, "GET /user/settings/security?"+params.Encode())
|
||||||
|
contexttest.LoadUser(t, ctx, 2)
|
||||||
|
Security(ctx)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.Code)
|
||||||
|
assert.Equal(t, "Nonce already used", ctx.Flash.ErrorMsg)
|
||||||
|
}
|
||||||
|
|
||||||
func TestDeleteOpenIDReturnsNotFoundForOtherUsersAddress(t *testing.T) {
|
func TestDeleteOpenIDReturnsNotFoundForOtherUsersAddress(t *testing.T) {
|
||||||
unittest.PrepareTestEnv(t)
|
unittest.PrepareTestEnv(t)
|
||||||
ctx, _ := contexttest.MockContext(t, "POST /user/settings/security?id=1")
|
ctx, _ := contexttest.MockContext(t, "POST /user/settings/security?id=1")
|
||||||
|
|||||||
@@ -197,7 +197,6 @@ func verifyAuthWithOptionsWeb(options *common.VerifyOptions) func(ctx *context.C
|
|||||||
ctx.HTTPError(http.StatusUnauthorized, ctx.Locale.TrString("auth.must_change_password"))
|
ctx.HTTPError(http.StatusUnauthorized, ctx.Locale.TrString("auth.must_change_password"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
|
|
||||||
ctx.Redirect(setting.AppSubURL + "/user/settings/change_password")
|
ctx.Redirect(setting.AppSubURL + "/user/settings/change_password")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
actions_model "gitea.dev/models/actions"
|
actions_model "gitea.dev/models/actions"
|
||||||
"gitea.dev/models/db"
|
"gitea.dev/models/db"
|
||||||
"gitea.dev/modules/actions/jobparser"
|
"gitea.dev/modules/actions/jobparser"
|
||||||
|
"gitea.dev/modules/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CancelRun cancels a run's cancellable jobs and returns the run's post-cancellation state.
|
// CancelRun cancels a run's cancellable jobs and returns the run's post-cancellation state.
|
||||||
@@ -52,6 +53,12 @@ func cancelRun(ctx context.Context, run *actions_model.ActionRun, jobs []*action
|
|||||||
if len(updatedJobs) > 0 || reloaded.Status != run.Status {
|
if len(updatedJobs) > 0 || reloaded.Status != run.Status {
|
||||||
NotifyWorkflowRunStatusUpdate(ctx, reloaded)
|
NotifyWorkflowRunStatusUpdate(ctx, reloaded)
|
||||||
}
|
}
|
||||||
|
if len(updatedJobs) == 0 && reloaded.Status != run.Status {
|
||||||
|
// the run's status was updated, so emit it for the emitter to check
|
||||||
|
if err := EmitJobsIfReadyByRun(run.ID); err != nil {
|
||||||
|
log.Error("Check jobs of run %d: %v", run.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
return reloaded, nil
|
return reloaded, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
actions_model "gitea.dev/models/actions"
|
||||||
|
"gitea.dev/models/db"
|
||||||
|
"gitea.dev/models/unittest"
|
||||||
|
"gitea.dev/modules/test"
|
||||||
|
"gitea.dev/modules/timeutil"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestForceCancelRun_SettledRunWakesConcurrencyWaiters(t *testing.T) {
|
||||||
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
ctx := t.Context()
|
||||||
|
|
||||||
|
// a run left cancelling although its caller and child are all cancelled already
|
||||||
|
run := &actions_model.ActionRun{
|
||||||
|
Title: "settled-run",
|
||||||
|
RepoID: 4,
|
||||||
|
Index: 9821,
|
||||||
|
OwnerID: 1,
|
||||||
|
WorkflowID: "test.yaml",
|
||||||
|
TriggerUserID: 1,
|
||||||
|
Ref: "refs/heads/master",
|
||||||
|
CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0",
|
||||||
|
Event: "push",
|
||||||
|
TriggerEvent: "push",
|
||||||
|
EventPayload: "{}",
|
||||||
|
Status: actions_model.StatusCancelling,
|
||||||
|
}
|
||||||
|
require.NoError(t, db.Insert(ctx, run))
|
||||||
|
attempt := &actions_model.ActionRunAttempt{RepoID: run.RepoID, RunID: run.ID, Attempt: 1, TriggerUserID: 1, Status: actions_model.StatusCancelling}
|
||||||
|
require.NoError(t, db.Insert(ctx, attempt))
|
||||||
|
run.LatestAttemptID = attempt.ID
|
||||||
|
require.NoError(t, actions_model.UpdateRun(ctx, run, "latest_attempt_id"))
|
||||||
|
|
||||||
|
newJob := func(name string, parentID int64, isCaller bool) *actions_model.ActionRunJob {
|
||||||
|
job := &actions_model.ActionRunJob{
|
||||||
|
RunID: run.ID,
|
||||||
|
RunAttemptID: attempt.ID,
|
||||||
|
RepoID: run.RepoID,
|
||||||
|
OwnerID: run.OwnerID,
|
||||||
|
CommitSHA: run.CommitSHA,
|
||||||
|
Name: name,
|
||||||
|
JobID: name,
|
||||||
|
Attempt: 1,
|
||||||
|
Status: actions_model.StatusCancelled,
|
||||||
|
Stopped: timeutil.TimeStampNow(),
|
||||||
|
ParentJobID: parentID,
|
||||||
|
IsReusableCaller: isCaller,
|
||||||
|
IsExpanded: isCaller,
|
||||||
|
}
|
||||||
|
require.NoError(t, db.Insert(ctx, job))
|
||||||
|
return job
|
||||||
|
}
|
||||||
|
caller := newJob("caller", 0, true)
|
||||||
|
child := newJob("child", caller.ID, false)
|
||||||
|
|
||||||
|
var emitted []int64
|
||||||
|
defer test.MockVariableValue(&EmitJobsIfReadyByRun, func(runID int64) error {
|
||||||
|
emitted = append(emitted, runID)
|
||||||
|
return nil
|
||||||
|
})()
|
||||||
|
|
||||||
|
got, err := ForceCancelRun(ctx, run, []*actions_model.ActionRunJob{caller, child})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, actions_model.StatusCancelled, got.Status)
|
||||||
|
assert.Equal(t, []int64{run.ID}, emitted)
|
||||||
|
}
|
||||||
@@ -12,7 +12,9 @@ import (
|
|||||||
actions_model "gitea.dev/models/actions"
|
actions_model "gitea.dev/models/actions"
|
||||||
"gitea.dev/models/db"
|
"gitea.dev/models/db"
|
||||||
repo_model "gitea.dev/models/repo"
|
repo_model "gitea.dev/models/repo"
|
||||||
|
unit_model "gitea.dev/models/unit"
|
||||||
"gitea.dev/modules/actions"
|
"gitea.dev/modules/actions"
|
||||||
|
"gitea.dev/modules/git"
|
||||||
"gitea.dev/modules/log"
|
"gitea.dev/modules/log"
|
||||||
"gitea.dev/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
"gitea.dev/modules/timeutil"
|
"gitea.dev/modules/timeutil"
|
||||||
@@ -62,6 +64,22 @@ func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) er
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SwitchSchedulesToDefaultBranch cancels the running schedule runs of the old default branch
|
||||||
|
// and replaces the schedules with the ones on the current default branch
|
||||||
|
func SwitchSchedulesToDefaultBranch(ctx context.Context, repo *repo_model.Repository, oldDefaultBranch string) {
|
||||||
|
if err := actions_model.DeleteScheduleTaskByRepo(ctx, repo.ID); err != nil {
|
||||||
|
log.Error("DeleteScheduleTaskByRepo: %v", err)
|
||||||
|
}
|
||||||
|
if err := CancelPreviousJobs(ctx, repo.ID, git.RefNameFromBranch(oldDefaultBranch).String(), "", webhook_module.HookEventSchedule); err != nil {
|
||||||
|
log.Error("CancelPreviousJobs: %v", err)
|
||||||
|
}
|
||||||
|
if repo.UnitEnabled(ctx, unit_model.TypeActions) {
|
||||||
|
if err := DetectAndHandleSchedules(ctx, repo); err != nil {
|
||||||
|
log.Error("DetectAndHandleSchedules: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func shouldBlockJobByConcurrency(ctx context.Context, job *actions_model.ActionRunJob) (bool, error) {
|
func shouldBlockJobByConcurrency(ctx context.Context, job *actions_model.ActionRunJob) (bool, error) {
|
||||||
if job.RawConcurrency != "" && !job.IsConcurrencyEvaluated {
|
if job.RawConcurrency != "" && !job.IsConcurrencyEvaluated {
|
||||||
// when the job depends on other jobs, we cannot evaluate its concurrency, so it should be blocked and will be evaluated again when its dependencies are done
|
// when the job depends on other jobs, we cannot evaluate its concurrency, so it should be blocked and will be evaluated again when its dependencies are done
|
||||||
|
|||||||
@@ -577,8 +577,9 @@ func handleSchedules(
|
|||||||
log.Error("CountSchedules: %v", err)
|
log.Error("CountSchedules: %v", err)
|
||||||
return err
|
return err
|
||||||
} else if count > 0 {
|
} else if count > 0 {
|
||||||
if err := CleanRepoScheduleTasks(ctx, input.Repo); err != nil {
|
// replacing the schedules leaves their in-progress runs alone, as GitHub does
|
||||||
log.Error("CleanRepoScheduleTasks: %v", err)
|
if err := actions_model.DeleteScheduleTaskByRepo(ctx, input.Repo.ID); err != nil {
|
||||||
|
log.Error("DeleteScheduleTaskByRepo: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -188,7 +188,10 @@ func insertRunJob(ctx context.Context, run *actions_model.ActionRun, runAttempt
|
|||||||
if err := workflowJob.SetJob(id, job.EraseNeeds()); err != nil {
|
if err := workflowJob.SetJob(id, job.EraseNeeds()); err != nil {
|
||||||
return nil, nil, false, err
|
return nil, nil, false, err
|
||||||
}
|
}
|
||||||
payload, _ := workflowJob.Marshal()
|
payload, err := workflowJob.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, false, fmt.Errorf("insertRunJob: marshal job %q: %w", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
isReusableWorkflowCaller := job.Uses != ""
|
isReusableWorkflowCaller := job.Uses != ""
|
||||||
status := util.Iif(runAttempt.Status == actions_model.StatusBlocked || run.NeedApproval, actions_model.StatusBlocked, actions_model.StatusWaiting)
|
status := util.Iif(runAttempt.Status == actions_model.StatusBlocked || run.NeedApproval, actions_model.StatusBlocked, actions_model.StatusWaiting)
|
||||||
|
|||||||
@@ -24,9 +24,10 @@ import (
|
|||||||
// If a device uses the token to login into the instance, a fresh token gets generated which has the same id but a new hash.
|
// If a device uses the token to login into the instance, a fresh token gets generated which has the same id but a new hash.
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrAuthTokenInvalidFormat = util.NewInvalidArgumentErrorf("auth token has an invalid format")
|
ErrAuthTokenInvalidFormat = util.NewInvalidArgumentErrorf("auth token has an invalid format")
|
||||||
ErrAuthTokenExpired = util.NewInvalidArgumentErrorf("auth token has expired")
|
ErrAuthTokenExpired = util.NewInvalidArgumentErrorf("auth token has expired")
|
||||||
ErrAuthTokenInvalidHash = util.NewInvalidArgumentErrorf("auth token is invalid")
|
ErrAuthTokenInvalidHash = util.NewInvalidArgumentErrorf("auth token is invalid")
|
||||||
|
ErrAuthTokenRotationConflict = util.NewInvalidArgumentErrorf("auth token was rotated by another request")
|
||||||
)
|
)
|
||||||
|
|
||||||
func CheckAuthToken(ctx context.Context, value string) (*auth_model.AuthToken, error) {
|
func CheckAuthToken(ctx context.Context, value string) (*auth_model.AuthToken, error) {
|
||||||
@@ -76,9 +77,23 @@ func RegenerateAuthToken(ctx context.Context, t *auth_model.AuthToken) (*auth_mo
|
|||||||
ExpiresUnix: timeutil.TimeStampNow().AddDuration(time.Duration(setting.LogInRememberDays*24) * time.Hour),
|
ExpiresUnix: timeutil.TimeStampNow().AddDuration(time.Duration(setting.LogInRememberDays*24) * time.Hour),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := auth_model.UpdateAuthTokenByID(ctx, newToken); err != nil {
|
updated, err := auth_model.UpdateAuthTokenByID(ctx, newToken, t.TokenHash)
|
||||||
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
if !updated {
|
||||||
|
current, err := auth_model.GetAuthTokenByID(ctx, t.ID)
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
return nil, "", ErrAuthTokenExpired
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
if current.ExpiresUnix < timeutil.TimeStampNow() {
|
||||||
|
return nil, "", ErrAuthTokenExpired
|
||||||
|
}
|
||||||
|
return nil, "", ErrAuthTokenRotationConflict
|
||||||
|
}
|
||||||
|
|
||||||
return newToken, token, nil
|
return newToken, token, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"gitea.dev/modules/util"
|
"gitea.dev/modules/util"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCheckAuthToken(t *testing.T) {
|
func TestCheckAuthToken(t *testing.T) {
|
||||||
@@ -105,5 +106,25 @@ func TestRegenerateAuthToken(t *testing.T) {
|
|||||||
assert.NotEqual(t, token, token2)
|
assert.NotEqual(t, token, token2)
|
||||||
assert.NotEqual(t, at.ExpiresUnix, at2.ExpiresUnix)
|
assert.NotEqual(t, at.ExpiresUnix, at2.ExpiresUnix)
|
||||||
|
|
||||||
|
// Only one request may rotate the same validated cookie.
|
||||||
|
competing, competingToken, err := RegenerateAuthToken(t.Context(), at)
|
||||||
|
require.ErrorIs(t, err, ErrAuthTokenRotationConflict)
|
||||||
|
require.Nil(t, competing)
|
||||||
|
require.Empty(t, competingToken)
|
||||||
|
current, err := CheckAuthToken(t.Context(), at2.ID+":"+token2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, at2.TokenHash, current.TokenHash)
|
||||||
|
|
||||||
assert.NoError(t, auth_model.DeleteAuthTokenByID(t.Context(), at.ID))
|
assert.NoError(t, auth_model.DeleteAuthTokenByID(t.Context(), at.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRegenerateRevokedAuthToken(t *testing.T) {
|
||||||
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
at, _, err := CreateAuthTokenForUserID(t.Context(), 2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, auth_model.DeleteAuthTokenByID(t.Context(), at.ID))
|
||||||
|
rotated, token, err := RegenerateAuthToken(t.Context(), at)
|
||||||
|
require.ErrorIs(t, err, ErrAuthTokenExpired)
|
||||||
|
require.Nil(t, rotated)
|
||||||
|
require.Empty(t, token)
|
||||||
|
}
|
||||||
@@ -389,6 +389,15 @@ func ComposeGoGetImport(ctx context.Context, owner, repo string) string {
|
|||||||
return path.Join(curAppURL.Host, setting.AppSubURL, url.PathEscape(owner), url.PathEscape(repo))
|
return path.Join(curAppURL.Host, setting.AppSubURL, url.PathEscape(owner), url.PathEscape(repo))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ComposeGoGetCloneURL returns the clone URL for the go-import meta content.
|
||||||
|
func ComposeGoGetCloneURL(ctx *Context, owner, repo string) string {
|
||||||
|
useSSH := setting.Repository.GoGetCloneURLProtocol == "ssh" || (setting.Repository.DisableHTTPGit && !setting.SSH.Disabled)
|
||||||
|
if useSSH {
|
||||||
|
return repo_model.ComposeSSHCloneURI(ctx.Doer, owner, repo)
|
||||||
|
}
|
||||||
|
return repo_model.ComposeHTTPSCloneURL(ctx, owner, repo)
|
||||||
|
}
|
||||||
|
|
||||||
// EarlyResponseForGoGetMeta responses appropriate go-get meta with status 200
|
// EarlyResponseForGoGetMeta responses appropriate go-get meta with status 200
|
||||||
// if user does not have actual access to the requested repository,
|
// if user does not have actual access to the requested repository,
|
||||||
// or the owner or repository does not exist at all.
|
// or the owner or repository does not exist at all.
|
||||||
@@ -402,13 +411,7 @@ func EarlyResponseForGoGetMeta(ctx *Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var cloneURL string
|
goImportContent := fmt.Sprintf("%s git %s", ComposeGoGetImport(ctx, username, reponame), ComposeGoGetCloneURL(ctx, username, reponame))
|
||||||
if setting.Repository.GoGetCloneURLProtocol == "ssh" {
|
|
||||||
cloneURL = repo_model.ComposeSSHCloneURL(ctx.Doer, username, reponame)
|
|
||||||
} else {
|
|
||||||
cloneURL = repo_model.ComposeHTTPSCloneURL(ctx, username, reponame)
|
|
||||||
}
|
|
||||||
goImportContent := fmt.Sprintf("%s git %s", ComposeGoGetImport(ctx, username, reponame), cloneURL)
|
|
||||||
htmlMeta := fmt.Sprintf(`<meta name="go-import" content="%s">`, html.EscapeString(goImportContent))
|
htmlMeta := fmt.Sprintf(`<meta name="go-import" content="%s">`, html.EscapeString(goImportContent))
|
||||||
ctx.PlainText(http.StatusOK, htmlMeta)
|
ctx.PlainText(http.StatusOK, htmlMeta)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ func checkStorage(opts *checkStorageOptions) func(ctx context.Context, logger lo
|
|||||||
&commonStorageCheckOptions{
|
&commonStorageCheckOptions{
|
||||||
storer: storage.LFS,
|
storer: storage.LFS,
|
||||||
isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) {
|
isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) {
|
||||||
|
// HINT: LFS-INTERNAL-PATH-PATTERN
|
||||||
// The oid of an LFS stored object is the name but with all the path.Separators removed
|
// The oid of an LFS stored object is the name but with all the path.Separators removed
|
||||||
oid := strings.ReplaceAll(strings.ReplaceAll(path, "\\", ""), "/", "")
|
oid := strings.ReplaceAll(strings.ReplaceAll(path, "\\", ""), "/", "")
|
||||||
exists, err := git.ExistsLFSObject(ctx, oid)
|
exists, err := git.ExistsLFSObject(ctx, oid)
|
||||||
|
|||||||
@@ -65,6 +65,6 @@ type AdminEditUserForm struct {
|
|||||||
// AdminDashboardForm form for admin dashboard operations
|
// AdminDashboardForm form for admin dashboard operations
|
||||||
type AdminDashboardForm struct {
|
type AdminDashboardForm struct {
|
||||||
middleware.FormDefaultValidator
|
middleware.FormDefaultValidator
|
||||||
Op string `binding:"required"`
|
Op string `binding:"Required"`
|
||||||
From string
|
From string
|
||||||
}
|
}
|
||||||
@@ -571,12 +571,6 @@ type AddTimeManuallyForm struct {
|
|||||||
Minutes int `binding:"Range(0,1000)"`
|
Minutes int `binding:"Range(0,1000)"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveTopicForm form for save topics for repository
|
|
||||||
type SaveTopicForm struct {
|
|
||||||
middleware.FormDefaultValidator
|
|
||||||
Topics []string `binding:"topics;Required;"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddDeployTokenForm form for adding a deploy token to a repository
|
// AddDeployTokenForm form for adding a deploy token to a repository
|
||||||
type AddDeployTokenForm struct {
|
type AddDeployTokenForm struct {
|
||||||
middleware.FormDefaultValidator
|
middleware.FormDefaultValidator
|
||||||
|
|||||||
Loaded 100 of 141 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user