Compare commits

..

1 Commits

Author SHA1 Message Date
Giteabot 15b8a5805a ci: Also release for other versions than 1 majors (#39475) (#39476) 2026-09-29 20:01:48 +00:00
98 changed files with 525 additions and 1216 deletions
+19 -18
View File
@@ -155,7 +155,7 @@
;; Username to use for the builtin SSH server. If blank, then it is the value of RUN_USER.
;BUILTIN_SSH_SERVER_USER =
;;
;; Domain name to be exposed in clone URL, defaults to the domain part of ROOT_URL
;; Domain name to be exposed in clone URL, defaults to DOMAIN or the domain part of ROOT_URL
;SSH_DOMAIN =
;;
;; 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
;; 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
;SSH_SERVER_HOST_KEYS=ssh/gitea.rsa, ssh/gitea.ed25519, ssh/gitea.ecdsa, ssh/gogs.rsa
;SSH_SERVER_HOST_KEYS=ssh/gitea.rsa, ssh/gogs.rsa
;;
;; Enable SSH Authorized Key Backup when rewriting all keys, default is false
;SSH_AUTHORIZED_KEYS_BACKUP = false
@@ -237,7 +237,7 @@
;SSH_PER_WRITE_PER_KB_TIMEOUT = 30s
;;
;; Indicate whether to check minimum key size with corresponding type
;MINIMUM_KEY_SIZE_CHECK = true
;MINIMUM_KEY_SIZE_CHECK = false
;;
;; TLS Settings: Either ACME or manual
;; (Other common TLS configuration are found before)
@@ -836,7 +836,7 @@ LEVEL = Info
;EMAIL_DOMAIN_BLOCKLIST =
;;
;; Disallow registration, only allow admins to create accounts.
;DISABLE_REGISTRATION = true
;DISABLE_REGISTRATION = false
;;
;; Allow registration only using gitea itself, it works only when DISABLE_REGISTRATION is false
;ALLOW_ONLY_INTERNAL_REGISTRATION = false
@@ -968,11 +968,12 @@ LEVEL = Info
;; 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
;; concatenation of the user name in lower case, "@" and NO_REPLY_ADDRESS. Default
;; value is "noreply." + the domain part of ROOT_URL
;NO_REPLY_ADDRESS =
;; value is "noreply." + DOMAIN, where DOMAIN resolves to the value from server.DOMAIN
;; Note: do not use the <DOMAIN> notation below
;NO_REPLY_ADDRESS = ; noreply.<DOMAIN>
;;
;; Show Registration button, defaults to true only if both DISABLE_REGISTRATION and ALLOW_ONLY_EXTERNAL_REGISTRATION are false
;SHOW_REGISTRATION_BUTTON = false
;; Show Registration button
;SHOW_REGISTRATION_BUTTON = true
;;
;; Show milestones dashboard page - a view of all the user's milestones
;SHOW_MILESTONES_DASHBOARD_PAGE = true
@@ -1669,13 +1670,13 @@ LEVEL = Info
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; General queue type, currently support: level, channel, redis, dummy
;; default to level
;TYPE = level
;; General queue queue type, currently support: persistable-channel, channel, level, redis, dummy
;; default to persistable-channel
;TYPE = persistable-channel
;;
;; data-dir for storing level queues, individual queues will default to `queues/common` meaning the queue is shared.
;; data-dir for storing persistable queues and level queues, individual queues will default to `queues/common` meaning the queue is shared.
;; Relative paths will be made absolute against "APP_DATA_PATH"
;DATADIR = queues/common
;DATADIR = queues/
;;
;; Default queue length before a channel queue will block
;LENGTH = 100000
@@ -1683,7 +1684,7 @@ LEVEL = Info
;; Batch size to send for batched queues
;BATCH_LENGTH = 20
;;
;; When `TYPE` is `level`, this provides a directory for the underlying leveldb
;; When `TYPE` is `persistable-channel`, this provides a directory for the underlying leveldb
;; 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.
;CONN_STR =
@@ -1751,6 +1752,7 @@ LEVEL = Info
;ENABLE_OPENID_SIGNIN = false
;;
;; Whether to allow registering via OpenID
;; Do not include to rely on rhw DISABLE_REGISTRATION setting
;;ENABLE_OPENID_SIGNUP = false
;;
;; Allowed URI patterns (POSIX regexp).
@@ -2013,8 +2015,8 @@ LEVEL = Info
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Either "memory", "file", "redis", "db", "mysql", "couchbase", "memcache" or "postgres"
;; Default is "file". "db" will reuse the configuration in [database]
;PROVIDER = file
;; Default is "memory". "db" will reuse the configuration in [database]
;PROVIDER = memory
;;
;; Provider config options
;; memory: doesn't have any config yet
@@ -2050,8 +2052,7 @@ LEVEL = Info
;; How Gitea deals with missing repository avatars
;; none = no avatar will be displayed; random = random avatar will be displayed; image = default image will be used
;REPOSITORY_AVATAR_FALLBACK = none
;; Image URL for the "image" fallback, used as-is, defaults to Gitea's builtin repository avatar
;REPOSITORY_AVATAR_FALLBACK_IMAGE =
;REPOSITORY_AVATAR_FALLBACK_IMAGE = /img/repo_default.png
;;
;; Max Width and Height of uploaded avatars.
;; This is to limit the amount of RAM used when resizing the image.
+3 -2
View File
@@ -295,8 +295,9 @@ func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, er
return &run, nil
}
func GetRunsByRepoAndID(ctx context.Context, repoID int64, runIDs []int64) (runs []*ActionRun, err error) {
err = db.GetEngine(ctx).In("id", runIDs).Where("repo_id=?", repoID).OrderBy("id").Find(&runs)
func GetRunsByRepoAndID(ctx context.Context, repoID int64, runIDs []int64) ([]*ActionRun, error) {
var runs []*ActionRun
err := db.GetEngine(ctx).In("id", runIDs).Where("repo_id=?", repoID).Find(&runs)
return runs, err
}
+1 -1
View File
@@ -200,7 +200,7 @@ func (r *ActionRunner) GenerateAndFillToken() {
// CanMatchLabels checks whether the runner's labels can match a job's "runs-on"
// See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idruns-on
func (r *ActionRunner) CanMatchLabels(jobRunsOn []string) bool {
return len(jobRunsOn) > 0 && !slices.ContainsFunc(jobRunsOn, func(label string) bool { return !util.SliceContainsString(r.AgentLabels, label, true) })
return !slices.ContainsFunc(jobRunsOn, func(label string) bool { return !util.SliceContainsString(r.AgentLabels, label, true) })
}
func init() {
-1
View File
@@ -86,5 +86,4 @@ func TestCanMatchLabelsCaseInsensitive(t *testing.T) {
runner := &ActionRunner{AgentLabels: []string{"self-hosted", "Linux", "X64"}}
assert.True(t, runner.CanMatchLabels([]string{"SELF-HOSTED", "linux"}))
assert.False(t, runner.CanMatchLabels([]string{"linux", "arm64"}))
assert.False(t, runner.CanMatchLabels(nil))
}
-6
View File
@@ -564,12 +564,6 @@ func (grant *OAuth2Grant) SetNonce(ctx context.Context, nonce string) error {
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
func GetOAuth2GrantByID(ctx context.Context, id int64) (grant *OAuth2Grant, err error) {
grant = new(OAuth2Grant)
+4 -4
View File
@@ -311,17 +311,17 @@ func (opts *CommitStatusOptions) ToConds() builder.Cond {
func (opts *CommitStatusOptions) ToOrders() string {
switch opts.SortType {
case "oldest":
return "created_unix ASC, `index` ASC"
return "created_unix ASC"
case "recentupdate":
return "updated_unix DESC, `index` DESC"
return "updated_unix DESC"
case "leastupdate":
return "updated_unix ASC, `index` ASC"
return "updated_unix ASC"
case "leastindex":
return "`index` DESC"
case "highestindex":
return "`index` ASC"
default:
return "created_unix DESC, `index` DESC" // timestamps have 1s resolution, `index` keeps paging stable
return "created_unix DESC"
}
}
+20 -7
View File
@@ -32,15 +32,28 @@ func TestGetCommitStatuses(t *testing.T) {
})
assert.NoError(t, err)
assert.Equal(t, 5, int(maxResults))
var indexes []int64
for _, status := range statuses {
indexes = append(indexes, status.Index)
}
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.Len(t, statuses, 5)
assert.Equal(t, "ci/awesomeness", statuses[0].Context)
assert.Equal(t, commitstatus.CommitStatusPending, 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, "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{
ListOptions: db.ListOptions{Page: 2, PageSize: 50},
RepoID: repo1.ID,
-15
View File
@@ -407,21 +407,6 @@ func (pr *PullRequest) GetGitHeadRefName() string { // TODO: make it return RefN
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)
func (pr *PullRequest) GetReviewCommentsCount(ctx context.Context) int {
opts := FindCommentsOptions{
+4 -4
View File
@@ -40,8 +40,8 @@ type SearchUserOptions struct {
Keyword string
Types []UserType
UID int64
LoginName string // 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
LoginName string // this option should be used only for admin user
SourceID int64 // this option should be used only for admin user
OrderBy db.SearchOrderBy
Visible []structs.VisibleType
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})
}
if opts.SourceID.Has() {
cond = cond.And(builder.Eq{"login_source": opts.SourceID.Value()})
if opts.SourceID > 0 {
cond = cond.And(builder.Eq{"login_source": opts.SourceID})
}
if opts.LoginName != "" {
cond = cond.And(builder.Eq{"login_name": opts.LoginName})
+8 -8
View File
@@ -225,6 +225,7 @@ func replaceScalars(node *yaml.Node, replace func(string) string) {
// buildMatrixCombos builds one Job per matrix combination from src, baking the combination into the
// strategy and interpolating the name, runs-on and continue-on-error with it.
func buildMatrixCombos(jobID string, src *Job, matrixes []map[string]any, gitCtx *model.GithubContext, results map[string]*JobResult, vars map[string]string, inputs map[string]any) ([]*Job, error) {
srcRunsOn := model.RunsOnFromNode(src.RawRunsOn)
order, names := make([]int, len(matrixes)), make([]string, len(matrixes))
for index, matrix := range matrixes {
order[index], names[index] = index, matrixName(matrix)
@@ -258,15 +259,14 @@ func buildMatrixCombos(jobID string, src *Job, matrixes []map[string]any, gitCtx
if err := evaluator.EvaluateYamlNode(&rawRunsOn); err != nil {
return nil, fmt.Errorf("interpolate runs-on for job %q: %w", jobID, err)
}
if rawRunsOn.Kind != 0 && runsOnProblem(&rawRunsOn) != "" {
combo.RawRunsOn = rawRunsOn
} else {
runsOn := model.RunsOnFromNode(rawRunsOn)
for i := range runsOn {
runsOn[i] = escapeExpressions(runsOn[i])
}
combo.RawRunsOn = model.RunsOnNode(runsOn, "")
runsOn := model.RunsOnFromNode(rawRunsOn)
if len(runsOn) == 0 && len(srcRunsOn) > 0 { // match no runner rather than every runner
runsOn = []string{""}
}
for i := range runsOn {
runsOn[i] = escapeExpressions(runsOn[i])
}
combo.RawRunsOn = model.RunsOnNode(runsOn, "")
}
if err := evaluator.EvaluateYamlNode(&combo.RawContinueOnError); err != nil {
return nil, fmt.Errorf("evaluate continue-on-error for job %q: %w", jobID, err)
+2 -42
View File
@@ -290,26 +290,17 @@ func TestParseInterpolatesRunName(t *testing.T) {
assert.Empty(t, result[0].RunName)
}
func TestParseRunsOnFromJSONKeepsWhatGitHubRejectsForTheJobToFail(t *testing.T) {
func TestParseRunsOnFromJSONArray(t *testing.T) {
content := []byte("on: push\njobs:\n build:\n runs-on: ${{ fromJSON(vars.RUNNER) }}\n steps: [{run: echo}]\n")
_, err := Parse(content)
require.NoError(t, err)
for runner, want := range map[string][]string{`["self-hosted", "linux"]`: {"self-hosted", "linux"}, "[]": {}, "{}": {}} {
for runner, want := range map[string][]string{`["self-hosted", "linux"]`: {"self-hosted", "linux"}, "[]": {""}} {
result, err := Parse(content, WithGitContext(&model.GithubContext{}), WithVars(map[string]string{"RUNNER": runner}))
require.NoError(t, err)
require.Len(t, result, 1)
_, job := result[0].Job()
assert.Equal(t, want, job.RunsOn(), runner)
}
for runner, problem := range map[string]string{`["a"]`: "", `""`: "Unexpected value ''", `[["a"]]`: "A sequence was not expected"} {
result, err := Parse(content, WithGitContext(&model.GithubContext{}), WithVars(map[string]string{"RUNNER": runner}))
require.NoError(t, err)
payload, err := result[0].Marshal()
require.NoError(t, err)
_, job, err := ParseRawSingleWorkflow(payload)
require.NoError(t, err)
assert.Equal(t, problem, job.RunsOnProblem(), runner)
}
}
func TestJobFieldsWithoutMatrix(t *testing.T) {
@@ -467,37 +458,6 @@ func TestReadWorkflowJobConditionContexts(t *testing.T) {
}
}
func TestValidateWorkflowStaticJobKindAndRunsOnLikeGitHub(t *testing.T) {
for job, want := range map[string]string{
"{runs-on: x, steps: [{run: echo}]}": "",
"{uses: o/r/.gitea/workflows/c.yml@main}": "",
"{runs-on: []}": "",
"{runs-on: {}}": "",
"{runs-on: {group: org/g, labels: [a, 1]}}": "",
"{runs-on: {group: '${{ vars.G }}'}}": "",
"{steps: [{run: echo}]}": "Required property is missing: runs-on",
"{with: {}}": "Required property is missing: uses",
"{runs-on: x, uses: o/r/.gitea/workflows/c.yml@main}": "Unexpected value 'uses'",
"{Runs-On: x}": "Unexpected value 'Runs-On'",
"{runs-on: ~}": "runs-on: Unexpected value ''",
"{runs-on: ['']}": "runs-on: Unexpected value ''",
"{runs-on: [[a]]}": "runs-on: A sequence was not expected",
"{runs-on: {labels: {a: b}}}": "runs-on: A mapping was not expected",
"{runs-on: {foo: x}}": "runs-on: Unexpected value 'foo'",
"{runs-on: {group: org/}}": "runs-on: Invalid runs-on group name 'org/'.",
"{runs-on: {group: a/b/c}}": "runs-on: Invalid runs-on group name 'a/b/c'. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.",
"{if: true}": "There's not enough info to determine what you meant. Add one of these properties: " +
"cancel-timeout-minutes, container, continue-on-error, defaults, env, environment, outputs, runs-on, secrets, services, snapshot, steps, timeout-minutes, uses, with",
} {
_, err := ValidateWorkflowStatic([]byte("on: push\njobs:\n build: " + job + "\n"))
if want == "" {
assert.NoError(t, err, job)
} else {
assert.EqualError(t, err, "job build: "+want, job)
}
}
}
func TestRejectsUnevaluatedMatrixFilters(t *testing.T) {
for _, filter := range []string{"include", "exclude"} {
t.Run(filter, func(t *testing.T) {
-8
View File
@@ -174,14 +174,6 @@ func (j *Job) EraseNeeds() *Job {
return j
}
// RunsOnProblem returns github.com's error for the job's runs-on, "" if valid.
func (j *Job) RunsOnProblem() string {
if j.RawRunsOn.Kind == 0 {
return ""
}
return runsOnProblem(&j.RawRunsOn)
}
// RunsOn returns the labels Gitea matches runners against, unescaped like DisplayName.
func (j *Job) RunsOn() []string {
runsOn := model.RunsOnFromNode(j.RawRunsOn)
+2 -120
View File
@@ -7,13 +7,10 @@ import (
"errors"
"fmt"
"slices"
"strings"
"gitea.dev/actionslib/pkg/expreval"
"gitea.dev/actionslib/pkg/exprparser"
"gitea.dev/actionslib/pkg/model"
"go.yaml.in/yaml/v4"
)
// jobConditionContexts are what github.com gives `jobs.<job_id>.if`, which it decides before the matrix, plus the `gitea` alias.
@@ -24,7 +21,7 @@ func ValidateWorkflowStatic(content []byte) ([]*Event, error) {
if err != nil {
return nil, err
}
// Keep unknown and case-distinct keys outside of jobs accepted for existing Gitea workflows.
// Keep unknown and case-distinct keys accepted for existing Gitea workflows.
workflow, err := readWorkflowDoc(doc)
if err != nil {
return nil, err
@@ -36,9 +33,6 @@ func ValidateWorkflowStatic(content []byte) ([]*Event, error) {
if err := validateWorkflowStructure(workflow); err != nil {
return nil, err
}
if err := validateJobKinds(doc); err != nil {
return nil, err
}
var header struct {
RunName string `yaml:"run-name"`
}
@@ -82,6 +76,7 @@ func validateWorkflowStructure(workflow *model.Workflow) error {
if job == nil {
return fmt.Errorf("job %q has no configuration", id)
}
// a job without runs-on is accepted and runs on any runner, github.com rejects it
for _, dependency := range job.Needs() {
if _, ok := workflow.Jobs[dependency]; !ok {
return fmt.Errorf("job %q needs unknown job %q", id, dependency)
@@ -115,116 +110,3 @@ func validateWorkflowStructure(workflow *model.Workflow) error {
}
return nil
}
// job keys of github.com's workflow schema, by the kind of job allowing them
var (
stepsJobKeys = []string{"cancel-timeout-minutes", "container", "continue-on-error", "defaults", "env", "environment", "outputs", "runs-on", "services", "snapshot", "steps", "timeout-minutes"}
callerJobKeys = []string{"secrets", "uses", "with"}
sharedJobKeys = []string{"concurrency", "if", "name", "needs", "permissions", "strategy"}
)
// validateJobKinds applies github.com's job kinds, decided by the first kind-specific key.
func validateJobKinds(doc *yaml.Node) error {
jobs := mappingValue(doc.Content[0], "jobs")
for i := 0; i+1 < len(jobs.Content); i += 2 {
id, job := jobs.Content[i].Value, jobs.Content[i+1]
var required string
for j := 0; j+1 < len(job.Content); j += 2 {
key := job.Content[j].Value
isStepsKey, isCallerKey := slices.Contains(stepsJobKeys, key), slices.Contains(callerJobKeys, key)
switch {
case required == "runs-on" && isCallerKey, required == "uses" && isStepsKey, !isStepsKey && !isCallerKey && !slices.Contains(sharedJobKeys, key):
return fmt.Errorf("job %s: Unexpected value '%s'", id, key)
case required == "" && isStepsKey:
required = "runs-on"
case required == "" && isCallerKey:
required = "uses"
}
}
if required == "" {
keys := slices.Concat(stepsJobKeys, callerJobKeys)
slices.Sort(keys)
return fmt.Errorf("job %s: There's not enough info to determine what you meant. Add one of these properties: %s", id, strings.Join(keys, ", "))
}
value := mappingValue(job, required)
if value == nil {
return fmt.Errorf("job %s: Required property is missing: %s", id, required)
}
if required != "runs-on" {
continue
}
if problem := runsOnProblem(value); problem != "" {
return fmt.Errorf("job %s: runs-on: %s", id, problem)
}
}
return nil
}
// runsOnProblem returns github.com's schema error for a runs-on, "" if valid.
func runsOnProblem(node *yaml.Node) string {
if node.Kind != yaml.MappingNode {
return runsOnLabelsProblem(node)
}
for i := 0; i+1 < len(node.Content); i += 2 {
var problem string
switch key := node.Content[i].Value; key {
case "labels":
problem = runsOnLabelsProblem(node.Content[i+1])
case "group":
problem = runsOnGroupProblem(node.Content[i+1])
default:
problem = fmt.Sprintf("Unexpected value '%s'", key)
}
if problem != "" {
return problem
}
}
return ""
}
func runsOnLabelsProblem(node *yaml.Node) string {
if node.Kind != yaml.SequenceNode {
return nonEmptyStringProblem(node)
}
for _, label := range node.Content {
if problem := nonEmptyStringProblem(label); problem != "" {
return problem
}
}
return ""
}
func runsOnGroupProblem(node *yaml.Node) string {
if problem := nonEmptyStringProblem(node); problem != "" || hasExpression(node.Value) {
return problem
}
switch prefix, name, found := strings.Cut(node.Value, "/"); {
case found && name == "":
return fmt.Sprintf("Invalid runs-on group name '%s'.", node.Value)
case found && (strings.Contains(name, "/") || !slices.Contains([]string{"org", "organization", "ent", "enterprise"}, prefix)):
return fmt.Sprintf("Invalid runs-on group name '%s'. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.", node.Value)
}
return ""
}
// nonEmptyStringProblem mirrors github.com's non-empty-string, which also accepts non-string scalars.
func nonEmptyStringProblem(node *yaml.Node) string {
switch {
case node.Kind == yaml.SequenceNode:
return "A sequence was not expected"
case node.Kind == yaml.MappingNode:
return "A mapping was not expected"
case node.Value == "" || node.ShortTag() == "!!null":
return "Unexpected value ''"
}
return ""
}
func mappingValue(node *yaml.Node, key string) *yaml.Node {
for i := 0; i+1 < len(node.Content); i += 2 {
if node.Content[i].Value == key {
return node.Content[i+1]
}
}
return nil
}
+7 -12
View File
@@ -30,23 +30,18 @@ jobs:
func TestReadWorkflowEventsStaticErrors(t *testing.T) {
for content, static := range map[string]bool{
"on: push\njobs: {}": true,
"on: push\njobs: {test: {runs-on: x, needs: absent}}": true,
"on: push\njobs: {one: {runs-on: x, needs: two}, two: {runs-on: x, needs: one}}": true,
"on: push\njobs: {test: {runs-on: x, strategy: {matrix: {os: []}}}}": true,
"on: push\nrun-name: ${{ secrets.TOKEN }}\njobs: {test: {runs-on: x}}": true,
"on: push\njobs: {test: {steps: [{run: echo}]}}": true,
"on: push\nrun-name: ${{ fromJSON(inputs.x) }}\njobs: {test: {runs-on: x, steps: [{run: echo}]}}": false,
"on: push\njobs: {}": true,
"on: push\njobs: {test: {needs: absent}}": true,
"on: push\njobs: {one: {needs: two}, two: {needs: one}}": true,
"on: push\njobs: {test: {strategy: {matrix: {os: []}}}}": true,
"on: push\nrun-name: ${{ secrets.TOKEN }}\njobs: {test: {}}": true,
"on: push\nrun-name: ${{ fromJSON(inputs.x) }}\njobs: {test: {steps: [{run: echo}]}}": false,
} {
_, gotStatic, err := readWorkflowEvents([]byte(content))
require.Error(t, err, content)
assert.Equal(t, static, gotStatic, content)
}
for _, content := range []string{
"on: push\njobs: {test: {runs-on: x, steps: [{run: echo}]}}",
"on: push\nrun-name: ${{ github.ref }}\njobs: {test: {runs-on: x}}",
"on: push\njobs: {call: {uses: ./.gitea/workflows/called.yml}}",
} {
for _, content := range []string{"on: push\njobs: {test: {steps: [{run: echo}]}}", "on: push\nrun-name: ${{ github.ref }}\njobs: {test: {}}"} {
_, _, err := readWorkflowEvents([]byte(content))
assert.NoError(t, err, content)
}
+1 -40
View File
@@ -8,13 +8,11 @@ import (
"fmt"
"html"
"io"
"strings"
"unicode"
"unicode/utf8"
"gitea.dev/modules/setting"
"gitea.dev/modules/translation"
"gitea.dev/modules/util"
)
type htmlChunkReader struct {
@@ -32,10 +30,6 @@ type escapeStreamer struct {
ambiguousTables []*AmbiguousTable
allowed map[rune]bool
tagPartial []byte // partial tag content, used to detect if we are in some tags
inTagMath bool // MathML operators like U+2212 are intended and wrapping them breaks the math layout
out io.Writer
}
@@ -68,7 +62,6 @@ func escapeStream(locale translation.Locale, in io.Reader, out io.Writer, opts .
for i, part := range parts {
if partInTag[i] {
lastIsTag = true
es.trackHtmlTag(part)
if _, err := out.Write(part); err != nil {
return nil, err
}
@@ -82,11 +75,7 @@ func escapeStream(locale translation.Locale, in io.Reader, out io.Writer, opts .
return nil, err
}
}
if es.inTagMath {
if _, err := out.Write(part); err != nil {
return nil, err
}
} else if err = es.detectAndWriteRunes(part); err != nil {
if err = es.detectAndWriteRunes(part); err != nil {
return nil, err
}
}
@@ -94,34 +83,6 @@ func escapeStream(locale translation.Locale, in io.Reader, out io.Writer, opts .
}
}
// trackHtmlTag receives tag parts, a tag might be split into multiple parts
func (e *escapeStreamer) trackHtmlTag(part []byte) {
const maxHeadLen = 100 // only read the first N bytes of the tag for detection purpose
if part[0] == '<' {
// start a new tag
e.tagPartial = e.tagPartial[:0]
}
if len(e.tagPartial) >= maxHeadLen {
return
}
e.tagPartial = append(e.tagPartial, part[:min(len(part), maxHeadLen-len(e.tagPartial))]...)
isTag := func(prefix string) bool {
if len(e.tagPartial) < len(prefix)+1 {
return false
}
if !util.AsciiEqualFold(e.tagPartial[:len(prefix)], []byte(prefix)) {
return false
}
return strings.IndexByte(" \t\n\r\f>", e.tagPartial[len(prefix)]) != -1
}
if isTag("<math") {
e.inTagMath = true
} else if isTag("</math") {
e.inTagMath = false
}
}
func (e *escapeStreamer) trimAndWriteBom(part []byte) ([]byte, error) {
remaining, ok := bytes.CutPrefix(part, globalVars().utf8Bom)
if ok {
-24
View File
@@ -141,12 +141,6 @@ then resh (ר), and finally heh (ה) (which should appear leftmost).`,
result: `O<span class="ambiguous-code-point" data-tooltip-content="repo.ambiguous_character:𝐾 [U+1D43E],K [U+004B]"><span class="char">𝐾</span></span>`,
status: EscapeStatus{Escaped: true, HasAmbiguous: true},
},
{
name: "ambiguous in math",
text: "<math><mo>−</mo><mi>b</mi></math> −",
result: `<math><mo>−</mo><mi>b</mi></math> <span class="ambiguous-code-point" data-tooltip-content="repo.ambiguous_character:− [U+2212],- [U+002D]"><span class="char">−</span></span>`,
status: EscapeStatus{Escaped: true, HasAmbiguous: true},
},
}
func TestEscapeControlReader(t *testing.T) {
@@ -162,24 +156,6 @@ func TestEscapeControlReader(t *testing.T) {
}
}
func TestTrackHtmlTag(t *testing.T) {
e := &escapeStreamer{}
for _, tt := range []struct {
parts []string
inMath bool
}{
{[]string{"<ma", `TH display="block">`}, true},
{[]string{"<mo>"}, true},
{[]string{"</MA", "th>"}, false},
{[]string{"<mathx>"}, false},
} {
for _, part := range tt.parts {
e.trackHtmlTag([]byte(part))
}
assert.Equal(t, tt.inMath, e.inTagMath, "%v", tt.parts)
}
}
func TestSettingAmbiguousUnicodeDetection(t *testing.T) {
defer test.MockVariableValue(&setting.UI.AmbiguousUnicodeDetection, true)()
_, out := EscapeControlHTML("a test", &translation.MockLocale{})
+5 -10
View File
@@ -49,8 +49,8 @@ type Command struct {
cmd *process.Cmd
cmdCtx context.Context
cmdCtxCancel process.CancelCauseFunc
cmdFinished func()
cmdCancel process.CancelCauseFunc
cmdFinished process.FinishedFunc
cmdStartTime time.Time
pipelineFunc func(Context) error
@@ -428,24 +428,19 @@ func (c *Command) Start(ctx context.Context) (retErr error) {
if c.callerInfo == "" {
c.WithParentCallerInfo()
}
// 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)
log.Debug("git.Command: %s", desc)
_, span := gtprof.GetTracer().Start(ctx, gtprof.TraceSpanGitRun)
defer span.End()
span.SetAttributeString(gtprof.TraceAttrFuncCaller, c.callerInfo)
span.SetAttributeString(gtprof.TraceAttrGitCommand, cmdLogString)
var cmdCtxFinished func()
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 {
c.cmdCtx, c.cmdCtxCancel, cmdCtxFinished = process.GetManager().AddContextTimeout(ctx, c.cmdTimeout, desc)
}
c.cmdFinished = func() {
cmdCtxFinished()
span.End()
c.cmdCtx, c.cmdCancel, c.cmdFinished = process.GetManager().AddContextTimeout(ctx, c.cmdTimeout, desc)
}
c.cmdStartTime = time.Now()
+1 -1
View File
@@ -27,6 +27,6 @@ func (c *cmdContext) CancelPipeline(err error) error {
// * context canceled by pipeline caller with/without error (normal cancellation)
// * context canceled by parent context (still context.Canceled error)
// * other causes
c.cmd.cmdCtxCancel(pipelineError{err})
c.cmd.cmdCancel(pipelineError{err})
return err
}
+3 -4
View File
@@ -20,7 +20,6 @@ import (
"github.com/go-git/go-git/v5/plumbing"
"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/dotgit"
)
const isGogit = true
@@ -32,8 +31,8 @@ type Repository struct {
gogitStorage *reindexingStorage
}
// reindexingStorage reloads the pack index when git added or removed packs after go-git loaded it
// https://github.com/go-git/go-git/issues/2439 https://github.com/go-git/go-git/issues/1623
// reindexingStorage picks up packs that git wrote after go-git loaded its index
// https://github.com/go-git/go-git/issues/2439
type reindexingStorage struct {
*filesystem.Storage
packs []plumbing.Hash
@@ -41,7 +40,7 @@ type reindexingStorage struct {
func (s *reindexingStorage) EncodedObject(t plumbing.ObjectType, h plumbing.Hash) (plumbing.EncodedObject, error) {
obj, err := s.Storage.EncodedObject(t, h)
if !errors.Is(err, plumbing.ErrObjectNotFound) && !errors.Is(err, dotgit.ErrPackfileNotFound) {
if !errors.Is(err, plumbing.ErrObjectNotFound) {
return obj, err
}
packs, _ := s.ObjectPacks()
-22
View File
@@ -7,8 +7,6 @@ import (
"path/filepath"
"testing"
"gitea.dev/modules/git/gitcmd"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -41,26 +39,6 @@ func TestRepository_GetBranches(t *testing.T) {
assert.ElementsMatch(t, []string{}, branches)
}
func TestGetBranchNamesAfterRepack(t *testing.T) {
repoDir := t.TempDir()
require.NoError(t, gitcmd.NewCommand("init", "--bare").AddDynamicArguments(repoDir).Run(t.Context()))
for _, from := range []string{"", "from refs/heads/main^0\n"} {
stdin := "commit refs/heads/main\ncommitter a <a@a> 0 +0000\ndata 0\n" + from
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"))
require.NoError(t, gitcmd.NewCommand("repack", "-a", "-d").WithDir(repoDir).Run(t.Context()))
branches, _, err := repo.GetBranchNames(t.Context(), 0, 0)
require.NoError(t, err)
assert.Equal(t, []string{"main"}, branches)
}
func BenchmarkRepository_GetBranches(b *testing.B) {
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
bareRepo1, err := OpenRepositoryLocal(b.Context(), bareRepo1Path)
-2
View File
@@ -122,8 +122,6 @@ func (t *Tracer) Start(ctx context.Context, spanName string) (context.Context, *
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
for internalSpanIdx, tsp := range starters {
var internalSpan traceSpanInternal
+4 -7
View File
@@ -6,17 +6,14 @@ package gtprof
// Some interesting names could be found in https://github.com/open-telemetry/opentelemetry-go/tree/main/semconv
const (
TraceSpanContext = "context"
TraceSpanHTTP = "http"
TraceSpanGitRun = "git-run"
TraceSpanDatabase = "database"
)
const (
TraceAttrGeneralName = "general.name"
TraceAttrGeneralDesc = "general.desc"
TraceAttrFuncCaller = "func.caller"
TraceAttrDbSQL = "db.sql"
TraceAttrGitCommand = "git.command"
TraceAttrHTTPRoute = "http.route"
TraceAttrFuncCaller = "func.caller"
TraceAttrDbSQL = "db.sql"
TraceAttrGitCommand = "git.command"
TraceAttrHTTPRoute = "http.route"
)
-4
View File
@@ -45,10 +45,6 @@ func MarshalKeepOptionalEmpty(v any) ([]byte, error) {
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) {
return jsonv2.Marshal(v, j.marshalOptions)
}
+2 -2
View File
@@ -349,8 +349,8 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod
// TextNode emoji will be converted to `<span class="emoji">`, then the next iteration will visit the "span"
// if we don't stop it, it will go into the TextNode again and create an infinite recursion
return node.NextSibling
} else if node.Data == "code" || node.Data == "pre" || node.Data == "math" {
return node.NextSibling // ignore code, pre and math nodes
} else if node.Data == "code" || node.Data == "pre" {
return node.NextSibling // ignore code and pre nodes
} else if node.Data == "img" {
return visitNodeImg(ctx, node)
} else if node.Data == "video" {
-3
View File
@@ -543,9 +543,6 @@ func TestPostProcess(t *testing.T) {
`Some text with <span class="emoji" data-alias="smile">😄</span> in the middle`)
test("http://localhost:3000/person/repo/issues/4#issuecomment-1234",
`<a href="http://localhost:3000/person/repo/issues/4#issuecomment-1234" class="ref-issue">person/repo#4 (comment)</a>`)
test(
"<math><mtext>:gitea: go-gitea/gitea#12345</mtext></math>",
"<math><mtext>:gitea: go-gitea/gitea#12345</mtext></math>")
// special tags, GitHub's behavior, and for unclosed tags, output as text content as much as possible
test("<script>a", `&lt;script&gt;a`)
+51 -23
View File
@@ -121,7 +121,6 @@ type PackageMetadataVersion struct {
Engines map[string]string `json:"engines,omitempty"`
CPU []string `json:"cpu,omitempty"`
OS []string `json:"os,omitempty"`
Libc []string `json:"libc,omitempty"`
Directories map[string]string `json:"directories,omitempty"`
Funding any `json:"funding,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
type PackageDistribution struct {
Integrity string `json:"integrity"`
Shasum string `json:"shasum"`
Tarball string `json:"tarball"`
Integrity string `json:"integrity"`
Shasum string `json:"shasum"`
Tarball string `json:"tarball"`
FileCount int `json:"fileCount,omitempty"`
UnpackedSize int `json:"unpackedSize,omitempty"`
NpmSignature string `json:"npm-signature,omitempty"`
}
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,
// 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
// 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,
// otherwise it is a "publish".
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 {
return nil, nil, err
}
@@ -278,6 +280,16 @@ func ParseUpload(r io.Reader) (*Package, *PackageDeprecation, error) {
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.
func parseUploadPackage(upload *packageUpload) (*Package, error) {
for _, meta := range upload.Versions {
@@ -331,7 +343,6 @@ func parseUploadPackage(upload *packageUpload) (*Package, error) {
Engines: meta.Engines,
CPU: meta.CPU,
OS: meta.OS,
Libc: meta.Libc,
Directories: meta.Directories,
Funding: meta.Funding,
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))
attachment := upload.Attachments[meta.Name+"-"+meta.Version+".tgz"] // not the sigstore bundle of `npm publish --provenance`
if attachment == nil && len(upload.Attachments) == 1 {
attachment := func() *PackageAttachment {
for _, a := range upload.Attachments {
attachment = a
return a
}
}
return nil
}()
if attachment == nil || len(attachment.Data) == 0 {
return nil, ErrInvalidAttachment
}
@@ -382,6 +393,8 @@ func parseUploadPackage(upload *packageUpload) (*Package, error) {
return nil, ErrInvalidIntegrity
}
// Derive _hasShrinkwrap and hasInstallScript from the tarball; the
// packument can lie about either.
p.Metadata.HasShrinkwrap, p.Metadata.HasInstallScript = inspectTarball(data)
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.
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) {
gr, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
@@ -405,16 +422,11 @@ func inspectTarball(data []byte) (hasShrinkwrap, hasInstallScript bool) {
}
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))
for {
hdr, err := tr.Next()
if err != nil {
break
return hasShrinkwrap, hasInstallScript
}
// npm pack puts files under a single root directory (usually "package/").
name := strings.TrimPrefix(hdr.Name, "./")
@@ -424,14 +436,30 @@ func inspectTarball(data []byte) (hasShrinkwrap, hasInstallScript bool) {
switch {
case strings.HasSuffix(name, "/npm-shrinkwrap.json"):
hasShrinkwrap = true
case strings.HasSuffix(name, ".gyp"):
hasGypFile = true
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 {
+54 -24
View File
@@ -41,20 +41,21 @@ func TestParsePackage(t *testing.T) {
integrity := "sha512-" + base64Sha512(dataBytes)
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.Error(t, err)
})
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.ErrorIs(t, err, ErrInvalidPackage)
})
t.Run("InvalidPackageName", func(t *testing.T) {
test := func(t *testing.T, name string) {
p, err := parseUploadPackage(&packageUpload{
b, _ := json.Marshal(packageUpload{
PackageMetadata: PackageMetadata{
ID: name,
Name: name,
@@ -65,6 +66,8 @@ func TestParsePackage(t *testing.T) {
},
},
})
p, err := ParsePackage(bytes.NewReader(b))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidPackageName)
}
@@ -91,7 +94,7 @@ func TestParsePackage(t *testing.T) {
t.Run("ValidPackageName", func(t *testing.T) {
test := func(t *testing.T, name string) {
p, err := parseUploadPackage(&packageUpload{
b, _ := json.Marshal(packageUpload{
PackageMetadata: PackageMetadata{
ID: name,
Name: name,
@@ -102,6 +105,8 @@ func TestParsePackage(t *testing.T) {
},
},
})
p, err := ParsePackage(bytes.NewReader(b))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidPackageVersion)
}
@@ -120,7 +125,7 @@ func TestParsePackage(t *testing.T) {
t.Run("InvalidPackageVersion", func(t *testing.T) {
version := "first-version"
p, err := parseUploadPackage(&packageUpload{
b, _ := json.Marshal(packageUpload{
PackageMetadata: PackageMetadata{
ID: packageFullName,
Name: packageFullName,
@@ -132,6 +137,8 @@ func TestParsePackage(t *testing.T) {
},
},
})
p, err := ParsePackage(bytes.NewReader(b))
assert.Nil(t, p)
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.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.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.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.ErrorIs(t, err, ErrInvalidIntegrity)
})
@@ -274,13 +281,10 @@ func TestParsePackage(t *testing.T) {
filename: {
Data: data,
},
packageFullName + "-" + packageVersion + ".sigstore": {
Data: "{}",
},
},
})
p, _, err := ParseUpload(bytes.NewReader(b))
p, err := ParsePackage(bytes.NewReader(b))
assert.NotNil(t, p)
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.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.Equal(t, "https://gitea.io/gitea/test.git", p.Metadata.Repository.URL)
// a string bin is named after the package
@@ -422,15 +426,6 @@ func TestInspectTarball(t *testing.T) {
// npm pack sometimes emits "./package/..." entries.
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 {
t.Run(c.name, func(t *testing.T) {
@@ -465,6 +460,41 @@ func TestParseUpload(t *testing.T) {
require.NotNil(t, dep)
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 {
-1
View File
@@ -29,7 +29,6 @@ type Metadata struct {
Engines map[string]string `json:"engines,omitempty"`
CPU []string `json:"cpu,omitempty"`
OS []string `json:"os,omitempty"`
Libc []string `json:"libc,omitempty"`
Directories map[string]string `json:"directories,omitempty"`
Funding any `json:"funding,omitempty"`
AcceptDependencies map[string]string `json:"accept_dependencies,omitempty"`
+1 -2
View File
@@ -36,7 +36,6 @@ import "strings"
const (
tildePrefix = '~'
commentPrefix = '#'
needsEscape = " \t\n|&;()<>${}[]*?!\"'`\\"
needsSingleQuote = "!\n"
)
@@ -75,7 +74,7 @@ func ShellEscape(toEscape string) 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
}
-4
View File
@@ -75,10 +75,6 @@ func TestShellEscape(t *testing.T) {
"Double quote and escape `...",
"~/gitea`",
"~/\"gitea\\`\"",
}, {
"Double quote leading #",
"#123",
`"#123"`,
}, {
"Double quotes can handle a number of things without having to escape them but not everything ...",
"~/<gitea> ${gitea} `gitea` [gitea] (gitea) \"gitea\" \\gitea\\ 'gitea'",
+1 -1
View File
@@ -121,7 +121,7 @@ func asciiLower(b byte) byte {
// AsciiEqualFold is from Golang https://cs.opensource.google/go/go/+/refs/tags/go1.24.4:src/net/http/internal/ascii/print.go
// ASCII only. In most cases for protocols, we should only use this but not [strings.EqualFold]
func AsciiEqualFold[T string | []byte](s, t T) bool {
func AsciiEqualFold(s, t string) bool {
if len(s) != len(t) {
return false
}
+2 -14
View File
@@ -5,7 +5,6 @@ package web
import (
"net/http"
"net/url"
"regexp"
"slices"
"strings"
@@ -20,17 +19,13 @@ type RouterPathGroup struct {
r *Router
pathParam string
matchers []*routerPathMatcher
unescape bool
}
func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
chiCtx := chi.RouteContext(req.Context())
path := chiCtx.URLParam(g.pathParam)
if g.unescape {
path, _ = url.PathUnescape(path)
}
for _, m := range g.matchers {
if m.matchPath(chiCtx, path, g.unescape) {
if m.matchPath(chiCtx, path) {
chiCtx.RoutePatterns = append(chiCtx.RoutePatterns, m.pattern)
executeMiddlewaresHandler(resp, req, m.middlewares, m.handlerFunc)
return
@@ -58,10 +53,6 @@ func (g *RouterPathGroup) MatchPattern(methods string, pattern *RouterPathGroupP
g.matchers = append(g.matchers, newRouterPathMatcher(methods, pattern, h...))
}
func (g *RouterPathGroup) UseUnescapedPath() {
g.unescape = true
}
type routerPathParam struct {
name string
pathSepEnd bool
@@ -77,7 +68,7 @@ type routerPathMatcher struct {
handlerFunc http.HandlerFunc
}
func (p *routerPathMatcher) matchPath(chiCtx *chi.Context, path string, unescaped bool) bool {
func (p *routerPathMatcher) matchPath(chiCtx *chi.Context, path string) bool {
if !p.methods.Contains(chiCtx.RouteMethod) {
return false
}
@@ -111,9 +102,6 @@ func (p *routerPathMatcher) matchPath(chiCtx *chi.Context, path string, unescape
if p.params[i].pathSepEnd {
val = strings.TrimSuffix(val, "/")
}
if unescaped {
val = url.PathEscape(val)
}
chiCtx.URLParams.Add(p.params[i].name, val)
}
return true
+1 -9
View File
@@ -7,7 +7,6 @@ import (
"bytes"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
@@ -98,16 +97,12 @@ func (r *testRecorder) test(t *testing.T, rt *Router, methodPath string, expecte
}
func TestPathProcessor(t *testing.T) {
unescape := false
testProcess := func(pattern, uri string, expectedPathParams map[string]string) {
chiCtx := chi.NewRouteContext()
chiCtx.RouteMethod = "GET"
p := newRouterPathMatcher("GET", patternRegexp(pattern), http.NotFound)
shouldProcess := expectedPathParams != nil
if unescape {
uri, _ = url.PathUnescape(uri)
}
assert.Equal(t, shouldProcess, p.matchPath(chiCtx, uri, unescape), "use pattern %s to process uri %s", pattern, uri)
assert.Equal(t, shouldProcess, p.matchPath(chiCtx, uri), "use pattern %s to process uri %s", pattern, uri)
assert.Equal(t, expectedPathParams, chiURLParamsToMap(chiCtx), "use pattern %s to process uri %s", pattern, uri)
}
@@ -124,9 +119,6 @@ func TestPathProcessor(t *testing.T) {
testProcess("/<p1:*>/part/<p2>", "/part/c", map[string]string{"p1": "", "p2": "c"})
testProcess("/<p1:*>/part/<p2>", "/a/other-part/c", nil)
testProcess("/<p1:*>-part/<p2>", "/a-other-part/c", map[string]string{"p1": "a-other", "p2": "c"})
unescape = true
testProcess("/<p1:@/x>", "/%40%2fx", map[string]string{"p1": "@%2Fx"})
}
func TestRouter(t *testing.T) {
-1
View File
@@ -433,7 +433,6 @@
"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_with_scopes": "With scopes: %s",
"auth.authorize_application_new_scopes": "New scopes: %s",
"auth.authorize_title": "Authorize \"%s\" to access your account?",
"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.",
-13
View File
@@ -277,10 +277,7 @@
"install.domain_helper": "服务器的域名或主机地址。",
"install.ssh_port": "SSH 服务端口",
"install.ssh_port_helper": "SSH 服务器的端口号,为空则禁用它。",
"install.http_port": "HTTP 服务端口",
"install.http_port_helper": "Gitea Web 服务器将侦听的端口号。",
"install.app_url": "Gitea 网站 URL",
"install.app_url_helper": "Gitea Web 应用程序用于 HTTP(S) 访问、克隆 URL 及电子邮件通知的公共 URL。",
"install.optional_title": "可选设置",
"install.email_title": "电子邮箱设置",
"install.smtp_addr": "SMTP 主机地址",
@@ -293,11 +290,8 @@
"install.register_confirm": "需要邮件确认注册",
"install.mail_notify": "启用邮件通知提醒",
"install.server_service_title": "服务器和第三方服务设置",
"install.disable_registration": "只有管理员可以创建用户帐户(禁止自助注册)",
"install.enable_captcha": "启用注册验证码",
"install.enable_captcha_popup": "要求在用户注册时输入预验证码",
"install.require_sign_in_view": "需要登录才能查看页面(推荐用于私有实例)",
"install.require_sign_in_view_popup": "仅允许已登录用户访问页面。访客只能看到注册和登录页。",
"install.admin_setting_desc": "创建管理员帐户是可选的。第一个注册用户将自动成为管理员。",
"install.admin_title": "管理员帐号设置",
"install.admin_name": "管理员用户名",
@@ -320,7 +314,6 @@
"install.default_allow_create_organization_popup": "默认情况下,允许新用户帐户创建组织。",
"install.no_reply_address": "隐藏邮件域",
"install.no_reply_address_helper": "具有隐藏邮箱地址的用户的域名。例如,如果隐藏邮箱域名设置为「noreply.example.org」,那么用户名「joe」在 Git 中将显示为「joe@noreply.example.org」。",
"install.enable_update_checker": "启用更新检查",
"install.env_config_keys": "环境配置",
"install.env_config_keys_prompt": "以下环境变量也将应用于您的配置文件:",
"install.config_write_file_prompt": "这些配置选项将写入以下位置: %s",
@@ -1098,7 +1091,6 @@
"repo.migrate.github_token_desc": "您可以在此处输入一个或多个令牌(以逗号分隔),以绕过 GitHub API 速率限制来加快迁移速度。警告:滥用此功能可能会违反服务提供商的政策并导致帐户被封禁。",
"repo.migrate.clone_local_path": "或服务器本地路径",
"repo.migrate.permission_denied": "您没有获得导入本地仓库的权限。",
"repo.migrate.permission_denied_blocked": "您不能从不允许的主机导入。请询问管理员以检查 [migrations] 部分中的ALLOWED_HOST_LIST/BLOCKED_HOST_LIST 设置。",
"repo.migrate.invalid_local_path": "本地路径无效。它不存在或不是一个目录。",
"repo.migrate.invalid_lfs_endpoint": "LFS 网址无效。",
"repo.migrate.failed": "迁移失败:%v",
@@ -3904,11 +3896,6 @@
"actions.workflow.has_no_workflow_dispatch": "工作流「%s」没有 workflow_dispatch 事件触发器。",
"actions.need_approval_desc": "该工作流由派生仓库的合并请求所触发,需要批准方可运行。",
"actions.approve_all_success": "所有工作流运行已成功批准。",
"actions.job_queue.title": "任务队列",
"actions.job_queue.runs_on": "运行于",
"actions.job_queue.waiting_or_started": "等待 / 已开始",
"actions.job_queue.no_jobs": "没有正在运行或等待处理的任务。",
"actions.job_queue.filter_owner_no_select": "所有所有者",
"actions.job_queue.filter_repo_no_select": "所有仓库",
"actions.variables": "变量",
"actions.variables.management": "变量管理",
+30 -20
View File
@@ -405,27 +405,37 @@ func CommonRoutes() *web.Router {
}, reqPackageAccess(perm.AccessModeRead))
})
r.Group("/npm", func() {
r.Get("/-/v1/search", npm.PackageSearch)
r.Get("/-/ping", npm.Ping)
r.Get("/-/whoami", npm.Whoami)
r.PathGroup("/*", func(g *web.RouterPathGroup) {
// HINT: NPM-ROUTE-PATH-PATTERN: search this keyword to see more details
packageId := `/<id:(@` + npm_module.RegexpNamePart + `/)?` + npm_module.RegexpNamePart + ">"
g.UseUnescapedPath()
g.MatchPath("DELETE", packageId+"/-/<version>/<filename>/-rev/<revision>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
g.MatchPath("DELETE", packageId+"/-/<filename>/-rev/<revision>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
g.MatchPath("GET", packageId+"/-/<version>/<filename>", npm.DownloadPackageFileByName) // former tarball URL, still in lockfiles
g.MatchPath("GET", packageId+"/-/<filename>", npm.DownloadPackageFileByName)
g.MatchPath("DELETE", packageId+"/-rev/<revision>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackage)
g.MatchPath("PUT", packageId+"/-rev/<revision>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePreview)
g.MatchPath("GET", packageId+"/<version>", npm.PackageVersionMetadata)
g.MatchPath("GET", packageId, npm.PackageMetadata)
g.MatchPath("PUT", packageId, reqPackageAccess(perm.AccessModeWrite), npm.UploadPackage)
// HINT: NPM-ROUTE-PATH-PATTERN: search this keyword to see more details
scopeRegexp := `^@` + npm_module.RegexpNamePart + `$`
idRegexp := `^(@` + npm_module.RegexpNamePart + `%2[fF])?` + npm_module.RegexpNamePart + `$`
addPackageHandlers := func() {
r.Get("", npm.PackageMetadata)
r.Put("", reqPackageAccess(perm.AccessModeWrite), npm.UploadPackage)
r.Get("/{version}", npm.PackageVersionMetadata)
r.Group("/-/{version}/{filename}", func() {
r.Get("", npm.DownloadPackageFile)
r.Delete("/-rev/{revision}", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
})
r.Get("/-/{filename}", npm.DownloadPackageFileByName)
r.Group("/-rev/{revision}", func() {
r.Delete("", npm.DeletePackage)
r.Put("", npm.DeletePreview)
}, reqPackageAccess(perm.AccessModeWrite))
}
r.Group("/{scope:"+scopeRegexp+"}/{id:"+idRegexp+"}", addPackageHandlers)
r.Group("/{id:"+idRegexp+"}", addPackageHandlers)
packageDistTags := "/-/package" + packageId + "/dist-tags"
g.MatchPath("GET", packageDistTags, npm.ListPackageTags)
g.MatchPath("PUT", packageDistTags+"/<tag>", reqPackageAccess(perm.AccessModeWrite), npm.AddPackageTag)
g.MatchPath("DELETE", packageDistTags+"/<tag>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageTag)
addPackageDistTagsHandlers := func() {
r.Get("", npm.ListPackageTags)
r.Group("/{tag}", func() {
r.Put("", npm.AddPackageTag)
r.Delete("", npm.DeletePackageTag)
}, reqPackageAccess(perm.AccessModeWrite))
}
r.Group("/-/package/{scope:"+scopeRegexp+"}/{id:"+idRegexp+"}/dist-tags", addPackageDistTagsHandlers)
r.Group("/-/package/{id:"+idRegexp+"}/dist-tags", addPackageDistTagsHandlers)
r.Group("/-/v1/search", func() {
r.Get("", npm.PackageSearch)
})
}, reqPackageAccess(perm.AccessModeRead))
r.Group("/pub", func() {
+4 -17
View File
@@ -8,7 +8,7 @@ import (
"encoding/base64"
"encoding/hex"
"fmt"
"slices"
"net/url"
"sort"
"time"
@@ -25,7 +25,6 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac
distTags := make(map[string]string)
times := make(map[string]time.Time)
firstPublished, lastPublished := pds[0].Version.CreatedUnix, pds[0].Version.CreatedUnix
var latest *packages_model.PackageDescriptor
for _, pd := range pds {
semVer := pd.SemVer.String()
versions[semVer] = createPackageMetadataVersion(registryURL, pd)
@@ -36,9 +35,6 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac
for _, pvp := range pd.VersionProperties {
if pvp.Name == npm_module.TagProperty {
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["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]
for _, pd := range slices.Backward(pds) {
if pd.SemVer.Prerelease() == "" {
latest = pd
break
}
}
distTags["latest"] = latest.Version.Version
}
latest := pds[len(pds)-1]
metadata := packages_model.DescriptorMetadata[*npm_module.Metadata](latest)
@@ -99,13 +86,13 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package
PeerDependencies: metadata.PeerDependencies,
PeerDependenciesMeta: metadata.PeerDependenciesMeta,
OptionalDependencies: metadata.OptionalDependencies,
Readme: metadata.Readme,
Bin: metadata.Bin,
HasInstallScript: metadata.HasInstallScript,
HasShrinkwrap: metadata.HasShrinkwrap,
Engines: metadata.Engines,
CPU: metadata.CPU,
OS: metadata.OS,
Libc: metadata.Libc,
Directories: metadata.Directories,
Funding: metadata.Funding,
AcceptDependencies: metadata.AcceptDependencies,
@@ -113,7 +100,7 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package
Dist: npm_module.PackageDistribution{
Shasum: pd.Files[0].Blob.HashSHA1,
Integrity: "sha512-" + base64.StdEncoding.EncodeToString(hashBytes),
Tarball: fmt.Sprintf("%s/%s/-/%s", registryURL, pd.Package.Name, pd.Files[0].File.LowerName), // npmjs shape, which npm parses for allowScripts and yarn keeps registry-relative
Tarball: fmt.Sprintf("%s/%s/-/%s/%s", registryURL, url.PathEscape(pd.Package.Name), url.PathEscape(pd.Version.Version), url.PathEscape(pd.Files[0].File.LowerName)),
},
}
}
+6 -11
View File
@@ -25,7 +25,7 @@ func TestCreatePackageMetadataResponse(t *testing.T) {
Owner: &user_model.User{Name: "alice"},
Version: &packages_model.PackageVersion{Version: v, CreatedUnix: timeutil.TimeStamp(publishedUnix)},
SemVer: version.Must(version.NewVersion(v)),
Metadata: &npm_module.Metadata{Readme: v, Keywords: []string{"gitea"}, Repository: repo},
Metadata: &npm_module.Metadata{Keywords: []string{"gitea"}, Repository: repo},
Files: []*packages_model.PackageFileDescriptor{{
File: &packages_model.PackageFile{LowerName: "test-" + v + ".tgz"},
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{
descriptor("1.1.0", 1000, npm_module.Repository{}),
descriptor("2.0.0-rc.1", 1500, repository),
descriptor("1.0.0", 2000, repository),
})
assert.Equal(t, map[string]time.Time{
"1.0.0": time.Unix(2000, 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(),
"modified": time.Unix(2000, 0).UTC(),
"1.0.0": time.Unix(2000, 0).UTC(),
"1.1.0": time.Unix(1000, 0).UTC(),
"created": time.Unix(1000, 0).UTC(),
"modified": time.Unix(2000, 0).UTC(),
}, 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, []string{"gitea"}, result.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,
"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,
)
assert.Equal(t, repository, result.Versions["1.0.0"].Repository)
+104 -90
View File
@@ -6,7 +6,6 @@ package npm
import (
"bytes"
std_ctx "context"
"crypto/sha256"
"errors"
"fmt"
"io"
@@ -45,53 +44,49 @@ func apiError(ctx *context.Context, status int, obj any) {
// packageNameFromParams gets the package name from the url parameters
func packageNameFromParams(ctx *context.Context) string {
// HINT: NPM-ROUTE-PATH-PATTERN: real examples: these cases all should work:
// Real examples: these 2 both should work:
// * "https://registry.npmjs.org/@angular/core"
// * "https://registry.npmjs.org/@angular%2Fcore"
// * "https://registry.npmjs.org/%40angular%2Fcore"
//
return ctx.PathParam("id") // id is the full package name, e.g.: "@angular/core" or "lodash"
// HINT: NPM-ROUTE-PATH-PATTERN: The cases for the path parameters:
// * ".../TheName/...": id="TheName"
// * ".../@TheScope/TheName/...": scope="@TheScope", id="TheName"
// * ".../@TheScope%2FTheName/...": id="@TheScope/TheName"
scope := ctx.PathParam("scope")
fullOrSub := ctx.PathParam("id") // may be a full name or a subpath of the full package name
if scope != "" {
// now id is the subpath of the full package name, e.g. "core" in "@angular/core"
return fmt.Sprintf("%s/%s", scope, fullOrSub)
}
return fullOrSub // id is the full package name, e.g.: "@angular/core" or "lodash"
}
func buildNpmRegistryURL(ctx std_ctx.Context, owner *user_model.User) string {
return httplib.GuessCurrentAppURL(ctx) + "api/packages/" + url.PathEscape(owner.Name) + "/npm"
}
func packageMetadata(ctx *context.Context) *npm_module.PackageMetadata {
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageNameFromParams(ctx))
// PackageMetadata returns the metadata for a single package
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 {
apiError(ctx, http.StatusInternalServerError, err)
return nil
return
}
if len(pvs) == 0 {
apiError(ctx, http.StatusNotFound, err)
return nil
return
}
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 {
apiError(ctx, http.StatusInternalServerError, err)
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
@@ -115,11 +110,7 @@ func PackageVersionMetadata(ctx *context.Context) {
return
}
if len(pvs) == 0 {
if versionOrTag != "latest" {
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"]])
}
apiError(ctx, http.StatusNotFound, "version not found: "+versionOrTag)
return
}
@@ -129,40 +120,25 @@ func PackageVersionMetadata(ctx *context.Context) {
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 {
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: 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]
}
// DownloadPackageFile serves the content of a package
func DownloadPackageFile(ctx *context.Context) {
packageName := packageNameFromParams(ctx)
packageVersion := ctx.PathParam("version")
filename := ctx.PathParam("filename")
// DownloadPackageFileByName finds the version and serves the contents of a package
func DownloadPackageFileByName(ctx *context.Context) {
pv := packageVersionByFilename(ctx)
if pv == nil {
return
}
s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion(
s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion(
ctx,
pv,
&packages_service.PackageInfo{
Owner: ctx.Package.Owner,
PackageType: packages_model.TypeNpm,
Name: packageName,
Version: packageVersion,
},
&packages_service.PackageFileInfo{
Filename: ctx.PathParam("filename"),
Filename: filename,
},
ctx.Req.Method,
)
@@ -174,14 +150,54 @@ func DownloadPackageFileByName(ctx *context.Context) {
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
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(http.MaxBytesReader(ctx.Resp, ctx.Req.Body, 256*1024*1024))
npmPackage, deprecation, err := npm_module.ParseUpload(ctx.Req.Body)
if err != nil {
if _, ok := errors.AsType[*http.MaxBytesError](err); ok {
apiError(ctx, http.StatusRequestEntityTooLarge, err)
} else if errors.Is(err, util.ErrInvalidArgument) {
if errors.Is(err, util.ErrInvalidArgument) {
apiError(ctx, http.StatusBadRequest, err)
} else {
apiError(ctx, http.StatusInternalServerError, err)
@@ -334,14 +350,26 @@ func deprecatePackage(ctx *context.Context, dep *npm_module.PackageDeprecation)
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) {
pv := packageVersionByFilename(ctx)
if pv == nil {
return
}
packageName := packageNameFromParams(ctx)
packageVersion := ctx.PathParam("version")
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)
return
}
@@ -376,7 +404,9 @@ func DeletePackage(ctx *context.Context) {
// ListPackageTags returns all tags for a package
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 {
apiError(ctx, http.StatusInternalServerError, err)
return
@@ -394,11 +424,7 @@ func ListPackageTags(ctx *context.Context) {
}
}
if _, ok := tags["latest"]; ok {
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)
}
ctx.JSON(http.StatusOK, tags)
}
// AddPackageTag adds a tag to the package
@@ -508,18 +534,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) {
pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
+2 -2
View File
@@ -400,7 +400,7 @@ func SearchUsers(ctx *context.APIContext) {
// parameters:
// - name: source_id
// in: query
// description: ID of the user's login source to search for, 0 means the local users
// description: ID of the user's login source to search for
// type: integer
// format: int64
// - name: login_name
@@ -483,7 +483,7 @@ func SearchUsers(ctx *context.APIContext) {
Actor: ctx.Doer,
Types: []user_model.UserType{user_model.UserTypeIndividual},
LoginName: ctx.FormTrim("login_name"),
SourceID: ctx.FormOptionalInt64("source_id"),
SourceID: ctx.FormInt64("source_id"),
Keyword: ctx.FormTrim("q"),
Visible: visible,
OrderBy: orderBy,
+3 -3
View File
@@ -162,7 +162,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
// if it's not a pointer, just serve the data directly
if !pointer.IsValid() {
httplib.ServeUserContentByReader(ctx.Req, ctx.Resp, int64(len(lfsPointerBuf)), bytes.NewReader(lfsPointerBuf), httplib.ServeHeaderOptions{Filename: blob.Name()})
_, _ = ctx.Resp.Write(lfsPointerBuf)
return
}
@@ -171,7 +171,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
// If there isn't one, just serve the data directly
if errors.Is(err, git_model.ErrLFSObjectNotExist) {
httplib.ServeUserContentByReader(ctx.Req, ctx.Resp, int64(len(lfsPointerBuf)), bytes.NewReader(lfsPointerBuf), httplib.ServeHeaderOptions{Filename: blob.Name()})
_, _ = ctx.Resp.Write(lfsPointerBuf)
return
} else if err != nil {
ctx.APIErrorInternal(err)
@@ -198,7 +198,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
return
}
defer lfsDataFile.Close()
httplib.ServeUserContentByFile(ctx.Base.Req, ctx.Base.Resp, lfsDataFile, httplib.ServeHeaderOptions{Filename: blob.Name()})
httplib.ServeUserContentByFile(ctx.Base.Req, ctx.Base.Resp, lfsDataFile, httplib.ServeHeaderOptions{Filename: ctx.Repo.TreePath})
}
func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEntry, lastModified *time.Time) {
+2 -2
View File
@@ -386,8 +386,8 @@ func attachmentBelongsToRepoOrIssue(ctx *context.APIContext, attachment *repo_mo
ctx.APIErrorNotFound("no such attachment in repo")
return false
}
if attachment.IssueID == 0 || attachment.CommentID != 0 {
log.Debug("Requested attachment[%d] is not an issue attachment.", attachment.ID)
if attachment.IssueID == 0 {
log.Debug("Requested attachment[%d] is not in an issue.", attachment.ID)
ctx.APIErrorNotFound("no such attachment in issue")
return false
} else if issue != nil && attachment.IssueID != issue.ID {
+1 -6
View File
@@ -291,11 +291,6 @@ func AddPushMirror(ctx *context.APIContext) {
return
}
if setting.Mirror.DisableNewPush {
ctx.APIError(http.StatusForbidden, "the site administrator has disabled the creation of new push mirrors")
return
}
pushMirror := web.GetForm[*api.CreatePushMirrorOption](ctx)
CreatePushMirror(ctx, pushMirror)
}
@@ -361,7 +356,7 @@ func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirro
address, err := git.ParseRemoteAddr(mirrorOption.RemoteAddress, mirrorOption.RemoteUsername, mirrorOption.RemotePassword)
if err == nil {
err = migrations.IsMigrateURLAllowed(address, ctx.Doer)
err = migrations.IsMigrateURLAllowed(address, ctx.ContextUser)
}
if err != nil {
HandleRemoteAddressError(ctx, err)
-23
View File
@@ -10,36 +10,13 @@ import (
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/test"
"gitea.dev/services/contexttest"
"github.com/stretchr/testify/assert"
)
func TestCreatePushMirrorUsesCallerPermission(t *testing.T) {
defer test.MockVariableValue(&setting.ImportLocalPaths, true)()
ctx, resp := contexttest.MockAPIContext(t, "user2/repo1")
ctx.Doer = &user_model.User{}
ctx.ContextUser = &user_model.User{AllowImportLocal: true}
CreatePushMirror(ctx, &api.CreatePushMirrorOption{RemoteAddress: "local-mirror", Interval: "0"})
assert.Equal(t, http.StatusUnauthorized, resp.Code)
}
func TestAddPushMirrorDisabled(t *testing.T) {
defer test.MockVariableValue(&setting.Mirror.DisableNewPush, true)()
ctx, resp := contexttest.MockAPIContext(t, "user2/repo1")
AddPushMirror(ctx)
assert.Equal(t, http.StatusForbidden, resp.Code)
assert.Contains(t, resp.Body.String(), "the site administrator has disabled the creation of new push mirrors")
}
// TestPushMirrorSync verifies the endpoint attempts every push mirror instead
// of aborting on the first failure, reporting all failed remotes with a 422.
// Each remote name is not a configured git remote, so SyncPushMirror fails fast
+1 -1
View File
@@ -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) {
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 {
+1 -2
View File
@@ -139,8 +139,7 @@ func hookPostReceiveUpdateRepoByOptions(ctx *gitea_context.PrivateContext, opts
// The repo is empty and being initialized by this push, so there is no
// dependent state (webhooks, notifications, visibility fan-out) to reconcile
// yet; setting the flags directly is sufficient in this push-to-create case.
if isPrivate.Has() && repo.IsPrivate != isPrivate.Value() &&
(isPrivate.Value() || !setting.Repository.ForcePrivate || ctx.Doer.IsAdmin) {
if isPrivate.Has() && repo.IsPrivate != isPrivate.Value() {
repo.IsPrivate = isPrivate.Value()
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil {
log.Error("failed to update repo is_private: %v", err)
-32
View File
@@ -48,13 +48,6 @@ const (
// UserSearchDefaultAdminSort is the default sort type for admin view
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
func Users(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.users")
@@ -83,30 +76,6 @@ func Users(ctx *context.Context) {
"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{
Actor: ctx.Doer,
Types: types,
@@ -119,7 +88,6 @@ func Users(ctx *context.Context) {
IsRestricted: optional.ParseBool(statusFilterMap["is_restricted"]),
IsTwoFactorEnabled: optional.ParseBool(statusFilterMap["is_2fa_enabled"]),
IsProhibitLogin: optional.ParseBool(statusFilterMap["is_prohibit_login"]),
SourceID: sourceIDFilter,
OrderBy: db.SearchOrderBy(sortType),
}, tplUsers)
}
+8 -18
View File
@@ -11,7 +11,6 @@ import (
"net/http"
"net/url"
"strconv"
"strings"
audit_model "gitea.dev/models/audit"
"gitea.dev/models/auth"
@@ -21,7 +20,6 @@ import (
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/services/audit"
auth_service "gitea.dev/services/auth"
@@ -323,18 +321,9 @@ func AuthorizeOAuth(ctx *context.Context) {
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
// 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)
if err != nil {
handleServerError(ctx, form.State, form.RedirectURI)
@@ -358,7 +347,6 @@ func AuthorizeOAuth(ctx *context.Context) {
// check if additional scopes
ctx.Data["AdditionalScopes"] = oauth2_provider.GrantAdditionalScopes(form.Scope) != auth.AccessTokenScopeAll
ctx.Data["AddedScopes"] = addedScopes
// show authorize page to grant access
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)
} else if grant.Scope != form.Scope {
if err := auth.UpdateGrantScope(ctx, grant, form.Scope); err != nil {
handleServerError(ctx, form.State, form.RedirectURI)
return
}
handleAuthorizeError(ctx, AuthorizeError{
State: form.State,
ErrorDescription: "a grant exists with different scope",
ErrorCode: ErrorCodeServerError,
}, form.RedirectURI)
return
}
if len(form.Nonce) > 0 {
@@ -586,7 +576,7 @@ func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, server
}
token, err := oauth2_provider.ParseToken(form.RefreshToken, serverKey)
if err != nil || token.Kind != oauth2_provider.KindRefreshToken {
if err != nil {
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
ErrorCode: oauth2_provider.AccessTokenErrorCodeUnauthorizedClient,
ErrorDescription: "unable to parse refresh token",
-28
View File
@@ -13,10 +13,6 @@ import (
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"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"
"github.com/golang-jwt/jwt/v5"
@@ -109,27 +105,3 @@ func TestOAuth2AvatarClientBlocksCloudMetadata(t *testing.T) {
assert.ErrorIs(t, err, policy.ErrDenied,
"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"})
}
+1 -10
View File
@@ -9,9 +9,7 @@ import (
activities_model "gitea.dev/models/activities"
"gitea.dev/models/organization"
"gitea.dev/models/renderhelper"
user_model "gitea.dev/models/user"
"gitea.dev/modules/markup/markdown"
"gitea.dev/modules/setting"
"gitea.dev/services/context"
feed_service "gitea.dev/services/feed"
@@ -30,15 +28,8 @@ func ShowUserFeedAtom(ctx *context.Context) {
// showUserFeed show user activity as RSS / Atom feed
func showUserFeed(ctx *context.Context, formatType string) {
isOrganisation := ctx.ContextUser.IsOrganization()
if !setting.Other.EnableFeed ||
isOrganisation && !organization.HasOrgOrUserVisible(ctx, ctx.ContextUser, ctx.Doer) ||
!isOrganisation && !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
ctx.NotFound(nil)
return
}
includePrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
isOrganisation := ctx.ContextUser.IsOrganization()
if ctx.IsSigned && isOrganisation && !includePrivate {
// When feed is requested by a member of the organization,
// include the private repo's the member has access to.
+2 -10
View File
@@ -569,7 +569,7 @@ func (data *actionRunListData) processActionRuns(ctx *context.Context) bool {
break
}
}
if job.Status.IsWaiting() && !job.IsReusableCaller {
if job.Status.IsWaiting() {
hasOnlineRunner := false
for _, runner := range runners {
if !runner.IsDisabled && runner.CanMatchLabels(job.RunsOn) {
@@ -642,15 +642,7 @@ func (data *actionRunListData) preparePartialRefreshRuns(ctx *context.Context) b
ctx.ServerError("GetRunsByRepoAndID", err)
return false
}
runsMap := make(map[int64]*actions_model.ActionRun, len(runs))
for _, run := range runs {
runsMap[run.ID] = run
}
for _, id := range data.refreshRunIDs {
if run, ok := runsMap[id]; ok {
data.ActionRuns = append(data.ActionRuns, run)
}
}
data.ActionRuns = runs
return true
}
-15
View File
@@ -15,7 +15,6 @@ import (
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
web_context "gitea.dev/services/context"
"gitea.dev/services/contexttest"
"github.com/stretchr/testify/assert"
)
@@ -75,17 +74,3 @@ func newWorkflowBadgeTestContext(t *testing.T) *web_context.Context {
}
return ctx
}
func TestActionRunListData(t *testing.T) {
unittest.PrepareTestEnv(t)
t.Run("preparePartialRefreshRuns", func(t *testing.T) {
ctx, _ := contexttest.MockContext(t, "user5/repo4/actions")
contexttest.LoadRepo(t, ctx, 4)
d := &actionRunListData{refreshRunIDs: []int64{791, 792}}
d.preparePartialRefreshRuns(ctx)
assert.Equal(t, []int64{791, 792}, []int64{d.ActionRuns[0].ID, d.ActionRuns[1].ID})
d = &actionRunListData{refreshRunIDs: []int64{792, 791}}
d.preparePartialRefreshRuns(ctx)
assert.Equal(t, []int64{792, 791}, []int64{d.ActionRuns[0].ID, d.ActionRuns[1].ID})
})
}
+1 -1
View File
@@ -773,7 +773,7 @@ func describePendingJobDetail(ctx *context_module.Context, current *actions_mode
if pending := pendingNeeds(current, jobs); len(pending) > 0 {
return ctx.Locale.TrString("actions.runs.waiting_for_dependent_jobs", strings.Join(pending, ", "))
}
case current.Status.IsWaiting() && !current.IsReusableCaller: // a caller waits on its called jobs, never on a runner
case current.Status.IsWaiting():
// A waiting job has no runner to pick it up yet. A busy runner is still
// "online", so distinguish three cases: no runner online at all, online
// runners but none match the labels, and a matching runner that is busy.
+1 -1
View File
@@ -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) {
ctx.JSONError(ctx.Tr("repo.pulls.invalid_merge_option"))
} else if conflictError, ok := err.(pull_service.ErrMergeConflicts); ok {
-5
View File
@@ -660,11 +660,6 @@ func deleteReleaseOrTag(ctx *context.Context, isDelTag bool) {
return
}
if isDelTag && !rel.IsTag {
ctx.HTTPError(http.StatusConflict, "a tag attached to a release cannot be deleted directly")
return
}
if err := release_service.DeleteReleaseByID(ctx, ctx.Repo.Repository, rel, ctx.Doer, isDelTag); err != nil {
if release_service.IsErrProtectedTagName(err) {
ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected"))
-16
View File
@@ -4,7 +4,6 @@
package repo
import (
"net/http"
"net/http/httptest"
"testing"
@@ -22,21 +21,6 @@ import (
"github.com/stretchr/testify/require"
)
func TestDeleteTagRetainsReleaseAndAttachments(t *testing.T) {
unittest.PrepareTestEnv(t)
ctx, resp := contexttest.MockContext(t, "POST user2/repo1/tags/delete?id=1")
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: 1})
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 9, ReleaseID: 1})
DeleteTag(ctx)
assert.Equal(t, http.StatusConflict, resp.Code)
assert.Equal(t, release, unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: 1}))
assert.Equal(t, attachment, unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 9}))
}
func TestNewReleasePost(t *testing.T) {
unittest.PrepareTestEnv(t)
+8
View File
@@ -734,10 +734,18 @@ func UsernameSubRoute(ctx *context.Context) {
ShowGPGKeys(ctx)
}
case strings.HasSuffix(username, ".rss"):
if !setting.Other.EnableFeed {
ctx.HTTPError(http.StatusNotFound)
return
}
if reloadParam(".rss") {
feed.ShowUserFeedRSS(ctx)
}
case strings.HasSuffix(username, ".atom"):
if !setting.Other.EnableFeed {
ctx.HTTPError(http.StatusNotFound)
return
}
if reloadParam(".atom") {
feed.ShowUserFeedAtom(ctx)
}
-7
View File
@@ -322,13 +322,6 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
// ActionUserFollow is for follow/unfollow user request
func ActionUserFollow(ctx *context.Context) {
isOrg := ctx.ContextUser.IsOrganization()
if isOrg && !organization.HasOrgOrUserVisible(ctx, ctx.ContextUser, ctx.Doer) ||
!isOrg && !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
ctx.NotFound(nil)
return
}
var err error
switch ctx.FormString("action") {
case "follow":
-15
View File
@@ -15,7 +15,6 @@ import (
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
"gitea.dev/modules/log"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"xorm.io/builder"
@@ -100,20 +99,6 @@ func ApproveRuns(ctx context.Context, repo *repo_model.Repository, doer *user_mo
if !slots.available(job) {
continue
}
if invalid := invalidRunsOn(job); invalid != nil {
job.Status, job.Stopped = actions_model.StatusFailure, timeutil.TimeStampNow()
n, err := actions_model.UpdateRunJob(ctx, job, nil, "status", "stopped")
if err != nil {
return err
}
if n > 0 {
updatedJobs = append(updatedJobs, job)
}
if err := upsertJobErrorSummary(ctx, job, "runs-on", invalid); err != nil {
return err
}
continue
}
var jobsToCancel []*actions_model.ActionRunJob
job.Status, jobsToCancel, err = PrepareToStartJobWithConcurrency(ctx, job)
if err != nil {
+4 -11
View File
@@ -98,7 +98,7 @@ jobs:
assert.NotEmpty(t, persisted.RawConcurrency)
}
func TestPrepareRunAndInsert_JobIfAndRunsOn(t *testing.T) {
func TestPrepareRunAndInsert_JobIf(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
defer test.MockVariableValue(&EmitJobsIfReadyByRun, func(int64) error { return nil })()
@@ -123,10 +123,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- run: echo
unset-runs-on:
runs-on: ${{ vars.UNSET }}
steps:
- run: echo
`, false)
jobs := map[string]*actions_model.ActionRunJob{}
@@ -138,12 +134,9 @@ jobs:
assert.False(t, jobs["skip"].IsConcurrencyEvaluated)
assert.Equal(t, actions_model.StatusSkipped, jobs["skip-caller"].Status)
assert.Equal(t, actions_model.StatusSkipped, jobs["invalid"].Status)
assert.Equal(t, actions_model.StatusFailure, jobs["unset-runs-on"].Status)
for id, key := range map[string]string{"invalid": "if", "unset-runs-on": "runs-on"} {
summary, err := actions_model.GetActionRunJobSummary(t.Context(), run.RepoID, run.ID, run.LatestAttemptID, jobs[id].ID, 0)
require.NoError(t, err)
assert.Contains(t, summary.Content, "Error when evaluating `"+key+"` for job `"+id+"`")
}
summary, err := actions_model.GetActionRunJobSummary(t.Context(), run.RepoID, run.ID, run.LatestAttemptID, jobs["invalid"].ID, 0)
require.NoError(t, err)
assert.Contains(t, summary.Content, "Error when evaluating `if` for job `invalid`")
}
func TestComputeReusableCallerOutputs(t *testing.T) {
-12
View File
@@ -183,18 +183,6 @@ func upsertJobErrorSummary(ctx context.Context, job *actions_model.ActionRunJob,
return actions_model.UpsertActionRunJobSummary(ctx, job.RepoID, job.RunID, job.RunAttemptID, job.ID, 0, actions_model.JobSummaryContentTypeMarkdown, []byte(content))
}
// invalidRunsOn returns github.com's error for the job's evaluated runs-on.
func invalidRunsOn(job *actions_model.ActionRunJob) error {
parsed, err := job.ParseJob()
if err != nil {
return err
}
if problem := parsed.RunsOnProblem(); problem != "" {
return errors.New(problem)
}
return nil
}
func findJobNeedsAndFillJobResults(ctx context.Context, job *actions_model.ActionRunJob) (map[string]*jobparser.JobResult, error) {
taskNeeds, jobsByID, err := FindTaskNeeds(ctx, job)
if err != nil {
+33 -39
View File
@@ -32,46 +32,40 @@ func handleInvalidWorkflows(ctx context.Context, input *notifyInput, ref git.Ref
if actionsConfig.IsWorkflowDisabled(entryName) {
continue
}
insertInvalidWorkflowRun(ctx, &actions_model.ActionRun{
Title: commit.MessageTitle(), RepoID: input.Repo.ID, Repo: input.Repo, OwnerID: input.Repo.OwnerID,
now := timeutil.TimeStampNow()
run := &actions_model.ActionRun{
Title: util.EllipsisDisplayString(commit.MessageTitle(), 255), RepoID: input.Repo.ID, Repo: input.Repo, OwnerID: input.Repo.OwnerID,
WorkflowID: entryName, TriggerUserID: input.Doer.ID, TriggerUser: input.Doer, Ref: ref.String(),
CommitSHA: commit.ID.String(), Event: input.Event, TriggerEvent: string(input.Event), EventPayload: string(payload),
WorkflowRepoID: input.Repo.ID, WorkflowCommitSHA: commit.ID.String(),
}, parseErr)
WorkflowRepoID: input.Repo.ID, WorkflowCommitSHA: commit.ID.String(), Status: actions_model.StatusFailure, Started: now, Stopped: now,
}
if err := db.WithTx(ctx, func(ctx context.Context) error {
if run.Index, err = db.GetNextResourceIndex(ctx, "action_run_index", run.RepoID); err != nil {
return err
}
if err := db.Insert(ctx, run); err != nil {
return err
}
attempt := &actions_model.ActionRunAttempt{RepoID: run.RepoID, RunID: run.ID, Attempt: 1, TriggerUserID: run.TriggerUserID, Status: run.Status, Started: now, Stopped: now}
if err := db.Insert(ctx, attempt); err != nil {
return err
}
run.LatestAttemptID = attempt.ID
if err := actions_model.UpdateRun(ctx, run, "latest_attempt_id"); err != nil {
return err
}
content := fmt.Sprintf("**Invalid workflow file: %s**\n\n```\n%v\n```\n", entryName, parseErr)
return db.Insert(ctx, &actions_model.ActionRunJobSummary{
RepoID: run.RepoID, RunID: run.ID, RunAttemptID: attempt.ID, Content: content, ContentSize: int64(len(content)), ContentType: actions_model.JobSummaryContentTypeMarkdown,
})
}); err != nil {
log.Error("insert run for invalid workflow %q: %v", entryName, err)
continue
}
if err := createWorkflowCommitStatus(ctx, run.Repo, run.CommitSHA, entryName+" ("+run.TriggerEvent+")", run.WorkflowID,
commitstatus.CommitStatusFailure, run.Link(), "Invalid workflow file", false); err != nil {
log.Error("create commit status for invalid workflow %q: %v", entryName, err)
}
NotifyWorkflowRunStatusUpdate(ctx, run)
}
}
// insertInvalidWorkflowRun records run as failed with parseErr as its summary.
func insertInvalidWorkflowRun(ctx context.Context, run *actions_model.ActionRun, parseErr error) {
now := timeutil.TimeStampNow()
run.Title = util.EllipsisDisplayString(run.Title, 255)
run.Status, run.Started, run.Stopped = actions_model.StatusFailure, now, now
if err := db.WithTx(ctx, func(ctx context.Context) (err error) {
if run.Index, err = db.GetNextResourceIndex(ctx, "action_run_index", run.RepoID); err != nil {
return err
}
if err := db.Insert(ctx, run); err != nil {
return err
}
attempt := &actions_model.ActionRunAttempt{RepoID: run.RepoID, RunID: run.ID, Attempt: 1, TriggerUserID: run.TriggerUserID, Status: run.Status, Started: now, Stopped: now}
if err := db.Insert(ctx, attempt); err != nil {
return err
}
run.LatestAttemptID = attempt.ID
if err := actions_model.UpdateRun(ctx, run, "latest_attempt_id"); err != nil {
return err
}
content := fmt.Sprintf("**Invalid workflow file: %s**\n\n```\n%v\n```\n", run.WorkflowID, parseErr)
return db.Insert(ctx, &actions_model.ActionRunJobSummary{
RepoID: run.RepoID, RunID: run.ID, RunAttemptID: attempt.ID, Content: content, ContentSize: int64(len(content)), ContentType: actions_model.JobSummaryContentTypeMarkdown,
})
}); err != nil {
log.Error("insert run for invalid workflow %q: %v", run.WorkflowID, err)
return
}
if err := createWorkflowCommitStatus(ctx, run.Repo, run.CommitSHA, run.WorkflowID+" ("+run.TriggerEvent+")", run.WorkflowID,
commitstatus.CommitStatusFailure, run.Link(), "Invalid workflow file", false); err != nil {
log.Error("create commit status for invalid workflow %q: %v", run.WorkflowID, err)
}
NotifyWorkflowRunStatusUpdate(ctx, run)
}
-8
View File
@@ -590,14 +590,6 @@ func (r *jobStatusResolver) resolve(ctx context.Context) (map[int64]actions_mode
continue
}
if err := invalidRunsOn(actionRunJob); err != nil {
if err := upsertJobErrorSummary(ctx, actionRunJob, "runs-on", err); err != nil {
return nil, err
}
ret[id] = actions_model.StatusFailure
continue
}
// update concurrency and check whether the job can run now
if err := updateConcurrencyEvaluationForJobWithNeeds(ctx, actionRunJob, r.vars); errors.Is(err, util.ErrInvalidArgument) {
if err := upsertJobErrorSummary(ctx, actionRunJob, "concurrency", err); err != nil {
-9
View File
@@ -173,15 +173,6 @@ jobs:
want: map[int64]actions_model.Status{2: actions_model.StatusFailure},
note: "Error when evaluating `concurrency` for job `job2`.",
},
{
name: "invalid evaluated `runs-on` fails the job with an annotation",
jobs: actions_model.ActionJobList{
{ID: 1, RepoID: 1, JobID: "job1", Status: actions_model.StatusSuccess},
{ID: 2, RepoID: 1, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte("jobs: {job2: {runs-on: ''}}")},
},
want: map[int64]actions_model.Status{2: actions_model.StatusFailure},
note: "Error when evaluating `runs-on` for job `job2`.",
},
{
name: "max-parallel: a freed slot promotes the lowest blocked job id",
jobs: actions_model.ActionJobList{
-8
View File
@@ -394,14 +394,6 @@ func buildApproveAndInsertRun(
IsScopedRun: isScopedRun,
}
if err := validateCalledWorkflows(ctx, run, dwf.Content); err != nil {
if isScopedRun {
return err
}
insertInvalidWorkflowRun(ctx, run, err)
return nil
}
approvalUsers, err := getApprovalUsers(ctx, input, isForkPullRequest)
if err != nil {
return err
+1 -50
View File
@@ -7,8 +7,6 @@ import (
"context"
"errors"
"fmt"
"maps"
"slices"
"strings"
"gitea.dev/actionslib/pkg/model"
@@ -99,50 +97,6 @@ func loadReusableWorkflowSource(ctx context.Context, run *actions_model.ActionRu
}
}
// validateCalledWorkflows validates all workflows content calls, recursively.
func validateCalledWorkflows(ctx context.Context, run *actions_model.ActionRun, content []byte) error {
validated := make(container.Set[string])
var validate func(content []byte, source *actions_model.ActionRunJob, level int) error
validate = func(content []byte, source *actions_model.ActionRunJob, level int) error {
workflow, err := jobparser.ReadWorkflow(content)
if err != nil {
return err
}
for _, id := range slices.Sorted(maps.Keys(workflow.Jobs)) {
uses := workflow.Jobs[id].Uses
if uses == "" {
continue
}
if level > MaxReusableCallLevels {
return errCallLevelExceeded(uses)
}
if !validated.Add(fmt.Sprintf("%d@%s:%s", source.WorkflowSourceRepoID, source.WorkflowSourceCommitSHA, uses)) {
continue
}
ref, err := ResolveUses(ctx, uses)
if err != nil {
return fmt.Errorf("job %s: %w", id, err)
}
called, repoID, commitSHA, err := loadReusableWorkflowSource(ctx, run, source, ref)
if err != nil {
return fmt.Errorf("job %s: %w", id, err)
}
if _, err = jobparser.ValidateWorkflowStatic(called); err == nil {
err = validate(called, &actions_model.ActionRunJob{WorkflowSourceRepoID: repoID, WorkflowSourceCommitSHA: commitSHA}, level+1)
}
if err != nil {
return fmt.Errorf("job %s: Error from called workflow %s: %w", id, uses, err)
}
}
return nil
}
return validate(content, &actions_model.ActionRunJob{WorkflowSourceRepoID: run.WorkflowRepoID, WorkflowSourceCommitSHA: run.WorkflowCommitSHA}, 0)
}
func errCallLevelExceeded(uses string) error {
return fmt.Errorf("reusable workflow call exceeds the maximum nesting level of %d at %q", MaxReusableCallLevels, uses)
}
// resolveSameRepoWorkflowSourceCommit returns the commit to read a same-repo reusable workflow from.
// pull_request_target runs must resolve local `uses:` at the PR base commit, not a stored head SHA.
func resolveSameRepoWorkflowSourceCommit(run *actions_model.ActionRun, caller *actions_model.ActionRunJob) string {
@@ -195,7 +149,7 @@ func checkCallerChain(ctx context.Context, caller *actions_model.ActionRunJob) e
current = next
depth++
if depth > MaxReusableCallLevels {
return errCallLevelExceeded(caller.CallUses)
return fmt.Errorf("reusable workflow call exceeds the maximum nesting level of %d at %q", MaxReusableCallLevels, caller.CallUses)
}
if current.IsReusableCaller && current.CallUses != "" && !visited.Add(canonicalCallUses(current)) {
return fmt.Errorf("reusable workflow call cycle detected: %q", current.CallUses)
@@ -271,9 +225,6 @@ func expandReusableWorkflowCaller(ctx context.Context, run *actions_model.Action
if err := checkResolvedCallerCycle(ctx, caller, contentSourceRepoID, contentSourceCommitSHA, ref.Path); err != nil {
return err
}
if _, err := jobparser.ValidateWorkflowStatic(content); err != nil {
return fmt.Errorf("invalid called workflow: %w", err)
}
// 4. Parse the called workflow's spec (used by both secret validation and input evaluation).
wcSpec, err := jobparser.ParseWorkflowCallConfig(content)
+5 -13
View File
@@ -5,7 +5,6 @@ package actions
import (
"context"
"errors"
"fmt"
act_model "gitea.dev/actionslib/pkg/model"
@@ -13,7 +12,6 @@ import (
"gitea.dev/models/db"
"gitea.dev/modules/actions/jobparser"
"gitea.dev/modules/log"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"go.yaml.in/yaml/v4"
@@ -187,7 +185,6 @@ func insertRunJob(ctx context.Context, run *actions_model.ActionRun, runAttempt
id, job := workflowJob.Job()
needs := job.Needs()
isMatrixDeferred := jobparser.HasDeferredMatrix(job)
runsOnProblem := job.RunsOnProblem() // SetJob's encoding drops the node's null tag
if err := workflowJob.SetJob(id, job.EraseNeeds()); err != nil {
return nil, nil, false, err
}
@@ -241,15 +238,10 @@ func insertRunJob(ctx context.Context, run *actions_model.ActionRun, runAttempt
}
// a skipped job must neither cancel its group peers nor take a slot
invalidErr, err := decideJobIf(ctx, run, runAttempt, runJob, vars)
invalidIf, err := decideJobIf(ctx, run, runAttempt, runJob, vars)
if err != nil {
return nil, nil, false, fmt.Errorf("evaluate job if: %w", err)
}
invalidKey := "if"
if runsOnProblem != "" && runJob.Status.IsWaiting() && slots.available(runJob) {
invalidKey, invalidErr = "runs-on", errors.New(runsOnProblem)
runJob.Status, runJob.Stopped = actions_model.StatusFailure, timeutil.TimeStampNow()
}
var cancelledConcurrencyJobs []*actions_model.ActionRunJob
// check job concurrency
@@ -283,8 +275,8 @@ func insertRunJob(ctx context.Context, run *actions_model.ActionRun, runAttempt
if err := db.Insert(ctx, runJob); err != nil {
return nil, nil, false, err
}
if invalidErr != nil {
if err := upsertJobErrorSummary(ctx, runJob, invalidKey, invalidErr); err != nil {
if invalidIf != nil {
if err := upsertJobErrorSummary(ctx, runJob, "if", invalidIf); err != nil {
return nil, nil, false, err
}
}
@@ -295,8 +287,8 @@ func insertRunJob(ctx context.Context, run *actions_model.ActionRun, runAttempt
}
}
// the emitter resolves an expanded caller's children and a skipped or failed job's dependents
return runJob, cancelledConcurrencyJobs, runJob.IsExpanded || runJob.Status.In(actions_model.StatusSkipped, actions_model.StatusFailure), nil
// the emitter resolves an expanded caller's children and a skipped job's dependents
return runJob, cancelledConcurrencyJobs, runJob.IsExpanded || runJob.Status == actions_model.StatusSkipped, nil
}
func expandInlineReusableCaller(ctx context.Context, run *actions_model.ActionRun, runAttempt *actions_model.ActionRunAttempt, caller *actions_model.ActionRunJob, vars map[string]string) error {
-9
View File
@@ -17,7 +17,6 @@ import (
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/actions/jobparser"
"gitea.dev/modules/json"
"gitea.dev/modules/log"
"gitea.dev/modules/timeutil"
@@ -145,14 +144,6 @@ func CreateScheduleTaskBySpec(ctx context.Context, spec *actions_model.ActionSch
WorkflowCommitSHA: cron.CommitSHA,
}
_, err := jobparser.ValidateWorkflowStatic(cron.Content)
if err == nil {
err = validateCalledWorkflows(ctx, run, cron.Content)
}
if err != nil {
return fmt.Errorf("invalid workflow: %w", err)
}
// FIXME cron.Content might be outdated if the workflow file has been changed.
// Load the latest sha from default branch
// Insert the action run and its associated jobs into the database
+1 -1
View File
@@ -103,7 +103,7 @@ func TestStartTasks(t *testing.T) {
}
due := timeutil.TimeStamp(time.Now().Add(-time.Minute).Unix())
validWorkflow := "on:\n schedule:\n - cron: '0 0 * * *'\njobs:\n job:\n runs-on: ubuntu-latest\n steps:\n - run: true\n"
validWorkflow := "jobs:\n job:\n runs-on: ubuntu-latest\n steps:\n - run: true\n"
// specs are processed by ascending id, so the broken one runs first and used to abort the whole pass
broken := insertSchedule(1, 2, "broken.yml", "@every 1m", "this: [is: not: a: workflow", due)
+1 -4
View File
@@ -140,10 +140,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
return 0, err
}
if _, err = jobparser.ValidateWorkflowStatic(content); err == nil {
err = validateCalledWorkflows(ctx, run, content)
}
if err != nil {
if _, err := jobparser.ValidateWorkflowStatic(content); err != nil {
return 0, util.ErrorWrapTranslatable(util.NewInvalidArgumentErrorf("invalid workflow %q: %v", workflowID, err), "actions.runs.invalid_workflow_helper", err.Error())
}
workflow, err := jobparser.ReadWorkflow(content)
+1 -1
View File
@@ -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
// 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) {
return errors.Join(errSkipAutoMerge, err)
}
+3 -2
View File
@@ -107,8 +107,9 @@ func NewGiteaDownloader(ctx context.Context, baseURL, repoPath, username, passwo
if err != nil {
log.Info("Unable to get global API settings. Ignoring these.")
log.Debug("giteaClient.GetGlobalAPISettings. Error: %v", err)
} else if apiConf != nil && apiConf.MaxResponseItems > 0 {
maxPerPage = min(apiConf.MaxResponseItems, 100)
}
if apiConf != nil {
maxPerPage = apiConf.MaxResponseItems
}
return &GiteaDownloader{
+7 -10
View File
@@ -5,7 +5,6 @@ package migrations
import (
"fmt"
"math"
"net/http"
"net/http/httptest"
"os"
@@ -318,16 +317,15 @@ func TestGiteaDownloadRepo(t *testing.T) {
func TestGiteaDownloadCommentsPaging(t *testing.T) {
for _, tc := range []struct {
maxResponseItems, pageSize, commentCount, requests int
paginated bool
maxResponseItems, commentCount, requests int
paginated bool
}{
{maxResponseItems: 2, pageSize: 2, commentCount: 2, requests: 2},
{maxResponseItems: 2, pageSize: 2, commentCount: 3, requests: 1},
{maxResponseItems: 2, pageSize: 2, commentCount: 4, requests: 3, paginated: true},
{maxResponseItems: 0, pageSize: 10, commentCount: 0, requests: 1},
{maxResponseItems: math.MaxInt, pageSize: 100, commentCount: 0, requests: 1},
{maxResponseItems: 2, commentCount: 2, requests: 2},
{maxResponseItems: 2, commentCount: 3, requests: 1},
{maxResponseItems: 2, commentCount: 4, requests: 3, paginated: true},
{maxResponseItems: 0, commentCount: 0, requests: 1},
} {
t.Run(fmt.Sprintf("maxResponseItems=%d/comments=%d", tc.maxResponseItems, tc.commentCount), func(t *testing.T) {
t.Run(strconv.Itoa(tc.commentCount), func(t *testing.T) {
commentRequests := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
@@ -354,7 +352,6 @@ func TestGiteaDownloadCommentsPaging(t *testing.T) {
downloader, err := NewGiteaDownloader(t.Context(), server.URL, "o/r", "", "", "")
require.NoError(t, err)
require.Equal(t, tc.pageSize, downloader.maxPerPage)
comments, _, err := downloader.GetComments(t.Context(), &base.Issue{Number: 1})
require.NoError(t, err)
+1 -16
View File
@@ -14,7 +14,6 @@ import (
"strconv"
"strings"
"unicode"
"uuid"
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
@@ -28,7 +27,6 @@ import (
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/globallock"
"gitea.dev/modules/graceful"
"gitea.dev/modules/gtprof"
"gitea.dev/modules/httplib"
"gitea.dev/modules/log"
"gitea.dev/modules/references"
@@ -291,22 +289,9 @@ func hasPullRequestCommitBeenMerged(ctx context.Context, pr *issues_model.PullRe
// Merge merges pull request to base repository.
// 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 {
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
func Merge(prID int64, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, wasAutoMerged bool) error {
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 {
pr, err := issues_model.GetPullRequestByID(ctx, prID)
if err != nil {
+6 -12
View File
@@ -7,26 +7,20 @@ import (
"bufio"
"bytes"
"context"
"strings"
"gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/setting"
)
const gitLogGraphFormatSep = "^" // disallowed char in git ref names
// 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) {
format := "DATA:" + strings.Join([]string{
"%D", // ref names without the " (", ")" wrapping.
"%H", // commit hash
"%ad", // author date (format respects --date= option)
"%h", // abbreviated commit hash
"%s", // subject
}, gitLogGraphFormatSep)
format := "DATA:%D|%H|%ad|%h|%s"
if page == 0 {
page = 1
}
page = max(page, 1)
graphCmd := gitcmd.NewCommand("log", "--graph", "--date-order", "--decorate=full")
if hidePRRefs {
@@ -37,7 +31,7 @@ func GetCommitGraph(ctx context.Context, gitRepo *git.Repository, page, maxAllow
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("--pretty=format:%s", format)
+1 -1
View File
@@ -216,7 +216,7 @@ func parseGitTime(timeStr string) time.Time {
// NewCommit creates a new commit from a provided line
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 {
return nil, fmt.Errorf("malformed data section on line %d with commit: %s", row, string(line))
}
+3 -3
View File
@@ -35,7 +35,7 @@ func BenchmarkGetCommitGraph(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.Reset()
@@ -224,14 +224,14 @@ func TestParseGlyphs(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 {
shouldPass bool
testName string
commitMessage string
}{
{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"},
}
+1 -17
View File
@@ -47,20 +47,6 @@
</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 -->
<div class="ui dropdown type jump item">
<span class="text">
@@ -89,7 +75,6 @@
{{SortArrow "alphabetically" "reversealphabetically" $.SortType true}}
</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.restricted"}}</th>
<th>{{ctx.Locale.Tr "admin.users.2fa"}}</th>
@@ -117,7 +102,6 @@
{{template "shared/user/user_type_label" .}}
</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 .IsRestricted "octicon-check" "octicon-x")}}</td>
<td>{{svg (Iif (index $.UsersTwoFaStatus .ID) "octicon-check" "octicon-x")}}</td>
@@ -135,7 +119,7 @@
</td>
</tr>
{{else}}
<tr class="no-results-row"><td class="tw-text-center" colspan="10">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
<tr class="no-results-row"><td class="tw-text-center" colspan="9">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
{{end}}
</tbody>
</table>
@@ -9,15 +9,18 @@
<h3 class="tw-m-0">{{ctx.Locale.Tr "repo.pulls.cmd_instruction_checkout_title"}}</h3>
{{ctx.Locale.Tr "repo.pulls.cmd_instruction_checkout_desc"}}
</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">
{{$gitRemoteName := ctx.RootData.SystemConfig.Repository.GitGuideRemoteName.Value ctx}}
{{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}}
<div>git fetch -u {{$gitRemoteName}} {{$pull.GetGitHeadRefName}}:{{$args.LocalBranchArg}}</div>
<div>git fetch -u {{$gitRemoteName}} {{$pull.GetGitHeadRefName}}:{{$localBranch}}</div>
{{end}}
<div>git checkout {{$args.LocalBranchArg}}</div>
<div>git checkout {{$localBranch}}</div>
</div>
{{if $data.ShowMergeInstructions}}
<div>
@@ -29,32 +32,32 @@
</div>
<div class="ui secondary segment tw-font-mono">
<div data-pull-merge-style="merge">
<div>git checkout {{$args.BaseBranchArg}}</div>
<div>git merge --no-ff {{$args.LocalBranchArg}}</div>
<div>git checkout {{$pull.BaseBranch}}</div>
<div>git merge --no-ff {{$localBranch}}</div>
</div>
<div class="tw-hidden" data-pull-merge-style="rebase">
<div>git checkout {{$args.BaseBranchArg}}</div>
<div>git merge --ff-only {{$args.LocalBranchArg}}</div>
<div>git checkout {{$pull.BaseBranch}}</div>
<div>git merge --ff-only {{$localBranch}}</div>
</div>
<div class="tw-hidden" data-pull-merge-style="rebase-merge">
<div>git checkout {{$args.LocalBranchArg}}</div>
<div>git rebase {{$args.BaseBranchArg}}</div>
<div>git checkout {{$args.BaseBranchArg}}</div>
<div>git merge --no-ff {{$args.LocalBranchArg}}</div>
<div>git checkout {{$localBranch}}</div>
<div>git rebase {{$pull.BaseBranch}}</div>
<div>git checkout {{$pull.BaseBranch}}</div>
<div>git merge --no-ff {{$localBranch}}</div>
</div>
<div class="tw-hidden" data-pull-merge-style="squash">
<div>git checkout {{$args.BaseBranchArg}}</div>
<div>git merge --squash {{$args.LocalBranchArg}}</div>
<div>git checkout {{$pull.BaseBranch}}</div>
<div>git merge --squash {{$localBranch}}</div>
</div>
<div class="tw-hidden" data-pull-merge-style="fast-forward-only">
<div>git checkout {{$args.BaseBranchArg}}</div>
<div>git merge --ff-only {{$args.LocalBranchArg}}</div>
<div>git checkout {{$pull.BaseBranch}}</div>
<div>git merge --ff-only {{$localBranch}}</div>
</div>
<div class="tw-hidden" data-pull-merge-style="manually-merged">
<div>git checkout {{$args.BaseBranchArg}}</div>
<div>git merge {{$args.LocalBranchArg}}</div>
<div>git checkout {{$pull.BaseBranch}}</div>
<div>git merge {{$localBranch}}</div>
</div>
<div>git push {{$gitRemoteName}} {{$args.BaseBranchArg}}</div>
<div>git push {{$gitRemoteName}} {{$pull.BaseBranch}}</div>
</div>
{{end}}
</div>
+1 -1
View File
@@ -11939,7 +11939,7 @@
"operationId": "adminSearchUsers",
"parameters": [
{
"description": "ID of the user's login source to search for, 0 means the local users",
"description": "ID of the user's login source to search for",
"in": "query",
"name": "source_id",
"schema": {
+1 -1
View File
@@ -825,7 +825,7 @@
{
"type": "integer",
"format": "int64",
"description": "ID of the user's login source to search for, 0 means the local users",
"description": "ID of the user's login source to search for",
"name": "source_id",
"in": "query"
},
-1
View File
@@ -12,7 +12,6 @@
{{end}}
{{ctx.Locale.Tr "auth.authorize_application_created_by" .ApplicationCreatorLinkHTML}}<br>
{{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>
</div>
<div class="ui attached segment">
-2
View File
@@ -17,8 +17,6 @@ test('create a bot and manage its access token', async ({page, request}) => {
await page.getByRole('row', {name: /^user /}).getByRole('radio', {name: 'Read', exact: true}).check();
await page.getByRole('button', {name: 'Generate Token'}).click();
const token = await page.getByRole('code').textContent();
await page.getByRole('button', {name: 'Copy', exact: true}).click();
await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe(token);
const response = await request.get('/api/v1/user', {headers: {Authorization: `token ${token}`}});
expect(await response.json()).toMatchObject({login: botName, type: 'Bot'});
-12
View File
@@ -39,18 +39,6 @@ test('pdf file', async ({page, request}) => {
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}) => {
const repoName = `e2e-asciicast-render-${randomString(8)}`;
const owner = env.GITEA_TEST_E2E_USER;
@@ -33,7 +33,7 @@ func TestActionsInvalidWorkflowPush(t *testing.T) {
content string
wantErrors []string
}{
{"expression", "on: push\nrun-name: '${{ github.ref'\njobs: {check: {runs-on: ubuntu-latest, if: unknown.x}}\n", []string{"Unrecognized named-value: &#39;unknown&#39;", "unclosed expression"}},
{"expression", "on: push\nrun-name: '${{ github.ref'\njobs: {check: {if: unknown.x}}\n", []string{"Unrecognized named-value: &#39;unknown&#39;", "unclosed expression"}},
{"trigger", "on:\njobs: {check: {runs-on: ubuntu-latest, steps: [{run: echo hello}]}}\n", []string{"invalid event"}},
} {
t.Run(testCase.name, func(t *testing.T) {
@@ -405,8 +405,8 @@ jobs:
from: 'consumer'
`)
// Phase 1: no grant.
assertInvalidWorkflowRun(t, consumerRepo.ID, "cross-caller.yaml", "reusable workflow repository user2/reusable-lib-private does not exist or is not readable")
// Phase 1: no grant. The cross-repo read check fails, and NO ActionRun row gets persisted.
assert.Equal(t, 0, unittest.GetCount(t, &actions_model.ActionRun{RepoID: consumerRepo.ID}))
runner.fetchNoTask(t)
// Phase 2: user2 (libRepo owner) adds user4 (consumer owner) as a Collaborative Owner of libRepo.
@@ -418,7 +418,7 @@ jobs:
// Phase 3: trigger the workflow again
createRepoWorkflowFile(t, user4, user4Token, consumerRepo, "marker.txt", "trigger after grant")
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: consumerRepo.ID, Index: 2})
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: consumerRepo.ID})
crossJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: run.ID, JobID: "cross_job"})
assert.True(t, crossJob.IsReusableCaller)
assert.True(t, crossJob.IsExpanded)
@@ -484,7 +484,8 @@ jobs:
uses: user2/reusable-lib-public-denied/.gitea/workflows/reusable_lib.yaml@main
`)
assertInvalidWorkflowRun(t, consumerRepo.ID, "cross-caller.yaml", "reusable workflow repository user2/reusable-lib-public-denied does not exist or is not readable")
// Denied: the cross-repo read check fails for the public caller, so NO ActionRun is persisted and no task is dispatched.
assert.Equal(t, 0, unittest.GetCount(t, &actions_model.ActionRun{RepoID: consumerRepo.ID}))
runner.fetchNoTask(t)
})
@@ -562,32 +563,35 @@ jobs:
unittest.AssertNotExistsBean(t, &actions_model.ActionRunJob{RunID: run.ID, JobID: "util_consumer_job"})
})
t.Run("Missing or invalid callee fails the run as an invalid workflow file", func(t *testing.T) {
for name, testCase := range map[string]struct{ callee, want string }{
"missing": {"", "job call: read user2/caller-missing-callee@"},
"no-runs-on": {"on: workflow_call\njobs:\n inner:\n steps:\n - run: echo\n", "job call: Error from called workflow ./.gitea/workflows/callee.yml: job inner: Required property is missing: runs-on"},
} {
apiRepo := createActionsTestRepo(t, user2Token, "caller-"+name+"-callee", false)
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
if testCase.callee != "" {
createRepoWorkflowFile(t, user2, user2Token, repo, ".gitea/workflows/callee.yml", testCase.callee)
}
createRepoWorkflowFile(t, user2, user2Token, repo, ".gitea/workflows/caller.yaml",
"on: push\njobs:\n plain_job:\n runs-on: ubuntu-latest\n steps:\n - run: echo\n call:\n needs: plain_job\n uses: ./.gitea/workflows/callee.yml\n")
assertInvalidWorkflowRun(t, repo.ID, "caller.yaml", testCase.want)
}
t.Run("Missing callee file", func(t *testing.T) {
// A caller workflow references a callee path that does not exist in the repo.
apiRepo := createActionsTestRepo(t, user2Token, "caller-missing-callee", false)
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
createRepoWorkflowFile(t, user2, user2Token, repo, ".gitea/workflows/caller.yaml",
`name: Caller
on: push
jobs:
plain_job:
runs-on: ubuntu-latest
steps:
- run: echo 'job'
call_missing:
uses: ./.gitea/workflows/does-not-exist.yml
`)
assert.Equal(t, 0, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID}))
})
t.Run("Nested caller failing to expand fails with the error as summary instead of blocking", func(t *testing.T) {
// When the expansion hits a terminal error, the emitter must fail the caller and let the run finish as failed, not retry the expansion forever.
apiRepo := createActionsTestRepo(t, user2Token, "nested-caller-bad-callee", false)
t.Run("Nested caller with missing callee fails with the error as summary instead of blocking", func(t *testing.T) {
// When the expansion hits a terminal error (e.g. missing callee), the emitter must fail the caller and let the run finish as failed, not retry the expansion forever.
apiRepo := createActionsTestRepo(t, user2Token, "nested-caller-missing-callee", false)
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
runner := newMockRunner()
runner.registerAsRepoRunner(t, repo.OwnerName, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
createRepoWorkflowFile(t, user2, user2Token, repo, ".gitea/workflows/lib.yml",
"on:\n workflow_call:\n secrets:\n token:\n required: true\njobs:\n inner:\n runs-on: ubuntu-latest\n steps:\n - run: echo\n")
createRepoWorkflowFile(t, user2, user2Token, repo, ".gitea/workflows/caller.yaml",
`name: Caller
on: push
@@ -598,7 +602,7 @@ jobs:
- run: echo 'job'
bad_caller:
needs: plain_job
uses: ./.gitea/workflows/lib.yml
uses: ./.gitea/workflows/does-not-exist.yml
`)
plainTask := runner.fetchTask(t)
@@ -610,7 +614,7 @@ jobs:
runner.execTask(t, plainTask, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
// The emitter now tries to expand bad_caller, misses the required secret, and fails the caller.
// The emitter now tries to expand bad_caller, hits the missing callee, and fails the caller.
badCaller := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: badCallerPre.ID})
assert.Equal(t, actions_model.StatusFailure, badCaller.Status)
// No children were inserted (the terminal error precedes the child inserts).
@@ -622,7 +626,7 @@ jobs:
runner.fetchNoTask(t) // no task scheduled for the failed caller; the run is not stuck
summary, err := actions_model.GetActionRunJobSummary(t.Context(), repo.ID, run.ID, badCaller.RunAttemptID, badCaller.ID, 0)
require.NoError(t, err)
assert.Contains(t, summary.Content, "secret token is required, but not provided while calling")
assert.Contains(t, summary.Content, "does-not-exist.yml")
})
t.Run("Fork PR with secrets: inherit does not leak base repo secrets", func(t *testing.T) {
@@ -985,16 +989,6 @@ jobs:
})
}
func assertInvalidWorkflowRun(t *testing.T, repoID int64, workflowID, want string) {
t.Helper()
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: repoID, WorkflowID: workflowID})
assert.Equal(t, actions_model.StatusFailure, run.Status)
assert.Zero(t, unittest.GetCount(t, &actions_model.ActionRunJob{RunID: run.ID}))
summary, err := actions_model.GetActionRunJobSummary(t.Context(), repoID, run.ID, run.LatestAttemptID, 0, 0)
require.NoError(t, err)
assert.Contains(t, summary.Content, want)
}
// token must belong to u (the commit identity) and have write access to repo. Reuse the caller's
// existing token rather than logging in per call, which would re-run bcrypt password verification each time.
func createRepoWorkflowFile(t *testing.T, u *user_model.User, token string, repo *repo_model.Repository, treePath, content string) {
-45
View File
@@ -16,10 +16,8 @@ import (
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/test"
"gitea.dev/services/auth/source/ldap"
"gitea.dev/tests"
"github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -36,49 +34,6 @@ func TestAdminViewUsers(t *testing.T) {
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) {
defer tests.PrepareTestEnv(t)()
@@ -38,11 +38,6 @@ func TestAPIGetIssueAttachment(t *testing.T) {
apiAttachment := DecodeJSON(t, resp, &api.Attachment{})
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID})
commentAttachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 3, RepoID: repo.ID})
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d", repoOwner.Name, repo.Name, unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: commentAttachment.IssueID}).Index, commentAttachment.ID)).
AddTokenAuth(token)
session.MakeRequest(t, req, http.StatusNotFound)
}
func TestAPIListIssueAttachments(t *testing.T) {
+21 -27
View File
@@ -104,7 +104,6 @@ func TestPackageNpm(t *testing.T) {
},
"cpu": ["x64", "arm64"],
"os": ["linux", "darwin"],
"libc": ["glibc"],
"directories": {
"doc": "./doc",
"man": "./man"
@@ -171,9 +170,8 @@ func TestPackageNpm(t *testing.T) {
defer tests.PrintCurrentTest(t)()
rootPaths := []string{
"/api/packages/user2/npm/@scope/test-package",
"/api/packages/user2/npm/@scope%2Ftest-package",
"/api/packages/user2/npm/%40scope%2ftest-package",
fmt.Sprintf("/api/packages/%s/npm/@scope/test-package", user.Name),
fmt.Sprintf("/api/packages/%s/npm/@scope%%2ftest-package", user.Name),
}
for _, root := range rootPaths {
req := NewRequest(t, "GET", fmt.Sprintf("%s/-/%s/%s", root, packageVersion, filename)).AddTokenAuth(token)
@@ -188,7 +186,7 @@ func TestPackageNpm(t *testing.T) {
pvs, err := packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeNpm)
assert.NoError(t, err)
assert.Len(t, pvs, 1)
assert.EqualValues(t, 6, pvs[0].DownloadCount)
assert.Equal(t, int64(4), pvs[0].DownloadCount)
})
t.Run("PackageMetadata", func(t *testing.T) {
@@ -219,7 +217,7 @@ func TestPackageNpm(t *testing.T) {
assert.Equal(t, packageBinPath, pmv.Bin[packageBinName])
assert.Equal(t, integrity, pmv.Dist.Integrity)
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, repoURL, result.Repository.URL)
assert.Equal(t, map[string]string{"tea": "2.x", "soy-milk": "1.2"}, pmv.PeerDependencies)
@@ -229,24 +227,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, []string{"x64", "arm64"}, pmv.CPU)
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, "https://example.com/fund", pmv.Funding)
assert.Equal(t, map[string]string{"left-pad": "1.x"}, pmv.AcceptDependencies)
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) {
@@ -305,6 +289,22 @@ func TestPackageNpm(t *testing.T) {
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) {
defer tests.PrintCurrentTest(t)()
@@ -318,12 +318,6 @@ func TestPackageNpm(t *testing.T) {
test(t, http.StatusBadRequest, "1.0")
test(t, http.StatusOK, "dummy")
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) {
@@ -528,7 +522,7 @@ func TestPackageNpm(t *testing.T) {
req := NewRequest(t, "DELETE", fmt.Sprintf("%s/-/%s/%s/-rev/dummy", root, packageVersion, filename))
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)
MakeRequest(t, req, http.StatusOK)
@@ -22,10 +22,6 @@ func TestAPIGetRawFileOrLFS(t *testing.T) {
resp := MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String())
req = NewRequest(t, "GET", "/api/v1/repos/user2/repo2/media/test.xml").AddTokenAuth(getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository))
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type"))
// Test with LFS
onGiteaRun(t, func(t *testing.T, u *url.URL) {
createLFSTestRepository(t, "repo-lfs-test")
-10
View File
@@ -16,8 +16,6 @@ import (
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
repo_service "gitea.dev/services/repository"
"github.com/stretchr/testify/assert"
@@ -177,14 +175,6 @@ func TestGitPushVisibilityOption(t *testing.T) {
doGitPushTestRepository(gitPath, "origin", "branch2", "-o", "repo.private=false")(t)
repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID})
assert.True(t, repo.IsPrivate, "repo.private option must be ignored on an existing repository")
defer test.MockVariableValue(&setting.Repository.ForcePrivate, true)()
forcedRepo, err := repo_service.CreateRepository(t.Context(), user, user, repo_service.CreateRepoOptions{Name: "repo-visibility-forced", DefaultBranch: "master", IsPrivate: true})
require.NoError(t, err)
u.Path = forcedRepo.FullName() + ".git"
doGitAddRemote(gitPath, "forced", u)(t)
doGitPushTestRepository(gitPath, "forced", "master", "-o", "repo.private=false")(t)
assert.True(t, unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: forcedRepo.ID}).IsPrivate)
})
}
-9
View File
@@ -571,15 +571,6 @@ func testRefreshTokenInvalidation(t *testing.T) {
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
assert.Equal(t, "unable to parse refresh token", parsedError.ErrorDescription)
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "refresh_token",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "https://example.com",
"refresh_token": parsed.AccessToken,
})
MakeRequest(t, req, http.StatusBadRequest)
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "refresh_token",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
+6 -6
View File
@@ -378,11 +378,11 @@ func TestCantMergeConflict(t *testing.T) {
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.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.True(t, pull_service.IsErrRebaseConflicts(err), "Merge error is not a conflict error")
})
@@ -473,7 +473,7 @@ func TestCantMergeUnrelated(t *testing.T) {
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.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",
})
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)
})
}
@@ -596,7 +596,7 @@ func TestFastForwardOnlyMergeWithRequiredSignedCommits(t *testing.T) {
pb.RequireSignedCommits = false
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",
})
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.True(t, pull_service.IsErrMergeDivergingFastForwardOnly(err), "Merge error is not a diverging fast-forward-only error")
})
-9
View File
@@ -84,7 +84,6 @@ func testViewLimitedAndPrivateUserAndRename(t *testing.T) {
org22 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 22})
req := NewRequest(t, "GET", "/"+org22.Name)
MakeRequest(t, req, http.StatusNotFound)
MakeRequest(t, NewRequest(t, "GET", "/"+org22.Name).SetHeader("Accept", "application/rss+xml"), http.StatusNotFound)
session := loginUser(t, "user1")
oldName := org22.Name
@@ -107,8 +106,6 @@ func testViewLimitedAndPrivateUserAndRename(t *testing.T) {
org23 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23})
req = NewRequest(t, "GET", "/"+org23.Name)
MakeRequest(t, req, http.StatusNotFound)
strangerSession := loginUser(t, "user4")
strangerSession.MakeRequest(t, NewRequest(t, "POST", "/"+org23.Name+"?action=follow"), http.StatusNotFound)
oldName = org23.Name
newName = "org23_renamed"
@@ -130,8 +127,6 @@ func testViewLimitedAndPrivateUserAndRename(t *testing.T) {
user31 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 31})
req = NewRequest(t, "GET", "/"+user31.Name)
MakeRequest(t, req, http.StatusNotFound)
MakeRequest(t, NewRequest(t, "GET", "/"+user31.Name).SetHeader("Accept", "application/rss+xml"), http.StatusNotFound)
strangerSession.MakeRequest(t, NewRequest(t, "POST", "/"+user31.Name+"?action=follow"), http.StatusNotFound)
oldName = user31.Name
newName = "user31_renamed"
@@ -335,10 +330,6 @@ func testGetUserRss(t *testing.T) {
session := loginUser(t, "user2")
req = NewRequestf(t, "GET", "/non-existent-user.rss")
session.MakeRequest(t, req, http.StatusNotFound)
defer test.MockVariableValue(&setting.Other.EnableFeed, false)()
MakeRequest(t, NewRequestf(t, "GET", "/%s.rss", user34), http.StatusNotFound)
MakeRequest(t, NewRequestf(t, "GET", "/%s", user34).SetHeader("Accept", "application/rss+xml"), http.StatusNotFound)
}
func testUserListStopWatches(t *testing.T) {
-1
View File
@@ -58,7 +58,6 @@ function selectRange(range: string): Element | null {
stopLineNum = tmp;
range = `${stop}-${start}`;
}
if (startLineNum < 1) return null;
const first = elLineNums[startLineNum - 1] ?? null;
for (let i = startLineNum - 1; i <= stopLineNum - 1 && i < elLineNums.length; i++) {
+4 -2
View File
@@ -64,7 +64,7 @@ function replaceWithFeedbackSvg(origSvg: SVGElement, success: boolean): () => vo
// Enable clipboard copy from HTML attributes. These properties are supported:
// - data-clipboard-text: Direct text to copy
// - data-clipboard-target: Holds a selector for an element. "value" of <input> or <textarea>, or "textContent" of other elements will be copied
// - data-clipboard-target: Holds a selector for an element. "value" of <input> or <textarea>, or "textContent" of <div> will be copied
export function initGlobalCopyToClipboardListener() {
document.addEventListener('click', async (e) => {
const target = (e.target as HTMLElement).closest<HTMLElement>('[data-clipboard-text], [data-clipboard-target]');
@@ -78,8 +78,10 @@ export function initGlobalCopyToClipboardListener() {
const textTarget = document.querySelector(textSelector)!;
if (textTarget.nodeName === 'INPUT' || textTarget.nodeName === 'TEXTAREA') {
text = (textTarget as HTMLInputElement | HTMLTextAreaElement).value;
} else {
} else if (textTarget.nodeName === 'DIV') {
text = textTarget.textContent;
} else {
throw new Error(`Unsupported element for clipboard target: ${textSelector}`);
}
}
// now, text can not be null
-4
View File
@@ -17,11 +17,7 @@ test('isGiteaError', () => {
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 ${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);
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', () => {
-1
View File
@@ -58,7 +58,6 @@ export function isGiteaError(filename: string, stack: string): boolean {
if (extensionRe.test(filename) || extensionRe.test(stack)) return false;
const assetBaseUrl = new URL(`${windowConfig()?.assetUrlPrefix}/`, window.location.origin).href;
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);
}