mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-04 07:33:44 +09:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e28e79a648 | |||
| 6a0e125018 | |||
| 1c19e2ba43 | |||
| b0d6cc1d22 | |||
| 8936303510 | |||
| 3d085dbaf1 | |||
| 9b2a3c267b | |||
| 590d2984d9 | |||
| 61e0343580 | |||
| 0b43bde974 | |||
| cc95f141f8 | |||
| a25fbd43c4 | |||
| f365a6b9c8 | |||
| e0095af8c3 |
@@ -19,7 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- uses: go-gitea/giteabot@f48c6a15e0d384f037aea19cc05ff5e9551096b9 # v1.1.0
|
- uses: go-gitea/giteabot@4c9d4d3fd913b5c35f59dcc0b004a3d11d5b22bf # v1.0.7
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITEABOT_TOKEN }}
|
github_token: ${{ secrets.GITEABOT_TOKEN }}
|
||||||
gitea_fork: giteabot/gitea
|
gitea_fork: giteabot/gitea
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
name: giteabot-review
|
|
||||||
|
|
||||||
# Relays PR reviews to giteabot.yml through its workflow_run trigger, because review
|
|
||||||
# runs on fork PRs get no secrets and a read-only token. The job itself does nothing.
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_review:
|
|
||||||
types:
|
|
||||||
- submitted
|
|
||||||
- edited
|
|
||||||
- dismissed
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
relay:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- run: "true"
|
|
||||||
@@ -20,12 +20,13 @@ on:
|
|||||||
- closed
|
- closed
|
||||||
- review_requested
|
- review_requested
|
||||||
- review_request_removed
|
- review_request_removed
|
||||||
# Reviews arrive through giteabot-review because fork PR review runs get no secrets
|
# Review events keep review-derived state such as lgtm labels and status checks
|
||||||
workflow_run:
|
# in sync after approvals, edits, or dismissals.
|
||||||
workflows:
|
pull_request_review:
|
||||||
- giteabot-review
|
|
||||||
types:
|
types:
|
||||||
- requested
|
- submitted
|
||||||
|
- edited
|
||||||
|
- dismissed
|
||||||
# Periodic maintenance is still useful as a backstop for queue cleanup and
|
# Periodic maintenance is still useful as a backstop for queue cleanup and
|
||||||
# other housekeeping, even though main pushes now trigger it promptly.
|
# other housekeeping, even though main pushes now trigger it promptly.
|
||||||
schedule:
|
schedule:
|
||||||
@@ -42,12 +43,12 @@ on:
|
|||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ format('{0}-{1}', github.workflow, github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number) || github.event_name == 'workflow_run' && format('review-{0}', github.event.workflow_run.head_sha) || 'maintenance') }}
|
group: ${{ format('{0}-{1}', github.workflow, (github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review') && format('pr-{0}', github.event.pull_request.number) || 'maintenance') }}
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
giteabot:
|
giteabot:
|
||||||
if: github.repository == 'go-gitea/gitea' && (github.event_name != 'workflow_run' || github.event.workflow_run.event == 'pull_request_review')
|
if: github.repository == 'go-gitea/gitea'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
permissions:
|
permissions:
|
||||||
@@ -56,7 +57,9 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
statuses: write
|
statuses: write
|
||||||
steps:
|
steps:
|
||||||
- uses: go-gitea/giteabot@f48c6a15e0d384f037aea19cc05ff5e9551096b9 # v1.1.0
|
# pull_request_review runs without repository secrets on fork PRs, so fall
|
||||||
|
# back to the workflow token for the non-backport checks handled here.
|
||||||
|
- uses: go-gitea/giteabot@4c9d4d3fd913b5c35f59dcc0b004a3d11d5b22bf # v1.0.7
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITEABOT_TOKEN }}
|
github_token: ${{ secrets.GITEABOT_TOKEN || github.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' }}
|
||||||
|
|||||||
+21
-1
@@ -21,8 +21,19 @@ import (
|
|||||||
"gitea.dev/modules/util"
|
"gitea.dev/modules/util"
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/mholt/acmez/v3/acme"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func acmeExternalAccountBinding() (acme.EAB, bool, error) {
|
||||||
|
if setting.AcmeEABKID == "" && setting.AcmeEABHMAC == "" {
|
||||||
|
return acme.EAB{}, false, nil
|
||||||
|
}
|
||||||
|
if setting.AcmeEABKID == "" || setting.AcmeEABHMAC == "" {
|
||||||
|
return acme.EAB{}, false, errors.New("both ACME_EAB_KID and ACME_EAB_HMAC must be set")
|
||||||
|
}
|
||||||
|
return acme.EAB{KeyID: setting.AcmeEABKID, MACKey: setting.AcmeEABHMAC}, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
func getCARoot(path string) (*x509.CertPool, error) {
|
func getCARoot(path string) (*x509.CertPool, error) {
|
||||||
r, err := os.ReadFile(path)
|
r, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -66,6 +77,14 @@ func runACME(listenAddr string, m http.Handler) error {
|
|||||||
log.Warn("Failed to parse CA Root certificate, using default CA trust: %v", err)
|
log.Warn("Failed to parse CA Root certificate, using default CA trust: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
externalAccountBinding, hasExternalAccount, err := acmeExternalAccountBinding()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var externalAccount *acme.EAB
|
||||||
|
if hasExternalAccount {
|
||||||
|
externalAccount = &externalAccountBinding
|
||||||
|
}
|
||||||
// FIXME: this path is not right, it uses "AppWorkPath" incorrectly, and writes the data into "AppWorkPath/https"
|
// FIXME: this path is not right, it uses "AppWorkPath" incorrectly, and writes the data into "AppWorkPath/https"
|
||||||
// Ideally it should migrate to AppDataPath write to "AppDataPath/https"
|
// Ideally it should migrate to AppDataPath write to "AppDataPath/https"
|
||||||
// And one more thing, no idea why we should set the global default variables here
|
// And one more thing, no idea why we should set the global default variables here
|
||||||
@@ -84,6 +103,7 @@ func runACME(listenAddr string, m http.Handler) error {
|
|||||||
Email: setting.AcmeEmail,
|
Email: setting.AcmeEmail,
|
||||||
Agreed: setting.AcmeTOS,
|
Agreed: setting.AcmeTOS,
|
||||||
Profile: setting.AcmeProfile,
|
Profile: setting.AcmeProfile,
|
||||||
|
ExternalAccount: externalAccount,
|
||||||
DisableHTTPChallenge: !enableHTTPChallenge,
|
DisableHTTPChallenge: !enableHTTPChallenge,
|
||||||
DisableTLSALPNChallenge: !enableTLSALPNChallenge,
|
DisableTLSALPNChallenge: !enableTLSALPNChallenge,
|
||||||
ListenHost: setting.HTTPAddr,
|
ListenHost: setting.HTTPAddr,
|
||||||
@@ -100,7 +120,7 @@ func runACME(listenAddr string, m http.Handler) error {
|
|||||||
// takes HTTPS down on restart (https://github.com/go-gitea/gitea/issues/38519).
|
// takes HTTPS down on restart (https://github.com/go-gitea/gitea/issues/38519).
|
||||||
// Prefer keeping the existing cert and retrying renewals asynchronously.
|
// Prefer keeping the existing cert and retrying renewals asynchronously.
|
||||||
ctx := graceful.GetManager().ShutdownContext()
|
ctx := graceful.GetManager().ShutdownContext()
|
||||||
err := magic.ManageSync(ctx, []string{setting.AppDomain})
|
err = magic.ManageSync(ctx, []string{setting.AppDomain})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cert, cacheErr := magic.CacheManagedCertificate(ctx, setting.AppDomain)
|
cert, cacheErr := magic.CacheManagedCertificate(ctx, setting.AppDomain)
|
||||||
if cacheErr != nil || cert.Expired() {
|
if cacheErr != nil || cert.Expired() {
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dev/modules/setting"
|
||||||
|
"gitea.dev/modules/test"
|
||||||
|
|
||||||
|
"github.com/mholt/acmez/v3/acme"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAcmeExternalAccountBinding(t *testing.T) {
|
||||||
|
t.Cleanup(test.MockVariableValue(&setting.AcmeEABKID, ""))
|
||||||
|
t.Cleanup(test.MockVariableValue(&setting.AcmeEABHMAC, ""))
|
||||||
|
|
||||||
|
binding, configured, err := acmeExternalAccountBinding()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, configured)
|
||||||
|
assert.Empty(t, binding)
|
||||||
|
|
||||||
|
setting.AcmeEABKID = "kid"
|
||||||
|
_, _, err = acmeExternalAccountBinding()
|
||||||
|
assert.ErrorContains(t, err, "both ACME_EAB_KID and ACME_EAB_HMAC must be set")
|
||||||
|
|
||||||
|
setting.AcmeEABHMAC = "hmac"
|
||||||
|
binding, configured, err = acmeExternalAccountBinding()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, configured)
|
||||||
|
assert.Equal(t, acme.EAB{KeyID: "kid", MACKey: "hmac"}, binding)
|
||||||
|
}
|
||||||
+21
-18
@@ -155,7 +155,7 @@
|
|||||||
;; Username to use for the builtin SSH server. If blank, then it is the value of RUN_USER.
|
;; Username to use for the builtin SSH server. If blank, then it is the value of RUN_USER.
|
||||||
;BUILTIN_SSH_SERVER_USER =
|
;BUILTIN_SSH_SERVER_USER =
|
||||||
;;
|
;;
|
||||||
;; Domain name to be exposed in clone URL, defaults to DOMAIN or the domain part of ROOT_URL
|
;; Domain name to be exposed in clone URL, defaults to the domain part of ROOT_URL
|
||||||
;SSH_DOMAIN =
|
;SSH_DOMAIN =
|
||||||
;;
|
;;
|
||||||
;; Port number to be exposed in clone URL.
|
;; Port number to be exposed in clone URL.
|
||||||
@@ -198,7 +198,7 @@
|
|||||||
;; For the built-in SSH server, choose the keypair to offer as the host key
|
;; For the built-in SSH server, choose the keypair to offer as the host key
|
||||||
;; The private key should be at SSH_SERVER_HOST_KEY and the public SSH_SERVER_HOST_KEY.pub
|
;; The private key should be at SSH_SERVER_HOST_KEY and the public SSH_SERVER_HOST_KEY.pub
|
||||||
;; relative paths are made absolute relative to the APP_DATA_PATH
|
;; relative paths are made absolute relative to the APP_DATA_PATH
|
||||||
;SSH_SERVER_HOST_KEYS=ssh/gitea.rsa, ssh/gogs.rsa
|
;SSH_SERVER_HOST_KEYS=ssh/gitea.rsa, ssh/gitea.ed25519, ssh/gitea.ecdsa, ssh/gogs.rsa
|
||||||
;;
|
;;
|
||||||
;; Enable SSH Authorized Key Backup when rewriting all keys, default is false
|
;; Enable SSH Authorized Key Backup when rewriting all keys, default is false
|
||||||
;SSH_AUTHORIZED_KEYS_BACKUP = false
|
;SSH_AUTHORIZED_KEYS_BACKUP = false
|
||||||
@@ -237,7 +237,7 @@
|
|||||||
;SSH_PER_WRITE_PER_KB_TIMEOUT = 30s
|
;SSH_PER_WRITE_PER_KB_TIMEOUT = 30s
|
||||||
;;
|
;;
|
||||||
;; Indicate whether to check minimum key size with corresponding type
|
;; Indicate whether to check minimum key size with corresponding type
|
||||||
;MINIMUM_KEY_SIZE_CHECK = false
|
;MINIMUM_KEY_SIZE_CHECK = true
|
||||||
;;
|
;;
|
||||||
;; TLS Settings: Either ACME or manual
|
;; TLS Settings: Either ACME or manual
|
||||||
;; (Other common TLS configuration are found before)
|
;; (Other common TLS configuration are found before)
|
||||||
@@ -264,6 +264,11 @@
|
|||||||
;; ACME profile to request from the CA (e.g. "shortlived" for raw-IP certificates)
|
;; ACME profile to request from the CA (e.g. "shortlived" for raw-IP certificates)
|
||||||
;ACME_PROFILE =
|
;ACME_PROFILE =
|
||||||
;;
|
;;
|
||||||
|
;; External account binding credentials; set both to enable EAB
|
||||||
|
;; ACME_EAB_HMAC should be a base64url-encoded MAC key
|
||||||
|
;ACME_EAB_KID =
|
||||||
|
;ACME_EAB_HMAC =
|
||||||
|
;;
|
||||||
;; ACME live directory (not to be confused with ACME directory URL: ACME_URL)
|
;; ACME live directory (not to be confused with ACME directory URL: ACME_URL)
|
||||||
;; (Refer to caddy's ACME manager https://github.com/caddyserver/certmagic)
|
;; (Refer to caddy's ACME manager https://github.com/caddyserver/certmagic)
|
||||||
;ACME_DIRECTORY = https
|
;ACME_DIRECTORY = https
|
||||||
@@ -836,7 +841,7 @@ LEVEL = Info
|
|||||||
;EMAIL_DOMAIN_BLOCKLIST =
|
;EMAIL_DOMAIN_BLOCKLIST =
|
||||||
;;
|
;;
|
||||||
;; Disallow registration, only allow admins to create accounts.
|
;; Disallow registration, only allow admins to create accounts.
|
||||||
;DISABLE_REGISTRATION = false
|
;DISABLE_REGISTRATION = true
|
||||||
;;
|
;;
|
||||||
;; Allow registration only using gitea itself, it works only when DISABLE_REGISTRATION is false
|
;; Allow registration only using gitea itself, it works only when DISABLE_REGISTRATION is false
|
||||||
;ALLOW_ONLY_INTERNAL_REGISTRATION = false
|
;ALLOW_ONLY_INTERNAL_REGISTRATION = false
|
||||||
@@ -968,12 +973,11 @@ LEVEL = Info
|
|||||||
;; Value for the domain part of the user's email address in the git log if user
|
;; Value for the domain part of the user's email address in the git log if user
|
||||||
;; has set KeepEmailPrivate to true. The user's email will be replaced with a
|
;; has set KeepEmailPrivate to true. The user's email will be replaced with a
|
||||||
;; concatenation of the user name in lower case, "@" and NO_REPLY_ADDRESS. Default
|
;; concatenation of the user name in lower case, "@" and NO_REPLY_ADDRESS. Default
|
||||||
;; value is "noreply." + DOMAIN, where DOMAIN resolves to the value from server.DOMAIN
|
;; value is "noreply." + the domain part of ROOT_URL
|
||||||
;; Note: do not use the <DOMAIN> notation below
|
;NO_REPLY_ADDRESS =
|
||||||
;NO_REPLY_ADDRESS = ; noreply.<DOMAIN>
|
|
||||||
;;
|
;;
|
||||||
;; Show Registration button
|
;; Show Registration button, defaults to true only if both DISABLE_REGISTRATION and ALLOW_ONLY_EXTERNAL_REGISTRATION are false
|
||||||
;SHOW_REGISTRATION_BUTTON = true
|
;SHOW_REGISTRATION_BUTTON = false
|
||||||
;;
|
;;
|
||||||
;; Show milestones dashboard page - a view of all the user's milestones
|
;; Show milestones dashboard page - a view of all the user's milestones
|
||||||
;SHOW_MILESTONES_DASHBOARD_PAGE = true
|
;SHOW_MILESTONES_DASHBOARD_PAGE = true
|
||||||
@@ -1670,13 +1674,13 @@ LEVEL = Info
|
|||||||
;;
|
;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;
|
;;
|
||||||
;; General queue queue type, currently support: persistable-channel, channel, level, redis, dummy
|
;; General queue type, currently support: level, channel, redis, dummy
|
||||||
;; default to persistable-channel
|
;; default to level
|
||||||
;TYPE = persistable-channel
|
;TYPE = level
|
||||||
;;
|
;;
|
||||||
;; data-dir for storing persistable queues and level queues, individual queues will default to `queues/common` meaning the queue is shared.
|
;; data-dir for storing level queues, individual queues will default to `queues/common` meaning the queue is shared.
|
||||||
;; Relative paths will be made absolute against "APP_DATA_PATH"
|
;; Relative paths will be made absolute against "APP_DATA_PATH"
|
||||||
;DATADIR = queues/
|
;DATADIR = queues/common
|
||||||
;;
|
;;
|
||||||
;; Default queue length before a channel queue will block
|
;; Default queue length before a channel queue will block
|
||||||
;LENGTH = 100000
|
;LENGTH = 100000
|
||||||
@@ -1684,7 +1688,7 @@ LEVEL = Info
|
|||||||
;; Batch size to send for batched queues
|
;; Batch size to send for batched queues
|
||||||
;BATCH_LENGTH = 20
|
;BATCH_LENGTH = 20
|
||||||
;;
|
;;
|
||||||
;; When `TYPE` is `persistable-channel`, this provides a directory for the underlying leveldb
|
;; When `TYPE` is `level`, this provides a directory for the underlying leveldb
|
||||||
;; or additional options of the form `leveldb://path/to/db?option=value&....`, and will override `DATADIR`.
|
;; or additional options of the form `leveldb://path/to/db?option=value&....`, and will override `DATADIR`.
|
||||||
;; When `TYPE` is `redis` and this is left empty, it falls back to the shared [redis] CONN_STR.
|
;; When `TYPE` is `redis` and this is left empty, it falls back to the shared [redis] CONN_STR.
|
||||||
;CONN_STR =
|
;CONN_STR =
|
||||||
@@ -1752,7 +1756,6 @@ LEVEL = Info
|
|||||||
;ENABLE_OPENID_SIGNIN = false
|
;ENABLE_OPENID_SIGNIN = false
|
||||||
;;
|
;;
|
||||||
;; Whether to allow registering via OpenID
|
;; Whether to allow registering via OpenID
|
||||||
;; Do not include to rely on rhw DISABLE_REGISTRATION setting
|
|
||||||
;;ENABLE_OPENID_SIGNUP = false
|
;;ENABLE_OPENID_SIGNUP = false
|
||||||
;;
|
;;
|
||||||
;; Allowed URI patterns (POSIX regexp).
|
;; Allowed URI patterns (POSIX regexp).
|
||||||
@@ -2015,8 +2018,8 @@ LEVEL = Info
|
|||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;
|
;;
|
||||||
;; Either "memory", "file", "redis", "db", "mysql", "couchbase", "memcache" or "postgres"
|
;; Either "memory", "file", "redis", "db", "mysql", "couchbase", "memcache" or "postgres"
|
||||||
;; Default is "memory". "db" will reuse the configuration in [database]
|
;; Default is "file". "db" will reuse the configuration in [database]
|
||||||
;PROVIDER = memory
|
;PROVIDER = file
|
||||||
;;
|
;;
|
||||||
;; Provider config options
|
;; Provider config options
|
||||||
;; memory: doesn't have any config yet
|
;; memory: doesn't have any config yet
|
||||||
|
|||||||
@@ -62,10 +62,6 @@ Operations that must roll back together should run inside `db.WithTx()` (or
|
|||||||
Functions that participate in a transaction take a `context.Context` as their first
|
Functions that participate in a transaction take a `context.Context` as their first
|
||||||
parameter so the transaction can be propagated.
|
parameter so the transaction can be propagated.
|
||||||
|
|
||||||
PostgreSQL, MySQL and MSSQL (via `READ_COMMITTED_SNAPSHOT`) read the last committed
|
|
||||||
row version, so reads never wait for writers. Guard read-then-write logic with a
|
|
||||||
conditional `UPDATE` or a lock.
|
|
||||||
|
|
||||||
### XORM gotchas
|
### XORM gotchas
|
||||||
|
|
||||||
- Never call `x.Update(exemplar)` without an explicit `WHERE` clause — it updates
|
- Never call `x.Update(exemplar)` without an explicit `WHERE` clause — it updates
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.24
|
github.com/mattn/go-isatty v0.0.24
|
||||||
github.com/mattn/go-sqlite3 v1.14.52
|
github.com/mattn/go-sqlite3 v1.14.52
|
||||||
github.com/meilisearch/meilisearch-go v0.36.3
|
github.com/meilisearch/meilisearch-go v0.36.3
|
||||||
|
github.com/mholt/acmez/v3 v3.1.6
|
||||||
github.com/mholt/archives v0.1.5
|
github.com/mholt/archives v0.1.5
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27
|
github.com/microcosm-cc/bluemonday v1.0.27
|
||||||
github.com/microsoft/go-mssqldb v1.11.2
|
github.com/microsoft/go-mssqldb v1.11.2
|
||||||
@@ -109,7 +110,7 @@ require (
|
|||||||
modernc.org/sqlite v1.59.0
|
modernc.org/sqlite v1.59.0
|
||||||
mvdan.cc/xurls/v2 v2.6.0
|
mvdan.cc/xurls/v2 v2.6.0
|
||||||
xorm.io/builder v0.3.13
|
xorm.io/builder v0.3.13
|
||||||
xorm.io/xorm v1.4.3
|
xorm.io/xorm v1.4.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -199,7 +200,6 @@ require (
|
|||||||
github.com/markbates/going v1.0.3 // indirect
|
github.com/markbates/going v1.0.3 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.24 // indirect
|
github.com/mattn/go-runewidth v0.0.24 // indirect
|
||||||
github.com/mholt/acmez/v3 v3.1.6 // indirect
|
|
||||||
github.com/miekg/dns v1.1.72 // indirect
|
github.com/miekg/dns v1.1.72 // indirect
|
||||||
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
||||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||||
|
|||||||
@@ -862,5 +862,5 @@ pgregory.net/rapid v0.4.2 h1:lsi9jhvZTYvzVpeG93WWgimPRmiJQfGFRNTEZh1dtY0=
|
|||||||
pgregory.net/rapid v0.4.2/go.mod h1:UYpPVyjFHzYBGHIxLFoupi8vwk6rXNzRY9OMvVxFIOU=
|
pgregory.net/rapid v0.4.2/go.mod h1:UYpPVyjFHzYBGHIxLFoupi8vwk6rXNzRY9OMvVxFIOU=
|
||||||
xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
|
xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
|
||||||
xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
||||||
xorm.io/xorm v1.4.3 h1:MwWFWzVr+/6D07qGCDhBAfABcuT0gvqY3XmTy1215BM=
|
xorm.io/xorm v1.4.1 h1:m7QlNd0eBGb31IV4Q/ow0Du83rtdC1CiwlvJZGvYde8=
|
||||||
xorm.io/xorm v1.4.3/go.mod h1:cs0ePc8O4a0jD78cNvD+0VFwhqotTvLQZv372QsDw7Q=
|
xorm.io/xorm v1.4.1/go.mod h1:cs0ePc8O4a0jD78cNvD+0VFwhqotTvLQZv372QsDw7Q=
|
||||||
|
|||||||
@@ -298,12 +298,6 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
|
|||||||
if err := e.Where(cond).Asc("updated", "id").Limit(pickTaskBatchSize).Find(&jobs); err != nil {
|
if err := e.Where(cond).Asc("updated", "id").Limit(pickTaskBatchSize).Find(&jobs); err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
// A short page means no waiting jobs remain beyond it.
|
|
||||||
isLastPage := len(jobs) < pickTaskBatchSize
|
|
||||||
if !isLastPage {
|
|
||||||
last := jobs[len(jobs)-1] // read before a lost claim bumps Updated
|
|
||||||
cursorUpdated, cursorID = last.Updated, last.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range jobs {
|
for _, v := range jobs {
|
||||||
if !runner.CanMatchLabels(v.RunsOn) {
|
if !runner.CanMatchLabels(v.RunsOn) {
|
||||||
@@ -319,9 +313,12 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
|
|||||||
// Another runner claimed this job concurrently; try the next one.
|
// Another runner claimed this job concurrently; try the next one.
|
||||||
}
|
}
|
||||||
|
|
||||||
if isLastPage {
|
// A short page means no waiting jobs remain beyond it.
|
||||||
|
if len(jobs) < pickTaskBatchSize {
|
||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
}
|
}
|
||||||
|
last := jobs[len(jobs)-1]
|
||||||
|
cursorUpdated, cursorID = last.Updated, last.ID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -564,12 +564,6 @@ func (grant *OAuth2Grant) SetNonce(ctx context.Context, nonce string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateGrantScope(ctx context.Context, grant *OAuth2Grant, newScope string) error {
|
|
||||||
grant.Scope = newScope
|
|
||||||
_, err := db.GetEngine(ctx).ID(grant.ID).Cols("scope").Update(grant)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetOAuth2GrantByID returns the grant with the given ID
|
// GetOAuth2GrantByID returns the grant with the given ID
|
||||||
func GetOAuth2GrantByID(ctx context.Context, id int64) (grant *OAuth2Grant, err error) {
|
func GetOAuth2GrantByID(ctx context.Context, id int64) (grant *OAuth2Grant, err error) {
|
||||||
grant = new(OAuth2Grant)
|
grant = new(OAuth2Grant)
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
|
||||||
|
|
||||||
"gitea.dev/modules/log"
|
"gitea.dev/modules/log"
|
||||||
"gitea.dev/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
@@ -61,11 +59,6 @@ func InitEngine(ctx context.Context) error {
|
|||||||
xe.SetMaxIdleConns(setting.Database.MaxIdleConns)
|
xe.SetMaxIdleConns(setting.Database.MaxIdleConns)
|
||||||
xe.SetConnMaxLifetime(setting.Database.ConnMaxLifetime)
|
xe.SetConnMaxLifetime(setting.Database.ConnMaxLifetime)
|
||||||
|
|
||||||
if setting.Database.Type.IsMySQL() {
|
|
||||||
// like PostgreSQL and MSSQL, avoids MariaDB snapshot isolation errors
|
|
||||||
xe.SetDefaultTxOptions(&sql.TxOptions{Isolation: sql.LevelReadCommitted})
|
|
||||||
}
|
|
||||||
|
|
||||||
if setting.Database.SlowQueryThreshold > 0 {
|
if setting.Database.SlowQueryThreshold > 0 {
|
||||||
xe.AddHook(&EngineHook{
|
xe.AddHook(&EngineHook{
|
||||||
Threshold: setting.Database.SlowQueryThreshold,
|
Threshold: setting.Database.SlowQueryThreshold,
|
||||||
@@ -110,10 +103,6 @@ func InitEngineWithMigration(ctx context.Context, migrateFunc func(context.Conte
|
|||||||
|
|
||||||
preprocessDatabaseCollation(xormEngine)
|
preprocessDatabaseCollation(xormEngine)
|
||||||
|
|
||||||
if setting.Database.Type.IsMSSQL() {
|
|
||||||
enableMSSQLReadCommittedSnapshot(ctx, xormEngine)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have to run migrateFunc here in case the user is re-running installation on a previously created DB.
|
// We have to run migrateFunc here in case the user is re-running installation on a previously created DB.
|
||||||
// If we do not then table schemas will be changed and there will be conflicts when the migrations run properly.
|
// If we do not then table schemas will be changed and there will be conflicts when the migrations run properly.
|
||||||
//
|
//
|
||||||
@@ -136,12 +125,3 @@ func InitEngineWithMigration(ctx context.Context, migrateFunc func(context.Conte
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// enableMSSQLReadCommittedSnapshot stops MSSQL reads waiting on writers, like PostgreSQL and MySQL
|
|
||||||
func enableMSSQLReadCommittedSnapshot(ctx context.Context, engine EngineMigration) {
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second) // ALTER waits for all other connections to close
|
|
||||||
defer cancel()
|
|
||||||
if _, err := engine.Context(ctx).Exec("IF (SELECT is_read_committed_snapshot_on FROM sys.databases WHERE database_id = DB_ID()) = 0 ALTER DATABASE CURRENT SET READ_COMMITTED_SNAPSHOT ON"); err != nil {
|
|
||||||
log.Error("Unable to set READ_COMMITTED_SNAPSHOT=ON: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -311,17 +311,17 @@ func (opts *CommitStatusOptions) ToConds() builder.Cond {
|
|||||||
func (opts *CommitStatusOptions) ToOrders() string {
|
func (opts *CommitStatusOptions) ToOrders() string {
|
||||||
switch opts.SortType {
|
switch opts.SortType {
|
||||||
case "oldest":
|
case "oldest":
|
||||||
return "created_unix ASC, `index` ASC"
|
return "created_unix ASC"
|
||||||
case "recentupdate":
|
case "recentupdate":
|
||||||
return "updated_unix DESC, `index` DESC"
|
return "updated_unix DESC"
|
||||||
case "leastupdate":
|
case "leastupdate":
|
||||||
return "updated_unix ASC, `index` ASC"
|
return "updated_unix ASC"
|
||||||
case "leastindex":
|
case "leastindex":
|
||||||
return "`index` DESC"
|
return "`index` DESC"
|
||||||
case "highestindex":
|
case "highestindex":
|
||||||
return "`index` ASC"
|
return "`index` ASC"
|
||||||
default:
|
default:
|
||||||
return "created_unix DESC, `index` DESC" // timestamps have 1s resolution, `index` keeps paging stable
|
return "created_unix DESC"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,15 +32,28 @@ func TestGetCommitStatuses(t *testing.T) {
|
|||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, 5, int(maxResults))
|
assert.Equal(t, 5, int(maxResults))
|
||||||
var indexes []int64
|
assert.Len(t, statuses, 5)
|
||||||
for _, status := range statuses {
|
|
||||||
indexes = append(indexes, status.Index)
|
assert.Equal(t, "ci/awesomeness", statuses[0].Context)
|
||||||
}
|
assert.Equal(t, commitstatus.CommitStatusPending, statuses[0].State)
|
||||||
assert.Equal(t, []int64{5, 4, 3, 2, 1}, indexes)
|
|
||||||
assert.Equal(t, "deploy/awesomeness", statuses[0].Context)
|
|
||||||
assert.Equal(t, commitstatus.CommitStatusError, statuses[0].State)
|
|
||||||
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[0].APIURL(t.Context()))
|
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[0].APIURL(t.Context()))
|
||||||
|
|
||||||
|
assert.Equal(t, "cov/awesomeness", statuses[1].Context)
|
||||||
|
assert.Equal(t, commitstatus.CommitStatusWarning, statuses[1].State)
|
||||||
|
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[1].APIURL(t.Context()))
|
||||||
|
|
||||||
|
assert.Equal(t, "cov/awesomeness", statuses[2].Context)
|
||||||
|
assert.Equal(t, commitstatus.CommitStatusSuccess, statuses[2].State)
|
||||||
|
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[2].APIURL(t.Context()))
|
||||||
|
|
||||||
|
assert.Equal(t, "ci/awesomeness", statuses[3].Context)
|
||||||
|
assert.Equal(t, commitstatus.CommitStatusFailure, statuses[3].State)
|
||||||
|
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[3].APIURL(t.Context()))
|
||||||
|
|
||||||
|
assert.Equal(t, "deploy/awesomeness", statuses[4].Context)
|
||||||
|
assert.Equal(t, commitstatus.CommitStatusError, statuses[4].State)
|
||||||
|
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[4].APIURL(t.Context()))
|
||||||
|
|
||||||
statuses, maxResults, err = db.FindAndCount[git_model.CommitStatus](t.Context(), &git_model.CommitStatusOptions{
|
statuses, maxResults, err = db.FindAndCount[git_model.CommitStatus](t.Context(), &git_model.CommitStatusOptions{
|
||||||
ListOptions: db.ListOptions{Page: 2, PageSize: 50},
|
ListOptions: db.ListOptions{Page: 2, PageSize: 50},
|
||||||
RepoID: repo1.ID,
|
RepoID: repo1.ID,
|
||||||
|
|||||||
@@ -123,13 +123,23 @@ func (protectBranch *ProtectedBranch) LoadRepo(ctx context.Context) (err error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CanUserPush returns if some user could push to this protected branch
|
// CanUserPush returns if some user could push to this protected branch
|
||||||
func (protectBranch *ProtectedBranch) CanUserPush(ctx context.Context, user *user_model.User, permissionInRepo access_model.Permission) bool {
|
func (protectBranch *ProtectedBranch) CanUserPush(ctx context.Context, user *user_model.User) bool {
|
||||||
if !protectBranch.CanPush {
|
if !protectBranch.CanPush {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !protectBranch.EnableWhitelist {
|
if !protectBranch.EnableWhitelist {
|
||||||
return permissionInRepo.CanWrite(unit.TypeCode)
|
if err := protectBranch.LoadRepo(ctx); err != nil {
|
||||||
|
log.Error("LoadRepo: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
writeAccess, err := access_model.HasAccessUnit(ctx, user, protectBranch.Repo, unit.TypeCode, perm.AccessModeWrite)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("HasAccessUnit: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return writeAccess
|
||||||
}
|
}
|
||||||
|
|
||||||
if slices.Contains(protectBranch.WhitelistUserIDs, user.ID) {
|
if slices.Contains(protectBranch.WhitelistUserIDs, user.ID) {
|
||||||
@@ -150,17 +160,17 @@ func (protectBranch *ProtectedBranch) CanUserPush(ctx context.Context, user *use
|
|||||||
|
|
||||||
// CanUserForcePush returns if some user could force push to this protected branch
|
// CanUserForcePush returns if some user could force push to this protected branch
|
||||||
// Since force-push extends normal push, we also check if user has regular push access
|
// Since force-push extends normal push, we also check if user has regular push access
|
||||||
func (protectBranch *ProtectedBranch) CanUserForcePush(ctx context.Context, user *user_model.User, permissionInRepo access_model.Permission) bool {
|
func (protectBranch *ProtectedBranch) CanUserForcePush(ctx context.Context, user *user_model.User) bool {
|
||||||
if !protectBranch.CanForcePush {
|
if !protectBranch.CanForcePush {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !protectBranch.EnableForcePushAllowlist {
|
if !protectBranch.EnableForcePushAllowlist {
|
||||||
return protectBranch.CanUserPush(ctx, user, permissionInRepo)
|
return protectBranch.CanUserPush(ctx, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
if slices.Contains(protectBranch.ForcePushAllowlistUserIDs, user.ID) {
|
if slices.Contains(protectBranch.ForcePushAllowlistUserIDs, user.ID) {
|
||||||
return protectBranch.CanUserPush(ctx, user, permissionInRepo)
|
return protectBranch.CanUserPush(ctx, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(protectBranch.ForcePushAllowlistTeamIDs) == 0 {
|
if len(protectBranch.ForcePushAllowlistTeamIDs) == 0 {
|
||||||
@@ -172,7 +182,7 @@ func (protectBranch *ProtectedBranch) CanUserForcePush(ctx context.Context, user
|
|||||||
log.Error("IsUserInTeams: %v", err)
|
log.Error("IsUserInTeams: %v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return in && protectBranch.CanUserPush(ctx, user, permissionInRepo)
|
return in && protectBranch.CanUserPush(ctx, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsUserMergeWhitelisted checks if some user is whitelisted to merge to this branch
|
// IsUserMergeWhitelisted checks if some user is whitelisted to merge to this branch
|
||||||
|
|||||||
@@ -407,21 +407,6 @@ func (pr *PullRequest) GetGitHeadRefName() string { // TODO: make it return RefN
|
|||||||
return git.RefNameFromPullIndex(pr.Index).String()
|
return git.RefNameFromPullIndex(pr.Index).String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pr *PullRequest) GetInstructionsCliArgs() (ret struct {
|
|
||||||
BaseBranchArg string
|
|
||||||
HeadBranchArg string
|
|
||||||
LocalBranchArg string
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
ret.BaseBranchArg = util.ShellEscape(pr.BaseBranch)
|
|
||||||
ret.HeadBranchArg = util.ShellEscape(pr.HeadBranch)
|
|
||||||
ret.LocalBranchArg = ret.HeadBranchArg
|
|
||||||
if pr.HeadRepo != nil && pr.HeadRepoID != pr.BaseRepoID {
|
|
||||||
ret.LocalBranchArg = util.ShellEscape(pr.HeadRepo.OwnerName) + "-" + ret.HeadBranchArg
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetReviewCommentsCount returns the number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR)
|
// GetReviewCommentsCount returns the number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR)
|
||||||
func (pr *PullRequest) GetReviewCommentsCount(ctx context.Context) int {
|
func (pr *PullRequest) GetReviewCommentsCount(ctx context.Context) int {
|
||||||
opts := FindCommentsOptions{
|
opts := FindCommentsOptions{
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ type SearchUserOptions struct {
|
|||||||
Keyword string
|
Keyword string
|
||||||
Types []UserType
|
Types []UserType
|
||||||
UID int64
|
UID int64
|
||||||
LoginName string // this option should be used only for admin user
|
LoginName string // this option should be used only for admin user
|
||||||
SourceID int64 // this option should be used only for admin user
|
SourceID optional.Option[int64] // this option should be used only for admin user, Some(0) means local users
|
||||||
OrderBy db.SearchOrderBy
|
OrderBy db.SearchOrderBy
|
||||||
Visible []structs.VisibleType
|
Visible []structs.VisibleType
|
||||||
Actor *User // The user doing the search
|
Actor *User // The user doing the search
|
||||||
@@ -106,8 +106,8 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) db.Session
|
|||||||
cond = cond.And(builder.Eq{"id": opts.UID})
|
cond = cond.And(builder.Eq{"id": opts.UID})
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.SourceID > 0 {
|
if opts.SourceID.Has() {
|
||||||
cond = cond.And(builder.Eq{"login_source": opts.SourceID})
|
cond = cond.And(builder.Eq{"login_source": opts.SourceID.Value()})
|
||||||
}
|
}
|
||||||
if opts.LoginName != "" {
|
if opts.LoginName != "" {
|
||||||
cond = cond.And(builder.Eq{"login_name": opts.LoginName})
|
cond = cond.And(builder.Eq{"login_name": opts.LoginName})
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"gitea.dev/modules/git"
|
"gitea.dev/modules/git"
|
||||||
"gitea.dev/modules/git/gitcmd"
|
"gitea.dev/modules/git/gitcmd"
|
||||||
"gitea.dev/modules/log"
|
"gitea.dev/modules/log"
|
||||||
"gitea.dev/modules/setting"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// BatchChecker provides a reader for check-attribute content that can be long running
|
// BatchChecker provides a reader for check-attribute content that can be long running
|
||||||
@@ -121,17 +120,12 @@ func (c *BatchChecker) CheckPath(path string) (rs *Attributes, err error) {
|
|||||||
return fmt.Errorf("CheckPath timeout: %s", debugMsg)
|
return fmt.Errorf("CheckPath timeout: %s", debugMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
timeout := time.NewTimer(5 * time.Second)
|
|
||||||
defer timeout.Stop()
|
|
||||||
|
|
||||||
rs = NewAttributes()
|
rs = NewAttributes()
|
||||||
for i := 0; i < c.attributesNum; i++ {
|
for i := 0; i < c.attributesNum; i++ {
|
||||||
select {
|
select {
|
||||||
case <-timeout.C:
|
case <-time.After(5 * time.Second):
|
||||||
// there is no "hang" problem now. This code is just used to catch other potential problems.
|
// there is no "hang" problem now. This code is just used to catch other potential problems.
|
||||||
err = reportTimeout()
|
return nil, reportTimeout()
|
||||||
setting.PanicInDevOrTesting("Unexpected timeout, need to investigate: %v", err)
|
|
||||||
return nil, err
|
|
||||||
case attr, ok := <-c.stdOut.ReadAttribute():
|
case attr, ok := <-c.stdOut.ReadAttribute():
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, c.ctx.Err()
|
return nil, c.ctx.Err()
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ type Command struct {
|
|||||||
cmd *process.Cmd
|
cmd *process.Cmd
|
||||||
|
|
||||||
cmdCtx context.Context
|
cmdCtx context.Context
|
||||||
cmdCtxCancel process.CancelCauseFunc
|
cmdCancel process.CancelCauseFunc
|
||||||
cmdFinished func()
|
cmdFinished process.FinishedFunc
|
||||||
cmdStartTime time.Time
|
cmdStartTime time.Time
|
||||||
|
|
||||||
pipelineFunc func(Context) error
|
pipelineFunc func(Context) error
|
||||||
@@ -428,24 +428,19 @@ func (c *Command) Start(ctx context.Context) (retErr error) {
|
|||||||
if c.callerInfo == "" {
|
if c.callerInfo == "" {
|
||||||
c.WithParentCallerInfo()
|
c.WithParentCallerInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
// these logs are for debugging purposes only, so no guarantee of correctness or stability
|
// these logs are for debugging purposes only, so no guarantee of correctness or stability
|
||||||
desc := fmt.Sprintf("git.Run(by:%s, repo:%s): %s", c.callerInfo, logArgSanitize(c.gitDir), cmdLogString)
|
desc := fmt.Sprintf("git.Run(by:%s, repo:%s): %s", c.callerInfo, logArgSanitize(c.gitDir), cmdLogString)
|
||||||
log.Debug("git.Command: %s", desc)
|
log.Debug("git.Command: %s", desc)
|
||||||
|
|
||||||
_, span := gtprof.GetTracer().Start(ctx, gtprof.TraceSpanGitRun)
|
_, span := gtprof.GetTracer().Start(ctx, gtprof.TraceSpanGitRun)
|
||||||
|
defer span.End()
|
||||||
span.SetAttributeString(gtprof.TraceAttrFuncCaller, c.callerInfo)
|
span.SetAttributeString(gtprof.TraceAttrFuncCaller, c.callerInfo)
|
||||||
span.SetAttributeString(gtprof.TraceAttrGitCommand, cmdLogString)
|
span.SetAttributeString(gtprof.TraceAttrGitCommand, cmdLogString)
|
||||||
|
|
||||||
var cmdCtxFinished func()
|
|
||||||
if c.cmdTimeout <= 0 {
|
if c.cmdTimeout <= 0 {
|
||||||
c.cmdCtx, c.cmdCtxCancel, cmdCtxFinished = process.GetManager().AddContext(ctx, desc)
|
c.cmdCtx, c.cmdCancel, c.cmdFinished = process.GetManager().AddContext(ctx, desc)
|
||||||
} else {
|
} else {
|
||||||
c.cmdCtx, c.cmdCtxCancel, cmdCtxFinished = process.GetManager().AddContextTimeout(ctx, c.cmdTimeout, desc)
|
c.cmdCtx, c.cmdCancel, c.cmdFinished = process.GetManager().AddContextTimeout(ctx, c.cmdTimeout, desc)
|
||||||
}
|
|
||||||
c.cmdFinished = func() {
|
|
||||||
cmdCtxFinished()
|
|
||||||
span.End()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.cmdStartTime = time.Now()
|
c.cmdStartTime = time.Now()
|
||||||
|
|||||||
@@ -27,6 +27,6 @@ func (c *cmdContext) CancelPipeline(err error) error {
|
|||||||
// * context canceled by pipeline caller with/without error (normal cancellation)
|
// * context canceled by pipeline caller with/without error (normal cancellation)
|
||||||
// * context canceled by parent context (still context.Canceled error)
|
// * context canceled by parent context (still context.Canceled error)
|
||||||
// * other causes
|
// * other causes
|
||||||
c.cmd.cmdCtxCancel(pipelineError{err})
|
c.cmd.cmdCancel(pipelineError{err})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,9 @@ package git
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gitea.dev/modules/container"
|
|
||||||
"gitea.dev/modules/git/gitrepo"
|
"gitea.dev/modules/git/gitrepo"
|
||||||
"gitea.dev/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
|
|
||||||
@@ -24,7 +20,6 @@ import (
|
|||||||
"github.com/go-git/go-git/v5/plumbing"
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
"github.com/go-git/go-git/v5/plumbing/cache"
|
"github.com/go-git/go-git/v5/plumbing/cache"
|
||||||
"github.com/go-git/go-git/v5/storage/filesystem"
|
"github.com/go-git/go-git/v5/storage/filesystem"
|
||||||
"github.com/go-git/go-git/v5/storage/filesystem/dotgit"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const isGogit = true
|
const isGogit = true
|
||||||
@@ -36,98 +31,25 @@ type Repository struct {
|
|||||||
gogitStorage *reindexingStorage
|
gogitStorage *reindexingStorage
|
||||||
}
|
}
|
||||||
|
|
||||||
// reindexingStorage reloads the pack index when git added or removed packs after go-git loaded it
|
// reindexingStorage picks up packs that git wrote after go-git loaded its index
|
||||||
// https://github.com/go-git/go-git/issues/2439 https://github.com/go-git/go-git/issues/1623
|
// https://github.com/go-git/go-git/issues/2439
|
||||||
// FIXME: gogit workaround, remove with the gogit build
|
|
||||||
type reindexingStorage struct {
|
type reindexingStorage struct {
|
||||||
*filesystem.Storage
|
*filesystem.Storage
|
||||||
packs []plumbing.Hash
|
packs []plumbing.Hash
|
||||||
}
|
}
|
||||||
|
|
||||||
func isRepackError(err error) bool {
|
func (s *reindexingStorage) EncodedObject(t plumbing.ObjectType, h plumbing.Hash) (plumbing.EncodedObject, error) {
|
||||||
return errors.Is(err, plumbing.ErrObjectNotFound) || errors.Is(err, dotgit.ErrPackfileNotFound) || errors.Is(err, os.ErrNotExist)
|
obj, err := s.Storage.EncodedObject(t, h)
|
||||||
}
|
if !errors.Is(err, plumbing.ErrObjectNotFound) {
|
||||||
|
return obj, err
|
||||||
// retry reruns fn while a concurrent repack keeps changing the packs
|
|
||||||
func (s *reindexingStorage) retry(fn func() error) error {
|
|
||||||
for {
|
|
||||||
err := fn()
|
|
||||||
if !isRepackError(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
packs, _ := s.ObjectPacks()
|
|
||||||
if slices.Equal(packs, s.packs) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.packs = packs
|
|
||||||
s.Reindex()
|
|
||||||
}
|
}
|
||||||
}
|
packs, _ := s.ObjectPacks()
|
||||||
|
if slices.Equal(packs, s.packs) {
|
||||||
func (s *reindexingStorage) EncodedObject(t plumbing.ObjectType, h plumbing.Hash) (obj plumbing.EncodedObject, err error) {
|
return obj, err
|
||||||
err = s.retry(func() (err error) {
|
|
||||||
obj, err = s.Storage.EncodedObject(t, h)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
if _, ok := obj.(*plumbing.MemoryObject); ok {
|
s.packs = packs
|
||||||
return obj, nil
|
s.Reindex()
|
||||||
}
|
return s.Storage.EncodedObject(t, h)
|
||||||
return &lazyObject{EncodedObject: obj, storage: s}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// lazyObject looks up a large object again when its file got removed before Reader reopened it
|
|
||||||
// FIXME: gogit workaround, remove with the gogit build
|
|
||||||
type lazyObject struct {
|
|
||||||
plumbing.EncodedObject
|
|
||||||
storage *reindexingStorage
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *lazyObject) Reader() (rc io.ReadCloser, err error) {
|
|
||||||
rc, err = o.EncodedObject.Reader()
|
|
||||||
if !isRepackError(err) {
|
|
||||||
return rc, err
|
|
||||||
}
|
|
||||||
err = o.storage.retry(func() error {
|
|
||||||
obj, err := o.storage.Storage.EncodedObject(o.Type(), o.Hash())
|
|
||||||
if err == nil {
|
|
||||||
o.EncodedObject = obj
|
|
||||||
rc, err = obj.Reader()
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
return rc, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// packIdxFS lists packs like git, only while their .idx exists
|
|
||||||
// FIXME: gogit workaround, remove with the gogit build
|
|
||||||
type packIdxFS struct {
|
|
||||||
billy.Filesystem
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f packIdxFS) ReadDir(dir string) ([]os.FileInfo, error) {
|
|
||||||
if dir != f.Join("objects", "pack") {
|
|
||||||
return f.Filesystem.ReadDir(dir)
|
|
||||||
}
|
|
||||||
dirFile, err := os.Open(filepath.Join(f.Root(), dir))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer dirFile.Close()
|
|
||||||
infos, err := dirFile.Readdir(-1) // skips files removed before their lstat, unlike billy's ReadDir
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
names := make(container.Set[string], len(infos))
|
|
||||||
for _, info := range infos {
|
|
||||||
names.Add(info.Name())
|
|
||||||
}
|
|
||||||
return slices.DeleteFunc(infos, func(info os.FileInfo) bool {
|
|
||||||
base, isPack := strings.CutSuffix(info.Name(), ".pack")
|
|
||||||
return isPack && !names.Contains(base+".idx")
|
|
||||||
}), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func openRepositoryInternal(gitRepo *Repository) error {
|
func openRepositoryInternal(gitRepo *Repository) error {
|
||||||
@@ -149,7 +71,7 @@ func openRepositoryInternal(gitRepo *Repository) error {
|
|||||||
altFs = osfs.New("/")
|
altFs = osfs.New("/")
|
||||||
}
|
}
|
||||||
gitRepo.objectFormatCache = ParseGogitHash(plumbing.ZeroHash).Type()
|
gitRepo.objectFormatCache = ParseGogitHash(plumbing.ZeroHash).Type()
|
||||||
storage := filesystem.NewStorageWithOptions(packIdxFS{fs}, cache.NewObjectLRUDefault(), filesystem.Options{KeepDescriptors: true, LargeObjectThreshold: setting.Git.LargeObjectThreshold, AlternatesFS: altFs})
|
storage := filesystem.NewStorageWithOptions(fs, cache.NewObjectLRUDefault(), filesystem.Options{KeepDescriptors: true, LargeObjectThreshold: setting.Git.LargeObjectThreshold, AlternatesFS: altFs})
|
||||||
packs, _ := storage.ObjectPacks()
|
packs, _ := storage.ObjectPacks()
|
||||||
gitRepo.gogitStorage = &reindexingStorage{Storage: storage, packs: packs}
|
gitRepo.gogitStorage = &reindexingStorage{Storage: storage, packs: packs}
|
||||||
gitRepo.gogitRepo, err = gogit.Open(gitRepo.gogitStorage, fs)
|
gitRepo.gogitRepo, err = gogit.Open(gitRepo.gogitStorage, fs)
|
||||||
|
|||||||
@@ -4,15 +4,9 @@
|
|||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.dev/modules/git/gitcmd"
|
|
||||||
"gitea.dev/modules/setting"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -45,41 +39,6 @@ func TestRepository_GetBranches(t *testing.T) {
|
|||||||
assert.ElementsMatch(t, []string{}, branches)
|
assert.ElementsMatch(t, []string{}, branches)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: covers the gogit workarounds in repo_base_gogit.go, remove with the gogit build
|
|
||||||
func TestReadsAfterConcurrentRepack(t *testing.T) {
|
|
||||||
repoDir := t.TempDir()
|
|
||||||
require.NoError(t, gitcmd.NewCommand("init", "--bare").AddDynamicArguments(repoDir).Run(t.Context()))
|
|
||||||
content := strings.Repeat("a", int(setting.Git.LargeObjectThreshold)+1)
|
|
||||||
for _, from := range []string{"", "from refs/heads/main^0\n"} {
|
|
||||||
stdin := fmt.Sprintf("commit refs/heads/main\ncommitter a <a@a> 0 +0000\ndata 0\n%sM 100644 inline f\ndata %d\n%s\n", from, len(content), content)
|
|
||||||
require.NoError(t, gitcmd.NewCommand("fast-import").WithDir(repoDir).WithStdinBytes([]byte(stdin)).Run(t.Context()))
|
|
||||||
require.NoError(t, gitcmd.NewCommand("repack", "-d").WithDir(repoDir).Run(t.Context()))
|
|
||||||
}
|
|
||||||
|
|
||||||
repo, err := OpenRepositoryLocal(t.Context(), repoDir)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer repo.Close()
|
|
||||||
require.False(t, repo.IsObjectExist(t.Context(), "0000000000000000000000000000000000000001"))
|
|
||||||
blobRepo, err := OpenRepositoryLocal(t.Context(), repoDir)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer blobRepo.Close()
|
|
||||||
commit, err := blobRepo.GetBranchCommit(t.Context(), "main")
|
|
||||||
require.NoError(t, err)
|
|
||||||
readBlob := func() string {
|
|
||||||
data, err := commit.GetFileContent(t.Context(), blobRepo, "f", len(content))
|
|
||||||
require.NoError(t, err)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
require.Equal(t, content, readBlob())
|
|
||||||
require.NoError(t, gitcmd.NewCommand("repack", "-a", "-d").WithDir(repoDir).Run(t.Context()))
|
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(repoDir, "objects", "pack", "pack-"+strings.Repeat("1", 40)+".pack"), nil, 0o644))
|
|
||||||
|
|
||||||
branches, _, err := repo.GetBranchNames(t.Context(), 0, 0)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, []string{"main"}, branches)
|
|
||||||
assert.Equal(t, content, readBlob())
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkRepository_GetBranches(b *testing.B) {
|
func BenchmarkRepository_GetBranches(b *testing.B) {
|
||||||
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
|
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
|
||||||
bareRepo1, err := OpenRepositoryLocal(b.Context(), bareRepo1Path)
|
bareRepo1, err := OpenRepositoryLocal(b.Context(), bareRepo1Path)
|
||||||
|
|||||||
@@ -122,8 +122,6 @@ func (t *Tracer) Start(ctx context.Context, spanName string) (context.Context, *
|
|||||||
ts.parent = parentSpan
|
ts.parent = parentSpan
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: this ctx handling is not right. The returned ctx should inherit the ctx passed in, but not from span's internal contexts
|
|
||||||
// The returned ctx only needs to inherit the values of the internal contexts of spans
|
|
||||||
parentCtx := ctx
|
parentCtx := ctx
|
||||||
for internalSpanIdx, tsp := range starters {
|
for internalSpanIdx, tsp := range starters {
|
||||||
var internalSpan traceSpanInternal
|
var internalSpan traceSpanInternal
|
||||||
|
|||||||
@@ -6,17 +6,14 @@ package gtprof
|
|||||||
// Some interesting names could be found in https://github.com/open-telemetry/opentelemetry-go/tree/main/semconv
|
// Some interesting names could be found in https://github.com/open-telemetry/opentelemetry-go/tree/main/semconv
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TraceSpanContext = "context"
|
|
||||||
TraceSpanHTTP = "http"
|
TraceSpanHTTP = "http"
|
||||||
TraceSpanGitRun = "git-run"
|
TraceSpanGitRun = "git-run"
|
||||||
TraceSpanDatabase = "database"
|
TraceSpanDatabase = "database"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TraceAttrGeneralName = "general.name"
|
TraceAttrFuncCaller = "func.caller"
|
||||||
TraceAttrGeneralDesc = "general.desc"
|
TraceAttrDbSQL = "db.sql"
|
||||||
TraceAttrFuncCaller = "func.caller"
|
TraceAttrGitCommand = "git.command"
|
||||||
TraceAttrDbSQL = "db.sql"
|
TraceAttrHTTPRoute = "http.route"
|
||||||
TraceAttrGitCommand = "git.command"
|
|
||||||
TraceAttrHTTPRoute = "http.route"
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,10 +45,6 @@ func MarshalKeepOptionalEmpty(v any) ([]byte, error) {
|
|||||||
return jsonv2.Marshal(v, jsonV2.marshalKeepOptionalEmptyOptions)
|
return jsonv2.Marshal(v, jsonV2.marshalKeepOptionalEmptyOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
func MarshalDeterministic(v any) ([]byte, error) {
|
|
||||||
return jsonv2.Marshal(v, jsonV2.marshalOptions, jsonv2.Deterministic(true))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *JSONv2) Marshal(v any) ([]byte, error) {
|
func (j *JSONv2) Marshal(v any) ([]byte, error) {
|
||||||
return jsonv2.Marshal(v, j.marshalOptions)
|
return jsonv2.Marshal(v, j.marshalOptions)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,6 @@ type PackageMetadataVersion struct {
|
|||||||
Engines map[string]string `json:"engines,omitempty"`
|
Engines map[string]string `json:"engines,omitempty"`
|
||||||
CPU []string `json:"cpu,omitempty"`
|
CPU []string `json:"cpu,omitempty"`
|
||||||
OS []string `json:"os,omitempty"`
|
OS []string `json:"os,omitempty"`
|
||||||
Libc []string `json:"libc,omitempty"`
|
|
||||||
Directories map[string]string `json:"directories,omitempty"`
|
Directories map[string]string `json:"directories,omitempty"`
|
||||||
Funding any `json:"funding,omitempty"`
|
Funding any `json:"funding,omitempty"`
|
||||||
AcceptDependencies map[string]string `json:"acceptDependencies,omitempty"`
|
AcceptDependencies map[string]string `json:"acceptDependencies,omitempty"`
|
||||||
@@ -130,9 +129,12 @@ type PackageMetadataVersion struct {
|
|||||||
|
|
||||||
// PackageDistribution https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version
|
// PackageDistribution https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version
|
||||||
type PackageDistribution struct {
|
type PackageDistribution struct {
|
||||||
Integrity string `json:"integrity"`
|
Integrity string `json:"integrity"`
|
||||||
Shasum string `json:"shasum"`
|
Shasum string `json:"shasum"`
|
||||||
Tarball string `json:"tarball"`
|
Tarball string `json:"tarball"`
|
||||||
|
FileCount int `json:"fileCount,omitempty"`
|
||||||
|
UnpackedSize int `json:"unpackedSize,omitempty"`
|
||||||
|
NpmSignature string `json:"npm-signature,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PackageSearch struct {
|
type PackageSearch struct {
|
||||||
@@ -224,7 +226,7 @@ func (r *Repository) UnmarshalJSON(data []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bin maps command names to executable files. npm also allows a single string,
|
// Bin maps command names to executable files. npm also allows a single string,
|
||||||
// in which case the command is named after the package (resolved in parseUploadPackage).
|
// in which case the command is named after the package (resolved in ParsePackage).
|
||||||
type Bin map[string]string
|
type Bin map[string]string
|
||||||
|
|
||||||
// UnmarshalJSON is needed because the bin field can be a string or an object.
|
// UnmarshalJSON is needed because the bin field can be a string or an object.
|
||||||
@@ -262,7 +264,7 @@ type packageUpload struct {
|
|||||||
// is non-nil on success; a body without `_attachments` is a deprecate request,
|
// is non-nil on success; a body without `_attachments` is a deprecate request,
|
||||||
// otherwise it is a "publish".
|
// otherwise it is a "publish".
|
||||||
func ParseUpload(r io.Reader) (*Package, *PackageDeprecation, error) {
|
func ParseUpload(r io.Reader) (*Package, *PackageDeprecation, error) {
|
||||||
body, err := io.ReadAll(r)
|
body, err := io.ReadAll(io.LimitReader(r, 10*1024*1024))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -278,6 +280,16 @@ func ParseUpload(r io.Reader) (*Package, *PackageDeprecation, error) {
|
|||||||
return p, nil, err
|
return p, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParsePackage parses a npm publish PUT body. Bodies without `_attachments`
|
||||||
|
// surface as ErrInvalidAttachment once name/version validation has passed.
|
||||||
|
func ParsePackage(r io.Reader) (*Package, error) {
|
||||||
|
var upload packageUpload
|
||||||
|
if err := json.NewDecoder(r).Decode(&upload); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return parseUploadPackage(&upload)
|
||||||
|
}
|
||||||
|
|
||||||
// parseUploadPackage builds a Package from a decoded publish body.
|
// parseUploadPackage builds a Package from a decoded publish body.
|
||||||
func parseUploadPackage(upload *packageUpload) (*Package, error) {
|
func parseUploadPackage(upload *packageUpload) (*Package, error) {
|
||||||
for _, meta := range upload.Versions {
|
for _, meta := range upload.Versions {
|
||||||
@@ -331,7 +343,6 @@ func parseUploadPackage(upload *packageUpload) (*Package, error) {
|
|||||||
Engines: meta.Engines,
|
Engines: meta.Engines,
|
||||||
CPU: meta.CPU,
|
CPU: meta.CPU,
|
||||||
OS: meta.OS,
|
OS: meta.OS,
|
||||||
Libc: meta.Libc,
|
|
||||||
Directories: meta.Directories,
|
Directories: meta.Directories,
|
||||||
Funding: meta.Funding,
|
Funding: meta.Funding,
|
||||||
AcceptDependencies: meta.AcceptDependencies,
|
AcceptDependencies: meta.AcceptDependencies,
|
||||||
@@ -345,12 +356,12 @@ func parseUploadPackage(upload *packageUpload) (*Package, error) {
|
|||||||
|
|
||||||
p.Filename = strings.ToLower(fmt.Sprintf("%s-%s.tgz", name, p.Version))
|
p.Filename = strings.ToLower(fmt.Sprintf("%s-%s.tgz", name, p.Version))
|
||||||
|
|
||||||
attachment := upload.Attachments[meta.Name+"-"+meta.Version+".tgz"] // not the sigstore bundle of `npm publish --provenance`
|
attachment := func() *PackageAttachment {
|
||||||
if attachment == nil && len(upload.Attachments) == 1 {
|
|
||||||
for _, a := range upload.Attachments {
|
for _, a := range upload.Attachments {
|
||||||
attachment = a
|
return a
|
||||||
}
|
}
|
||||||
}
|
return nil
|
||||||
|
}()
|
||||||
if attachment == nil || len(attachment.Data) == 0 {
|
if attachment == nil || len(attachment.Data) == 0 {
|
||||||
return nil, ErrInvalidAttachment
|
return nil, ErrInvalidAttachment
|
||||||
}
|
}
|
||||||
@@ -382,6 +393,8 @@ func parseUploadPackage(upload *packageUpload) (*Package, error) {
|
|||||||
return nil, ErrInvalidIntegrity
|
return nil, ErrInvalidIntegrity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Derive _hasShrinkwrap and hasInstallScript from the tarball; the
|
||||||
|
// packument can lie about either.
|
||||||
p.Metadata.HasShrinkwrap, p.Metadata.HasInstallScript = inspectTarball(data)
|
p.Metadata.HasShrinkwrap, p.Metadata.HasInstallScript = inspectTarball(data)
|
||||||
|
|
||||||
return p, nil
|
return p, nil
|
||||||
@@ -397,7 +410,11 @@ const maxNpmTarballScanBytes = int64(32 * 1024 * 1024) // 32 MiB
|
|||||||
// maxNpmPackageJSONBytes caps the package.json bytes decoded from the tarball.
|
// maxNpmPackageJSONBytes caps the package.json bytes decoded from the tarball.
|
||||||
const maxNpmPackageJSONBytes = int64(1 * 1024 * 1024) // 1 MiB
|
const maxNpmPackageJSONBytes = int64(1 * 1024 * 1024) // 1 MiB
|
||||||
|
|
||||||
// inspectTarball trusts the tarball over the client's packument, read errors yield zero values to not block publishing
|
// inspectTarball reports hasShrinkwrap (presence of package/npm-shrinkwrap.json)
|
||||||
|
// and hasInstallScript (package/package.json declares any of preinstall,
|
||||||
|
// install, postinstall). Both must be derived server-side because the client
|
||||||
|
// can lie in the packument. Any read/decode error yields (false, false) so a
|
||||||
|
// malformed archive does not block publishing.
|
||||||
func inspectTarball(data []byte) (hasShrinkwrap, hasInstallScript bool) {
|
func inspectTarball(data []byte) (hasShrinkwrap, hasInstallScript bool) {
|
||||||
gr, err := gzip.NewReader(bytes.NewReader(data))
|
gr, err := gzip.NewReader(bytes.NewReader(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -405,16 +422,11 @@ func inspectTarball(data []byte) (hasShrinkwrap, hasInstallScript bool) {
|
|||||||
}
|
}
|
||||||
defer gr.Close()
|
defer gr.Close()
|
||||||
|
|
||||||
var hasGypFile bool
|
|
||||||
var pkg struct {
|
|
||||||
Scripts map[string]string `json:"scripts"`
|
|
||||||
Gypfile any `json:"gypfile"`
|
|
||||||
}
|
|
||||||
tr := tar.NewReader(io.LimitReader(gr, maxNpmTarballScanBytes))
|
tr := tar.NewReader(io.LimitReader(gr, maxNpmTarballScanBytes))
|
||||||
for {
|
for {
|
||||||
hdr, err := tr.Next()
|
hdr, err := tr.Next()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
return hasShrinkwrap, hasInstallScript
|
||||||
}
|
}
|
||||||
// npm pack puts files under a single root directory (usually "package/").
|
// npm pack puts files under a single root directory (usually "package/").
|
||||||
name := strings.TrimPrefix(hdr.Name, "./")
|
name := strings.TrimPrefix(hdr.Name, "./")
|
||||||
@@ -424,14 +436,30 @@ func inspectTarball(data []byte) (hasShrinkwrap, hasInstallScript bool) {
|
|||||||
switch {
|
switch {
|
||||||
case strings.HasSuffix(name, "/npm-shrinkwrap.json"):
|
case strings.HasSuffix(name, "/npm-shrinkwrap.json"):
|
||||||
hasShrinkwrap = true
|
hasShrinkwrap = true
|
||||||
case strings.HasSuffix(name, ".gyp"):
|
|
||||||
hasGypFile = true
|
|
||||||
case strings.HasSuffix(name, "/package.json"):
|
case strings.HasSuffix(name, "/package.json"):
|
||||||
_ = json.NewDecoder(io.LimitReader(tr, maxNpmPackageJSONBytes)).Decode(&pkg)
|
hasInstallScript = tarballDeclaresInstallScript(tr)
|
||||||
|
}
|
||||||
|
if hasShrinkwrap && hasInstallScript {
|
||||||
|
return hasShrinkwrap, hasInstallScript
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// npm publish adds "install": "node-gyp rebuild" for a root *.gyp file to the manifest, but not to the tarball
|
}
|
||||||
return hasShrinkwrap, strings.TrimSpace(pkg.Scripts["preinstall"]+pkg.Scripts["install"]+pkg.Scripts["postinstall"]) != "" || hasGypFile && pkg.Gypfile != false
|
|
||||||
|
// tarballDeclaresInstallScript reports whether a package.json declares any
|
||||||
|
// of preinstall, install, postinstall.
|
||||||
|
func tarballDeclaresInstallScript(r io.Reader) bool {
|
||||||
|
var pkg struct {
|
||||||
|
Scripts map[string]string `json:"scripts"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(io.LimitReader(r, maxNpmPackageJSONBytes)).Decode(&pkg); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, name := range []string{"preinstall", "install", "postinstall"} {
|
||||||
|
if strings.TrimSpace(pkg.Scripts[name]) != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateName(name string) bool {
|
func validateName(name string) bool {
|
||||||
|
|||||||
@@ -41,20 +41,21 @@ func TestParsePackage(t *testing.T) {
|
|||||||
integrity := "sha512-" + base64Sha512(dataBytes)
|
integrity := "sha512-" + base64Sha512(dataBytes)
|
||||||
|
|
||||||
t.Run("InvalidUpload", func(t *testing.T) {
|
t.Run("InvalidUpload", func(t *testing.T) {
|
||||||
p, _, err := ParseUpload(bytes.NewReader([]byte{0}))
|
p, err := ParsePackage(bytes.NewReader([]byte{0}))
|
||||||
assert.Nil(t, p)
|
assert.Nil(t, p)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("InvalidUploadNoData", func(t *testing.T) {
|
t.Run("InvalidUploadNoData", func(t *testing.T) {
|
||||||
p, err := parseUploadPackage(&packageUpload{})
|
b, _ := json.Marshal(packageUpload{})
|
||||||
|
p, err := ParsePackage(bytes.NewReader(b))
|
||||||
assert.Nil(t, p)
|
assert.Nil(t, p)
|
||||||
assert.ErrorIs(t, err, ErrInvalidPackage)
|
assert.ErrorIs(t, err, ErrInvalidPackage)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("InvalidPackageName", func(t *testing.T) {
|
t.Run("InvalidPackageName", func(t *testing.T) {
|
||||||
test := func(t *testing.T, name string) {
|
test := func(t *testing.T, name string) {
|
||||||
p, err := parseUploadPackage(&packageUpload{
|
b, _ := json.Marshal(packageUpload{
|
||||||
PackageMetadata: PackageMetadata{
|
PackageMetadata: PackageMetadata{
|
||||||
ID: name,
|
ID: name,
|
||||||
Name: name,
|
Name: name,
|
||||||
@@ -65,6 +66,8 @@ func TestParsePackage(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
p, err := ParsePackage(bytes.NewReader(b))
|
||||||
assert.Nil(t, p)
|
assert.Nil(t, p)
|
||||||
assert.ErrorIs(t, err, ErrInvalidPackageName)
|
assert.ErrorIs(t, err, ErrInvalidPackageName)
|
||||||
}
|
}
|
||||||
@@ -91,7 +94,7 @@ func TestParsePackage(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("ValidPackageName", func(t *testing.T) {
|
t.Run("ValidPackageName", func(t *testing.T) {
|
||||||
test := func(t *testing.T, name string) {
|
test := func(t *testing.T, name string) {
|
||||||
p, err := parseUploadPackage(&packageUpload{
|
b, _ := json.Marshal(packageUpload{
|
||||||
PackageMetadata: PackageMetadata{
|
PackageMetadata: PackageMetadata{
|
||||||
ID: name,
|
ID: name,
|
||||||
Name: name,
|
Name: name,
|
||||||
@@ -102,6 +105,8 @@ func TestParsePackage(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
p, err := ParsePackage(bytes.NewReader(b))
|
||||||
assert.Nil(t, p)
|
assert.Nil(t, p)
|
||||||
assert.ErrorIs(t, err, ErrInvalidPackageVersion)
|
assert.ErrorIs(t, err, ErrInvalidPackageVersion)
|
||||||
}
|
}
|
||||||
@@ -120,7 +125,7 @@ func TestParsePackage(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("InvalidPackageVersion", func(t *testing.T) {
|
t.Run("InvalidPackageVersion", func(t *testing.T) {
|
||||||
version := "first-version"
|
version := "first-version"
|
||||||
p, err := parseUploadPackage(&packageUpload{
|
b, _ := json.Marshal(packageUpload{
|
||||||
PackageMetadata: PackageMetadata{
|
PackageMetadata: PackageMetadata{
|
||||||
ID: packageFullName,
|
ID: packageFullName,
|
||||||
Name: packageFullName,
|
Name: packageFullName,
|
||||||
@@ -132,6 +137,8 @@ func TestParsePackage(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
p, err := ParsePackage(bytes.NewReader(b))
|
||||||
assert.Nil(t, p)
|
assert.Nil(t, p)
|
||||||
assert.ErrorIs(t, err, ErrInvalidPackageVersion)
|
assert.ErrorIs(t, err, ErrInvalidPackageVersion)
|
||||||
})
|
})
|
||||||
@@ -153,7 +160,7 @@ func TestParsePackage(t *testing.T) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
p, _, err := ParseUpload(bytes.NewReader(b))
|
p, err := ParsePackage(bytes.NewReader(b))
|
||||||
assert.Nil(t, p)
|
assert.Nil(t, p)
|
||||||
assert.ErrorIs(t, err, ErrInvalidAttachment)
|
assert.ErrorIs(t, err, ErrInvalidAttachment)
|
||||||
})
|
})
|
||||||
@@ -178,7 +185,7 @@ func TestParsePackage(t *testing.T) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
p, _, err := ParseUpload(bytes.NewReader(b))
|
p, err := ParsePackage(bytes.NewReader(b))
|
||||||
assert.Nil(t, p)
|
assert.Nil(t, p)
|
||||||
assert.ErrorIs(t, err, ErrInvalidAttachment)
|
assert.ErrorIs(t, err, ErrInvalidAttachment)
|
||||||
})
|
})
|
||||||
@@ -206,7 +213,7 @@ func TestParsePackage(t *testing.T) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
p, _, err := ParseUpload(bytes.NewReader(b))
|
p, err := ParsePackage(bytes.NewReader(b))
|
||||||
assert.Nil(t, p)
|
assert.Nil(t, p)
|
||||||
assert.ErrorIs(t, err, ErrInvalidIntegrity)
|
assert.ErrorIs(t, err, ErrInvalidIntegrity)
|
||||||
})
|
})
|
||||||
@@ -234,7 +241,7 @@ func TestParsePackage(t *testing.T) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
p, _, err := ParseUpload(bytes.NewReader(b))
|
p, err := ParsePackage(bytes.NewReader(b))
|
||||||
assert.Nil(t, p)
|
assert.Nil(t, p)
|
||||||
assert.ErrorIs(t, err, ErrInvalidIntegrity)
|
assert.ErrorIs(t, err, ErrInvalidIntegrity)
|
||||||
})
|
})
|
||||||
@@ -274,13 +281,10 @@ func TestParsePackage(t *testing.T) {
|
|||||||
filename: {
|
filename: {
|
||||||
Data: data,
|
Data: data,
|
||||||
},
|
},
|
||||||
packageFullName + "-" + packageVersion + ".sigstore": {
|
|
||||||
Data: "{}",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
p, _, err := ParseUpload(bytes.NewReader(b))
|
p, err := ParsePackage(bytes.NewReader(b))
|
||||||
assert.NotNil(t, p)
|
assert.NotNil(t, p)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
@@ -325,7 +329,7 @@ func TestParsePackage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
p, _, err := ParseUpload(strings.NewReader(packageJSON))
|
p, err := ParsePackage(strings.NewReader(packageJSON))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "MIT", string(p.Metadata.License))
|
require.Equal(t, "MIT", string(p.Metadata.License))
|
||||||
})
|
})
|
||||||
@@ -350,7 +354,7 @@ func TestParsePackage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
p, _, err := ParseUpload(strings.NewReader(packageJSON))
|
p, err := ParsePackage(strings.NewReader(packageJSON))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "https://gitea.io/gitea/test.git", p.Metadata.Repository.URL)
|
require.Equal(t, "https://gitea.io/gitea/test.git", p.Metadata.Repository.URL)
|
||||||
// a string bin is named after the package
|
// a string bin is named after the package
|
||||||
@@ -422,15 +426,6 @@ func TestInspectTarball(t *testing.T) {
|
|||||||
// npm pack sometimes emits "./package/..." entries.
|
// npm pack sometimes emits "./package/..." entries.
|
||||||
wantShrinkwrap: true,
|
wantShrinkwrap: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "gyp file implies node-gyp install",
|
|
||||||
files: map[string]string{"package/binding.gyp": "{}"},
|
|
||||||
wantInstaller: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "gypfile false disables gyp install",
|
|
||||||
files: map[string]string{"package/binding.gyp": "{}", "package/package.json": `{"gypfile":false}`},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
@@ -465,6 +460,41 @@ func TestParseUpload(t *testing.T) {
|
|||||||
require.NotNil(t, dep)
|
require.NotNil(t, dep)
|
||||||
assert.Equal(t, map[string]string{"1.0.0": "gone", "1.0.1": ""}, dep.Versions)
|
assert.Equal(t, map[string]string{"1.0.0": "gone", "1.0.1": ""}, dep.Versions)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("dispatches publish when _attachments present", func(t *testing.T) {
|
||||||
|
// Reuse a minimal tarball with a package.json.
|
||||||
|
data := buildTarball(map[string]string{"package/package.json": `{}`})
|
||||||
|
integrity := "sha512-" + base64Sha512(data)
|
||||||
|
body := fmt.Sprintf(
|
||||||
|
`{"name":%q,"versions":{"1.0.0":{"name":%q,"version":"1.0.0","dist":{"integrity":%q}}},"_attachments":{"x.tgz":{"data":%q}}}`,
|
||||||
|
pkg, pkg, integrity, base64.StdEncoding.EncodeToString(data),
|
||||||
|
)
|
||||||
|
p, dep, err := ParseUpload(strings.NewReader(body))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, dep)
|
||||||
|
require.NotNil(t, p)
|
||||||
|
assert.Equal(t, pkg, p.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("publish whose readme mentions deprecated is not misrouted", func(t *testing.T) {
|
||||||
|
// The old fast-path used a substring check for "deprecated"; make sure
|
||||||
|
// the new dispatch keys off _attachments only.
|
||||||
|
data := buildTarball(map[string]string{"package/package.json": `{}`})
|
||||||
|
integrity := "sha512-" + base64Sha512(data)
|
||||||
|
body := fmt.Sprintf(
|
||||||
|
`{"name":%q,"versions":{"1.0.0":{"name":%q,"version":"1.0.0","readme":"this package is deprecated!","dist":{"integrity":%q}}},"_attachments":{"x.tgz":{"data":%q}}}`,
|
||||||
|
pkg, pkg, integrity, base64.StdEncoding.EncodeToString(data),
|
||||||
|
)
|
||||||
|
p, dep, err := ParseUpload(strings.NewReader(body))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, dep)
|
||||||
|
require.NotNil(t, p)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid json errors out", func(t *testing.T) {
|
||||||
|
_, _, err := ParseUpload(strings.NewReader("not json"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func base64Sha512(data []byte) string {
|
func base64Sha512(data []byte) string {
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ type Metadata struct {
|
|||||||
Engines map[string]string `json:"engines,omitempty"`
|
Engines map[string]string `json:"engines,omitempty"`
|
||||||
CPU []string `json:"cpu,omitempty"`
|
CPU []string `json:"cpu,omitempty"`
|
||||||
OS []string `json:"os,omitempty"`
|
OS []string `json:"os,omitempty"`
|
||||||
Libc []string `json:"libc,omitempty"`
|
|
||||||
Directories map[string]string `json:"directories,omitempty"`
|
Directories map[string]string `json:"directories,omitempty"`
|
||||||
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"`
|
||||||
|
|||||||
@@ -102,6 +102,8 @@ var (
|
|||||||
AcmeEmail string
|
AcmeEmail string
|
||||||
AcmeURL string
|
AcmeURL string
|
||||||
AcmeProfile string
|
AcmeProfile string
|
||||||
|
AcmeEABKID string
|
||||||
|
AcmeEABHMAC string
|
||||||
AcmeCARoot string
|
AcmeCARoot string
|
||||||
SSLMinimumVersion string
|
SSLMinimumVersion string
|
||||||
SSLMaximumVersion string
|
SSLMaximumVersion string
|
||||||
@@ -144,6 +146,11 @@ func loadServerDomainAndURL(sec ConfigSection, protocol string) {
|
|||||||
AppDomain = appURL.Hostname()
|
AppDomain = appURL.Hostname()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadAcmeEABFrom(sec ConfigSection) {
|
||||||
|
AcmeEABKID = sec.Key("ACME_EAB_KID").MustString("")
|
||||||
|
AcmeEABHMAC = sec.Key("ACME_EAB_HMAC").MustString("")
|
||||||
|
}
|
||||||
|
|
||||||
func loadServerFrom(rootCfg ConfigProvider) {
|
func loadServerFrom(rootCfg ConfigProvider) {
|
||||||
sec := rootCfg.Section("server")
|
sec := rootCfg.Section("server")
|
||||||
AppName = rootCfg.Section("").Key("APP_NAME").MustString("Gitea: Git with a cup of tea")
|
AppName = rootCfg.Section("").Key("APP_NAME").MustString("Gitea: Git with a cup of tea")
|
||||||
@@ -173,6 +180,7 @@ func loadServerFrom(rootCfg ConfigProvider) {
|
|||||||
if EnableAcme {
|
if EnableAcme {
|
||||||
AcmeURL = sec.Key("ACME_URL").MustString("")
|
AcmeURL = sec.Key("ACME_URL").MustString("")
|
||||||
AcmeProfile = sec.Key("ACME_PROFILE").MustString("")
|
AcmeProfile = sec.Key("ACME_PROFILE").MustString("")
|
||||||
|
loadAcmeEABFrom(sec)
|
||||||
AcmeCARoot = sec.Key("ACME_CA_ROOT").MustString("")
|
AcmeCARoot = sec.Key("ACME_CA_ROOT").MustString("")
|
||||||
|
|
||||||
if sec.HasKey("ACME_ACCEPTTOS") {
|
if sec.HasKey("ACME_ACCEPTTOS") {
|
||||||
@@ -208,6 +216,7 @@ func loadServerFrom(rootCfg ConfigProvider) {
|
|||||||
KeyFile = filepath.Join(CustomPath, KeyFile)
|
KeyFile = filepath.Join(CustomPath, KeyFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SSLMinimumVersion = sec.Key("SSL_MIN_VERSION").MustString("")
|
SSLMinimumVersion = sec.Key("SSL_MIN_VERSION").MustString("")
|
||||||
SSLMaximumVersion = sec.Key("SSL_MAX_VERSION").MustString("")
|
SSLMaximumVersion = sec.Key("SSL_MAX_VERSION").MustString("")
|
||||||
SSLCurvePreferences = sec.Key("SSL_CURVE_PREFERENCES").Strings(",")
|
SSLCurvePreferences = sec.Key("SSL_CURVE_PREFERENCES").Strings(",")
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dev/modules/test"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadAcmeEABFrom(t *testing.T) {
|
||||||
|
t.Cleanup(test.MockVariableValue(&AcmeEABKID, ""))
|
||||||
|
t.Cleanup(test.MockVariableValue(&AcmeEABHMAC, ""))
|
||||||
|
|
||||||
|
cfg, err := NewConfigProviderFromData(`
|
||||||
|
[server]
|
||||||
|
ACME_EAB_KID = kid
|
||||||
|
ACME_EAB_HMAC = hmac
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
loadAcmeEABFrom(cfg.Section("server"))
|
||||||
|
|
||||||
|
assert.Equal(t, "kid", AcmeEABKID)
|
||||||
|
assert.Equal(t, "hmac", AcmeEABHMAC)
|
||||||
|
}
|
||||||
@@ -36,7 +36,6 @@ import "strings"
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
tildePrefix = '~'
|
tildePrefix = '~'
|
||||||
commentPrefix = '#'
|
|
||||||
needsEscape = " \t\n|&;()<>${}[]*?!\"'`\\"
|
needsEscape = " \t\n|&;()<>${}[]*?!\"'`\\"
|
||||||
needsSingleQuote = "!\n"
|
needsSingleQuote = "!\n"
|
||||||
)
|
)
|
||||||
@@ -75,7 +74,7 @@ func ShellEscape(toEscape string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Now for simplicity we'll look at the rest of the string
|
// Now for simplicity we'll look at the rest of the string
|
||||||
if !strings.ContainsAny(toEscape[start:], needsEscape) && toEscape[0] != commentPrefix {
|
if !strings.ContainsAny(toEscape[start:], needsEscape) {
|
||||||
return toEscape
|
return toEscape
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,10 +75,6 @@ func TestShellEscape(t *testing.T) {
|
|||||||
"Double quote and escape `...",
|
"Double quote and escape `...",
|
||||||
"~/gitea`",
|
"~/gitea`",
|
||||||
"~/\"gitea\\`\"",
|
"~/\"gitea\\`\"",
|
||||||
}, {
|
|
||||||
"Double quote leading #",
|
|
||||||
"#123",
|
|
||||||
`"#123"`,
|
|
||||||
}, {
|
}, {
|
||||||
"Double quotes can handle a number of things without having to escape them but not everything ...",
|
"Double quotes can handle a number of things without having to escape them but not everything ...",
|
||||||
"~/<gitea> ${gitea} `gitea` [gitea] (gitea) \"gitea\" \\gitea\\ 'gitea'",
|
"~/<gitea> ${gitea} `gitea` [gitea] (gitea) \"gitea\" \\gitea\\ 'gitea'",
|
||||||
|
|||||||
@@ -433,7 +433,6 @@
|
|||||||
"auth.authorize_application_created_by": "This application was created by %s.",
|
"auth.authorize_application_created_by": "This application was created by %s.",
|
||||||
"auth.authorize_application_description": "If you grant access, it will be able to access and write to all your account information, including private repos and organizations.",
|
"auth.authorize_application_description": "If you grant access, it will be able to access and write to all your account information, including private repos and organizations.",
|
||||||
"auth.authorize_application_with_scopes": "With scopes: %s",
|
"auth.authorize_application_with_scopes": "With scopes: %s",
|
||||||
"auth.authorize_application_new_scopes": "New scopes: %s",
|
|
||||||
"auth.authorize_title": "Authorize \"%s\" to access your account?",
|
"auth.authorize_title": "Authorize \"%s\" to access your account?",
|
||||||
"auth.authorization_failed": "Authorization failed",
|
"auth.authorization_failed": "Authorization failed",
|
||||||
"auth.authorization_failed_desc": "The authorization failed because we detected an invalid request. Please contact the maintainer of the app you tried to authorize.",
|
"auth.authorization_failed_desc": "The authorization failed because we detected an invalid request. Please contact the maintainer of the app you tried to authorize.",
|
||||||
|
|||||||
@@ -406,15 +406,12 @@ func CommonRoutes() *web.Router {
|
|||||||
})
|
})
|
||||||
r.Group("/npm", func() {
|
r.Group("/npm", func() {
|
||||||
r.Get("/-/v1/search", npm.PackageSearch)
|
r.Get("/-/v1/search", npm.PackageSearch)
|
||||||
r.Get("/-/ping", npm.Ping)
|
|
||||||
r.Get("/-/whoami", npm.Whoami)
|
|
||||||
r.PathGroup("/*", func(g *web.RouterPathGroup) {
|
r.PathGroup("/*", func(g *web.RouterPathGroup) {
|
||||||
// HINT: NPM-ROUTE-PATH-PATTERN: search this keyword to see more details
|
// HINT: NPM-ROUTE-PATH-PATTERN: search this keyword to see more details
|
||||||
packageId := `/<id:(@` + npm_module.RegexpNamePart + `/)?` + npm_module.RegexpNamePart + ">"
|
packageId := `/<id:(@` + npm_module.RegexpNamePart + `/)?` + npm_module.RegexpNamePart + ">"
|
||||||
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("GET", packageId+"/-/<version>/<filename>", npm.DownloadPackageFile)
|
||||||
g.MatchPath("GET", packageId+"/-/<version>/<filename>", npm.DownloadPackageFileByName) // former tarball URL, still in lockfiles
|
|
||||||
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,7 +8,7 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -25,7 +25,6 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac
|
|||||||
distTags := make(map[string]string)
|
distTags := make(map[string]string)
|
||||||
times := make(map[string]time.Time)
|
times := make(map[string]time.Time)
|
||||||
firstPublished, lastPublished := pds[0].Version.CreatedUnix, pds[0].Version.CreatedUnix
|
firstPublished, lastPublished := pds[0].Version.CreatedUnix, pds[0].Version.CreatedUnix
|
||||||
var latest *packages_model.PackageDescriptor
|
|
||||||
for _, pd := range pds {
|
for _, pd := range pds {
|
||||||
semVer := pd.SemVer.String()
|
semVer := pd.SemVer.String()
|
||||||
versions[semVer] = createPackageMetadataVersion(registryURL, pd)
|
versions[semVer] = createPackageMetadataVersion(registryURL, pd)
|
||||||
@@ -36,9 +35,6 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac
|
|||||||
for _, pvp := range pd.VersionProperties {
|
for _, pvp := range pd.VersionProperties {
|
||||||
if pvp.Name == npm_module.TagProperty {
|
if pvp.Name == npm_module.TagProperty {
|
||||||
distTags[pvp.Value] = pd.Version.Version
|
distTags[pvp.Value] = pd.Version.Version
|
||||||
if pvp.Value == "latest" {
|
|
||||||
latest = pd
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,16 +43,7 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac
|
|||||||
times["created"] = firstPublished.AsTimeInLocation(time.UTC)
|
times["created"] = firstPublished.AsTimeInLocation(time.UTC)
|
||||||
times["modified"] = lastPublished.AsTimeInLocation(time.UTC)
|
times["modified"] = lastPublished.AsTimeInLocation(time.UTC)
|
||||||
|
|
||||||
if latest == nil { // yarn and pnpm fail without it, e.g. after its version got deleted
|
latest := pds[len(pds)-1]
|
||||||
latest = pds[len(pds)-1]
|
|
||||||
for _, pd := range slices.Backward(pds) {
|
|
||||||
if pd.SemVer.Prerelease() == "" {
|
|
||||||
latest = pd
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
distTags["latest"] = latest.Version.Version
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata := packages_model.DescriptorMetadata[*npm_module.Metadata](latest)
|
metadata := packages_model.DescriptorMetadata[*npm_module.Metadata](latest)
|
||||||
|
|
||||||
@@ -99,13 +86,13 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package
|
|||||||
PeerDependencies: metadata.PeerDependencies,
|
PeerDependencies: metadata.PeerDependencies,
|
||||||
PeerDependenciesMeta: metadata.PeerDependenciesMeta,
|
PeerDependenciesMeta: metadata.PeerDependenciesMeta,
|
||||||
OptionalDependencies: metadata.OptionalDependencies,
|
OptionalDependencies: metadata.OptionalDependencies,
|
||||||
|
Readme: metadata.Readme,
|
||||||
Bin: metadata.Bin,
|
Bin: metadata.Bin,
|
||||||
HasInstallScript: metadata.HasInstallScript,
|
HasInstallScript: metadata.HasInstallScript,
|
||||||
HasShrinkwrap: metadata.HasShrinkwrap,
|
HasShrinkwrap: metadata.HasShrinkwrap,
|
||||||
Engines: metadata.Engines,
|
Engines: metadata.Engines,
|
||||||
CPU: metadata.CPU,
|
CPU: metadata.CPU,
|
||||||
OS: metadata.OS,
|
OS: metadata.OS,
|
||||||
Libc: metadata.Libc,
|
|
||||||
Directories: metadata.Directories,
|
Directories: metadata.Directories,
|
||||||
Funding: metadata.Funding,
|
Funding: metadata.Funding,
|
||||||
AcceptDependencies: metadata.AcceptDependencies,
|
AcceptDependencies: metadata.AcceptDependencies,
|
||||||
@@ -113,7 +100,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: fmt.Sprintf("%s/%s/-/%s/%s", registryURL, url.PathEscape(pd.Package.Name), url.PathEscape(pd.Version.Version), url.PathEscape(pd.Files[0].File.LowerName)),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func TestCreatePackageMetadataResponse(t *testing.T) {
|
|||||||
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{Keywords: []string{"gitea"}, Repository: repo},
|
||||||
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{},
|
||||||
@@ -35,26 +35,21 @@ 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{}),
|
||||||
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{
|
||||||
"1.0.0": time.Unix(2000, 0).UTC(),
|
"1.0.0": time.Unix(2000, 0).UTC(),
|
||||||
"1.1.0": time.Unix(1000, 0).UTC(),
|
"1.1.0": time.Unix(1000, 0).UTC(),
|
||||||
"2.0.0-rc.1": time.Unix(1500, 0).UTC(),
|
"created": time.Unix(1000, 0).UTC(),
|
||||||
"created": time.Unix(1000, 0).UTC(),
|
"modified": time.Unix(2000, 0).UTC(),
|
||||||
"modified": time.Unix(2000, 0).UTC(),
|
|
||||||
}, result.Time)
|
}, result.Time)
|
||||||
assert.Equal(t, map[string]string{"latest": "1.1.0"}, result.DistTags)
|
|
||||||
assert.Equal(t, "1.1.0", result.Readme)
|
|
||||||
assert.Empty(t, result.Versions["1.1.0"].Readme)
|
|
||||||
assert.Equal(t, []npm_module.User{{Name: "alice"}}, result.Maintainers)
|
assert.Equal(t, []npm_module.User{{Name: "alice"}}, result.Maintainers)
|
||||||
assert.Equal(t, []string{"gitea"}, result.Keywords)
|
assert.Equal(t, []string{"gitea"}, result.Keywords)
|
||||||
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/@scope%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, repository, result.Versions["1.0.0"].Repository)
|
assert.Equal(t, repository, result.Versions["1.0.0"].Repository)
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ package npm
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
std_ctx "context"
|
std_ctx "context"
|
||||||
"crypto/sha256"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -57,41 +55,28 @@ func buildNpmRegistryURL(ctx std_ctx.Context, owner *user_model.User) string {
|
|||||||
return httplib.GuessCurrentAppURL(ctx) + "api/packages/" + url.PathEscape(owner.Name) + "/npm"
|
return httplib.GuessCurrentAppURL(ctx) + "api/packages/" + url.PathEscape(owner.Name) + "/npm"
|
||||||
}
|
}
|
||||||
|
|
||||||
func packageMetadata(ctx *context.Context) *npm_module.PackageMetadata {
|
// PackageMetadata returns the metadata for a single package
|
||||||
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageNameFromParams(ctx))
|
func PackageMetadata(ctx *context.Context) {
|
||||||
|
packageName := packageNameFromParams(ctx)
|
||||||
|
|
||||||
|
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
apiError(ctx, http.StatusInternalServerError, err)
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
if len(pvs) == 0 {
|
if len(pvs) == 0 {
|
||||||
apiError(ctx, http.StatusNotFound, err)
|
apiError(ctx, http.StatusNotFound, err)
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
|
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
|
||||||
if err != nil {
|
|
||||||
apiError(ctx, http.StatusInternalServerError, err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return createPackageMetadataResponse(buildNpmRegistryURL(ctx, ctx.Package.Owner), pds)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PackageMetadata returns the metadata for a single package
|
|
||||||
func PackageMetadata(ctx *context.Context) {
|
|
||||||
if metadata := packageMetadata(ctx); metadata != nil {
|
|
||||||
serveMetadata(ctx, metadata)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveMetadata(ctx *context.Context, obj any) {
|
|
||||||
body, err := json.MarshalDeterministic(obj)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
apiError(ctx, http.StatusInternalServerError, err)
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Resp.Header().Set("ETag", fmt.Sprintf(`W/"%x"`, sha256.Sum256(body)))
|
|
||||||
ctx.ServeContent(bytes.NewReader(body), context.ServeHeaderOptions{ContentType: "application/json;charset=utf-8"})
|
resp := createPackageMetadataResponse(buildNpmRegistryURL(ctx, ctx.Package.Owner), pds)
|
||||||
|
ctx.JSON(http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PackageVersionMetadata returns the metadata for a single version or dist-tag
|
// PackageVersionMetadata returns the metadata for a single version or dist-tag
|
||||||
@@ -115,11 +100,7 @@ func PackageVersionMetadata(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(pvs) == 0 {
|
if len(pvs) == 0 {
|
||||||
if versionOrTag != "latest" {
|
apiError(ctx, http.StatusNotFound, "version not found: "+versionOrTag)
|
||||||
apiError(ctx, http.StatusNotFound, "version not found: "+versionOrTag)
|
|
||||||
} else if metadata := packageMetadata(ctx); metadata != nil { // unset, so serve the packument's fallback
|
|
||||||
serveMetadata(ctx, metadata.Versions[metadata.DistTags["latest"]])
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,40 +110,25 @@ func PackageVersionMetadata(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
serveMetadata(ctx, createPackageMetadataVersion(buildNpmRegistryURL(ctx, ctx.Package.Owner), pd))
|
ctx.JSON(http.StatusOK, createPackageMetadataVersion(buildNpmRegistryURL(ctx, ctx.Package.Owner), pd))
|
||||||
}
|
}
|
||||||
|
|
||||||
func packageVersionByFilename(ctx *context.Context) *packages_model.PackageVersion {
|
// DownloadPackageFile serves the content of a package
|
||||||
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
func DownloadPackageFile(ctx *context.Context) {
|
||||||
OwnerID: ctx.Package.Owner.ID,
|
packageName := packageNameFromParams(ctx)
|
||||||
Type: packages_model.TypeNpm,
|
packageVersion := ctx.PathParam("version")
|
||||||
Name: packages_model.SearchValue{ExactMatch: true, Value: packageNameFromParams(ctx)},
|
filename := ctx.PathParam("filename")
|
||||||
HasFileWithName: ctx.PathParam("filename"),
|
|
||||||
IsInternal: optional.Some(false),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
apiError(ctx, http.StatusInternalServerError, err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if len(pvs) != 1 {
|
|
||||||
apiError(ctx, http.StatusNotFound, nil)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return pvs[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// DownloadPackageFileByName finds the version and serves the contents of a package
|
s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion(
|
||||||
func DownloadPackageFileByName(ctx *context.Context) {
|
|
||||||
pv := packageVersionByFilename(ctx)
|
|
||||||
if pv == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion(
|
|
||||||
ctx,
|
ctx,
|
||||||
pv,
|
&packages_service.PackageInfo{
|
||||||
|
Owner: ctx.Package.Owner,
|
||||||
|
PackageType: packages_model.TypeNpm,
|
||||||
|
Name: packageName,
|
||||||
|
Version: packageVersion,
|
||||||
|
},
|
||||||
&packages_service.PackageFileInfo{
|
&packages_service.PackageFileInfo{
|
||||||
Filename: ctx.PathParam("filename"),
|
Filename: filename,
|
||||||
},
|
},
|
||||||
ctx.Req.Method,
|
ctx.Req.Method,
|
||||||
)
|
)
|
||||||
@@ -174,14 +140,54 @@ func DownloadPackageFileByName(ctx *context.Context) {
|
|||||||
helper.ServePackageFile(ctx, s, u, pf)
|
helper.ServePackageFile(ctx, s, u, pf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DownloadPackageFileByName finds the version and serves the contents of a package
|
||||||
|
func DownloadPackageFileByName(ctx *context.Context) {
|
||||||
|
filename := ctx.PathParam("filename")
|
||||||
|
|
||||||
|
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||||
|
OwnerID: ctx.Package.Owner.ID,
|
||||||
|
Type: packages_model.TypeNpm,
|
||||||
|
Name: packages_model.SearchValue{
|
||||||
|
ExactMatch: true,
|
||||||
|
Value: packageNameFromParams(ctx),
|
||||||
|
},
|
||||||
|
HasFileWithName: filename,
|
||||||
|
IsInternal: optional.Some(false),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(pvs) != 1 {
|
||||||
|
apiError(ctx, http.StatusNotFound, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion(
|
||||||
|
ctx,
|
||||||
|
pvs[0],
|
||||||
|
&packages_service.PackageFileInfo{
|
||||||
|
Filename: filename,
|
||||||
|
},
|
||||||
|
ctx.Req.Method,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, packages_model.ErrPackageFileNotExist) {
|
||||||
|
apiError(ctx, http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.ServePackageFile(ctx, s, u, pf)
|
||||||
|
}
|
||||||
|
|
||||||
// UploadPackage creates a new package
|
// UploadPackage creates a new package
|
||||||
func UploadPackage(ctx *context.Context) {
|
func UploadPackage(ctx *context.Context) {
|
||||||
// about the npmjs and GitHub Packages limit, fits base64 tarballs up to ~200 MB
|
npmPackage, deprecation, err := npm_module.ParseUpload(ctx.Req.Body)
|
||||||
npmPackage, deprecation, err := npm_module.ParseUpload(http.MaxBytesReader(ctx.Resp, ctx.Req.Body, 256*1024*1024))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := errors.AsType[*http.MaxBytesError](err); ok {
|
if errors.Is(err, util.ErrInvalidArgument) {
|
||||||
apiError(ctx, http.StatusRequestEntityTooLarge, err)
|
|
||||||
} else if errors.Is(err, util.ErrInvalidArgument) {
|
|
||||||
apiError(ctx, http.StatusBadRequest, err)
|
apiError(ctx, http.StatusBadRequest, err)
|
||||||
} else {
|
} else {
|
||||||
apiError(ctx, http.StatusInternalServerError, err)
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
@@ -334,14 +340,26 @@ func deprecatePackage(ctx *context.Context, dep *npm_module.PackageDeprecation)
|
|||||||
ctx.Status(http.StatusOK)
|
ctx.Status(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeletePackageVersion deletes the package version, which `npm unpublish` addresses by its tarball
|
// DeletePackageVersion deletes the package version
|
||||||
func DeletePackageVersion(ctx *context.Context) {
|
func DeletePackageVersion(ctx *context.Context) {
|
||||||
pv := packageVersionByFilename(ctx)
|
packageName := packageNameFromParams(ctx)
|
||||||
if pv == nil {
|
packageVersion := ctx.PathParam("version")
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := packages_service.RemovePackageVersion(ctx, ctx.Doer, pv); err != nil {
|
err := packages_service.RemovePackageVersionByNameAndVersion(
|
||||||
|
ctx,
|
||||||
|
ctx.Doer,
|
||||||
|
&packages_service.PackageInfo{
|
||||||
|
Owner: ctx.Package.Owner,
|
||||||
|
PackageType: packages_model.TypeNpm,
|
||||||
|
Name: packageName,
|
||||||
|
Version: packageVersion,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, packages_model.ErrPackageNotExist) {
|
||||||
|
apiError(ctx, http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
apiError(ctx, http.StatusInternalServerError, err)
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -376,7 +394,9 @@ func DeletePackage(ctx *context.Context) {
|
|||||||
|
|
||||||
// ListPackageTags returns all tags for a package
|
// ListPackageTags returns all tags for a package
|
||||||
func ListPackageTags(ctx *context.Context) {
|
func ListPackageTags(ctx *context.Context) {
|
||||||
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageNameFromParams(ctx))
|
packageName := packageNameFromParams(ctx)
|
||||||
|
|
||||||
|
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
apiError(ctx, http.StatusInternalServerError, err)
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
@@ -394,11 +414,7 @@ func ListPackageTags(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := tags["latest"]; ok {
|
ctx.JSON(http.StatusOK, tags)
|
||||||
ctx.JSON(http.StatusOK, tags)
|
|
||||||
} else if metadata := packageMetadata(ctx); metadata != nil { // unset, so list the packument's fallback
|
|
||||||
ctx.JSON(http.StatusOK, metadata.DistTags)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddPackageTag adds a tag to the package
|
// AddPackageTag adds a tag to the package
|
||||||
@@ -508,18 +524,6 @@ func setPackageTag(ctx std_ctx.Context, tag string, pv *packages_model.PackageVe
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func Ping(ctx *context.Context) {
|
|
||||||
ctx.JSON(http.StatusOK, map[string]any{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func Whoami(ctx *context.Context) {
|
|
||||||
if ctx.Doer == nil {
|
|
||||||
apiError(ctx, http.StatusUnauthorized, "Unauthorized")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.JSON(http.StatusOK, map[string]string{"username": ctx.Doer.Name})
|
|
||||||
}
|
|
||||||
|
|
||||||
func PackageSearch(ctx *context.Context) {
|
func PackageSearch(ctx *context.Context) {
|
||||||
pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
|
pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
|
||||||
OwnerID: ctx.Package.Owner.ID,
|
OwnerID: ctx.Package.Owner.ID,
|
||||||
|
|||||||
@@ -400,7 +400,7 @@ func SearchUsers(ctx *context.APIContext) {
|
|||||||
// parameters:
|
// parameters:
|
||||||
// - name: source_id
|
// - name: source_id
|
||||||
// in: query
|
// in: query
|
||||||
// description: ID of the user's login source to search for
|
// description: ID of the user's login source to search for, 0 means the local users
|
||||||
// type: integer
|
// type: integer
|
||||||
// format: int64
|
// format: int64
|
||||||
// - name: login_name
|
// - name: login_name
|
||||||
@@ -483,7 +483,7 @@ func SearchUsers(ctx *context.APIContext) {
|
|||||||
Actor: ctx.Doer,
|
Actor: ctx.Doer,
|
||||||
Types: []user_model.UserType{user_model.UserTypeIndividual},
|
Types: []user_model.UserType{user_model.UserTypeIndividual},
|
||||||
LoginName: ctx.FormTrim("login_name"),
|
LoginName: ctx.FormTrim("login_name"),
|
||||||
SourceID: ctx.FormInt64("source_id"),
|
SourceID: ctx.FormOptionalInt64("source_id"),
|
||||||
Keyword: ctx.FormTrim("q"),
|
Keyword: ctx.FormTrim("q"),
|
||||||
Visible: visible,
|
Visible: visible,
|
||||||
OrderBy: orderBy,
|
OrderBy: orderBy,
|
||||||
|
|||||||
+34
-18
@@ -76,7 +76,6 @@ import (
|
|||||||
repo_model "gitea.dev/models/repo"
|
repo_model "gitea.dev/models/repo"
|
||||||
"gitea.dev/models/unit"
|
"gitea.dev/models/unit"
|
||||||
user_model "gitea.dev/models/user"
|
user_model "gitea.dev/models/user"
|
||||||
"gitea.dev/modules/httplib"
|
|
||||||
"gitea.dev/modules/log"
|
"gitea.dev/modules/log"
|
||||||
"gitea.dev/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
api "gitea.dev/modules/structs"
|
api "gitea.dev/modules/structs"
|
||||||
@@ -936,22 +935,31 @@ func apiAuth(authMethod auth.Method) func(*context.APIContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// verifyAuthWithOptionsAPI checks authentication according to options
|
// verifyAuthWithOptions checks authentication according to options
|
||||||
func verifyAuthWithOptionsAPI(options *common.VerifyOptions) func(ctx *context.APIContext) {
|
func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.APIContext) {
|
||||||
return func(ctx *context.APIContext) {
|
return func(ctx *context.APIContext) {
|
||||||
// Check prohibit login users.
|
// Check prohibit login users.
|
||||||
if ctx.IsSigned {
|
if ctx.IsSigned {
|
||||||
check := common.CheckSignedInUser(ctx.Doer, nil)
|
if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm {
|
||||||
if check.NeedActivateAccount {
|
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
|
||||||
ctx.JSON(http.StatusForbidden, map[string]string{"message": "This account is not activated."})
|
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||||
|
"message": "This account is not activated.",
|
||||||
|
})
|
||||||
return
|
return
|
||||||
} else if check.LoginIsProhibited {
|
}
|
||||||
|
if !ctx.Doer.IsActive || ctx.Doer.ProhibitLogin {
|
||||||
log.Info("Failed authentication attempt for %s from %s", ctx.Doer.Name, ctx.RemoteAddr())
|
log.Info("Failed authentication attempt for %s from %s", ctx.Doer.Name, ctx.RemoteAddr())
|
||||||
ctx.JSON(http.StatusForbidden, map[string]string{"message": "This account is prohibited from signing in, please contact your site administrator."})
|
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
|
||||||
|
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||||
|
"message": "This account is prohibited from signing in, please contact your site administrator.",
|
||||||
|
})
|
||||||
return
|
return
|
||||||
} else if check.NeedChangePassword {
|
}
|
||||||
msg := "You must change your password. Change it at: " + httplib.MakeAbsoluteURL(ctx, setting.AppSubURL+"/user/settings/change_password")
|
|
||||||
ctx.JSON(http.StatusForbidden, map[string]string{"message": msg})
|
if ctx.Doer.MustChangePassword {
|
||||||
|
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||||
|
"message": "You must change your password. Change it at: " + setting.AppURL + "/user/change_password",
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -962,12 +970,20 @@ func verifyAuthWithOptionsAPI(options *common.VerifyOptions) func(ctx *context.A
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.SignInRequired && !ctx.IsSigned {
|
if options.SignInRequired {
|
||||||
// Restrict API calls with error message.
|
if !ctx.IsSigned {
|
||||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
// Restrict API calls with error message.
|
||||||
"message": "Only signed in user is allowed to call APIs.",
|
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||||
})
|
"message": "Only signed in user is allowed to call APIs.",
|
||||||
return
|
})
|
||||||
|
return
|
||||||
|
} else if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm {
|
||||||
|
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
|
||||||
|
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||||
|
"message": "This account is not activated.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.AdminRequired {
|
if options.AdminRequired {
|
||||||
@@ -1019,7 +1035,7 @@ func Routes() *web.Router {
|
|||||||
// Get user from session if logged in.
|
// Get user from session if logged in.
|
||||||
m.AfterRouting(apiAuth(buildAuthGroup()))
|
m.AfterRouting(apiAuth(buildAuthGroup()))
|
||||||
|
|
||||||
m.AfterRouting(verifyAuthWithOptionsAPI(&common.VerifyOptions{
|
m.AfterRouting(verifyAuthWithOptions(&common.VerifyOptions{
|
||||||
SignInRequired: setting.Service.RequireSignInViewStrict,
|
SignInRequired: setting.Service.RequireSignInViewStrict,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
@@ -1032,7 +1032,7 @@ func MergePullRequest(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := pull_service.Merge(ctx, pr.ID, ctx.Doer, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
|
if err := pull_service.Merge(pr.ID, ctx.Doer, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
|
||||||
if pull_service.IsErrInvalidMergeStyle(err) {
|
if pull_service.IsErrInvalidMergeStyle(err) {
|
||||||
ctx.APIError(http.StatusMethodNotAllowed, fmt.Sprintf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do)))
|
ctx.APIError(http.StatusMethodNotAllowed, fmt.Sprintf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do)))
|
||||||
} else if conflictError, ok := err.(pull_service.ErrMergeConflicts); ok {
|
} else if conflictError, ok := err.(pull_service.ErrMergeConflicts); ok {
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ package common
|
|||||||
import (
|
import (
|
||||||
user_model "gitea.dev/models/user"
|
user_model "gitea.dev/models/user"
|
||||||
"gitea.dev/modules/log"
|
"gitea.dev/modules/log"
|
||||||
"gitea.dev/modules/session"
|
|
||||||
"gitea.dev/modules/setting"
|
|
||||||
"gitea.dev/modules/web/middleware"
|
"gitea.dev/modules/web/middleware"
|
||||||
auth_service "gitea.dev/services/auth"
|
auth_service "gitea.dev/services/auth"
|
||||||
"gitea.dev/services/context"
|
"gitea.dev/services/context"
|
||||||
@@ -58,16 +56,3 @@ type VerifyOptions struct {
|
|||||||
AdminRequired bool
|
AdminRequired bool
|
||||||
DisableCrossOriginProtection bool
|
DisableCrossOriginProtection bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func CheckSignedInUser(doer *user_model.User, sess session.Store) (ret struct {
|
|
||||||
NeedActivateAccount bool
|
|
||||||
LoginIsProhibited bool
|
|
||||||
NeedChangePassword bool
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
ret.NeedActivateAccount = !doer.IsActive && setting.Service.RegisterEmailConfirm
|
|
||||||
ret.LoginIsProhibited = !doer.IsActive || doer.ProhibitLogin
|
|
||||||
isImpersonated := sess != nil && context.IsDoerSessionImpersonated(sess)
|
|
||||||
ret.NeedChangePassword = doer.MustChangePassword && !isImpersonated && !doer.IsTypeBot()
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
user_model "gitea.dev/models/user"
|
|
||||||
"gitea.dev/modules/session"
|
|
||||||
"gitea.dev/modules/setting"
|
|
||||||
"gitea.dev/modules/test"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCheckSignedInUser(t *testing.T) {
|
|
||||||
defer test.MockVariableValue(&setting.Service.RegisterEmailConfirm)()
|
|
||||||
sessNormal := session.NewMockMemStore("session-a")
|
|
||||||
sessImpersonated := session.NewMockMemStore("session-b")
|
|
||||||
_ = sessImpersonated.Set(session.KeyImpersonatorData, "any-value")
|
|
||||||
|
|
||||||
setting.Service.RegisterEmailConfirm = false
|
|
||||||
ret := CheckSignedInUser(&user_model.User{IsActive: false}, nil)
|
|
||||||
assert.False(t, ret.NeedActivateAccount)
|
|
||||||
assert.True(t, ret.LoginIsProhibited)
|
|
||||||
|
|
||||||
setting.Service.RegisterEmailConfirm = true
|
|
||||||
ret = CheckSignedInUser(&user_model.User{IsActive: false}, nil)
|
|
||||||
assert.True(t, ret.NeedActivateAccount)
|
|
||||||
assert.True(t, ret.LoginIsProhibited)
|
|
||||||
|
|
||||||
ret = CheckSignedInUser(&user_model.User{IsActive: true}, nil)
|
|
||||||
assert.False(t, ret.NeedActivateAccount)
|
|
||||||
assert.False(t, ret.LoginIsProhibited)
|
|
||||||
assert.False(t, ret.NeedChangePassword)
|
|
||||||
|
|
||||||
ret = CheckSignedInUser(&user_model.User{IsActive: true, ProhibitLogin: true}, nil)
|
|
||||||
assert.False(t, ret.NeedActivateAccount)
|
|
||||||
assert.True(t, ret.LoginIsProhibited)
|
|
||||||
|
|
||||||
ret = CheckSignedInUser(&user_model.User{MustChangePassword: true}, nil)
|
|
||||||
assert.True(t, ret.NeedChangePassword)
|
|
||||||
|
|
||||||
ret = CheckSignedInUser(&user_model.User{MustChangePassword: true}, sessNormal)
|
|
||||||
assert.True(t, ret.NeedChangePassword)
|
|
||||||
|
|
||||||
ret = CheckSignedInUser(&user_model.User{MustChangePassword: true, Type: user_model.UserTypeBot}, sessNormal)
|
|
||||||
assert.False(t, ret.NeedChangePassword)
|
|
||||||
|
|
||||||
ret = CheckSignedInUser(&user_model.User{MustChangePassword: true}, sessImpersonated)
|
|
||||||
assert.False(t, ret.NeedChangePassword)
|
|
||||||
}
|
|
||||||
@@ -229,9 +229,9 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if isForcePush {
|
if isForcePush {
|
||||||
canPush = !changedProtectedfiles && protectBranch.CanUserForcePush(ctx, ctx.Doer, ctx.Repo.Permission)
|
canPush = !changedProtectedfiles && protectBranch.CanUserForcePush(ctx, ctx.Doer)
|
||||||
} else {
|
} else {
|
||||||
canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx, ctx.Doer, ctx.Repo.Permission)
|
canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx, ctx.Doer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,67 +6,16 @@ package private
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.dev/models/db"
|
|
||||||
git_model "gitea.dev/models/git"
|
|
||||||
issues_model "gitea.dev/models/issues"
|
issues_model "gitea.dev/models/issues"
|
||||||
repo_model "gitea.dev/models/repo"
|
repo_model "gitea.dev/models/repo"
|
||||||
"gitea.dev/models/unittest"
|
"gitea.dev/models/unittest"
|
||||||
user_model "gitea.dev/models/user"
|
|
||||||
"gitea.dev/modules/git"
|
"gitea.dev/modules/git"
|
||||||
"gitea.dev/modules/private"
|
|
||||||
"gitea.dev/services/contexttest"
|
"gitea.dev/services/contexttest"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPreReceiveActionsProtectedBranch(t *testing.T) {
|
|
||||||
require.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
for _, tc := range []struct {
|
|
||||||
name string
|
|
||||||
protection git_model.ProtectedBranch
|
|
||||||
forcePush bool
|
|
||||||
allowed bool
|
|
||||||
}{
|
|
||||||
{name: "push", protection: git_model.ProtectedBranch{CanPush: true}, allowed: true},
|
|
||||||
{name: "push allowlist", protection: git_model.ProtectedBranch{CanPush: true, EnableWhitelist: true}},
|
|
||||||
{name: "force push", protection: git_model.ProtectedBranch{CanPush: true, CanForcePush: true}, forcePush: true, allowed: true},
|
|
||||||
{name: "force push allowlist", protection: git_model.ProtectedBranch{CanPush: true, CanForcePush: true, EnableForcePushAllowlist: true}, forcePush: true},
|
|
||||||
} {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
mockCtx, resp := contexttest.MockPrivateContext(t, "/")
|
|
||||||
ctx := &preReceiveContext{PrivateContext: mockCtx, opts: &private.HookOptions{UserID: user_model.ActionsUserID}}
|
|
||||||
ctx.SetPathParam("owner", "user2")
|
|
||||||
ctx.SetPathParam("repo", "repo2")
|
|
||||||
RepoAssignment(ctx.PrivateContext)
|
|
||||||
require.False(t, ctx.Written())
|
|
||||||
defer ctx.Repo.GitRepo.Close()
|
|
||||||
|
|
||||||
doer := user_model.NewActionsUserWithTaskID(53)
|
|
||||||
loadContextDoerPermission(ctx.PrivateContext, doer.ID, doer.ExtDoerData.EncodeToString())
|
|
||||||
|
|
||||||
protection := tc.protection
|
|
||||||
protection.RepoID = ctx.Repo.Repository.ID
|
|
||||||
protection.RuleName = "probe"
|
|
||||||
require.NoError(t, db.Insert(t.Context(), &protection))
|
|
||||||
defer func() {
|
|
||||||
require.NoError(t, git_model.DeleteProtectedBranch(t.Context(), ctx.Repo.Repository, protection.ID))
|
|
||||||
}()
|
|
||||||
|
|
||||||
oldCommitID, newCommitID := "205ac761f3326a7ebe416e8673760016450b5cec", "1032bbf17fbc0d9c95bb5418dabe8f8c99278700"
|
|
||||||
if tc.forcePush {
|
|
||||||
oldCommitID, newCommitID = newCommitID, oldCommitID
|
|
||||||
}
|
|
||||||
preReceiveBranch(ctx, oldCommitID, newCommitID, git.RefNameFromBranch("probe"))
|
|
||||||
if tc.allowed {
|
|
||||||
assert.False(t, ctx.Written(), resp.Body.String())
|
|
||||||
} else {
|
|
||||||
assert.Contains(t, resp.Body.String(), "Not allowed to")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestPreReceiveCanWriteCodePerBranch ensures the maintainer-edit write grant is evaluated against
|
// TestPreReceiveCanWriteCodePerBranch ensures the maintainer-edit write grant is evaluated against
|
||||||
// the exact ref being pushed on every call, derived from that ref rather than shared mutable state.
|
// the exact ref being pushed on every call, derived from that ref rather than shared mutable state.
|
||||||
// Otherwise, a per-branch grant (an open PR with "allow edits from maintainers") could be batched
|
// Otherwise, a per-branch grant (an open PR with "allow edits from maintainers") could be batched
|
||||||
|
|||||||
@@ -48,6 +48,13 @@ const (
|
|||||||
// UserSearchDefaultAdminSort is the default sort type for admin view
|
// UserSearchDefaultAdminSort is the default sort type for admin view
|
||||||
const UserSearchDefaultAdminSort = "alphabetically"
|
const UserSearchDefaultAdminSort = "alphabetically"
|
||||||
|
|
||||||
|
// authSourceFilterOption is one radio item of the authentication source filter dropdown
|
||||||
|
type authSourceFilterOption struct {
|
||||||
|
Value string
|
||||||
|
Label string
|
||||||
|
Selected bool
|
||||||
|
}
|
||||||
|
|
||||||
// Users show all the users
|
// Users show all the users
|
||||||
func Users(ctx *context.Context) {
|
func Users(ctx *context.Context) {
|
||||||
ctx.Data["Title"] = ctx.Tr("admin.users")
|
ctx.Data["Title"] = ctx.Tr("admin.users")
|
||||||
@@ -76,6 +83,30 @@ func Users(ctx *context.Context) {
|
|||||||
"SortType": sortType,
|
"SortType": sortType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// inactive sources are listed too, users stay attached to a source after it is deactivated
|
||||||
|
sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{})
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("auth.Sources", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sourceIDFilter := ctx.FormOptionalInt64("source_id")
|
||||||
|
sourceNames := make(map[int64]string, len(sources))
|
||||||
|
authSourceFilterOptions := []*authSourceFilterOption{
|
||||||
|
{Value: "", Label: ctx.Locale.TrString("all"), Selected: !sourceIDFilter.Has()},
|
||||||
|
{Value: "0", Label: ctx.Locale.TrString("admin.users.local"), Selected: sourceIDFilter.Has() && sourceIDFilter.Value() == 0},
|
||||||
|
}
|
||||||
|
for _, source := range sources {
|
||||||
|
sourceNames[source.ID] = source.Name
|
||||||
|
authSourceFilterOptions = append(authSourceFilterOptions, &authSourceFilterOption{
|
||||||
|
Value: strconv.FormatInt(source.ID, 10),
|
||||||
|
Label: source.Name,
|
||||||
|
Selected: sourceIDFilter.Has() && sourceIDFilter.Value() == source.ID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ctx.Data["HasAuthSources"] = len(sources) > 0
|
||||||
|
ctx.Data["SourceNames"] = sourceNames
|
||||||
|
ctx.Data["AuthSourceFilterOptions"] = authSourceFilterOptions
|
||||||
|
|
||||||
explore.RenderUserSearch(ctx, user_model.SearchUserOptions{
|
explore.RenderUserSearch(ctx, user_model.SearchUserOptions{
|
||||||
Actor: ctx.Doer,
|
Actor: ctx.Doer,
|
||||||
Types: types,
|
Types: types,
|
||||||
@@ -88,6 +119,7 @@ func Users(ctx *context.Context) {
|
|||||||
IsRestricted: optional.ParseBool(statusFilterMap["is_restricted"]),
|
IsRestricted: optional.ParseBool(statusFilterMap["is_restricted"]),
|
||||||
IsTwoFactorEnabled: optional.ParseBool(statusFilterMap["is_2fa_enabled"]),
|
IsTwoFactorEnabled: optional.ParseBool(statusFilterMap["is_2fa_enabled"]),
|
||||||
IsProhibitLogin: optional.ParseBool(statusFilterMap["is_prohibit_login"]),
|
IsProhibitLogin: optional.ParseBool(statusFilterMap["is_prohibit_login"]),
|
||||||
|
SourceID: sourceIDFilter,
|
||||||
OrderBy: db.SearchOrderBy(sortType),
|
OrderBy: db.SearchOrderBy(sortType),
|
||||||
}, tplUsers)
|
}, tplUsers)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -374,7 +374,7 @@ func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_m
|
|||||||
// Reactivate user only if they were disabled by the OAuth2 auto sync cron (invalid_grant),
|
// Reactivate user only if they were disabled by the OAuth2 auto sync cron (invalid_grant),
|
||||||
// which clears AccessToken/RefreshToken/ExpiresAt on the ExternalLoginUser row
|
// which clears AccessToken/RefreshToken/ExpiresAt on the ExternalLoginUser row
|
||||||
// An admin-disabled user has no such signature, so we leave IsActive alone
|
// An admin-disabled user has no such signature, so we leave IsActive alone
|
||||||
// and let verifyAuthWithOptionsWeb route them through the prohibit-login / activate page.
|
// and let verifyAuthWithOptions route them through the prohibit-login / activate page.
|
||||||
if !u.IsActive {
|
if !u.IsActive {
|
||||||
extLogin, hasExt, err := user_model.GetExternalLogin(ctx, authSource.ID, gothUser.UserID)
|
extLogin, hasExt, err := user_model.GetExternalLogin(ctx, authSource.ID, gothUser.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
audit_model "gitea.dev/models/audit"
|
audit_model "gitea.dev/models/audit"
|
||||||
"gitea.dev/models/auth"
|
"gitea.dev/models/auth"
|
||||||
@@ -21,7 +20,6 @@ import (
|
|||||||
"gitea.dev/modules/log"
|
"gitea.dev/modules/log"
|
||||||
"gitea.dev/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
"gitea.dev/modules/templates"
|
"gitea.dev/modules/templates"
|
||||||
"gitea.dev/modules/util"
|
|
||||||
"gitea.dev/modules/web"
|
"gitea.dev/modules/web"
|
||||||
"gitea.dev/services/audit"
|
"gitea.dev/services/audit"
|
||||||
auth_service "gitea.dev/services/auth"
|
auth_service "gitea.dev/services/auth"
|
||||||
@@ -323,18 +321,9 @@ func AuthorizeOAuth(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var addedScopes, removedScopes []string
|
|
||||||
if grant != nil {
|
|
||||||
if form.Scope == "" {
|
|
||||||
form.Scope = grant.Scope
|
|
||||||
}
|
|
||||||
addedScopes, removedScopes = util.DiffSlice(strings.Fields(grant.Scope), strings.Fields(form.Scope))
|
|
||||||
}
|
|
||||||
scopeChanged := len(addedScopes) > 0 || len(removedScopes) > 0
|
|
||||||
|
|
||||||
// Redirect if user already granted access and the application is confidential or trusted otherwise
|
// Redirect if user already granted access and the application is confidential or trusted otherwise
|
||||||
// I.e. always require authorization for untrusted public clients as recommended by RFC 6749 Section 10.2
|
// I.e. always require authorization for untrusted public clients as recommended by RFC 6749 Section 10.2
|
||||||
if (app.ConfidentialClient || app.SkipSecondaryAuthorization) && grant != nil && !scopeChanged {
|
if (app.ConfidentialClient || app.SkipSecondaryAuthorization) && grant != nil {
|
||||||
code, err := grant.GenerateNewAuthorizationCode(ctx, form.RedirectURI, form.CodeChallenge, form.CodeChallengeMethod)
|
code, err := grant.GenerateNewAuthorizationCode(ctx, form.RedirectURI, form.CodeChallenge, form.CodeChallengeMethod)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleServerError(ctx, form.State, form.RedirectURI)
|
handleServerError(ctx, form.State, form.RedirectURI)
|
||||||
@@ -358,7 +347,6 @@ func AuthorizeOAuth(ctx *context.Context) {
|
|||||||
|
|
||||||
// check if additional scopes
|
// check if additional scopes
|
||||||
ctx.Data["AdditionalScopes"] = oauth2_provider.GrantAdditionalScopes(form.Scope) != auth.AccessTokenScopeAll
|
ctx.Data["AdditionalScopes"] = oauth2_provider.GrantAdditionalScopes(form.Scope) != auth.AccessTokenScopeAll
|
||||||
ctx.Data["AddedScopes"] = addedScopes
|
|
||||||
|
|
||||||
// show authorize page to grant access
|
// show authorize page to grant access
|
||||||
ctx.Data["Application"] = app
|
ctx.Data["Application"] = app
|
||||||
@@ -444,10 +432,12 @@ func GrantApplicationOAuth(ctx *context.Context) {
|
|||||||
|
|
||||||
audit.Record(ctx, audit_model.UserOAuth2ApplicationGrant, ctx.Doer, "oauth2_application", app.Name, "granted_scope", form.Scope)
|
audit.Record(ctx, audit_model.UserOAuth2ApplicationGrant, ctx.Doer, "oauth2_application", app.Name, "granted_scope", form.Scope)
|
||||||
} else if grant.Scope != form.Scope {
|
} else if grant.Scope != form.Scope {
|
||||||
if err := auth.UpdateGrantScope(ctx, grant, form.Scope); err != nil {
|
handleAuthorizeError(ctx, AuthorizeError{
|
||||||
handleServerError(ctx, form.State, form.RedirectURI)
|
State: form.State,
|
||||||
return
|
ErrorDescription: "a grant exists with different scope",
|
||||||
}
|
ErrorCode: ErrorCodeServerError,
|
||||||
|
}, form.RedirectURI)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(form.Nonce) > 0 {
|
if len(form.Nonce) > 0 {
|
||||||
|
|||||||
@@ -13,10 +13,6 @@ import (
|
|||||||
"gitea.dev/models/unittest"
|
"gitea.dev/models/unittest"
|
||||||
user_model "gitea.dev/models/user"
|
user_model "gitea.dev/models/user"
|
||||||
"gitea.dev/modules/egress/policy"
|
"gitea.dev/modules/egress/policy"
|
||||||
"gitea.dev/modules/session"
|
|
||||||
"gitea.dev/modules/web"
|
|
||||||
"gitea.dev/services/contexttest"
|
|
||||||
"gitea.dev/services/forms"
|
|
||||||
"gitea.dev/services/oauth2_provider"
|
"gitea.dev/services/oauth2_provider"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
@@ -109,27 +105,3 @@ func TestOAuth2AvatarClientBlocksCloudMetadata(t *testing.T) {
|
|||||||
assert.ErrorIs(t, err, policy.ErrDenied,
|
assert.ErrorIs(t, err, policy.ErrDenied,
|
||||||
"avatar client must refuse a link-local cloud-metadata address")
|
"avatar client must refuse a link-local cloud-metadata address")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOAuth2ScopeChange(t *testing.T) {
|
|
||||||
require.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
app := unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{ID: 1})
|
|
||||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
|
||||||
mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockMemStore("oauth2-scope-change")}
|
|
||||||
authorize := func(scope string) int {
|
|
||||||
ctx, resp := contexttest.MockContext(t, "/login/oauth/authorize", mockOpt)
|
|
||||||
ctx.Doer = doer
|
|
||||||
web.SetForm(ctx, &forms.AuthorizationForm{ResponseType: "code", ClientID: app.ClientID, RedirectURI: app.RedirectURIs[0], State: "state", Scope: scope})
|
|
||||||
AuthorizeOAuth(ctx)
|
|
||||||
return resp.Code
|
|
||||||
}
|
|
||||||
assert.Equal(t, http.StatusSeeOther, authorize(""))
|
|
||||||
assert.Equal(t, http.StatusSeeOther, authorize("profile openid"))
|
|
||||||
assert.Equal(t, http.StatusOK, authorize("openid profile email"))
|
|
||||||
|
|
||||||
ctx, resp := contexttest.MockContext(t, "/login/oauth/grant", mockOpt)
|
|
||||||
ctx.Doer = doer
|
|
||||||
web.SetForm(ctx, &forms.GrantApplicationForm{ClientID: app.ClientID, Granted: true, RedirectURI: app.RedirectURIs[0], State: "state", Scope: "openid profile email"})
|
|
||||||
GrantApplicationOAuth(ctx)
|
|
||||||
assert.Equal(t, http.StatusSeeOther, resp.Code)
|
|
||||||
unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Grant{ID: 1, Scope: "openid profile email"})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import (
|
|||||||
"gitea.dev/modules/templates"
|
"gitea.dev/modules/templates"
|
||||||
"gitea.dev/modules/timeutil"
|
"gitea.dev/modules/timeutil"
|
||||||
"gitea.dev/modules/web"
|
"gitea.dev/modules/web"
|
||||||
"gitea.dev/routers/common"
|
|
||||||
"gitea.dev/services/audit"
|
"gitea.dev/services/audit"
|
||||||
"gitea.dev/services/context"
|
"gitea.dev/services/context"
|
||||||
"gitea.dev/services/forms"
|
"gitea.dev/services/forms"
|
||||||
@@ -283,9 +282,10 @@ func MustChangePasswordPost(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !common.CheckSignedInUser(ctx.Doer, ctx.Session).NeedChangePassword {
|
// Make sure only requests for users who are eligible to change their password via
|
||||||
log.Debug("User %s attempted to access the must change password page, but they are not required to change their password", ctx.Doer.Name)
|
// this method passes through
|
||||||
ctx.NotFound(nil)
|
if !ctx.Doer.MustChangePassword {
|
||||||
|
ctx.ServerError("MustUpdatePassword", errors.New("cannot update password. Please visit the settings page"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
-3
@@ -17,18 +17,38 @@ import (
|
|||||||
"gitea.dev/modules/sitemap"
|
"gitea.dev/modules/sitemap"
|
||||||
"gitea.dev/modules/structs"
|
"gitea.dev/modules/structs"
|
||||||
"gitea.dev/modules/templates"
|
"gitea.dev/modules/templates"
|
||||||
|
"gitea.dev/modules/web/middleware"
|
||||||
|
"gitea.dev/routers/web/auth"
|
||||||
"gitea.dev/routers/web/user"
|
"gitea.dev/routers/web/user"
|
||||||
"gitea.dev/services/context"
|
"gitea.dev/services/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
const tplHome templates.TplName = "home"
|
const (
|
||||||
|
// tplHome home page template
|
||||||
|
tplHome templates.TplName = "home"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Home render home page
|
||||||
func Home(ctx *context.Context) {
|
func Home(ctx *context.Context) {
|
||||||
if ctx.IsSigned {
|
if ctx.IsSigned {
|
||||||
user.Dashboard(ctx)
|
if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm {
|
||||||
|
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
|
||||||
|
ctx.HTML(http.StatusOK, auth.TplActivate)
|
||||||
|
} else if !ctx.Doer.IsActive || ctx.Doer.ProhibitLogin {
|
||||||
|
log.Info("Failed authentication attempt for %s from %s", ctx.Doer.Name, ctx.RemoteAddr())
|
||||||
|
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
|
||||||
|
ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
|
||||||
|
} else if doerMustChangePassword(ctx) {
|
||||||
|
ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
|
||||||
|
ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/change_password"
|
||||||
|
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
|
||||||
|
ctx.Redirect(setting.AppSubURL + "/user/settings/change_password")
|
||||||
|
} else {
|
||||||
|
user.Dashboard(ctx)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
|
// Check non-logged users landing page.
|
||||||
} else if setting.LandingPageURL != setting.LandingPageHome {
|
} else if setting.LandingPageURL != setting.LandingPageHome {
|
||||||
// Check non-logged users landing page
|
|
||||||
ctx.Redirect(setting.AppSubURL + string(setting.LandingPageURL))
|
ctx.Redirect(setting.AppSubURL + string(setting.LandingPageURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1145,7 +1145,7 @@ func MergePullRequest(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := pull_service.Merge(ctx, pr.ID, ctx.Doer, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
|
if err := pull_service.Merge(pr.ID, ctx.Doer, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
|
||||||
if pull_service.IsErrInvalidMergeStyle(err) {
|
if pull_service.IsErrInvalidMergeStyle(err) {
|
||||||
ctx.JSONError(ctx.Tr("repo.pulls.invalid_merge_option"))
|
ctx.JSONError(ctx.Tr("repo.pulls.invalid_merge_option"))
|
||||||
} else if conflictError, ok := err.(pull_service.ErrMergeConflicts); ok {
|
} else if conflictError, ok := err.(pull_service.ErrMergeConflicts); ok {
|
||||||
|
|||||||
+32
-18
@@ -174,29 +174,38 @@ func newWebAuthMiddleware() *AuthMiddleware {
|
|||||||
return webAuth
|
return webAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
// verifyAuthWithOptionsWeb checks authentication according to options
|
func doerMustChangePassword(ctx *context.Context) bool {
|
||||||
func verifyAuthWithOptionsWeb(options *common.VerifyOptions) func(ctx *context.Context) {
|
// an impersonating admin must not be forced to set the impersonated user's password
|
||||||
|
return ctx.Doer != nil && ctx.Doer.MustChangePassword && !ctx.DoerIsImpersonated()
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyAuthWithOptions checks authentication according to options
|
||||||
|
func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Context) {
|
||||||
crossOriginProtection := http.NewCrossOriginProtection()
|
crossOriginProtection := http.NewCrossOriginProtection()
|
||||||
|
|
||||||
return func(ctx *context.Context) {
|
return func(ctx *context.Context) {
|
||||||
// Check prohibit login users.
|
// Check prohibit login users.
|
||||||
if ctx.IsSigned {
|
if ctx.IsSigned {
|
||||||
check := common.CheckSignedInUser(ctx.Doer, ctx.Session)
|
if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm {
|
||||||
if check.NeedActivateAccount {
|
|
||||||
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
|
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
|
||||||
ctx.HTML(http.StatusOK, "user/auth/activate")
|
ctx.HTML(http.StatusOK, "user/auth/activate")
|
||||||
return
|
return
|
||||||
} else if check.LoginIsProhibited {
|
}
|
||||||
|
if !ctx.Doer.IsActive || ctx.Doer.ProhibitLogin {
|
||||||
log.Info("Failed authentication attempt for %s from %s", ctx.Doer.Name, ctx.RemoteAddr())
|
log.Info("Failed authentication attempt for %s from %s", ctx.Doer.Name, ctx.RemoteAddr())
|
||||||
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
|
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
|
||||||
ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
|
ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
|
||||||
return
|
return
|
||||||
} else if check.NeedChangePassword {
|
}
|
||||||
|
|
||||||
|
if doerMustChangePassword(ctx) {
|
||||||
if ctx.Req.URL.Path != "/user/settings/change_password" {
|
if ctx.Req.URL.Path != "/user/settings/change_password" {
|
||||||
if strings.HasPrefix(ctx.Req.UserAgent(), "git") {
|
if strings.HasPrefix(ctx.Req.UserAgent(), "git") {
|
||||||
ctx.HTTPError(http.StatusUnauthorized, ctx.Locale.TrString("auth.must_change_password"))
|
ctx.HTTPError(http.StatusUnauthorized, ctx.Locale.TrString("auth.must_change_password"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
|
||||||
|
ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/change_password"
|
||||||
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
|
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
|
||||||
@@ -221,9 +230,15 @@ func verifyAuthWithOptionsWeb(options *common.VerifyOptions) func(ctx *context.C
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.SignInRequired && !ctx.IsSigned {
|
if options.SignInRequired {
|
||||||
ctx.Redirect(middleware.RedirectLinkUserLogin(ctx.Req))
|
if !ctx.IsSigned {
|
||||||
return
|
ctx.Redirect(middleware.RedirectLinkUserLogin(ctx.Req))
|
||||||
|
return
|
||||||
|
} else if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm {
|
||||||
|
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
|
||||||
|
ctx.HTML(http.StatusOK, "user/auth/activate")
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect to log in page if auto-signin info is provided and has not signed in.
|
// Redirect to log in page if auto-signin info is provided and has not signed in.
|
||||||
@@ -321,7 +336,7 @@ func Routes() *web.Router {
|
|||||||
// The CORS mechanism already protects cross-origin requests, and the CrossOriginProtection has no "allowed origin" list, so disable CrossOriginProtection.
|
// The CORS mechanism already protects cross-origin requests, and the CrossOriginProtection has no "allowed origin" list, so disable CrossOriginProtection.
|
||||||
// - For non-browser client requests: git clone via http, no Sec-Fetch-Site header.
|
// - For non-browser client requests: git clone via http, no Sec-Fetch-Site header.
|
||||||
// Such requests are not cross-origin requests, so disable CrossOriginProtection.
|
// Such requests are not cross-origin requests, so disable CrossOriginProtection.
|
||||||
var optSignInFromAnyOrigin = verifyAuthWithOptionsWeb(&common.VerifyOptions{DisableCrossOriginProtection: true})
|
var optSignInFromAnyOrigin = verifyAuthWithOptions(&common.VerifyOptions{DisableCrossOriginProtection: true})
|
||||||
|
|
||||||
// addProjectBoardRoutes registers a board's column and card routes, shared by the
|
// addProjectBoardRoutes registers a board's column and card routes, shared by the
|
||||||
// repository and owner mount points.
|
// repository and owner mount points.
|
||||||
@@ -340,14 +355,13 @@ func addProjectBoardRoutes(m *web.Router) {
|
|||||||
// registerWebRoutes register routes
|
// registerWebRoutes register routes
|
||||||
func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||||
// middleware: required to be signed in or signed out
|
// middleware: required to be signed in or signed out
|
||||||
reqSignIn := verifyAuthWithOptionsWeb(&common.VerifyOptions{SignInRequired: true})
|
reqSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true})
|
||||||
reqSignOut := verifyAuthWithOptionsWeb(&common.VerifyOptions{SignOutRequired: true})
|
reqSignOut := verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true})
|
||||||
// middleware: optional sign in (if signed in, use the user as doer, if not, no doer)
|
// middleware: optional sign in (if signed in, use the user as doer, if not, no doer)
|
||||||
optSignInHome := verifyAuthWithOptionsWeb(&common.VerifyOptions{SignInRequired: false}) // site home doesn't need "require sign-in" protection
|
optSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict})
|
||||||
optSignIn := verifyAuthWithOptionsWeb(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict})
|
optExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView})
|
||||||
optExploreSignIn := verifyAuthWithOptionsWeb(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView})
|
|
||||||
// middleware: only apply CrossOriginProtection
|
// middleware: only apply CrossOriginProtection
|
||||||
crossOriginProtect := verifyAuthWithOptionsWeb(&common.VerifyOptions{DisableCrossOriginProtection: false})
|
crossOriginProtect := verifyAuthWithOptions(&common.VerifyOptions{DisableCrossOriginProtection: false})
|
||||||
|
|
||||||
openIDSignInEnabled := func(ctx *context.Context) {
|
openIDSignInEnabled := func(ctx *context.Context) {
|
||||||
if !setting.Service.EnableOpenIDSignIn {
|
if !setting.Service.EnableOpenIDSignIn {
|
||||||
@@ -519,7 +533,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
|||||||
// FIXME: not all routes need go through same middleware.
|
// FIXME: not all routes need go through same middleware.
|
||||||
// Especially some AJAX requests, we can reduce middleware number to improve performance.
|
// Especially some AJAX requests, we can reduce middleware number to improve performance.
|
||||||
|
|
||||||
m.Get("/", optSignInHome, Home)
|
m.Get("/", Home)
|
||||||
m.Get("/sitemap.xml", sitemapEnabled, optExploreSignIn, HomeSitemap)
|
m.Get("/sitemap.xml", sitemapEnabled, optExploreSignIn, HomeSitemap)
|
||||||
m.Group("/.well-known", func() {
|
m.Group("/.well-known", func() {
|
||||||
m.Get("/openid-configuration", auth.OIDCWellKnown)
|
m.Get("/openid-configuration", auth.OIDCWellKnown)
|
||||||
@@ -766,7 +780,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
|||||||
|
|
||||||
m.Get("/avatar/{hash}", user.AvatarByEmailHash)
|
m.Get("/avatar/{hash}", user.AvatarByEmailHash)
|
||||||
|
|
||||||
adminReq := verifyAuthWithOptionsWeb(&common.VerifyOptions{SignInRequired: true, AdminRequired: true})
|
adminReq := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true, AdminRequired: true})
|
||||||
|
|
||||||
// ***** START: Admin *****
|
// ***** START: Admin *****
|
||||||
m.Group("/-/admin", func() {
|
m.Group("/-/admin", func() {
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ func handlePullRequestAutoMerge(ctx context.Context, pr *issues_model.PullReques
|
|||||||
|
|
||||||
// although expectedHeadCommitID is checked before, we should pass it to the Merge function to
|
// although expectedHeadCommitID is checked before, we should pass it to the Merge function to
|
||||||
// make it be checked again in case the head commit id changed after the previous check.
|
// make it be checked again in case the head commit id changed after the previous check.
|
||||||
if err := pull_service.Merge(ctx, pr.ID, doer, scheduledPRM.MergeStyle, expectedHeadCommitID, scheduledPRM.Message, true); err != nil {
|
if err := pull_service.Merge(pr.ID, doer, scheduledPRM.MergeStyle, expectedHeadCommitID, scheduledPRM.Message, true); err != nil {
|
||||||
if pull_service.IsErrSHADoesNotMatch(err) {
|
if pull_service.IsErrSHADoesNotMatch(err) {
|
||||||
return errors.Join(errSkipAutoMerge, err)
|
return errors.Join(errSkipAutoMerge, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,6 +211,11 @@ func (ctx *Context) DoerNeedTwoFactorAuth() bool {
|
|||||||
return ctx.Session.Get(session.KeyUserHasTwoFactorAuth) == false
|
return ctx.Session.Get(session.KeyUserHasTwoFactorAuth) == false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DoerIsImpersonated returns true if the current session is an admin impersonating the doer
|
||||||
|
func (ctx *Context) DoerIsImpersonated() bool {
|
||||||
|
return ctx.Session.Get(session.KeyImpersonatorData) != nil
|
||||||
|
}
|
||||||
|
|
||||||
// HasError returns true if error occurs in form validation.
|
// HasError returns true if error occurs in form validation.
|
||||||
// Attention: this function changes ctx.Data and ctx.Flash
|
// Attention: this function changes ctx.Data and ctx.Flash
|
||||||
// If HasError is called, then before Redirect, the error message should be stored by ctx.Flash.Error(ctx.GetErrMsg()) again.
|
// If HasError is called, then before Redirect, the error message should be stored by ctx.Flash.Error(ctx.GetErrMsg()) again.
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ func (c TemplateContext) CurrentWebTheme() *webtheme.ThemeMetaInfo {
|
|||||||
|
|
||||||
func (c TemplateContext) ImpersonatedUser() *user_model.User {
|
func (c TemplateContext) ImpersonatedUser() *user_model.User {
|
||||||
webCtx := GetWebContext(c)
|
webCtx := GetWebContext(c)
|
||||||
if webCtx == nil || webCtx.Doer == nil || !IsDoerSessionImpersonated(webCtx.Session) {
|
if webCtx == nil || webCtx.Doer == nil || !webCtx.DoerIsImpersonated() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return webCtx.Doer
|
return webCtx.Doer
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ func PrepareCommitFormOptions(ctx *Context, doer *user_model.User, targetRepo *r
|
|||||||
protectionRequireSigned := false
|
protectionRequireSigned := false
|
||||||
if protectedBranch != nil {
|
if protectedBranch != nil {
|
||||||
protectedBranch.Repo = targetRepo
|
protectedBranch.Repo = targetRepo
|
||||||
canPushWithProtection = protectedBranch.CanUserPush(ctx, doer, doerRepoPerm)
|
canPushWithProtection = protectedBranch.CanUserPush(ctx, doer)
|
||||||
protectionRequireSigned = protectedBranch.RequireSignedCommits
|
protectionRequireSigned = protectedBranch.RequireSignedCommits
|
||||||
// If branch-wide push is restricted, allow direct commit when the
|
// If branch-wide push is restricted, allow direct commit when the
|
||||||
// URL-derived tree path matches an unprotected file pattern. The
|
// URL-derived tree path matches an unprotected file pattern. The
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
user_model "gitea.dev/models/user"
|
user_model "gitea.dev/models/user"
|
||||||
"gitea.dev/modules/session"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// UserAssignmentWeb returns a middleware to handle context-user assignment for web routes
|
// UserAssignmentWeb returns a middleware to handle context-user assignment for web routes
|
||||||
@@ -59,7 +58,3 @@ func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, string)) (
|
|||||||
}
|
}
|
||||||
return contextUser
|
return contextUser
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsDoerSessionImpersonated(sess session.Store) bool {
|
|
||||||
return sess.Get(session.KeyImpersonatorData) != nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ func ToBranch(ctx context.Context, repo *repo_model.Repository, branchName strin
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
bp.Repo = repo
|
bp.Repo = repo
|
||||||
branch.UserCanPush = bp.CanUserPush(ctx, user, permission)
|
branch.UserCanPush = bp.CanUserPush(ctx, user)
|
||||||
branch.UserCanMerge = git_model.IsUserMergeWhitelisted(ctx, bp, user.ID, permission)
|
branch.UserCanMerge = git_model.IsUserMergeWhitelisted(ctx, bp, user.ID, permission)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-16
@@ -14,7 +14,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
"uuid"
|
|
||||||
|
|
||||||
"gitea.dev/models/db"
|
"gitea.dev/models/db"
|
||||||
git_model "gitea.dev/models/git"
|
git_model "gitea.dev/models/git"
|
||||||
@@ -28,7 +27,6 @@ import (
|
|||||||
"gitea.dev/modules/git/gitcmd"
|
"gitea.dev/modules/git/gitcmd"
|
||||||
"gitea.dev/modules/globallock"
|
"gitea.dev/modules/globallock"
|
||||||
"gitea.dev/modules/graceful"
|
"gitea.dev/modules/graceful"
|
||||||
"gitea.dev/modules/gtprof"
|
|
||||||
"gitea.dev/modules/httplib"
|
"gitea.dev/modules/httplib"
|
||||||
"gitea.dev/modules/log"
|
"gitea.dev/modules/log"
|
||||||
"gitea.dev/modules/references"
|
"gitea.dev/modules/references"
|
||||||
@@ -291,22 +289,9 @@ func hasPullRequestCommitBeenMerged(ctx context.Context, pr *issues_model.PullRe
|
|||||||
|
|
||||||
// Merge merges pull request to base repository.
|
// Merge merges pull request to base repository.
|
||||||
// Caller should check PR is ready to be merged (review and status checks)
|
// Caller should check PR is ready to be merged (review and status checks)
|
||||||
func Merge(outerCtx context.Context, prID int64, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, wasAutoMerged bool) error {
|
func Merge(prID int64, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, wasAutoMerged bool) error {
|
||||||
outerCtxId := uuid.NewV4().String()
|
|
||||||
|
|
||||||
_, outerSpan := gtprof.GetTracer().Start(outerCtx, gtprof.TraceSpanContext)
|
|
||||||
outerSpan.SetAttributeString("context.trace-id", outerCtxId) // this attribute is only used internally for debugging purpose
|
|
||||||
defer outerSpan.End()
|
|
||||||
|
|
||||||
// TODO: in the future, the contexts from graceful.GetManager() should be wrapped with gtprof tracing, refactor the code to framework-level support
|
|
||||||
ctx := graceful.GetManager().HammerContext() // don't abort the git operation even if the user's request is canceled
|
ctx := graceful.GetManager().HammerContext() // don't abort the git operation even if the user's request is canceled
|
||||||
|
|
||||||
ctx, span := gtprof.GetTracer().Start(ctx, gtprof.TraceSpanContext)
|
|
||||||
span.SetAttributeString(gtprof.TraceAttrGeneralName, "merge-pull-request")
|
|
||||||
span.SetAttributeString(gtprof.TraceAttrGeneralDesc, fmt.Sprintf("merge pull request %d with merge style %s", prID, mergeStyle))
|
|
||||||
span.SetAttributeString("context.trace-id-outer", outerCtxId) // this attribute is only used internally for debugging purpose
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
err := globallock.LockAndDo(ctx, getPullWorkingLockKey(prID), func(ctx context.Context) error {
|
err := globallock.LockAndDo(ctx, getPullWorkingLockKey(prID), func(ctx context.Context) error {
|
||||||
pr, err := issues_model.GetPullRequestByID(ctx, prID)
|
pr, err := issues_model.GetPullRequestByID(ctx, prID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -123,8 +123,8 @@ func isUserAllowedToPushOrForcePushInRepoBranch(ctx context.Context, user *user_
|
|||||||
}
|
}
|
||||||
if pb != nil { // override previous results if there is a branch protection rule
|
if pb != nil { // override previous results if there is a branch protection rule
|
||||||
pb.Repo = repo
|
pb.Repo = repo
|
||||||
pushAllowed = pb.CanUserPush(ctx, user, repoPerm)
|
pushAllowed = pb.CanUserPush(ctx, user)
|
||||||
forcePushAllowed = pb.CanUserForcePush(ctx, user, repoPerm)
|
forcePushAllowed = pb.CanUserForcePush(ctx, user)
|
||||||
}
|
}
|
||||||
return pushAllowed, forcePushAllowed, nil
|
return pushAllowed, forcePushAllowed, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -447,7 +447,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if rule != nil && !rule.CanUserPush(ctx, doer, perm) {
|
if rule != nil && !rule.CanUserPush(ctx, doer) {
|
||||||
return "", git_model.ErrBranchIsProtected
|
return "", git_model.ErrBranchIsProtected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
git_model "gitea.dev/models/git"
|
git_model "gitea.dev/models/git"
|
||||||
"gitea.dev/models/perm/access"
|
|
||||||
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/git"
|
||||||
@@ -89,11 +88,7 @@ func (opts *ApplyDiffPatchOptions) Validate(ctx context.Context, repo *repo_mode
|
|||||||
}
|
}
|
||||||
if protectedBranch != nil {
|
if protectedBranch != nil {
|
||||||
protectedBranch.Repo = repo
|
protectedBranch.Repo = repo
|
||||||
perm, err := access.GetDoerRepoPermission(ctx, repo, doer)
|
if !protectedBranch.CanUserPush(ctx, doer) {
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !protectedBranch.CanUserPush(ctx, doer, perm) {
|
|
||||||
return ErrUserCannotCommit{
|
return ErrUserCannotCommit{
|
||||||
UserName: doer.LowerName,
|
UserName: doer.LowerName,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
git_model "gitea.dev/models/git"
|
git_model "gitea.dev/models/git"
|
||||||
"gitea.dev/models/perm/access"
|
|
||||||
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/git"
|
||||||
@@ -668,11 +667,7 @@ func VerifyBranchProtection(ctx context.Context, repo *repo_model.Repository, gi
|
|||||||
protectedBranch.Repo = repo
|
protectedBranch.Repo = repo
|
||||||
globUnprotected := protectedBranch.GetUnprotectedFilePatterns()
|
globUnprotected := protectedBranch.GetUnprotectedFilePatterns()
|
||||||
globProtected := protectedBranch.GetProtectedFilePatterns()
|
globProtected := protectedBranch.GetProtectedFilePatterns()
|
||||||
perm, err := access.GetDoerRepoPermission(ctx, repo, doer)
|
canUserPush := protectedBranch.CanUserPush(ctx, doer)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
canUserPush := protectedBranch.CanUserPush(ctx, doer, perm)
|
|
||||||
for _, treePath := range treePaths {
|
for _, treePath := range treePaths {
|
||||||
isUnprotectedFile := false
|
isUnprotectedFile := false
|
||||||
if len(globUnprotected) != 0 {
|
if len(globUnprotected) != 0 {
|
||||||
|
|||||||
@@ -7,26 +7,20 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gitea.dev/modules/git"
|
"gitea.dev/modules/git"
|
||||||
"gitea.dev/modules/git/gitcmd"
|
"gitea.dev/modules/git/gitcmd"
|
||||||
"gitea.dev/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
const gitLogGraphFormatSep = "^" // disallowed char in git ref names
|
|
||||||
|
|
||||||
// GetCommitGraph return a list of commit (GraphItems) from all branches
|
// GetCommitGraph return a list of commit (GraphItems) from all branches
|
||||||
func GetCommitGraph(ctx context.Context, gitRepo *git.Repository, page, maxAllowedColors int, hidePRRefs bool, refs, files []string) (*Graph, error) {
|
func GetCommitGraph(ctx context.Context, gitRepo *git.Repository, page, maxAllowedColors int, hidePRRefs bool, refs, files []string) (*Graph, error) {
|
||||||
format := "DATA:" + strings.Join([]string{
|
format := "DATA:%D|%H|%ad|%h|%s"
|
||||||
"%D", // ref names without the " (", ")" wrapping.
|
|
||||||
"%H", // commit hash
|
if page == 0 {
|
||||||
"%ad", // author date (format respects --date= option)
|
page = 1
|
||||||
"%h", // abbreviated commit hash
|
}
|
||||||
"%s", // subject
|
|
||||||
}, gitLogGraphFormatSep)
|
|
||||||
|
|
||||||
page = max(page, 1)
|
|
||||||
graphCmd := gitcmd.NewCommand("log", "--graph", "--date-order", "--decorate=full")
|
graphCmd := gitcmd.NewCommand("log", "--graph", "--date-order", "--decorate=full")
|
||||||
|
|
||||||
if hidePRRefs {
|
if hidePRRefs {
|
||||||
@@ -37,7 +31,7 @@ func GetCommitGraph(ctx context.Context, gitRepo *git.Repository, page, maxAllow
|
|||||||
graphCmd.AddArguments("--tags", "--branches")
|
graphCmd.AddArguments("--tags", "--branches")
|
||||||
}
|
}
|
||||||
|
|
||||||
graphCmd.AddArguments("--find-copies", "--find-renames", "--date=iso-strict").
|
graphCmd.AddArguments("-C", "-M", "--date=iso-strict").
|
||||||
AddOptionFormat("-n %d", setting.UI.GraphMaxCommitNum*page).
|
AddOptionFormat("-n %d", setting.UI.GraphMaxCommitNum*page).
|
||||||
AddOptionFormat("--pretty=format:%s", format)
|
AddOptionFormat("--pretty=format:%s", format)
|
||||||
|
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ func parseGitTime(timeStr string) time.Time {
|
|||||||
|
|
||||||
// NewCommit creates a new commit from a provided line
|
// NewCommit creates a new commit from a provided line
|
||||||
func NewCommit(row, column int, line []byte) (*Commit, error) {
|
func NewCommit(row, column int, line []byte) (*Commit, error) {
|
||||||
data := bytes.SplitN(line, []byte(gitLogGraphFormatSep), 5)
|
data := bytes.SplitN(line, []byte("|"), 5)
|
||||||
if len(data) < 5 {
|
if len(data) < 5 {
|
||||||
return nil, fmt.Errorf("malformed data section on line %d with commit: %s", row, string(line))
|
return nil, fmt.Errorf("malformed data section on line %d with commit: %s", row, string(line))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func BenchmarkGetCommitGraph(b *testing.B) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkParseCommitString(b *testing.B) {
|
func BenchmarkParseCommitString(b *testing.B) {
|
||||||
testString := "* DATA:^4e61bacab44e9b4730e44a6615d04098dd3a8eaf^2016-12-20 21:10:41 +0100^4e61bac^Add route for graph"
|
testString := "* DATA:|4e61bacab44e9b4730e44a6615d04098dd3a8eaf|2016-12-20 21:10:41 +0100|4e61bac|Add route for graph"
|
||||||
|
|
||||||
parser := &Parser{}
|
parser := &Parser{}
|
||||||
parser.Reset()
|
parser.Reset()
|
||||||
@@ -224,14 +224,14 @@ func TestParseGlyphs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCommitStringParsing(t *testing.T) {
|
func TestCommitStringParsing(t *testing.T) {
|
||||||
dataFirstPart := "* DATA:^4e61bacab44e9b4730e44a6615d04098dd3a8eaf^2016-12-20 21:10:41 +0100^4e61bac^"
|
dataFirstPart := "* DATA:|4e61bacab44e9b4730e44a6615d04098dd3a8eaf|2016-12-20 21:10:41 +0100|4e61bac|"
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
shouldPass bool
|
shouldPass bool
|
||||||
testName string
|
testName string
|
||||||
commitMessage string
|
commitMessage string
|
||||||
}{
|
}{
|
||||||
{true, "normal", "not a fancy message"},
|
{true, "normal", "not a fancy message"},
|
||||||
{true, "extra sep", "An extra sep"},
|
{true, "extra pipe", "An extra pipe: |"},
|
||||||
{true, "extra 'Data:'", "DATA: might be trouble"},
|
{true, "extra 'Data:'", "DATA: might be trouble"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Authentication Source Filter Menu Item -->
|
||||||
|
{{if .HasAuthSources}}
|
||||||
|
<div class="ui dropdown type jump item">
|
||||||
|
<span class="text">{{ctx.Locale.Tr "admin.users.auth_source"}}</span>
|
||||||
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
|
<div class="menu flex-items-menu">
|
||||||
|
{{range $index, $option := .AuthSourceFilterOptions}}
|
||||||
|
{{if eq $index 1}}<div class="divider"></div>{{end}}
|
||||||
|
<label class="item"><input type="radio" name="source_id" value="{{$option.Value}}" {{if $option.Selected}}checked{{end}}> {{$option.Label}}</label>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<!-- Sort Menu Item -->
|
<!-- Sort Menu Item -->
|
||||||
<div class="ui dropdown type jump item">
|
<div class="ui dropdown type jump item">
|
||||||
<span class="text">
|
<span class="text">
|
||||||
@@ -75,6 +89,7 @@
|
|||||||
{{SortArrow "alphabetically" "reversealphabetically" $.SortType true}}
|
{{SortArrow "alphabetically" "reversealphabetically" $.SortType true}}
|
||||||
</th>
|
</th>
|
||||||
<th>{{ctx.Locale.Tr "email"}}</th>
|
<th>{{ctx.Locale.Tr "email"}}</th>
|
||||||
|
<th>{{ctx.Locale.Tr "admin.users.auth_source"}}</th>
|
||||||
<th>{{ctx.Locale.Tr "admin.users.activated"}}</th>
|
<th>{{ctx.Locale.Tr "admin.users.activated"}}</th>
|
||||||
<th>{{ctx.Locale.Tr "admin.users.restricted"}}</th>
|
<th>{{ctx.Locale.Tr "admin.users.restricted"}}</th>
|
||||||
<th>{{ctx.Locale.Tr "admin.users.2fa"}}</th>
|
<th>{{ctx.Locale.Tr "admin.users.2fa"}}</th>
|
||||||
@@ -102,6 +117,7 @@
|
|||||||
{{template "shared/user/user_type_label" .}}
|
{{template "shared/user/user_type_label" .}}
|
||||||
</td>
|
</td>
|
||||||
<td class="gt-ellipsis tw-max-w-48">{{.Email}}</td>
|
<td class="gt-ellipsis tw-max-w-48">{{.Email}}</td>
|
||||||
|
<td class="gt-ellipsis tw-max-w-32">{{if .LoginSource}}{{index $.SourceNames .LoginSource}}{{else}}{{ctx.Locale.Tr "admin.users.local"}}{{end}}</td>
|
||||||
<td>{{svg (Iif .IsActive "octicon-check" "octicon-x")}}</td>
|
<td>{{svg (Iif .IsActive "octicon-check" "octicon-x")}}</td>
|
||||||
<td>{{svg (Iif .IsRestricted "octicon-check" "octicon-x")}}</td>
|
<td>{{svg (Iif .IsRestricted "octicon-check" "octicon-x")}}</td>
|
||||||
<td>{{svg (Iif (index $.UsersTwoFaStatus .ID) "octicon-check" "octicon-x")}}</td>
|
<td>{{svg (Iif (index $.UsersTwoFaStatus .ID) "octicon-check" "octicon-x")}}</td>
|
||||||
@@ -119,7 +135,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{else}}
|
{{else}}
|
||||||
<tr class="no-results-row"><td class="tw-text-center" colspan="9">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
|
<tr class="no-results-row"><td class="tw-text-center" colspan="10">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
{{- $activeStopwatch := call $data.GetActiveStopwatch -}}
|
{{- $activeStopwatch := call $data.GetActiveStopwatch -}}
|
||||||
{{- $notificationUnreadCount := call $data.GetNotificationUnreadCount -}}
|
{{- $notificationUnreadCount := call $data.GetNotificationUnreadCount -}}
|
||||||
{{/* always rendered so a real-time push can reveal it without a reload */}}
|
{{/* always rendered so a real-time push can reveal it without a reload */}}
|
||||||
<a class="item active-stopwatch{{if not $activeStopwatch}} tw-hidden{{end}} {{$itemExtraClass}}" {{if $activeStopwatch}}data-seconds="{{$activeStopwatch.Seconds}}"{{end}} title="{{ctx.Locale.Tr "active_stopwatch"}}">
|
<a class="item active-stopwatch{{if not $activeStopwatch}} tw-hidden{{end}} {{$itemExtraClass}}" {{if $activeStopwatch}}href="{{$activeStopwatch.IssueLink}}" data-seconds="{{$activeStopwatch.Seconds}}"{{end}} title="{{ctx.Locale.Tr "active_stopwatch"}}">
|
||||||
<div class="tw-relative flex-text-block">
|
<div class="tw-relative flex-text-block">
|
||||||
{{svg "octicon-stopwatch"}}
|
{{svg "octicon-stopwatch"}}
|
||||||
<span class="header-stopwatch-dot"></span>
|
<span class="header-stopwatch-dot"></span>
|
||||||
|
|||||||
@@ -9,15 +9,18 @@
|
|||||||
<h3 class="tw-m-0">{{ctx.Locale.Tr "repo.pulls.cmd_instruction_checkout_title"}}</h3>
|
<h3 class="tw-m-0">{{ctx.Locale.Tr "repo.pulls.cmd_instruction_checkout_title"}}</h3>
|
||||||
{{ctx.Locale.Tr "repo.pulls.cmd_instruction_checkout_desc"}}
|
{{ctx.Locale.Tr "repo.pulls.cmd_instruction_checkout_desc"}}
|
||||||
</div>
|
</div>
|
||||||
{{$args := $pull.GetInstructionsCliArgs}}
|
{{$localBranch := $pull.HeadBranch}}
|
||||||
|
{{if ne $pull.HeadRepo.ID $pull.BaseRepo.ID}}
|
||||||
|
{{$localBranch = print $pull.HeadRepo.OwnerName "-" $pull.HeadBranch}}
|
||||||
|
{{end}}
|
||||||
<div class="ui secondary segment tw-font-mono">
|
<div class="ui secondary segment tw-font-mono">
|
||||||
{{$gitRemoteName := ctx.RootData.SystemConfig.Repository.GitGuideRemoteName.Value ctx}}
|
{{$gitRemoteName := ctx.RootData.SystemConfig.Repository.GitGuideRemoteName.Value ctx}}
|
||||||
{{if eq $pull.Flow 0}}
|
{{if eq $pull.Flow 0}}
|
||||||
<div>git fetch -u {{if ne $pull.HeadRepo.ID $pull.BaseRepo.ID}}{{$pull.HeadRepo.HTMLURL ctx}}{{else}}{{$gitRemoteName}}{{end}} {{$args.HeadBranchArg}}:{{$args.LocalBranchArg}}</div>
|
<div>git fetch -u {{if ne $pull.HeadRepo.ID $pull.BaseRepo.ID}}{{$pull.HeadRepo.HTMLURL ctx}}{{else}}{{$gitRemoteName}}{{end}} {{$pull.HeadBranch}}:{{$localBranch}}</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div>git fetch -u {{$gitRemoteName}} {{$pull.GetGitHeadRefName}}:{{$args.LocalBranchArg}}</div>
|
<div>git fetch -u {{$gitRemoteName}} {{$pull.GetGitHeadRefName}}:{{$localBranch}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<div>git checkout {{$args.LocalBranchArg}}</div>
|
<div>git checkout {{$localBranch}}</div>
|
||||||
</div>
|
</div>
|
||||||
{{if $data.ShowMergeInstructions}}
|
{{if $data.ShowMergeInstructions}}
|
||||||
<div>
|
<div>
|
||||||
@@ -29,32 +32,32 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="ui secondary segment tw-font-mono">
|
<div class="ui secondary segment tw-font-mono">
|
||||||
<div data-pull-merge-style="merge">
|
<div data-pull-merge-style="merge">
|
||||||
<div>git checkout {{$args.BaseBranchArg}}</div>
|
<div>git checkout {{$pull.BaseBranch}}</div>
|
||||||
<div>git merge --no-ff {{$args.LocalBranchArg}}</div>
|
<div>git merge --no-ff {{$localBranch}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-hidden" data-pull-merge-style="rebase">
|
<div class="tw-hidden" data-pull-merge-style="rebase">
|
||||||
<div>git checkout {{$args.BaseBranchArg}}</div>
|
<div>git checkout {{$pull.BaseBranch}}</div>
|
||||||
<div>git merge --ff-only {{$args.LocalBranchArg}}</div>
|
<div>git merge --ff-only {{$localBranch}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-hidden" data-pull-merge-style="rebase-merge">
|
<div class="tw-hidden" data-pull-merge-style="rebase-merge">
|
||||||
<div>git checkout {{$args.LocalBranchArg}}</div>
|
<div>git checkout {{$localBranch}}</div>
|
||||||
<div>git rebase {{$args.BaseBranchArg}}</div>
|
<div>git rebase {{$pull.BaseBranch}}</div>
|
||||||
<div>git checkout {{$args.BaseBranchArg}}</div>
|
<div>git checkout {{$pull.BaseBranch}}</div>
|
||||||
<div>git merge --no-ff {{$args.LocalBranchArg}}</div>
|
<div>git merge --no-ff {{$localBranch}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-hidden" data-pull-merge-style="squash">
|
<div class="tw-hidden" data-pull-merge-style="squash">
|
||||||
<div>git checkout {{$args.BaseBranchArg}}</div>
|
<div>git checkout {{$pull.BaseBranch}}</div>
|
||||||
<div>git merge --squash {{$args.LocalBranchArg}}</div>
|
<div>git merge --squash {{$localBranch}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-hidden" data-pull-merge-style="fast-forward-only">
|
<div class="tw-hidden" data-pull-merge-style="fast-forward-only">
|
||||||
<div>git checkout {{$args.BaseBranchArg}}</div>
|
<div>git checkout {{$pull.BaseBranch}}</div>
|
||||||
<div>git merge --ff-only {{$args.LocalBranchArg}}</div>
|
<div>git merge --ff-only {{$localBranch}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-hidden" data-pull-merge-style="manually-merged">
|
<div class="tw-hidden" data-pull-merge-style="manually-merged">
|
||||||
<div>git checkout {{$args.BaseBranchArg}}</div>
|
<div>git checkout {{$pull.BaseBranch}}</div>
|
||||||
<div>git merge {{$args.LocalBranchArg}}</div>
|
<div>git merge {{$localBranch}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>git push {{$gitRemoteName}} {{$args.BaseBranchArg}}</div>
|
<div>git push {{$gitRemoteName}} {{$pull.BaseBranch}}</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -11939,7 +11939,7 @@
|
|||||||
"operationId": "adminSearchUsers",
|
"operationId": "adminSearchUsers",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "ID of the user's login source to search for",
|
"description": "ID of the user's login source to search for, 0 means the local users",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"name": "source_id",
|
"name": "source_id",
|
||||||
"schema": {
|
"schema": {
|
||||||
|
|||||||
+1
-1
@@ -825,7 +825,7 @@
|
|||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int64",
|
"format": "int64",
|
||||||
"description": "ID of the user's login source to search for",
|
"description": "ID of the user's login source to search for, 0 means the local users",
|
||||||
"name": "source_id",
|
"name": "source_id",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{{if .EnableCaptcha}}{{if eq .CaptchaType "image"}}
|
{{if .EnableCaptcha}}{{if eq .CaptchaType "image"}}
|
||||||
<div class="inline field tw-text-center" data-global-init="initImageCaptcha">
|
<div class="inline field tw-text-center">
|
||||||
{{.Captcha.CreateHTML}}
|
{{.Captcha.CreateHTML}}
|
||||||
</div>
|
</div>
|
||||||
<div class="required field {{if .Err_Captcha}}error{{end}}">
|
<div class="required field {{if .Err_Captcha}}error{{end}}">
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
{{ctx.Locale.Tr "auth.authorize_application_created_by" .ApplicationCreatorLinkHTML}}<br>
|
{{ctx.Locale.Tr "auth.authorize_application_created_by" .ApplicationCreatorLinkHTML}}<br>
|
||||||
{{ctx.Locale.Tr "auth.authorize_application_with_scopes" (HTMLFormat "<b>%s</b>" .Scope)}}
|
{{ctx.Locale.Tr "auth.authorize_application_with_scopes" (HTMLFormat "<b>%s</b>" .Scope)}}
|
||||||
{{if .AddedScopes}}<br>{{ctx.Locale.Tr "auth.authorize_application_new_scopes" (HTMLFormat "<b>%s</b>" (StringUtils.Join .AddedScopes " "))}}{{end}}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui attached segment">
|
<div class="ui attached segment">
|
||||||
|
|||||||
@@ -39,6 +39,18 @@ test('pdf file', async ({page, request}) => {
|
|||||||
await assertFlushWithParent(container, page.locator('.file-view'));
|
await assertFlushWithParent(container, page.locator('.file-view'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('code line anchors', async ({page, request}) => {
|
||||||
|
const repoName = `e2e-line-anchor-${randomString(8)}`;
|
||||||
|
const owner = env.GITEA_TEST_E2E_USER;
|
||||||
|
await apiCreateRepo(request, {name: repoName});
|
||||||
|
await apiCreateFiles(request, owner, repoName, [{path: 'test.txt', content: 'a\n'}]);
|
||||||
|
const url = `/${owner}/${repoName}/src/branch/main/test.txt`;
|
||||||
|
await page.goto(`${url}#L0`);
|
||||||
|
await page.goto(`${url}#L1`);
|
||||||
|
await expect(page.locator('.code-view tr.active')).toHaveCount(1);
|
||||||
|
await assertNoJsError(page);
|
||||||
|
});
|
||||||
|
|
||||||
test('asciicast file', async ({page, request}) => {
|
test('asciicast file', async ({page, request}) => {
|
||||||
const repoName = `e2e-asciicast-render-${randomString(8)}`;
|
const repoName = `e2e-asciicast-render-${randomString(8)}`;
|
||||||
const owner = env.GITEA_TEST_E2E_USER;
|
const owner = env.GITEA_TEST_E2E_USER;
|
||||||
|
|||||||
@@ -4,20 +4,16 @@
|
|||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
actions_model "gitea.dev/models/actions"
|
actions_model "gitea.dev/models/actions"
|
||||||
"gitea.dev/models/db"
|
"gitea.dev/models/db"
|
||||||
"gitea.dev/models/unittest"
|
"gitea.dev/models/unittest"
|
||||||
"gitea.dev/modules/setting"
|
|
||||||
"gitea.dev/tests"
|
"gitea.dev/tests"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"xorm.io/builder"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// minimalWorkflowPayload returns the minimal YAML for a single-job workflow with no steps.
|
// minimalWorkflowPayload returns the minimal YAML for a single-job workflow with no steps.
|
||||||
@@ -120,42 +116,3 @@ func TestCreateTaskForRunnerConcurrentClaim(t *testing.T) {
|
|||||||
assert.NotZero(t, updated.TaskID)
|
assert.NotZero(t, updated.TaskID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareWaitingRunJob(t *testing.T) *actions_model.ActionRunJob {
|
|
||||||
if setting.Database.Type.IsSQLite3() {
|
|
||||||
t.Skip("SQLite serializes write transactions")
|
|
||||||
}
|
|
||||||
job := &actions_model.ActionRunJob{RepoID: 1, Status: actions_model.StatusWaiting, RunsOn: []string{"ubuntu-latest"}}
|
|
||||||
require.NoError(t, db.Insert(t.Context(), job))
|
|
||||||
return job
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateTaskForRunnerDuringOpenClaimDoesNotWait(t *testing.T) {
|
|
||||||
job := prepareWaitingRunJob(t)
|
|
||||||
|
|
||||||
require.NoError(t, db.WithTx(t.Context(), func(ctx context.Context) error {
|
|
||||||
_, err := db.GetEngine(ctx).ID(job.ID).Cols("name").Update(&actions_model.ActionRunJob{Name: "claiming"})
|
|
||||||
require.NoError(t, err)
|
|
||||||
pickupCtx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
_, _, err = actions_model.CreateTaskForRunner(pickupCtx, &actions_model.ActionRunner{})
|
|
||||||
return err
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClaimRunJobAfterConcurrentCancelUpdatesNothing(t *testing.T) {
|
|
||||||
job := prepareWaitingRunJob(t)
|
|
||||||
|
|
||||||
require.NoError(t, db.WithTx(t.Context(), func(ctx context.Context) error {
|
|
||||||
claimed, err := actions_model.GetRunJobByRepoAndID(ctx, job.RepoID, job.ID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
_, err = db.GetEngine(t.Context()).ID(job.ID).Cols("status").Update(&actions_model.ActionRunJob{Status: actions_model.StatusCancelled})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
claimed.TaskID, claimed.Status = 1, actions_model.StatusRunning
|
|
||||||
affected, err := actions_model.UpdateRunJob(ctx, claimed, builder.Eq{"task_id": 0, "status": actions_model.StatusWaiting}, "task_id", "status")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Zero(t, affected)
|
|
||||||
return nil
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ import (
|
|||||||
"gitea.dev/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
api "gitea.dev/modules/structs"
|
api "gitea.dev/modules/structs"
|
||||||
"gitea.dev/modules/test"
|
"gitea.dev/modules/test"
|
||||||
|
"gitea.dev/services/auth/source/ldap"
|
||||||
"gitea.dev/tests"
|
"gitea.dev/tests"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -34,6 +36,49 @@ func TestAdminViewUsers(t *testing.T) {
|
|||||||
session.MakeRequest(t, req, http.StatusForbidden)
|
session.MakeRequest(t, req, http.StatusForbidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAdminViewUsersFilterAuthSource(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
source := &auth_model.Source{Type: auth_model.LDAP, Name: "test-user-list-filter", IsActive: false, Cfg: &ldap.Source{}} // users stay attached to a deactivated source
|
||||||
|
require.NoError(t, auth_model.CreateSource(t.Context(), source))
|
||||||
|
|
||||||
|
user2 := &user_model.User{ID: 2, LoginType: auth_model.LDAP, LoginSource: source.ID}
|
||||||
|
require.NoError(t, user_model.UpdateUserCols(t.Context(), user2, "login_type", "login_source"))
|
||||||
|
|
||||||
|
session := loginUser(t, "user1")
|
||||||
|
listUsers := func(query string) (*HTMLDoc, []string) {
|
||||||
|
req := NewRequest(t, "GET", "/-/admin/users?"+query)
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
doc := NewHTMLParser(t, resp.Body)
|
||||||
|
return doc, doc.Find("table tbody tr td:nth-child(2) a").Map(func(_ int, s *goquery.Selection) string {
|
||||||
|
return s.Text()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
doc, users := listUsers("source_id=") // the "All" option submits an empty value
|
||||||
|
AssertHTMLElement(t, doc, `input[name="source_id"][value=""][checked]`, true)
|
||||||
|
assert.Subset(t, users, []string{"user1", "user2"})
|
||||||
|
|
||||||
|
doc, users = listUsers(fmt.Sprintf("source_id=%d", source.ID)) // the "test-user-list-filter" LDAP source
|
||||||
|
AssertHTMLElement(t, doc, fmt.Sprintf(`input[name="source_id"][value="%d"][checked]`, source.ID), true)
|
||||||
|
assert.Equal(t, []string{"user2"}, users)
|
||||||
|
assert.Equal(t, source.Name, doc.Find("table tbody tr td:nth-child(4)").Text())
|
||||||
|
|
||||||
|
_, users = listUsers("source_id=0") // 0 means the "Local" source
|
||||||
|
assert.Contains(t, users, "user1")
|
||||||
|
assert.NotContains(t, users, "user2")
|
||||||
|
|
||||||
|
token := getUserToken(t, "user1", auth_model.AccessTokenScopeReadAdmin)
|
||||||
|
req := NewRequest(t, "GET", "/api/v1/admin/users?source_id=0").AddTokenAuth(token) // the API also treats 0 as local users
|
||||||
|
apiUsers := DecodeJSON(t, MakeRequest(t, req, http.StatusOK), []api.User{})
|
||||||
|
apiUserNames := make([]string, 0, len(apiUsers))
|
||||||
|
for _, u := range apiUsers {
|
||||||
|
apiUserNames = append(apiUserNames, u.UserName)
|
||||||
|
}
|
||||||
|
assert.Contains(t, apiUserNames, "user1")
|
||||||
|
assert.NotContains(t, apiUserNames, "user2")
|
||||||
|
}
|
||||||
|
|
||||||
func TestAdminViewUser(t *testing.T) {
|
func TestAdminViewUser(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
@@ -235,17 +280,6 @@ func TestAdminBotUser(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("TokenIgnoresMustChangePassword", func(t *testing.T) {
|
|
||||||
bot := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "bot-user"})
|
|
||||||
bot.IsActive, bot.MustChangePassword = true, true
|
|
||||||
require.NoError(t, user_model.UpdateUserCols(t.Context(), bot, "is_active", "must_change_password"))
|
|
||||||
token := &auth_model.AccessToken{UID: bot.ID, Name: "git", Scope: auth_model.AccessTokenScopeAll}
|
|
||||||
require.NoError(t, auth_model.NewAccessToken(t.Context(), token))
|
|
||||||
|
|
||||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/repos/user2/repo1").AddTokenAuth(token.Token), http.StatusOK)
|
|
||||||
MakeRequest(t, NewRequest(t, "GET", "/user2/repo1.git/info/refs?service=git-upload-pack").AddBasicAuth(bot.Name, token.Token), http.StatusOK)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("APIRejectsAuthSource", func(t *testing.T) {
|
t.Run("APIRejectsAuthSource", func(t *testing.T) {
|
||||||
bot := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "bot-user"})
|
bot := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "bot-user"})
|
||||||
req := NewRequestWithJSON(t, "PATCH", "/api/v1/admin/users/"+bot.Name, map[string]any{"source_id": 1}).AddBasicAuth("user1")
|
req := NewRequestWithJSON(t, "PATCH", "/api/v1/admin/users/"+bot.Name, map[string]any{"source_id": 1}).AddBasicAuth("user1")
|
||||||
|
|||||||
@@ -104,7 +104,6 @@ func TestPackageNpm(t *testing.T) {
|
|||||||
},
|
},
|
||||||
"cpu": ["x64", "arm64"],
|
"cpu": ["x64", "arm64"],
|
||||||
"os": ["linux", "darwin"],
|
"os": ["linux", "darwin"],
|
||||||
"libc": ["glibc"],
|
|
||||||
"directories": {
|
"directories": {
|
||||||
"doc": "./doc",
|
"doc": "./doc",
|
||||||
"man": "./man"
|
"man": "./man"
|
||||||
@@ -219,7 +218,7 @@ func TestPackageNpm(t *testing.T) {
|
|||||||
assert.Equal(t, packageBinPath, pmv.Bin[packageBinName])
|
assert.Equal(t, packageBinPath, pmv.Bin[packageBinName])
|
||||||
assert.Equal(t, integrity, pmv.Dist.Integrity)
|
assert.Equal(t, integrity, pmv.Dist.Integrity)
|
||||||
assert.Equal(t, sha1SumHex, pmv.Dist.Shasum)
|
assert.Equal(t, sha1SumHex, pmv.Dist.Shasum)
|
||||||
assert.Equal(t, fmt.Sprintf("%sapi/packages/%s/npm/%s/-/%s", setting.AppURL, user.Name, packageName, filename), pmv.Dist.Tarball)
|
assert.Equal(t, fmt.Sprintf("%s%s/-/%s/%s", setting.AppURL, root[1:], packageVersion, filename), pmv.Dist.Tarball)
|
||||||
assert.Equal(t, repoType, result.Repository.Type)
|
assert.Equal(t, repoType, result.Repository.Type)
|
||||||
assert.Equal(t, repoURL, result.Repository.URL)
|
assert.Equal(t, repoURL, result.Repository.URL)
|
||||||
assert.Equal(t, map[string]string{"tea": "2.x", "soy-milk": "1.2"}, pmv.PeerDependencies)
|
assert.Equal(t, map[string]string{"tea": "2.x", "soy-milk": "1.2"}, pmv.PeerDependencies)
|
||||||
@@ -229,24 +228,10 @@ func TestPackageNpm(t *testing.T) {
|
|||||||
assert.Equal(t, map[string]string{"node": ">=22.7.0", "npm": ">=10.8.2"}, pmv.Engines)
|
assert.Equal(t, map[string]string{"node": ">=22.7.0", "npm": ">=10.8.2"}, pmv.Engines)
|
||||||
assert.Equal(t, []string{"x64", "arm64"}, pmv.CPU)
|
assert.Equal(t, []string{"x64", "arm64"}, pmv.CPU)
|
||||||
assert.Equal(t, []string{"linux", "darwin"}, pmv.OS)
|
assert.Equal(t, []string{"linux", "darwin"}, pmv.OS)
|
||||||
assert.Equal(t, []string{"glibc"}, pmv.Libc)
|
|
||||||
assert.Equal(t, map[string]string{"doc": "./doc", "man": "./man"}, pmv.Directories)
|
assert.Equal(t, map[string]string{"doc": "./doc", "man": "./man"}, pmv.Directories)
|
||||||
assert.Equal(t, "https://example.com/fund", pmv.Funding)
|
assert.Equal(t, "https://example.com/fund", pmv.Funding)
|
||||||
assert.Equal(t, map[string]string{"left-pad": "1.x"}, pmv.AcceptDependencies)
|
assert.Equal(t, map[string]string{"left-pad": "1.x"}, pmv.AcceptDependencies)
|
||||||
assert.Empty(t, pmv.Deprecated)
|
assert.Empty(t, pmv.Deprecated)
|
||||||
|
|
||||||
req = NewRequest(t, "GET", root).AddTokenAuth(token).SetHeader("If-None-Match", resp.Header().Get("ETag"))
|
|
||||||
MakeRequest(t, req, http.StatusNotModified)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("PingWhoami", func(t *testing.T) {
|
|
||||||
defer tests.PrintCurrentTest(t)()
|
|
||||||
|
|
||||||
registry := fmt.Sprintf("/api/packages/%s/npm/-/", user.Name)
|
|
||||||
MakeRequest(t, NewRequest(t, "GET", registry+"ping"), http.StatusOK)
|
|
||||||
MakeRequest(t, NewRequest(t, "GET", registry+"whoami"), http.StatusUnauthorized)
|
|
||||||
resp := MakeRequest(t, NewRequest(t, "GET", registry+"whoami").AddTokenAuth(token), http.StatusOK)
|
|
||||||
assert.JSONEq(t, `{"username":"`+user.Name+`"}`, resp.Body.String())
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("PackageVersionMetadata", func(t *testing.T) {
|
t.Run("PackageVersionMetadata", func(t *testing.T) {
|
||||||
@@ -305,6 +290,22 @@ func TestPackageNpm(t *testing.T) {
|
|||||||
assert.Equal(t, packageVersion, result[packageTag2])
|
assert.Equal(t, packageVersion, result[packageTag2])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("PackageMetadataDistTags", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", root).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
result := DecodeJSON(t, resp, &npm.PackageMetadata{})
|
||||||
|
|
||||||
|
assert.Len(t, result.DistTags, 2)
|
||||||
|
assert.Contains(t, result.DistTags, packageTag)
|
||||||
|
assert.Equal(t, packageVersion, result.DistTags[packageTag])
|
||||||
|
assert.Contains(t, result.DistTags, packageTag2)
|
||||||
|
assert.Equal(t, packageVersion, result.DistTags[packageTag2])
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("DeleteTag", func(t *testing.T) {
|
t.Run("DeleteTag", func(t *testing.T) {
|
||||||
defer tests.PrintCurrentTest(t)()
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
@@ -318,12 +319,6 @@ func TestPackageNpm(t *testing.T) {
|
|||||||
test(t, http.StatusBadRequest, "1.0")
|
test(t, http.StatusBadRequest, "1.0")
|
||||||
test(t, http.StatusOK, "dummy")
|
test(t, http.StatusOK, "dummy")
|
||||||
test(t, http.StatusOK, packageTag2)
|
test(t, http.StatusOK, packageTag2)
|
||||||
test(t, http.StatusOK, packageTag)
|
|
||||||
|
|
||||||
resp := MakeRequest(t, NewRequest(t, "GET", tagsRoot).AddTokenAuth(token), http.StatusOK)
|
|
||||||
assert.Equal(t, map[string]string{packageTag: packageVersion}, DecodeJSON(t, resp, map[string]string{}))
|
|
||||||
resp = MakeRequest(t, NewRequest(t, "GET", root+"/"+packageTag).AddTokenAuth(token), http.StatusOK)
|
|
||||||
assert.Equal(t, packageVersion, DecodeJSON(t, resp, &npm.PackageMetadataVersion{}).Version)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Search", func(t *testing.T) {
|
t.Run("Search", func(t *testing.T) {
|
||||||
@@ -528,7 +523,7 @@ func TestPackageNpm(t *testing.T) {
|
|||||||
req := NewRequest(t, "DELETE", fmt.Sprintf("%s/-/%s/%s/-rev/dummy", root, packageVersion, filename))
|
req := NewRequest(t, "DELETE", fmt.Sprintf("%s/-/%s/%s/-rev/dummy", root, packageVersion, filename))
|
||||||
MakeRequest(t, req, http.StatusUnauthorized)
|
MakeRequest(t, req, http.StatusUnauthorized)
|
||||||
|
|
||||||
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/-/%s/-rev/dummy", root, filename)).
|
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/-/%s/%s/-rev/dummy", root, packageVersion, filename)).
|
||||||
AddTokenAuth(token)
|
AddTokenAuth(token)
|
||||||
MakeRequest(t, req, http.StatusOK)
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
|||||||
@@ -378,11 +378,11 @@ func TestCantMergeConflict(t *testing.T) {
|
|||||||
BaseBranch: "base",
|
BaseBranch: "base",
|
||||||
})
|
})
|
||||||
|
|
||||||
err := pull_service.Merge(t.Context(), pr.ID, user1, repo_model.MergeStyleMerge, "", "CONFLICT", false)
|
err := pull_service.Merge(pr.ID, user1, repo_model.MergeStyleMerge, "", "CONFLICT", false)
|
||||||
assert.Error(t, err, "Merge should return an error due to conflict")
|
assert.Error(t, err, "Merge should return an error due to conflict")
|
||||||
assert.True(t, pull_service.IsErrMergeConflicts(err), "Merge error is not a conflict error")
|
assert.True(t, pull_service.IsErrMergeConflicts(err), "Merge error is not a conflict error")
|
||||||
|
|
||||||
err = pull_service.Merge(t.Context(), pr.ID, user1, repo_model.MergeStyleRebase, "", "CONFLICT", false)
|
err = pull_service.Merge(pr.ID, user1, repo_model.MergeStyleRebase, "", "CONFLICT", false)
|
||||||
assert.Error(t, err, "Merge should return an error due to conflict")
|
assert.Error(t, err, "Merge should return an error due to conflict")
|
||||||
assert.True(t, pull_service.IsErrRebaseConflicts(err), "Merge error is not a conflict error")
|
assert.True(t, pull_service.IsErrRebaseConflicts(err), "Merge error is not a conflict error")
|
||||||
})
|
})
|
||||||
@@ -473,7 +473,7 @@ func TestCantMergeUnrelated(t *testing.T) {
|
|||||||
BaseBranch: "base",
|
BaseBranch: "base",
|
||||||
})
|
})
|
||||||
|
|
||||||
err = pull_service.Merge(t.Context(), pr.ID, user1, repo_model.MergeStyleMerge, "", "UNRELATED", false)
|
err = pull_service.Merge(pr.ID, user1, repo_model.MergeStyleMerge, "", "UNRELATED", false)
|
||||||
assert.Error(t, err, "Merge should return an error due to unrelated")
|
assert.Error(t, err, "Merge should return an error due to unrelated")
|
||||||
assert.True(t, pull_service.IsErrMergeUnrelatedHistories(err), "Merge error is not a unrelated histories error")
|
assert.True(t, pull_service.IsErrMergeUnrelatedHistories(err), "Merge error is not a unrelated histories error")
|
||||||
})
|
})
|
||||||
@@ -509,7 +509,7 @@ func TestFastForwardOnlyMerge(t *testing.T) {
|
|||||||
BaseBranch: "master",
|
BaseBranch: "master",
|
||||||
})
|
})
|
||||||
|
|
||||||
err := pull_service.Merge(t.Context(), pr.ID, user1, repo_model.MergeStyleFastForwardOnly, "", "FAST-FORWARD-ONLY", false)
|
err := pull_service.Merge(pr.ID, user1, repo_model.MergeStyleFastForwardOnly, "", "FAST-FORWARD-ONLY", false)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -596,7 +596,7 @@ func TestFastForwardOnlyMergeWithRequiredSignedCommits(t *testing.T) {
|
|||||||
pb.RequireSignedCommits = false
|
pb.RequireSignedCommits = false
|
||||||
require.NoError(t, git_model.UpdateProtectBranch(t.Context(), repo1, pb, git_model.WhitelistOptions{}))
|
require.NoError(t, git_model.UpdateProtectBranch(t.Context(), repo1, pb, git_model.WhitelistOptions{}))
|
||||||
|
|
||||||
require.NoError(t, pull_service.Merge(t.Context(), pr.ID, user1, repo_model.MergeStyleFastForwardOnly, "", "FAST-FORWARD-ONLY", false))
|
require.NoError(t, pull_service.Merge(pr.ID, user1, repo_model.MergeStyleFastForwardOnly, "", "FAST-FORWARD-ONLY", false))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,7 +631,7 @@ func TestCantFastForwardOnlyMergeDiverging(t *testing.T) {
|
|||||||
BaseBranch: "master",
|
BaseBranch: "master",
|
||||||
})
|
})
|
||||||
|
|
||||||
err := pull_service.Merge(t.Context(), pr.ID, user1, repo_model.MergeStyleFastForwardOnly, "", "DIVERGING", false)
|
err := pull_service.Merge(pr.ID, user1, repo_model.MergeStyleFastForwardOnly, "", "DIVERGING", false)
|
||||||
assert.Error(t, err, "Merge should return an error due to being for a diverging branch")
|
assert.Error(t, err, "Merge should return an error due to being for a diverging branch")
|
||||||
assert.True(t, pull_service.IsErrMergeDivergingFastForwardOnly(err), "Merge error is not a diverging fast-forward-only error")
|
assert.True(t, pull_service.IsErrMergeDivergingFastForwardOnly(err), "Merge error is not a diverging fast-forward-only error")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
/* fonts */
|
/* fonts */
|
||||||
--fonts-proportional: -apple-system, "Segoe UI", system-ui, Roboto, "Helvetica Neue", Arial;
|
--fonts-proportional: -apple-system, "Segoe UI", system-ui, Roboto, "Helvetica Neue", Arial;
|
||||||
--fonts-monospace: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace, var(--fonts-emoji);
|
--fonts-monospace: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace, var(--fonts-emoji);
|
||||||
/* Chromium's "math" is a font Linux and ChromeOS don't ship, so fall back to the math fonts they do, https://issues.chromium.org/issues/40069293 */
|
|
||||||
--fonts-math: math, "STIX Two Math", "DejaVu Math TeX Gyre", "Noto Sans Math";
|
|
||||||
/* GitHub explicitly sets font names like: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", "Twemoji Mozilla";
|
/* GitHub explicitly sets font names like: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", "Twemoji Mozilla";
|
||||||
Actually "Twemoji Mozilla" emoji font is widely used by browsers like Firefox, Pale Moon, and it is more likely up-to-dated than the system emoji font.
|
Actually "Twemoji Mozilla" emoji font is widely used by browsers like Firefox, Pale Moon, and it is more likely up-to-dated than the system emoji font.
|
||||||
So not setting emoji font seems to be the best choice, here we just use a non-existing dummy font name and let browsers choose. */
|
So not setting emoji font seems to be the best choice, here we just use a non-existing dummy font name and let browsers choose. */
|
||||||
@@ -109,11 +107,6 @@ samp,
|
|||||||
font-size: 0.95em; /* compensate for monospace fonts being usually slightly larger */
|
font-size: 0.95em; /* compensate for monospace fonts being usually slightly larger */
|
||||||
}
|
}
|
||||||
|
|
||||||
math {
|
|
||||||
font-family: var(--fonts-math);
|
|
||||||
font-size-adjust: ex-height 0.52; /* match KaTeX's x-height (0.431 × 1.21em) regardless of math font, https://github.com/w3c/mathml-core/issues/41 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* there are many <code> blocks in non-markup(.markup code) / non-code-diff(code.code-inner) containers, for example: translation strings, etc,
|
/* there are many <code> blocks in non-markup(.markup code) / non-code-diff(code.code-inner) containers, for example: translation strings, etc,
|
||||||
so we need to make <code> have default global styles, ".markup code" has its own styles and these styles sometimes conflict.
|
so we need to make <code> have default global styles, ".markup code" has its own styles and these styles sometimes conflict.
|
||||||
TODO: in the future, we should use `div` instead of `code` for `.code-inner` because it is a container for highlighted code line, then drop this ":not" patch */
|
TODO: in the future, we should use `div` instead of `code` for `.code-inner` because it is a container for highlighted code line, then drop this ":not" patch */
|
||||||
|
|||||||
@@ -1,19 +1,6 @@
|
|||||||
import {isDarkTheme} from '../utils.ts';
|
import {isDarkTheme} from '../utils.ts';
|
||||||
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
|
||||||
|
|
||||||
export async function initCaptcha() {
|
export async function initCaptcha() {
|
||||||
registerGlobalInitFunc('initImageCaptcha', (el: HTMLElement) => {
|
|
||||||
const a = el.querySelector('a')!;
|
|
||||||
a.removeAttribute('href'); // remove generated href="javascript:"
|
|
||||||
const img = el.querySelector('img')!;
|
|
||||||
img.removeAttribute('onclick'); // remove generated onclick="...." and use our own event listener
|
|
||||||
img.addEventListener('click', () => {
|
|
||||||
const url = new URL(img.src);
|
|
||||||
url.searchParams.set('reload', String(Date.now()));
|
|
||||||
img.src = url.href;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const captchaEl = document.querySelector('#captcha');
|
const captchaEl = document.querySelector('#captcha');
|
||||||
if (!captchaEl) return;
|
if (!captchaEl) return;
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ function selectRange(range: string): Element | null {
|
|||||||
stopLineNum = tmp;
|
stopLineNum = tmp;
|
||||||
range = `${stop}-${start}`;
|
range = `${stop}-${start}`;
|
||||||
}
|
}
|
||||||
|
if (startLineNum < 1) return null;
|
||||||
|
|
||||||
const first = elLineNums[startLineNum - 1] ?? null;
|
const first = elLineNums[startLineNum - 1] ?? null;
|
||||||
for (let i = startLineNum - 1; i <= stopLineNum - 1 && i < elLineNums.length; i++) {
|
for (let i = startLineNum - 1; i <= stopLineNum - 1 && i < elLineNums.length; i++) {
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ function updateStopwatchData(data: Array<StopwatchData>) {
|
|||||||
} else {
|
} else {
|
||||||
const {repo_owner_name, repo_name, issue_index, seconds} = watch;
|
const {repo_owner_name, repo_name, issue_index, seconds} = watch;
|
||||||
const issueUrl = `${appSubUrl}/${repo_owner_name}/${repo_name}/issues/${issue_index}`;
|
const issueUrl = `${appSubUrl}/${repo_owner_name}/${repo_name}/issues/${issue_index}`;
|
||||||
|
for (const btnEl of btnEls) btnEl.setAttribute('href', issueUrl);
|
||||||
document.querySelector('.stopwatch-link')?.setAttribute('href', issueUrl);
|
document.querySelector('.stopwatch-link')?.setAttribute('href', issueUrl);
|
||||||
document.querySelector('.stopwatch-commit')?.setAttribute('action', `${issueUrl}/times/stopwatch/stop`);
|
document.querySelector('.stopwatch-commit')?.setAttribute('action', `${issueUrl}/times/stopwatch/stop`);
|
||||||
document.querySelector('.stopwatch-cancel')?.setAttribute('action', `${issueUrl}/times/stopwatch/cancel`);
|
document.querySelector('.stopwatch-cancel')?.setAttribute('action', `${issueUrl}/times/stopwatch/cancel`);
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ test('isGiteaError', () => {
|
|||||||
expect(isGiteaError('', `Error\n at chrome-extension://abc/content.js:1:1`)).toBe(false);
|
expect(isGiteaError('', `Error\n at chrome-extension://abc/content.js:1:1`)).toBe(false);
|
||||||
expect(isGiteaError('', `Error\n at https://other-site.com/script.js:1:1`)).toBe(false);
|
expect(isGiteaError('', `Error\n at https://other-site.com/script.js:1:1`)).toBe(false);
|
||||||
expect(isGiteaError('', `Error\n at ${origin}/assets/js/index.abc123.js:1:1`)).toBe(true);
|
expect(isGiteaError('', `Error\n at ${origin}/assets/js/index.abc123.js:1:1`)).toBe(true);
|
||||||
|
expect(isGiteaError('', `Error\n at ${origin}/web_src/js/index.ts:1:1`)).toBe(false);
|
||||||
expect(isGiteaError(`${origin}/assets/js/index.js`, `Error\n at chrome-extension://abc/content.js:1:1`)).toBe(false);
|
expect(isGiteaError(`${origin}/assets/js/index.js`, `Error\n at chrome-extension://abc/content.js:1:1`)).toBe(false);
|
||||||
|
vi.spyOn(window.config, 'runModeIsProd', 'get').mockReturnValue(false);
|
||||||
|
expect(isGiteaError('', `Error\n at ${origin}/web_src/js/index.ts:1:1`)).toBe(true);
|
||||||
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('showGlobalErrorMessage', () => {
|
test('showGlobalErrorMessage', () => {
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export function isGiteaError(filename: string, stack: string): boolean {
|
|||||||
if (extensionRe.test(filename) || extensionRe.test(stack)) return false;
|
if (extensionRe.test(filename) || extensionRe.test(stack)) return false;
|
||||||
const assetBaseUrl = new URL(`${windowConfig()?.assetUrlPrefix}/`, window.location.origin).href;
|
const assetBaseUrl = new URL(`${windowConfig()?.assetUrlPrefix}/`, window.location.origin).href;
|
||||||
if (filename && !filename.startsWith(assetBaseUrl) && !filename.startsWith(window.location.origin)) return false;
|
if (filename && !filename.startsWith(assetBaseUrl) && !filename.startsWith(window.location.origin)) return false;
|
||||||
|
if (!windowConfig()?.runModeIsProd && stack.includes(`${window.location.origin}/web_src/`)) return true;
|
||||||
return !stack || stack.includes(assetBaseUrl);
|
return !stack || stack.includes(assetBaseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user