diff --git a/.eslintrc.yaml b/.eslintrc.yaml index fd1b6da2c0..f4b1e67534 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -10,10 +10,12 @@ parserOptions: plugins: - "@eslint-community/eslint-plugin-eslint-comments" + - eslint-plugin-array-func - eslint-plugin-custom-elements - eslint-plugin-import - eslint-plugin-jquery - eslint-plugin-no-jquery + - eslint-plugin-no-use-extend-native - eslint-plugin-regexp - eslint-plugin-sonarjs - eslint-plugin-unicorn @@ -59,6 +61,12 @@ rules: array-bracket-spacing: [2, never] array-callback-return: [2, {checkForEach: true}] array-element-newline: [0] + array-func/avoid-reverse: [2] + array-func/from-map: [2] + array-func/no-unnecessary-this-arg: [2] + array-func/prefer-array-from: [2] + array-func/prefer-flat-map: [0] # handled by unicorn/prefer-array-flat-map + array-func/prefer-flat: [0] # handled by unicorn/prefer-array-flat arrow-body-style: [0] arrow-parens: [2, always] arrow-spacing: [2, {before: true, after: true}] @@ -444,6 +452,7 @@ rules: no-unused-private-class-members: [2] no-unused-vars: [2, {args: all, argsIgnorePattern: ^_, varsIgnorePattern: ^_, caughtErrorsIgnorePattern: ^_, destructuredArrayIgnorePattern: ^_, ignoreRestSiblings: false}] no-use-before-define: [2, {functions: false, classes: true, variables: true, allowNamedExports: true}] + no-use-extend-native/no-use-extend-native: [2] no-useless-backreference: [2] no-useless-call: [2] no-useless-catch: [2] diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index bef1df1515..b3b02f15ca 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -41,7 +41,7 @@ jobs: with: go-version: ">=1.20.0" - name: Add hosts to /etc/hosts - run: echo "127.0.0.1 pgsql ldap minio" | sudo tee -a /etc/hosts + run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 pgsql ldap minio" | sudo tee -a /etc/hosts' - run: make deps-backend - run: make backend env: @@ -119,7 +119,7 @@ jobs: with: go-version: ">=1.20.0" - name: Add hosts to /etc/hosts - run: echo "127.0.0.1 mysql elasticsearch smtpimap" | sudo tee -a /etc/hosts + run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mysql elasticsearch smtpimap" | sudo tee -a /etc/hosts' - run: make deps-backend - run: make backend env: @@ -166,7 +166,7 @@ jobs: with: go-version: ">=1.20.0" - name: Add hosts to /etc/hosts - run: echo "127.0.0.1 mysql elasticsearch smtpimap" | sudo tee -a /etc/hosts + run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mysql elasticsearch smtpimap" | sudo tee -a /etc/hosts' - run: make deps-backend - run: make backend env: @@ -195,7 +195,7 @@ jobs: with: go-version: ">=1.20.0" - name: Add hosts to /etc/hosts - run: echo "127.0.0.1 mysql8" | sudo tee -a /etc/hosts + run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mysql8" | sudo tee -a /etc/hosts' - run: make deps-backend - run: make backend env: @@ -223,7 +223,7 @@ jobs: with: go-version: ">=1.20.0" - name: Add hosts to /etc/hosts - run: echo "127.0.0.1 mssql" | sudo tee -a /etc/hosts + run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mssql" | sudo tee -a /etc/hosts' - run: make deps-backend - run: make backend env: diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 38ee18410f..e22b1822c3 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1335,6 +1335,7 @@ ROUTER = console ;; Comma separated list of custom URL-Schemes that are allowed as links when rendering Markdown ;; for example git,magnet,ftp (more at https://en.wikipedia.org/wiki/List_of_URI_schemes) ;; URLs starting with http and https are always displayed, whatever is put in this entry. +;; If this entry is empty, all URL schemes are allowed. ;CUSTOM_URL_SCHEMES = ;; ;; List of file extensions that should be rendered/edited as Markdown diff --git a/docs/content/doc/administration/config-cheat-sheet.en-us.md b/docs/content/doc/administration/config-cheat-sheet.en-us.md index bca8769c20..9201027ae3 100644 --- a/docs/content/doc/administration/config-cheat-sheet.en-us.md +++ b/docs/content/doc/administration/config-cheat-sheet.en-us.md @@ -279,7 +279,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a trailing whitespace to paragraphs is not necessary to force a line break. - `CUSTOM_URL_SCHEMES`: Use a comma separated list (ftp,git,svn) to indicate additional URL hyperlinks to be rendered in Markdown. URLs beginning in http and https are - always displayed + always displayed. If this entry is empty, all URL schemes are allowed - `FILE_EXTENSIONS`: **.md,.markdown,.mdown,.mkd,.livemd**: List of file extensions that should be rendered/edited as Markdown. Separate the extensions with a comma. To render files without any extension as markdown, just put a comma. - `ENABLE_MATH`: **true**: Enables detection of `\(...\)`, `\[...\]`, `$...$` and `$$...$$` blocks as math blocks. diff --git a/go.mod b/go.mod index 24765df181..93b8052daf 100644 --- a/go.mod +++ b/go.mod @@ -76,7 +76,7 @@ require ( github.com/mattn/go-sqlite3 v1.14.16 github.com/meilisearch/meilisearch-go v0.24.0 github.com/mholt/archiver/v3 v3.5.1 - github.com/microcosm-cc/bluemonday v1.0.23 + github.com/microcosm-cc/bluemonday v1.0.24 github.com/minio/minio-go/v7 v7.0.52 github.com/minio/sha256-simd v1.0.0 github.com/msteinert/pam v1.1.0 @@ -109,7 +109,7 @@ require ( github.com/yuin/goldmark-meta v1.1.0 golang.org/x/crypto v0.8.0 golang.org/x/image v0.7.0 - golang.org/x/net v0.9.0 + golang.org/x/net v0.10.0 golang.org/x/oauth2 v0.7.0 golang.org/x/sys v0.8.0 golang.org/x/text v0.9.0 @@ -288,7 +288,6 @@ require ( go.uber.org/zap v1.24.0 // indirect golang.org/x/mod v0.10.0 // indirect golang.org/x/sync v0.2.0 // indirect - golang.org/x/term v0.8.0 // indirect golang.org/x/time v0.3.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect diff --git a/go.sum b/go.sum index 62c01c690a..8e5b728aac 100644 --- a/go.sum +++ b/go.sum @@ -876,8 +876,8 @@ github.com/mholt/acmez v1.1.0 h1:IQ9CGHKOHokorxnffsqDvmmE30mDenO1lptYZ1AYkHY= github.com/mholt/acmez v1.1.0/go.mod h1:zwo5+fbLLTowAX8o8ETfQzbDtwGEXnPhkmGdKIP+bgs= github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= -github.com/microcosm-cc/bluemonday v1.0.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY= -github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4= +github.com/microcosm-cc/bluemonday v1.0.24 h1:NGQoPtwGVcbGkKfvyYk1yRqknzBuoMiUrO6R7uFTPlw= +github.com/microcosm-cc/bluemonday v1.0.24/go.mod h1:ArQySAMps0790cHSkdPEJ7bGkF2VePWH773hsJNSHf8= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.54 h1:5jon9mWcb0sFJGpnI99tOMhCPyJ+RPVz5b63MQG0VWI= github.com/miekg/dns v1.1.54/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= @@ -1418,8 +1418,9 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1543,7 +1544,6 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/models/actions/artifact.go b/models/actions/artifact.go new file mode 100644 index 0000000000..1b45fce067 --- /dev/null +++ b/models/actions/artifact.go @@ -0,0 +1,122 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +// This artifact server is inspired by https://github.com/nektos/act/blob/master/pkg/artifacts/server.go. +// It updates url setting and uses ObjectStore to handle artifacts persistence. + +package actions + +import ( + "context" + "errors" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" +) + +const ( + // ArtifactStatusUploadPending is the status of an artifact upload that is pending + ArtifactStatusUploadPending = 1 + // ArtifactStatusUploadConfirmed is the status of an artifact upload that is confirmed + ArtifactStatusUploadConfirmed = 2 + // ArtifactStatusUploadError is the status of an artifact upload that is errored + ArtifactStatusUploadError = 3 +) + +func init() { + db.RegisterModel(new(ActionArtifact)) +} + +// ActionArtifact is a file that is stored in the artifact storage. +type ActionArtifact struct { + ID int64 `xorm:"pk autoincr"` + RunID int64 `xorm:"index UNIQUE(runid_name)"` // The run id of the artifact + RunnerID int64 + RepoID int64 `xorm:"index"` + OwnerID int64 + CommitSHA string + StoragePath string // The path to the artifact in the storage + FileSize int64 // The size of the artifact in bytes + FileCompressedSize int64 // The size of the artifact in bytes after gzip compression + ContentEncoding string // The content encoding of the artifact + ArtifactPath string // The path to the artifact when runner uploads it + ArtifactName string `xorm:"UNIQUE(runid_name)"` // The name of the artifact when runner uploads it + Status int64 `xorm:"index"` // The status of the artifact, uploading, expired or need-delete + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated index"` +} + +// CreateArtifact create a new artifact with task info or get same named artifact in the same run +func CreateArtifact(ctx context.Context, t *ActionTask, artifactName string) (*ActionArtifact, error) { + if err := t.LoadJob(ctx); err != nil { + return nil, err + } + artifact, err := getArtifactByArtifactName(ctx, t.Job.RunID, artifactName) + if errors.Is(err, util.ErrNotExist) { + artifact := &ActionArtifact{ + RunID: t.Job.RunID, + RunnerID: t.RunnerID, + RepoID: t.RepoID, + OwnerID: t.OwnerID, + CommitSHA: t.CommitSHA, + Status: ArtifactStatusUploadPending, + } + if _, err := db.GetEngine(ctx).Insert(artifact); err != nil { + return nil, err + } + return artifact, nil + } else if err != nil { + return nil, err + } + return artifact, nil +} + +func getArtifactByArtifactName(ctx context.Context, runID int64, name string) (*ActionArtifact, error) { + var art ActionArtifact + has, err := db.GetEngine(ctx).Where("run_id = ? AND artifact_name = ?", runID, name).Get(&art) + if err != nil { + return nil, err + } else if !has { + return nil, util.ErrNotExist + } + return &art, nil +} + +// GetArtifactByID returns an artifact by id +func GetArtifactByID(ctx context.Context, id int64) (*ActionArtifact, error) { + var art ActionArtifact + has, err := db.GetEngine(ctx).ID(id).Get(&art) + if err != nil { + return nil, err + } else if !has { + return nil, util.ErrNotExist + } + + return &art, nil +} + +// UpdateArtifactByID updates an artifact by id +func UpdateArtifactByID(ctx context.Context, id int64, art *ActionArtifact) error { + art.ID = id + _, err := db.GetEngine(ctx).ID(id).AllCols().Update(art) + return err +} + +// ListArtifactsByRunID returns all artifacts of a run +func ListArtifactsByRunID(ctx context.Context, runID int64) ([]*ActionArtifact, error) { + arts := make([]*ActionArtifact, 0, 10) + return arts, db.GetEngine(ctx).Where("run_id=?", runID).Find(&arts) +} + +// ListUploadedArtifactsByRunID returns all uploaded artifacts of a run +func ListUploadedArtifactsByRunID(ctx context.Context, runID int64) ([]*ActionArtifact, error) { + arts := make([]*ActionArtifact, 0, 10) + return arts, db.GetEngine(ctx).Where("run_id=? AND status=?", runID, ArtifactStatusUploadConfirmed).Find(&arts) +} + +// ListArtifactsByRepoID returns all artifacts of a repo +func ListArtifactsByRepoID(ctx context.Context, repoID int64) ([]*ActionArtifact, error) { + arts := make([]*ActionArtifact, 0, 10) + return arts, db.GetEngine(ctx).Where("repo_id=?", repoID).Find(&arts) +} diff --git a/models/fixtures/access_token.yml b/models/fixtures/access_token.yml index 5ff5d66f66..791b3da1c4 100644 --- a/models/fixtures/access_token.yml +++ b/models/fixtures/access_token.yml @@ -30,4 +30,4 @@ token_last_eight: 69d28c91 created_unix: 946687980 updated_unix: 946687980 -#commented out tokens so you can see what they are in plaintext \ No newline at end of file +#commented out tokens so you can see what they are in plaintext diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml new file mode 100644 index 0000000000..2c2151f354 --- /dev/null +++ b/models/fixtures/action_run.yml @@ -0,0 +1,19 @@ +- + id: 791 + title: "update actions" + repo_id: 4 + owner_id: 1 + workflow_id: "artifact.yaml" + index: 187 + trigger_user_id: 1 + ref: "refs/heads/master" + commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" + event: "push" + is_fork_pull_request: 0 + status: 1 + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 diff --git a/models/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml new file mode 100644 index 0000000000..071998b979 --- /dev/null +++ b/models/fixtures/action_run_job.yml @@ -0,0 +1,14 @@ +- + id: 192 + run_id: 791 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + name: job_2 + attempt: 1 + job_id: job_2 + task_id: 47 + status: 1 + started: 1683636528 + stopped: 1683636626 diff --git a/models/fixtures/action_task.yml b/models/fixtures/action_task.yml new file mode 100644 index 0000000000..c78fb3c5d6 --- /dev/null +++ b/models/fixtures/action_task.yml @@ -0,0 +1,20 @@ +- + id: 47 + job_id: 192 + attempt: 3 + runner_id: 1 + status: 6 # 6 is the status code for "running", running task can upload artifacts + started: 1683636528 + stopped: 1683636626 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + token_hash: 6d8ef48297195edcc8e22c70b3020eaa06c52976db67d39b4260c64a69a2cc1508825121b7b8394e48e00b1bf8718b2a867e + token_salt: jVuKnSPGgy + token_last_eight: eeb1a71a + log_filename: artifact-test2/2f/47.log + log_in_storage: 1 + log_length: 707 + log_size: 90179 + log_expired: 0 diff --git a/models/git/lfs.go b/models/git/lfs.go index 0ba8e919d0..0f90b29200 100644 --- a/models/git/lfs.go +++ b/models/git/lfs.go @@ -390,7 +390,7 @@ func IterateLFSMetaObjectsForRepo(ctx context.Context, repoID int64, f func(cont for { beans := make([]*CountLFSMetaObject, 0, batchSize) - sess := engine.Select("`lfs_meta_object`.*, COUNT(`l1`.oid) AS `count`"). + sess := engine.Table("lfs_meta_object").Select("`lfs_meta_object`.*, COUNT(`l1`.oid) AS `count`"). Join("INNER", "`lfs_meta_object` AS l1", "`lfs_meta_object`.oid = `l1`.oid"). Where("`lfs_meta_object`.repository_id = ?", repoID) if !opts.OlderThan.IsZero() { diff --git a/models/issues/issue.go b/models/issues/issue.go index df38e68519..bf41a7ec28 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -8,28 +8,17 @@ import ( "context" "fmt" "regexp" - "sort" - "strings" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/organization" - "code.gitea.io/gitea/models/perm" - access_model "code.gitea.io/gitea/models/perm/access" project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" - system_model "code.gitea.io/gitea/models/system" - "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/base" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/references" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "xorm.io/builder" - "xorm.io/xorm" ) // ErrIssueNotExist represents a "IssueNotExist" kind of error. @@ -221,17 +210,6 @@ func (issue *Issue) GetPullRequest() (pr *PullRequest, err error) { return pr, err } -// LoadLabels loads labels -func (issue *Issue) LoadLabels(ctx context.Context) (err error) { - if issue.Labels == nil && issue.ID != 0 { - issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID) - if err != nil { - return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err) - } - } - return nil -} - // LoadPoster loads poster func (issue *Issue) LoadPoster(ctx context.Context) (err error) { if issue.Poster == nil && issue.PosterID != 0 { @@ -468,421 +446,6 @@ func (issue *Issue) IsPoster(uid int64) bool { return issue.OriginalAuthorID == 0 && issue.PosterID == uid } -func (issue *Issue) getLabels(ctx context.Context) (err error) { - if len(issue.Labels) > 0 { - return nil - } - - issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID) - if err != nil { - return fmt.Errorf("getLabelsByIssueID: %w", err) - } - return nil -} - -func clearIssueLabels(ctx context.Context, issue *Issue, doer *user_model.User) (err error) { - if err = issue.getLabels(ctx); err != nil { - return fmt.Errorf("getLabels: %w", err) - } - - for i := range issue.Labels { - if err = deleteIssueLabel(ctx, issue, issue.Labels[i], doer); err != nil { - return fmt.Errorf("removeLabel: %w", err) - } - } - - return nil -} - -// ClearIssueLabels removes all issue labels as the given user. -// Triggers appropriate WebHooks, if any. -func ClearIssueLabels(issue *Issue, doer *user_model.User) (err error) { - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return err - } - defer committer.Close() - - if err := issue.LoadRepo(ctx); err != nil { - return err - } else if err = issue.LoadPullRequest(ctx); err != nil { - return err - } - - perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) - if err != nil { - return err - } - if !perm.CanWriteIssuesOrPulls(issue.IsPull) { - return ErrRepoLabelNotExist{} - } - - if err = clearIssueLabels(ctx, issue, doer); err != nil { - return err - } - - if err = committer.Commit(); err != nil { - return fmt.Errorf("Commit: %w", err) - } - - return nil -} - -type labelSorter []*Label - -func (ts labelSorter) Len() int { - return len([]*Label(ts)) -} - -func (ts labelSorter) Less(i, j int) bool { - return []*Label(ts)[i].ID < []*Label(ts)[j].ID -} - -func (ts labelSorter) Swap(i, j int) { - []*Label(ts)[i], []*Label(ts)[j] = []*Label(ts)[j], []*Label(ts)[i] -} - -// Ensure only one label of a given scope exists, with labels at the end of the -// array getting preference over earlier ones. -func RemoveDuplicateExclusiveLabels(labels []*Label) []*Label { - validLabels := make([]*Label, 0, len(labels)) - - for i, label := range labels { - scope := label.ExclusiveScope() - if scope != "" { - foundOther := false - for _, otherLabel := range labels[i+1:] { - if otherLabel.ExclusiveScope() == scope { - foundOther = true - break - } - } - if foundOther { - continue - } - } - validLabels = append(validLabels, label) - } - - return validLabels -} - -// ReplaceIssueLabels removes all current labels and add new labels to the issue. -// Triggers appropriate WebHooks, if any. -func ReplaceIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (err error) { - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return err - } - defer committer.Close() - - if err = issue.LoadRepo(ctx); err != nil { - return err - } - - if err = issue.LoadLabels(ctx); err != nil { - return err - } - - labels = RemoveDuplicateExclusiveLabels(labels) - - sort.Sort(labelSorter(labels)) - sort.Sort(labelSorter(issue.Labels)) - - var toAdd, toRemove []*Label - - addIndex, removeIndex := 0, 0 - for addIndex < len(labels) && removeIndex < len(issue.Labels) { - addLabel := labels[addIndex] - removeLabel := issue.Labels[removeIndex] - if addLabel.ID == removeLabel.ID { - // Silently drop invalid labels - if removeLabel.RepoID != issue.RepoID && removeLabel.OrgID != issue.Repo.OwnerID { - toRemove = append(toRemove, removeLabel) - } - - addIndex++ - removeIndex++ - } else if addLabel.ID < removeLabel.ID { - // Only add if the label is valid - if addLabel.RepoID == issue.RepoID || addLabel.OrgID == issue.Repo.OwnerID { - toAdd = append(toAdd, addLabel) - } - addIndex++ - } else { - toRemove = append(toRemove, removeLabel) - removeIndex++ - } - } - toAdd = append(toAdd, labels[addIndex:]...) - toRemove = append(toRemove, issue.Labels[removeIndex:]...) - - if len(toAdd) > 0 { - if err = newIssueLabels(ctx, issue, toAdd, doer); err != nil { - return fmt.Errorf("addLabels: %w", err) - } - } - - for _, l := range toRemove { - if err = deleteIssueLabel(ctx, issue, l, doer); err != nil { - return fmt.Errorf("removeLabel: %w", err) - } - } - - issue.Labels = nil - if err = issue.LoadLabels(ctx); err != nil { - return err - } - - return committer.Commit() -} - -// UpdateIssueCols updates cols of issue -func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error { - if _, err := db.GetEngine(ctx).ID(issue.ID).Cols(cols...).Update(issue); err != nil { - return err - } - return nil -} - -func changeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed, isMergePull bool) (*Comment, error) { - // Reload the issue - currentIssue, err := GetIssueByID(ctx, issue.ID) - if err != nil { - return nil, err - } - - // Nothing should be performed if current status is same as target status - if currentIssue.IsClosed == isClosed { - if !issue.IsPull { - return nil, ErrIssueWasClosed{ - ID: issue.ID, - } - } - return nil, ErrPullWasClosed{ - ID: issue.ID, - } - } - - issue.IsClosed = isClosed - return doChangeIssueStatus(ctx, issue, doer, isMergePull) -} - -func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isMergePull bool) (*Comment, error) { - // Check for open dependencies - if issue.IsClosed && issue.Repo.IsDependenciesEnabled(ctx) { - // only check if dependencies are enabled and we're about to close an issue, otherwise reopening an issue would fail when there are unsatisfied dependencies - noDeps, err := IssueNoDependenciesLeft(ctx, issue) - if err != nil { - return nil, err - } - - if !noDeps { - return nil, ErrDependenciesLeft{issue.ID} - } - } - - if issue.IsClosed { - issue.ClosedUnix = timeutil.TimeStampNow() - } else { - issue.ClosedUnix = 0 - } - - if err := UpdateIssueCols(ctx, issue, "is_closed", "closed_unix"); err != nil { - return nil, err - } - - // Update issue count of labels - if err := issue.getLabels(ctx); err != nil { - return nil, err - } - for idx := range issue.Labels { - if err := updateLabelCols(ctx, issue.Labels[idx], "num_issues", "num_closed_issue"); err != nil { - return nil, err - } - } - - // Update issue count of milestone - if issue.MilestoneID > 0 { - if err := UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil { - return nil, err - } - } - - // update repository's issue closed number - if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, true); err != nil { - return nil, err - } - - // New action comment - cmtType := CommentTypeClose - if !issue.IsClosed { - cmtType = CommentTypeReopen - } else if isMergePull { - cmtType = CommentTypeMergePull - } - - return CreateComment(ctx, &CreateCommentOptions{ - Type: cmtType, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - }) -} - -// ChangeIssueStatus changes issue status to open or closed. -func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed bool) (*Comment, error) { - if err := issue.LoadRepo(ctx); err != nil { - return nil, err - } - if err := issue.LoadPoster(ctx); err != nil { - return nil, err - } - - return changeIssueStatus(ctx, issue, doer, isClosed, false) -} - -// ChangeIssueTitle changes the title of this issue, as the given user. -func ChangeIssueTitle(ctx context.Context, issue *Issue, doer *user_model.User, oldTitle string) (err error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - if err = UpdateIssueCols(ctx, issue, "name"); err != nil { - return fmt.Errorf("updateIssueCols: %w", err) - } - - if err = issue.LoadRepo(ctx); err != nil { - return fmt.Errorf("loadRepo: %w", err) - } - - opts := &CreateCommentOptions{ - Type: CommentTypeChangeTitle, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - OldTitle: oldTitle, - NewTitle: issue.Title, - } - if _, err = CreateComment(ctx, opts); err != nil { - return fmt.Errorf("createComment: %w", err) - } - if err = issue.AddCrossReferences(ctx, doer, true); err != nil { - return err - } - - return committer.Commit() -} - -// ChangeIssueRef changes the branch of this issue, as the given user. -func ChangeIssueRef(issue *Issue, doer *user_model.User, oldRef string) (err error) { - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return err - } - defer committer.Close() - - if err = UpdateIssueCols(ctx, issue, "ref"); err != nil { - return fmt.Errorf("updateIssueCols: %w", err) - } - - if err = issue.LoadRepo(ctx); err != nil { - return fmt.Errorf("loadRepo: %w", err) - } - oldRefFriendly := strings.TrimPrefix(oldRef, git.BranchPrefix) - newRefFriendly := strings.TrimPrefix(issue.Ref, git.BranchPrefix) - - opts := &CreateCommentOptions{ - Type: CommentTypeChangeIssueRef, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - OldRef: oldRefFriendly, - NewRef: newRefFriendly, - } - if _, err = CreateComment(ctx, opts); err != nil { - return fmt.Errorf("createComment: %w", err) - } - - return committer.Commit() -} - -// AddDeletePRBranchComment adds delete branch comment for pull request issue -func AddDeletePRBranchComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issueID int64, branchName string) error { - issue, err := GetIssueByID(ctx, issueID) - if err != nil { - return err - } - opts := &CreateCommentOptions{ - Type: CommentTypeDeleteBranch, - Doer: doer, - Repo: repo, - Issue: issue, - OldRef: branchName, - } - _, err = CreateComment(ctx, opts) - return err -} - -// UpdateIssueAttachments update attachments by UUIDs for the issue -func UpdateIssueAttachments(issueID int64, uuids []string) (err error) { - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return err - } - defer committer.Close() - attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids) - if err != nil { - return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err) - } - for i := 0; i < len(attachments); i++ { - attachments[i].IssueID = issueID - if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { - return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err) - } - } - return committer.Commit() -} - -// ChangeIssueContent changes issue content, as the given user. -func ChangeIssueContent(issue *Issue, doer *user_model.User, content string) (err error) { - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return err - } - defer committer.Close() - - hasContentHistory, err := HasIssueContentHistory(ctx, issue.ID, 0) - if err != nil { - return fmt.Errorf("HasIssueContentHistory: %w", err) - } - if !hasContentHistory { - if err = SaveIssueContentHistory(ctx, issue.PosterID, issue.ID, 0, - issue.CreatedUnix, issue.Content, true); err != nil { - return fmt.Errorf("SaveIssueContentHistory: %w", err) - } - } - - issue.Content = content - - if err = UpdateIssueCols(ctx, issue, "content"); err != nil { - return fmt.Errorf("UpdateIssueCols: %w", err) - } - - if err = SaveIssueContentHistory(ctx, doer.ID, issue.ID, 0, - timeutil.TimeStampNow(), issue.Content, false); err != nil { - return fmt.Errorf("SaveIssueContentHistory: %w", err) - } - - if err = issue.AddCrossReferences(ctx, doer, true); err != nil { - return fmt.Errorf("addCrossReferences: %w", err) - } - - return committer.Commit() -} - // GetTasks returns the amount of tasks in the issues content func (issue *Issue) GetTasks() int { return len(issueTasksPat.FindAllStringIndex(issue.Content, -1)) @@ -937,149 +500,6 @@ func (issue *Issue) GetLastEventLabelFake() string { return "repo.issues.opened_by_fake" } -// NewIssueOptions represents the options of a new issue. -type NewIssueOptions struct { - Repo *repo_model.Repository - Issue *Issue - LabelIDs []int64 - Attachments []string // In UUID format. - IsPull bool -} - -// NewIssueWithIndex creates issue with given index -func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssueOptions) (err error) { - e := db.GetEngine(ctx) - opts.Issue.Title = strings.TrimSpace(opts.Issue.Title) - - if opts.Issue.MilestoneID > 0 { - milestone, err := GetMilestoneByRepoID(ctx, opts.Issue.RepoID, opts.Issue.MilestoneID) - if err != nil && !IsErrMilestoneNotExist(err) { - return fmt.Errorf("getMilestoneByID: %w", err) - } - - // Assume milestone is invalid and drop silently. - opts.Issue.MilestoneID = 0 - if milestone != nil { - opts.Issue.MilestoneID = milestone.ID - opts.Issue.Milestone = milestone - } - } - - if opts.Issue.Index <= 0 { - return fmt.Errorf("no issue index provided") - } - if opts.Issue.ID > 0 { - return fmt.Errorf("issue exist") - } - - if _, err := e.Insert(opts.Issue); err != nil { - return err - } - - if opts.Issue.MilestoneID > 0 { - if err := UpdateMilestoneCounters(ctx, opts.Issue.MilestoneID); err != nil { - return err - } - - opts := &CreateCommentOptions{ - Type: CommentTypeMilestone, - Doer: doer, - Repo: opts.Repo, - Issue: opts.Issue, - OldMilestoneID: 0, - MilestoneID: opts.Issue.MilestoneID, - } - if _, err = CreateComment(ctx, opts); err != nil { - return err - } - } - - if err := repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.IsPull, false); err != nil { - return err - } - - if len(opts.LabelIDs) > 0 { - // During the session, SQLite3 driver cannot handle retrieve objects after update something. - // So we have to get all needed labels first. - labels := make([]*Label, 0, len(opts.LabelIDs)) - if err = e.In("id", opts.LabelIDs).Find(&labels); err != nil { - return fmt.Errorf("find all labels [label_ids: %v]: %w", opts.LabelIDs, err) - } - - if err = opts.Issue.LoadPoster(ctx); err != nil { - return err - } - - for _, label := range labels { - // Silently drop invalid labels. - if label.RepoID != opts.Repo.ID && label.OrgID != opts.Repo.OwnerID { - continue - } - - if err = newIssueLabel(ctx, opts.Issue, label, opts.Issue.Poster); err != nil { - return fmt.Errorf("addLabel [id: %d]: %w", label.ID, err) - } - } - } - - if err = NewIssueUsers(ctx, opts.Repo, opts.Issue); err != nil { - return err - } - - if len(opts.Attachments) > 0 { - attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments) - if err != nil { - return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err) - } - - for i := 0; i < len(attachments); i++ { - attachments[i].IssueID = opts.Issue.ID - if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil { - return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err) - } - } - } - if err = opts.Issue.LoadAttributes(ctx); err != nil { - return err - } - - return opts.Issue.AddCrossReferences(ctx, doer, false) -} - -// NewIssue creates new issue with labels for repository. -func NewIssue(repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) { - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return err - } - defer committer.Close() - - idx, err := db.GetNextResourceIndex(ctx, "issue_index", repo.ID) - if err != nil { - return fmt.Errorf("generate issue index failed: %w", err) - } - - issue.Index = idx - - if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ - Repo: repo, - Issue: issue, - LabelIDs: labelIDs, - Attachments: uuids, - }); err != nil { - if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { - return err - } - return fmt.Errorf("newIssue: %w", err) - } - - if err = committer.Commit(); err != nil { - return fmt.Errorf("Commit: %w", err) - } - - return nil -} - // GetIssueByIndex returns raw issue without loading attributes by index in a repository. func GetIssueByIndex(repoID, index int64) (*Issue, error) { if index < 1 { @@ -1141,449 +561,6 @@ func GetIssueIDsByRepoID(ctx context.Context, repoID int64) ([]int64, error) { return ids, err } -// IssuesOptions represents options of an issue. -type IssuesOptions struct { //nolint - db.ListOptions - RepoID int64 // overwrites RepoCond if not 0 - RepoCond builder.Cond - AssigneeID int64 - PosterID int64 - MentionedID int64 - ReviewRequestedID int64 - ReviewedID int64 - SubscriberID int64 - MilestoneIDs []int64 - ProjectID int64 - ProjectBoardID int64 - IsClosed util.OptionalBool - IsPull util.OptionalBool - LabelIDs []int64 - IncludedLabelNames []string - ExcludedLabelNames []string - IncludeMilestones []string - SortType string - IssueIDs []int64 - UpdatedAfterUnix int64 - UpdatedBeforeUnix int64 - // prioritize issues from this repo - PriorityRepoID int64 - IsArchived util.OptionalBool - Org *organization.Organization // issues permission scope - Team *organization.Team // issues permission scope - User *user_model.User // issues permission scope -} - -// sortIssuesSession sort an issues-related session based on the provided -// sortType string -func sortIssuesSession(sess *xorm.Session, sortType string, priorityRepoID int64) { - switch sortType { - case "oldest": - sess.Asc("issue.created_unix").Asc("issue.id") - case "recentupdate": - sess.Desc("issue.updated_unix").Desc("issue.created_unix").Desc("issue.id") - case "leastupdate": - sess.Asc("issue.updated_unix").Asc("issue.created_unix").Asc("issue.id") - case "mostcomment": - sess.Desc("issue.num_comments").Desc("issue.created_unix").Desc("issue.id") - case "leastcomment": - sess.Asc("issue.num_comments").Desc("issue.created_unix").Desc("issue.id") - case "priority": - sess.Desc("issue.priority").Desc("issue.created_unix").Desc("issue.id") - case "nearduedate": - // 253370764800 is 01/01/9999 @ 12:00am (UTC) - sess.Join("LEFT", "milestone", "issue.milestone_id = milestone.id"). - OrderBy("CASE " + - "WHEN issue.deadline_unix = 0 AND (milestone.deadline_unix = 0 OR milestone.deadline_unix IS NULL) THEN 253370764800 " + - "WHEN milestone.deadline_unix = 0 OR milestone.deadline_unix IS NULL THEN issue.deadline_unix " + - "WHEN milestone.deadline_unix < issue.deadline_unix OR issue.deadline_unix = 0 THEN milestone.deadline_unix " + - "ELSE issue.deadline_unix END ASC"). - Desc("issue.created_unix"). - Desc("issue.id") - case "farduedate": - sess.Join("LEFT", "milestone", "issue.milestone_id = milestone.id"). - OrderBy("CASE " + - "WHEN milestone.deadline_unix IS NULL THEN issue.deadline_unix " + - "WHEN milestone.deadline_unix < issue.deadline_unix OR issue.deadline_unix = 0 THEN milestone.deadline_unix " + - "ELSE issue.deadline_unix END DESC"). - Desc("issue.created_unix"). - Desc("issue.id") - case "priorityrepo": - sess.OrderBy("CASE "+ - "WHEN issue.repo_id = ? THEN 1 "+ - "ELSE 2 END ASC", priorityRepoID). - Desc("issue.created_unix"). - Desc("issue.id") - case "project-column-sorting": - sess.Asc("project_issue.sorting").Desc("issue.created_unix").Desc("issue.id") - default: - sess.Desc("issue.created_unix").Desc("issue.id") - } -} - -func (opts *IssuesOptions) setupSessionWithLimit(sess *xorm.Session) { - if opts.Page >= 0 && opts.PageSize > 0 { - var start int - if opts.Page == 0 { - start = 0 - } else { - start = (opts.Page - 1) * opts.PageSize - } - sess.Limit(opts.PageSize, start) - } - opts.setupSessionNoLimit(sess) -} - -func (opts *IssuesOptions) setupSessionNoLimit(sess *xorm.Session) { - if len(opts.IssueIDs) > 0 { - sess.In("issue.id", opts.IssueIDs) - } - - if opts.RepoID != 0 { - opts.RepoCond = builder.Eq{"issue.repo_id": opts.RepoID} - } - if opts.RepoCond != nil { - sess.And(opts.RepoCond) - } - - if !opts.IsClosed.IsNone() { - sess.And("issue.is_closed=?", opts.IsClosed.IsTrue()) - } - - if opts.AssigneeID > 0 { - applyAssigneeCondition(sess, opts.AssigneeID) - } else if opts.AssigneeID == db.NoConditionID { - sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)") - } - - if opts.PosterID > 0 { - applyPosterCondition(sess, opts.PosterID) - } - - if opts.MentionedID > 0 { - applyMentionedCondition(sess, opts.MentionedID) - } - - if opts.ReviewRequestedID > 0 { - applyReviewRequestedCondition(sess, opts.ReviewRequestedID) - } - - if opts.ReviewedID > 0 { - applyReviewedCondition(sess, opts.ReviewedID) - } - - if opts.SubscriberID > 0 { - applySubscribedCondition(sess, opts.SubscriberID) - } - - if len(opts.MilestoneIDs) == 1 && opts.MilestoneIDs[0] == db.NoConditionID { - sess.And("issue.milestone_id = 0") - } else if len(opts.MilestoneIDs) > 0 { - sess.In("issue.milestone_id", opts.MilestoneIDs) - } - - if opts.UpdatedAfterUnix != 0 { - sess.And(builder.Gte{"issue.updated_unix": opts.UpdatedAfterUnix}) - } - if opts.UpdatedBeforeUnix != 0 { - sess.And(builder.Lte{"issue.updated_unix": opts.UpdatedBeforeUnix}) - } - - if opts.ProjectID > 0 { - sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id"). - And("project_issue.project_id=?", opts.ProjectID) - } else if opts.ProjectID == db.NoConditionID { // show those that are in no project - sess.And(builder.NotIn("issue.id", builder.Select("issue_id").From("project_issue"))) - } - - if opts.ProjectBoardID != 0 { - if opts.ProjectBoardID > 0 { - sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectBoardID})) - } else { - sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0})) - } - } - - switch opts.IsPull { - case util.OptionalBoolTrue: - sess.And("issue.is_pull=?", true) - case util.OptionalBoolFalse: - sess.And("issue.is_pull=?", false) - } - - if opts.IsArchived != util.OptionalBoolNone { - sess.And(builder.Eq{"repository.is_archived": opts.IsArchived.IsTrue()}) - } - - if len(opts.LabelIDs) > 0 { - if opts.LabelIDs[0] == 0 { - sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_label)") - } else { - for i, labelID := range opts.LabelIDs { - if labelID > 0 { - sess.Join("INNER", fmt.Sprintf("issue_label il%d", i), - fmt.Sprintf("issue.id = il%[1]d.issue_id AND il%[1]d.label_id = %[2]d", i, labelID)) - } else if labelID < 0 { // 0 is not supported here, so just ignore it - sess.Where("issue.id not in (select issue_id from issue_label where label_id = ?)", -labelID) - } - } - } - } - - if len(opts.IncludedLabelNames) > 0 { - sess.In("issue.id", BuildLabelNamesIssueIDsCondition(opts.IncludedLabelNames)) - } - - if len(opts.ExcludedLabelNames) > 0 { - sess.And(builder.NotIn("issue.id", BuildLabelNamesIssueIDsCondition(opts.ExcludedLabelNames))) - } - - if len(opts.IncludeMilestones) > 0 { - sess.In("issue.milestone_id", - builder.Select("id"). - From("milestone"). - Where(builder.In("name", opts.IncludeMilestones))) - } - - if opts.User != nil { - sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.IsTrue())) - } -} - -// teamUnitsRepoCond returns query condition for those repo id in the special org team with special units access -func teamUnitsRepoCond(id string, userID, orgID, teamID int64, units ...unit.Type) builder.Cond { - return builder.In(id, - builder.Select("repo_id").From("team_repo").Where( - builder.Eq{ - "team_id": teamID, - }.And( - builder.Or( - // Check if the user is member of the team. - builder.In( - "team_id", builder.Select("team_id").From("team_user").Where( - builder.Eq{ - "uid": userID, - }, - ), - ), - // Check if the user is in the owner team of the organisation. - builder.Exists(builder.Select("team_id").From("team_user"). - Where(builder.Eq{ - "org_id": orgID, - "team_id": builder.Select("id").From("team").Where( - builder.Eq{ - "org_id": orgID, - "lower_name": strings.ToLower(organization.OwnerTeamName), - }), - "uid": userID, - }), - ), - )).And( - builder.In( - "team_id", builder.Select("team_id").From("team_unit").Where( - builder.Eq{ - "`team_unit`.org_id": orgID, - }.And( - builder.In("`team_unit`.type", units), - ), - ), - ), - ), - )) -} - -// issuePullAccessibleRepoCond userID must not be zero, this condition require join repository table -func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *organization.Organization, team *organization.Team, isPull bool) builder.Cond { - cond := builder.NewCond() - unitType := unit.TypeIssues - if isPull { - unitType = unit.TypePullRequests - } - if org != nil { - if team != nil { - cond = cond.And(teamUnitsRepoCond(repoIDstr, userID, org.ID, team.ID, unitType)) // special team member repos - } else { - cond = cond.And( - builder.Or( - repo_model.UserOrgUnitRepoCond(repoIDstr, userID, org.ID, unitType), // team member repos - repo_model.UserOrgPublicUnitRepoCond(userID, org.ID), // user org public non-member repos, TODO: check repo has issues - ), - ) - } - } else { - cond = cond.And( - builder.Or( - repo_model.UserOwnedRepoCond(userID), // owned repos - repo_model.UserAccessRepoCond(repoIDstr, userID), // user can access repo in a unit independent way - repo_model.UserAssignedRepoCond(repoIDstr, userID), // user has been assigned accessible public repos - repo_model.UserMentionedRepoCond(repoIDstr, userID), // user has been mentioned accessible public repos - repo_model.UserCreateIssueRepoCond(repoIDstr, userID, isPull), // user has created issue/pr accessible public repos - ), - ) - } - return cond -} - -func applyAssigneeCondition(sess *xorm.Session, assigneeID int64) *xorm.Session { - return sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). - And("issue_assignees.assignee_id = ?", assigneeID) -} - -func applyPosterCondition(sess *xorm.Session, posterID int64) *xorm.Session { - return sess.And("issue.poster_id=?", posterID) -} - -func applyMentionedCondition(sess *xorm.Session, mentionedID int64) *xorm.Session { - return sess.Join("INNER", "issue_user", "issue.id = issue_user.issue_id"). - And("issue_user.is_mentioned = ?", true). - And("issue_user.uid = ?", mentionedID) -} - -func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64) *xorm.Session { - return sess.Join("INNER", []string{"review", "r"}, "issue.id = r.issue_id"). - And("issue.poster_id <> ?", reviewRequestedID). - And("r.type = ?", ReviewTypeRequest). - And("r.reviewer_id = ? and r.id in (select max(id) from review where issue_id = r.issue_id and reviewer_id = r.reviewer_id and type in (?, ?, ?))"+ - " or r.reviewer_team_id in (select team_id from team_user where uid = ?)", - reviewRequestedID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, reviewRequestedID) -} - -func applyReviewedCondition(sess *xorm.Session, reviewedID int64) *xorm.Session { - // Query for pull requests where you are a reviewer or commenter, excluding - // any pull requests already returned by the the review requested filter. - notPoster := builder.Neq{"issue.poster_id": reviewedID} - reviewed := builder.In("issue.id", builder. - Select("issue_id"). - From("review"). - Where(builder.And( - builder.Neq{"type": ReviewTypeRequest}, - builder.Or( - builder.Eq{"reviewer_id": reviewedID}, - builder.In("reviewer_team_id", builder. - Select("team_id"). - From("team_user"). - Where(builder.Eq{"uid": reviewedID}), - ), - ), - )), - ) - commented := builder.In("issue.id", builder. - Select("issue_id"). - From("comment"). - Where(builder.And( - builder.Eq{"poster_id": reviewedID}, - builder.In("type", CommentTypeComment, CommentTypeCode, CommentTypeReview), - )), - ) - return sess.And(notPoster, builder.Or(reviewed, commented)) -} - -func applySubscribedCondition(sess *xorm.Session, subscriberID int64) *xorm.Session { - return sess.And( - builder. - NotIn("issue.id", - builder.Select("issue_id"). - From("issue_watch"). - Where(builder.Eq{"is_watching": false, "user_id": subscriberID}), - ), - ).And( - builder.Or( - builder.In("issue.id", builder. - Select("issue_id"). - From("issue_watch"). - Where(builder.Eq{"is_watching": true, "user_id": subscriberID}), - ), - builder.In("issue.id", builder. - Select("issue_id"). - From("comment"). - Where(builder.Eq{"poster_id": subscriberID}), - ), - builder.Eq{"issue.poster_id": subscriberID}, - builder.In("issue.repo_id", builder. - Select("id"). - From("watch"). - Where(builder.And(builder.Eq{"user_id": subscriberID}, - builder.In("mode", repo_model.WatchModeNormal, repo_model.WatchModeAuto))), - ), - ), - ) -} - -// CountIssuesByRepo map from repoID to number of issues matching the options -func CountIssuesByRepo(ctx context.Context, opts *IssuesOptions) (map[int64]int64, error) { - sess := db.GetEngine(ctx). - Join("INNER", "repository", "`issue`.repo_id = `repository`.id") - - opts.setupSessionNoLimit(sess) - - countsSlice := make([]*struct { - RepoID int64 - Count int64 - }, 0, 10) - if err := sess.GroupBy("issue.repo_id"). - Select("issue.repo_id AS repo_id, COUNT(*) AS count"). - Table("issue"). - Find(&countsSlice); err != nil { - return nil, fmt.Errorf("unable to CountIssuesByRepo: %w", err) - } - - countMap := make(map[int64]int64, len(countsSlice)) - for _, c := range countsSlice { - countMap[c.RepoID] = c.Count - } - return countMap, nil -} - -// GetRepoIDsForIssuesOptions find all repo ids for the given options -func GetRepoIDsForIssuesOptions(opts *IssuesOptions, user *user_model.User) ([]int64, error) { - repoIDs := make([]int64, 0, 5) - e := db.GetEngine(db.DefaultContext) - - sess := e.Join("INNER", "repository", "`issue`.repo_id = `repository`.id") - - opts.setupSessionNoLimit(sess) - - accessCond := repo_model.AccessibleRepositoryCondition(user, unit.TypeInvalid) - if err := sess.Where(accessCond). - Distinct("issue.repo_id"). - Table("issue"). - Find(&repoIDs); err != nil { - return nil, fmt.Errorf("unable to GetRepoIDsForIssuesOptions: %w", err) - } - - return repoIDs, nil -} - -// Issues returns a list of issues by given conditions. -func Issues(ctx context.Context, opts *IssuesOptions) ([]*Issue, error) { - sess := db.GetEngine(ctx). - Join("INNER", "repository", "`issue`.repo_id = `repository`.id") - opts.setupSessionWithLimit(sess) - - sortIssuesSession(sess, opts.SortType, opts.PriorityRepoID) - - issues := make([]*Issue, 0, opts.ListOptions.PageSize) - if err := sess.Find(&issues); err != nil { - return nil, fmt.Errorf("unable to query Issues: %w", err) - } - - if err := IssueList(issues).LoadAttributes(); err != nil { - return nil, fmt.Errorf("unable to LoadAttributes for Issues: %w", err) - } - - return issues, nil -} - -// CountIssues number return of issues by given conditions. -func CountIssues(ctx context.Context, opts *IssuesOptions) (int64, error) { - sess := db.GetEngine(ctx). - Select("COUNT(issue.id) AS count"). - Table("issue"). - Join("INNER", "repository", "`issue`.repo_id = `repository`.id") - opts.setupSessionNoLimit(sess) - - return sess.Count() -} - // GetParticipantsIDsByIssueID returns the IDs of all users who participated in comments of an issue, // but skips joining with `user` for performance reasons. // User permissions must be verified elsewhere if required. @@ -1608,515 +585,6 @@ func IsUserParticipantsOfIssue(user *user_model.User, issue *Issue) bool { return util.SliceContains(userIDs, user.ID) } -// UpdateIssueMentions updates issue-user relations for mentioned users. -func UpdateIssueMentions(ctx context.Context, issueID int64, mentions []*user_model.User) error { - if len(mentions) == 0 { - return nil - } - ids := make([]int64, len(mentions)) - for i, u := range mentions { - ids[i] = u.ID - } - if err := UpdateIssueUsersByMentions(ctx, issueID, ids); err != nil { - return fmt.Errorf("UpdateIssueUsersByMentions: %w", err) - } - return nil -} - -// IssueStats represents issue statistic information. -type IssueStats struct { - OpenCount, ClosedCount int64 - YourRepositoriesCount int64 - AssignCount int64 - CreateCount int64 - MentionCount int64 - ReviewRequestedCount int64 - ReviewedCount int64 -} - -// Filter modes. -const ( - FilterModeAll = iota - FilterModeAssign - FilterModeCreate - FilterModeMention - FilterModeReviewRequested - FilterModeReviewed - FilterModeYourRepositories -) - -// IssueStatsOptions contains parameters accepted by GetIssueStats. -type IssueStatsOptions struct { - RepoID int64 - Labels string - MilestoneID int64 - ProjectID int64 - AssigneeID int64 - MentionedID int64 - PosterID int64 - ReviewRequestedID int64 - ReviewedID int64 - IsPull util.OptionalBool - IssueIDs []int64 -} - -const ( - // MaxQueryParameters represents the max query parameters - // When queries are broken down in parts because of the number - // of parameters, attempt to break by this amount - MaxQueryParameters = 300 -) - -// GetIssueStats returns issue statistic information by given conditions. -func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) { - if len(opts.IssueIDs) <= MaxQueryParameters { - return getIssueStatsChunk(opts, opts.IssueIDs) - } - - // If too long a list of IDs is provided, we get the statistics in - // smaller chunks and get accumulates. Note: this could potentially - // get us invalid results. The alternative is to insert the list of - // ids in a temporary table and join from them. - accum := &IssueStats{} - for i := 0; i < len(opts.IssueIDs); { - chunk := i + MaxQueryParameters - if chunk > len(opts.IssueIDs) { - chunk = len(opts.IssueIDs) - } - stats, err := getIssueStatsChunk(opts, opts.IssueIDs[i:chunk]) - if err != nil { - return nil, err - } - accum.OpenCount += stats.OpenCount - accum.ClosedCount += stats.ClosedCount - accum.YourRepositoriesCount += stats.YourRepositoriesCount - accum.AssignCount += stats.AssignCount - accum.CreateCount += stats.CreateCount - accum.OpenCount += stats.MentionCount - accum.ReviewRequestedCount += stats.ReviewRequestedCount - accum.ReviewedCount += stats.ReviewedCount - i = chunk - } - return accum, nil -} - -func getIssueStatsChunk(opts *IssueStatsOptions, issueIDs []int64) (*IssueStats, error) { - stats := &IssueStats{} - - countSession := func(opts *IssueStatsOptions, issueIDs []int64) *xorm.Session { - sess := db.GetEngine(db.DefaultContext). - Where("issue.repo_id = ?", opts.RepoID) - - if len(issueIDs) > 0 { - sess.In("issue.id", issueIDs) - } - - if len(opts.Labels) > 0 { - labelIDs, err := base.StringsToInt64s(strings.Split(opts.Labels, ",")) - if err != nil { - log.Warn("Malformed Labels argument: %s", opts.Labels) - } else { - if labelIDs[0] == 0 { - sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_label)") - } else { - for i, labelID := range labelIDs { - if labelID > 0 { - sess.Join("INNER", fmt.Sprintf("issue_label il%d", i), - fmt.Sprintf("issue.id = il%[1]d.issue_id AND il%[1]d.label_id = %[2]d", i, labelID)) - } else if labelID < 0 { // 0 is not supported here, so just ignore it - sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_label WHERE label_id = ?)", -labelID) - } - } - } - } - } - - if opts.MilestoneID > 0 { - sess.And("issue.milestone_id = ?", opts.MilestoneID) - } else if opts.MilestoneID == db.NoConditionID { - sess.And("issue.milestone_id = 0") - } - - if opts.ProjectID > 0 { - sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id"). - And("project_issue.project_id=?", opts.ProjectID) - } - - if opts.AssigneeID > 0 { - applyAssigneeCondition(sess, opts.AssigneeID) - } else if opts.AssigneeID == db.NoConditionID { - sess.Where("id NOT IN (SELECT issue_id FROM issue_assignees)") - } - - if opts.PosterID > 0 { - applyPosterCondition(sess, opts.PosterID) - } - - if opts.MentionedID > 0 { - applyMentionedCondition(sess, opts.MentionedID) - } - - if opts.ReviewRequestedID > 0 { - applyReviewRequestedCondition(sess, opts.ReviewRequestedID) - } - - if opts.ReviewedID > 0 { - applyReviewedCondition(sess, opts.ReviewedID) - } - - switch opts.IsPull { - case util.OptionalBoolTrue: - sess.And("issue.is_pull=?", true) - case util.OptionalBoolFalse: - sess.And("issue.is_pull=?", false) - } - - return sess - } - - var err error - stats.OpenCount, err = countSession(opts, issueIDs). - And("issue.is_closed = ?", false). - Count(new(Issue)) - if err != nil { - return stats, err - } - stats.ClosedCount, err = countSession(opts, issueIDs). - And("issue.is_closed = ?", true). - Count(new(Issue)) - return stats, err -} - -// UserIssueStatsOptions contains parameters accepted by GetUserIssueStats. -type UserIssueStatsOptions struct { - UserID int64 - RepoIDs []int64 - FilterMode int - IsPull bool - IsClosed bool - IssueIDs []int64 - IsArchived util.OptionalBool - LabelIDs []int64 - RepoCond builder.Cond - Org *organization.Organization - Team *organization.Team -} - -// GetUserIssueStats returns issue statistic information for dashboard by given conditions. -func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) { - var err error - stats := &IssueStats{} - - cond := builder.NewCond() - cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull}) - if len(opts.RepoIDs) > 0 { - cond = cond.And(builder.In("issue.repo_id", opts.RepoIDs)) - } - if len(opts.IssueIDs) > 0 { - cond = cond.And(builder.In("issue.id", opts.IssueIDs)) - } - if opts.RepoCond != nil { - cond = cond.And(opts.RepoCond) - } - - if opts.UserID > 0 { - cond = cond.And(issuePullAccessibleRepoCond("issue.repo_id", opts.UserID, opts.Org, opts.Team, opts.IsPull)) - } - - sess := func(cond builder.Cond) *xorm.Session { - s := db.GetEngine(db.DefaultContext).Where(cond) - if len(opts.LabelIDs) > 0 { - s.Join("INNER", "issue_label", "issue_label.issue_id = issue.id"). - In("issue_label.label_id", opts.LabelIDs) - } - if opts.UserID > 0 || opts.IsArchived != util.OptionalBoolNone { - s.Join("INNER", "repository", "issue.repo_id = repository.id") - if opts.IsArchived != util.OptionalBoolNone { - s.And(builder.Eq{"repository.is_archived": opts.IsArchived.IsTrue()}) - } - } - return s - } - - switch opts.FilterMode { - case FilterModeAll, FilterModeYourRepositories: - stats.OpenCount, err = sess(cond). - And("issue.is_closed = ?", false). - Count(new(Issue)) - if err != nil { - return nil, err - } - stats.ClosedCount, err = sess(cond). - And("issue.is_closed = ?", true). - Count(new(Issue)) - if err != nil { - return nil, err - } - case FilterModeAssign: - stats.OpenCount, err = applyAssigneeCondition(sess(cond), opts.UserID). - And("issue.is_closed = ?", false). - Count(new(Issue)) - if err != nil { - return nil, err - } - stats.ClosedCount, err = applyAssigneeCondition(sess(cond), opts.UserID). - And("issue.is_closed = ?", true). - Count(new(Issue)) - if err != nil { - return nil, err - } - case FilterModeCreate: - stats.OpenCount, err = applyPosterCondition(sess(cond), opts.UserID). - And("issue.is_closed = ?", false). - Count(new(Issue)) - if err != nil { - return nil, err - } - stats.ClosedCount, err = applyPosterCondition(sess(cond), opts.UserID). - And("issue.is_closed = ?", true). - Count(new(Issue)) - if err != nil { - return nil, err - } - case FilterModeMention: - stats.OpenCount, err = applyMentionedCondition(sess(cond), opts.UserID). - And("issue.is_closed = ?", false). - Count(new(Issue)) - if err != nil { - return nil, err - } - stats.ClosedCount, err = applyMentionedCondition(sess(cond), opts.UserID). - And("issue.is_closed = ?", true). - Count(new(Issue)) - if err != nil { - return nil, err - } - case FilterModeReviewRequested: - stats.OpenCount, err = applyReviewRequestedCondition(sess(cond), opts.UserID). - And("issue.is_closed = ?", false). - Count(new(Issue)) - if err != nil { - return nil, err - } - stats.ClosedCount, err = applyReviewRequestedCondition(sess(cond), opts.UserID). - And("issue.is_closed = ?", true). - Count(new(Issue)) - if err != nil { - return nil, err - } - case FilterModeReviewed: - stats.OpenCount, err = applyReviewedCondition(sess(cond), opts.UserID). - And("issue.is_closed = ?", false). - Count(new(Issue)) - if err != nil { - return nil, err - } - stats.ClosedCount, err = applyReviewedCondition(sess(cond), opts.UserID). - And("issue.is_closed = ?", true). - Count(new(Issue)) - if err != nil { - return nil, err - } - } - - cond = cond.And(builder.Eq{"issue.is_closed": opts.IsClosed}) - stats.AssignCount, err = applyAssigneeCondition(sess(cond), opts.UserID).Count(new(Issue)) - if err != nil { - return nil, err - } - - stats.CreateCount, err = applyPosterCondition(sess(cond), opts.UserID).Count(new(Issue)) - if err != nil { - return nil, err - } - - stats.MentionCount, err = applyMentionedCondition(sess(cond), opts.UserID).Count(new(Issue)) - if err != nil { - return nil, err - } - - stats.YourRepositoriesCount, err = sess(cond).Count(new(Issue)) - if err != nil { - return nil, err - } - - stats.ReviewRequestedCount, err = applyReviewRequestedCondition(sess(cond), opts.UserID).Count(new(Issue)) - if err != nil { - return nil, err - } - - stats.ReviewedCount, err = applyReviewedCondition(sess(cond), opts.UserID).Count(new(Issue)) - if err != nil { - return nil, err - } - - return stats, nil -} - -// GetRepoIssueStats returns number of open and closed repository issues by given filter mode. -func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen, numClosed int64) { - countSession := func(isClosed, isPull bool, repoID int64) *xorm.Session { - sess := db.GetEngine(db.DefaultContext). - Where("is_closed = ?", isClosed). - And("is_pull = ?", isPull). - And("repo_id = ?", repoID) - - return sess - } - - openCountSession := countSession(false, isPull, repoID) - closedCountSession := countSession(true, isPull, repoID) - - switch filterMode { - case FilterModeAssign: - applyAssigneeCondition(openCountSession, uid) - applyAssigneeCondition(closedCountSession, uid) - case FilterModeCreate: - applyPosterCondition(openCountSession, uid) - applyPosterCondition(closedCountSession, uid) - } - - openResult, _ := openCountSession.Count(new(Issue)) - closedResult, _ := closedCountSession.Count(new(Issue)) - - return openResult, closedResult -} - -// SearchIssueIDsByKeyword search issues on database -func SearchIssueIDsByKeyword(ctx context.Context, kw string, repoIDs []int64, limit, start int) (int64, []int64, error) { - repoCond := builder.In("repo_id", repoIDs) - subQuery := builder.Select("id").From("issue").Where(repoCond) - cond := builder.And( - repoCond, - builder.Or( - db.BuildCaseInsensitiveLike("name", kw), - db.BuildCaseInsensitiveLike("content", kw), - builder.In("id", builder.Select("issue_id"). - From("comment"). - Where(builder.And( - builder.Eq{"type": CommentTypeComment}, - builder.In("issue_id", subQuery), - db.BuildCaseInsensitiveLike("content", kw), - )), - ), - ), - ) - - ids := make([]int64, 0, limit) - res := make([]struct { - ID int64 - UpdatedUnix int64 - }, 0, limit) - err := db.GetEngine(ctx).Distinct("id", "updated_unix").Table("issue").Where(cond). - OrderBy("`updated_unix` DESC").Limit(limit, start). - Find(&res) - if err != nil { - return 0, nil, err - } - for _, r := range res { - ids = append(ids, r.ID) - } - - total, err := db.GetEngine(ctx).Distinct("id").Table("issue").Where(cond).Count() - if err != nil { - return 0, nil, err - } - - return total, ids, nil -} - -// UpdateIssueByAPI updates all allowed fields of given issue. -// If the issue status is changed a statusChangeComment is returned -// similarly if the title is changed the titleChanged bool is set to true -func UpdateIssueByAPI(issue *Issue, doer *user_model.User) (statusChangeComment *Comment, titleChanged bool, err error) { - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return nil, false, err - } - defer committer.Close() - - if err := issue.LoadRepo(ctx); err != nil { - return nil, false, fmt.Errorf("loadRepo: %w", err) - } - - // Reload the issue - currentIssue, err := GetIssueByID(ctx, issue.ID) - if err != nil { - return nil, false, err - } - - if _, err := db.GetEngine(ctx).ID(issue.ID).Cols( - "name", "content", "milestone_id", "priority", - "deadline_unix", "updated_unix", "is_locked"). - Update(issue); err != nil { - return nil, false, err - } - - titleChanged = currentIssue.Title != issue.Title - if titleChanged { - opts := &CreateCommentOptions{ - Type: CommentTypeChangeTitle, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - OldTitle: currentIssue.Title, - NewTitle: issue.Title, - } - _, err := CreateComment(ctx, opts) - if err != nil { - return nil, false, fmt.Errorf("createComment: %w", err) - } - } - - if currentIssue.IsClosed != issue.IsClosed { - statusChangeComment, err = doChangeIssueStatus(ctx, issue, doer, false) - if err != nil { - return nil, false, err - } - } - - if err := issue.AddCrossReferences(ctx, doer, true); err != nil { - return nil, false, err - } - return statusChangeComment, titleChanged, committer.Commit() -} - -// UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it. -func UpdateIssueDeadline(issue *Issue, deadlineUnix timeutil.TimeStamp, doer *user_model.User) (err error) { - // if the deadline hasn't changed do nothing - if issue.DeadlineUnix == deadlineUnix { - return nil - } - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return err - } - defer committer.Close() - - // Update the deadline - if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, DeadlineUnix: deadlineUnix}, "deadline_unix"); err != nil { - return err - } - - // Make the comment - if _, err = createDeadlineComment(ctx, doer, issue, deadlineUnix); err != nil { - return fmt.Errorf("createRemovedDueDateComment: %w", err) - } - - return committer.Commit() -} - -// DeleteInIssue delete records in beans with external key issue_id = ? -func DeleteInIssue(ctx context.Context, issueID int64, beans ...interface{}) error { - e := db.GetEngine(ctx) - for _, bean := range beans { - if _, err := e.In("issue_id", issueID).Delete(bean); err != nil { - return err - } - } - return nil -} - // DependencyInfo represents high level information about an issue which is a dependency of another issue. type DependencyInfo struct { Issue `xorm:"extends"` @@ -2184,166 +652,6 @@ func (issue *Issue) BlockingDependencies(ctx context.Context) (issueDeps []*Depe return issueDeps, err } -// FindAndUpdateIssueMentions finds users mentioned in the given content string, and saves them in the database. -func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_model.User, content string) (mentions []*user_model.User, err error) { - rawMentions := references.FindAllMentionsMarkdown(content) - mentions, err = ResolveIssueMentionsByVisibility(ctx, issue, doer, rawMentions) - if err != nil { - return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err) - } - if err = UpdateIssueMentions(ctx, issue.ID, mentions); err != nil { - return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err) - } - return mentions, err -} - -// ResolveIssueMentionsByVisibility returns the users mentioned in an issue, removing those that -// don't have access to reading it. Teams are expanded into their users, but organizations are ignored. -func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *user_model.User, mentions []string) (users []*user_model.User, err error) { - if len(mentions) == 0 { - return - } - if err = issue.LoadRepo(ctx); err != nil { - return - } - - resolved := make(map[string]bool, 10) - var mentionTeams []string - - if err := issue.Repo.LoadOwner(ctx); err != nil { - return nil, err - } - - repoOwnerIsOrg := issue.Repo.Owner.IsOrganization() - if repoOwnerIsOrg { - mentionTeams = make([]string, 0, 5) - } - - resolved[doer.LowerName] = true - for _, name := range mentions { - name := strings.ToLower(name) - if _, ok := resolved[name]; ok { - continue - } - if repoOwnerIsOrg && strings.Contains(name, "/") { - names := strings.Split(name, "/") - if len(names) < 2 || names[0] != issue.Repo.Owner.LowerName { - continue - } - mentionTeams = append(mentionTeams, names[1]) - resolved[name] = true - } else { - resolved[name] = false - } - } - - if issue.Repo.Owner.IsOrganization() && len(mentionTeams) > 0 { - teams := make([]*organization.Team, 0, len(mentionTeams)) - if err := db.GetEngine(ctx). - Join("INNER", "team_repo", "team_repo.team_id = team.id"). - Where("team_repo.repo_id=?", issue.Repo.ID). - In("team.lower_name", mentionTeams). - Find(&teams); err != nil { - return nil, fmt.Errorf("find mentioned teams: %w", err) - } - if len(teams) != 0 { - checked := make([]int64, 0, len(teams)) - unittype := unit.TypeIssues - if issue.IsPull { - unittype = unit.TypePullRequests - } - for _, team := range teams { - if team.AccessMode >= perm.AccessModeAdmin { - checked = append(checked, team.ID) - resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true - continue - } - has, err := db.GetEngine(ctx).Get(&organization.TeamUnit{OrgID: issue.Repo.Owner.ID, TeamID: team.ID, Type: unittype}) - if err != nil { - return nil, fmt.Errorf("get team units (%d): %w", team.ID, err) - } - if has { - checked = append(checked, team.ID) - resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true - } - } - if len(checked) != 0 { - teamusers := make([]*user_model.User, 0, 20) - if err := db.GetEngine(ctx). - Join("INNER", "team_user", "team_user.uid = `user`.id"). - In("`team_user`.team_id", checked). - And("`user`.is_active = ?", true). - And("`user`.prohibit_login = ?", false). - Find(&teamusers); err != nil { - return nil, fmt.Errorf("get teams users: %w", err) - } - if len(teamusers) > 0 { - users = make([]*user_model.User, 0, len(teamusers)) - for _, user := range teamusers { - if already, ok := resolved[user.LowerName]; !ok || !already { - users = append(users, user) - resolved[user.LowerName] = true - } - } - } - } - } - } - - // Remove names already in the list to avoid querying the database if pending names remain - mentionUsers := make([]string, 0, len(resolved)) - for name, already := range resolved { - if !already { - mentionUsers = append(mentionUsers, name) - } - } - if len(mentionUsers) == 0 { - return - } - - if users == nil { - users = make([]*user_model.User, 0, len(mentionUsers)) - } - - unchecked := make([]*user_model.User, 0, len(mentionUsers)) - if err := db.GetEngine(ctx). - Where("`user`.is_active = ?", true). - And("`user`.prohibit_login = ?", false). - In("`user`.lower_name", mentionUsers). - Find(&unchecked); err != nil { - return nil, fmt.Errorf("find mentioned users: %w", err) - } - for _, user := range unchecked { - if already := resolved[user.LowerName]; already || user.IsOrganization() { - continue - } - // Normal users must have read access to the referencing issue - perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, user) - if err != nil { - return nil, fmt.Errorf("GetUserRepoPermission [%d]: %w", user.ID, err) - } - if !perm.CanReadIssuesOrPulls(issue.IsPull) { - continue - } - users = append(users, user) - } - - return users, err -} - -// UpdateIssuesMigrationsByType updates all migrated repositories' issues from gitServiceType to replace originalAuthorID to posterID -func UpdateIssuesMigrationsByType(gitServiceType api.GitServiceType, originalAuthorID string, posterID int64) error { - _, err := db.GetEngine(db.DefaultContext).Table("issue"). - Where("repo_id IN (SELECT id FROM repository WHERE original_service_type = ?)", gitServiceType). - And("original_author_id = ?", originalAuthorID). - Update(map[string]interface{}{ - "poster_id": posterID, - "original_author": "", - "original_author_id": 0, - }) - return err -} - func migratedIssueCond(tp api.GitServiceType) builder.Cond { return builder.In("issue_id", builder.Select("issue.id"). @@ -2355,105 +663,6 @@ func migratedIssueCond(tp api.GitServiceType) builder.Cond { ) } -// UpdateReactionsMigrationsByType updates all migrated repositories' reactions from gitServiceType to replace originalAuthorID to posterID -func UpdateReactionsMigrationsByType(gitServiceType api.GitServiceType, originalAuthorID string, userID int64) error { - _, err := db.GetEngine(db.DefaultContext).Table("reaction"). - Where("original_author_id = ?", originalAuthorID). - And(migratedIssueCond(gitServiceType)). - Update(map[string]interface{}{ - "user_id": userID, - "original_author": "", - "original_author_id": 0, - }) - return err -} - -// DeleteIssuesByRepoID deletes issues by repositories id -func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) { - deleteCond := builder.Select("id").From("issue").Where(builder.Eq{"issue.repo_id": repoID}) - - sess := db.GetEngine(ctx) - // Delete content histories - if _, err = sess.In("issue_id", deleteCond). - Delete(&ContentHistory{}); err != nil { - return - } - - // Delete comments and attachments - if _, err = sess.In("issue_id", deleteCond). - Delete(&Comment{}); err != nil { - return - } - - // Dependencies for issues in this repository - if _, err = sess.In("issue_id", deleteCond). - Delete(&IssueDependency{}); err != nil { - return - } - - // Delete dependencies for issues in other repositories - if _, err = sess.In("dependency_id", deleteCond). - Delete(&IssueDependency{}); err != nil { - return - } - - if _, err = sess.In("issue_id", deleteCond). - Delete(&IssueUser{}); err != nil { - return - } - - if _, err = sess.In("issue_id", deleteCond). - Delete(&Reaction{}); err != nil { - return - } - - if _, err = sess.In("issue_id", deleteCond). - Delete(&IssueWatch{}); err != nil { - return - } - - if _, err = sess.In("issue_id", deleteCond). - Delete(&Stopwatch{}); err != nil { - return - } - - if _, err = sess.In("issue_id", deleteCond). - Delete(&TrackedTime{}); err != nil { - return - } - - if _, err = sess.In("issue_id", deleteCond). - Delete(&project_model.ProjectIssue{}); err != nil { - return - } - - if _, err = sess.In("dependent_issue_id", deleteCond). - Delete(&Comment{}); err != nil { - return - } - - var attachments []*repo_model.Attachment - if err = sess.In("issue_id", deleteCond). - Find(&attachments); err != nil { - return - } - - for j := range attachments { - attachmentPaths = append(attachmentPaths, attachments[j].RelativePath()) - } - - if _, err = sess.In("issue_id", deleteCond). - Delete(&repo_model.Attachment{}); err != nil { - return - } - - if _, err = db.DeleteByBean(ctx, &Issue{RepoID: repoID}); err != nil { - return - } - - return attachmentPaths, err -} - // RemapExternalUser ExternalUserRemappable interface func (issue *Issue) RemapExternalUser(externalName string, externalID, userID int64) error { issue.OriginalAuthor = externalName @@ -2471,50 +680,6 @@ func (issue *Issue) GetExternalName() string { return issue.OriginalAuthor } // GetExternalID ExternalUserRemappable interface func (issue *Issue) GetExternalID() int64 { return issue.OriginalAuthorID } -// CountOrphanedIssues count issues without a repo -func CountOrphanedIssues(ctx context.Context) (int64, error) { - return db.GetEngine(ctx). - Table("issue"). - Join("LEFT", "repository", "issue.repo_id=repository.id"). - Where(builder.IsNull{"repository.id"}). - Select("COUNT(`issue`.`id`)"). - Count() -} - -// DeleteOrphanedIssues delete issues without a repo -func DeleteOrphanedIssues(ctx context.Context) error { - var attachmentPaths []string - err := db.WithTx(ctx, func(ctx context.Context) error { - var ids []int64 - - if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id"). - Join("LEFT", "repository", "issue.repo_id=repository.id"). - Where(builder.IsNull{"repository.id"}).GroupBy("issue.repo_id"). - Find(&ids); err != nil { - return err - } - - for i := range ids { - paths, err := DeleteIssuesByRepoID(ctx, ids[i]) - if err != nil { - return err - } - attachmentPaths = append(attachmentPaths, paths...) - } - - return nil - }) - if err != nil { - return err - } - - // Remove issue attachment files. - for i := range attachmentPaths { - system_model.RemoveAllWithNotice(db.DefaultContext, "Delete issue attachment", attachmentPaths[i]) - } - return nil -} - // HasOriginalAuthor returns if an issue was migrated and has an original author. func (issue *Issue) HasOriginalAuthor() bool { return issue.OriginalAuthor != "" && issue.OriginalAuthorID != 0 diff --git a/models/issues/issue_label.go b/models/issues/issue_label.go new file mode 100644 index 0000000000..f4060b1402 --- /dev/null +++ b/models/issues/issue_label.go @@ -0,0 +1,490 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package issues + +import ( + "context" + "fmt" + "sort" + + "code.gitea.io/gitea/models/db" + access_model "code.gitea.io/gitea/models/perm/access" + user_model "code.gitea.io/gitea/models/user" + + "xorm.io/builder" +) + +// IssueLabel represents an issue-label relation. +type IssueLabel struct { + ID int64 `xorm:"pk autoincr"` + IssueID int64 `xorm:"UNIQUE(s)"` + LabelID int64 `xorm:"UNIQUE(s)"` +} + +// HasIssueLabel returns true if issue has been labeled. +func HasIssueLabel(ctx context.Context, issueID, labelID int64) bool { + has, _ := db.GetEngine(ctx).Where("issue_id = ? AND label_id = ?", issueID, labelID).Get(new(IssueLabel)) + return has +} + +// newIssueLabel this function creates a new label it does not check if the label is valid for the issue +// YOU MUST CHECK THIS BEFORE THIS FUNCTION +func newIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) { + if err = db.Insert(ctx, &IssueLabel{ + IssueID: issue.ID, + LabelID: label.ID, + }); err != nil { + return err + } + + if err = issue.LoadRepo(ctx); err != nil { + return + } + + opts := &CreateCommentOptions{ + Type: CommentTypeLabel, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + Label: label, + Content: "1", + } + if _, err = CreateComment(ctx, opts); err != nil { + return err + } + + return updateLabelCols(ctx, label, "num_issues", "num_closed_issue") +} + +// Remove all issue labels in the given exclusive scope +func RemoveDuplicateExclusiveIssueLabels(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) { + scope := label.ExclusiveScope() + if scope == "" { + return nil + } + + var toRemove []*Label + for _, issueLabel := range issue.Labels { + if label.ID != issueLabel.ID && issueLabel.ExclusiveScope() == scope { + toRemove = append(toRemove, issueLabel) + } + } + + for _, issueLabel := range toRemove { + if err = deleteIssueLabel(ctx, issue, issueLabel, doer); err != nil { + return err + } + } + + return nil +} + +// NewIssueLabel creates a new issue-label relation. +func NewIssueLabel(issue *Issue, label *Label, doer *user_model.User) (err error) { + if HasIssueLabel(db.DefaultContext, issue.ID, label.ID) { + return nil + } + + ctx, committer, err := db.TxContext(db.DefaultContext) + if err != nil { + return err + } + defer committer.Close() + + if err = issue.LoadRepo(ctx); err != nil { + return err + } + + // Do NOT add invalid labels + if issue.RepoID != label.RepoID && issue.Repo.OwnerID != label.OrgID { + return nil + } + + if err = RemoveDuplicateExclusiveIssueLabels(ctx, issue, label, doer); err != nil { + return nil + } + + if err = newIssueLabel(ctx, issue, label, doer); err != nil { + return err + } + + issue.Labels = nil + if err = issue.LoadLabels(ctx); err != nil { + return err + } + + return committer.Commit() +} + +// newIssueLabels add labels to an issue. It will check if the labels are valid for the issue +func newIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *user_model.User) (err error) { + if err = issue.LoadRepo(ctx); err != nil { + return err + } + for _, l := range labels { + // Don't add already present labels and invalid labels + if HasIssueLabel(ctx, issue.ID, l.ID) || + (l.RepoID != issue.RepoID && l.OrgID != issue.Repo.OwnerID) { + continue + } + + if err = newIssueLabel(ctx, issue, l, doer); err != nil { + return fmt.Errorf("newIssueLabel: %w", err) + } + } + + return nil +} + +// NewIssueLabels creates a list of issue-label relations. +func NewIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (err error) { + ctx, committer, err := db.TxContext(db.DefaultContext) + if err != nil { + return err + } + defer committer.Close() + + if err = newIssueLabels(ctx, issue, labels, doer); err != nil { + return err + } + + issue.Labels = nil + if err = issue.LoadLabels(ctx); err != nil { + return err + } + + return committer.Commit() +} + +func deleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) { + if count, err := db.DeleteByBean(ctx, &IssueLabel{ + IssueID: issue.ID, + LabelID: label.ID, + }); err != nil { + return err + } else if count == 0 { + return nil + } + + if err = issue.LoadRepo(ctx); err != nil { + return + } + + opts := &CreateCommentOptions{ + Type: CommentTypeLabel, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + Label: label, + } + if _, err = CreateComment(ctx, opts); err != nil { + return err + } + + return updateLabelCols(ctx, label, "num_issues", "num_closed_issue") +} + +// DeleteIssueLabel deletes issue-label relation. +func DeleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) error { + if err := deleteIssueLabel(ctx, issue, label, doer); err != nil { + return err + } + + issue.Labels = nil + return issue.LoadLabels(ctx) +} + +// DeleteLabelsByRepoID deletes labels of some repository +func DeleteLabelsByRepoID(ctx context.Context, repoID int64) error { + deleteCond := builder.Select("id").From("label").Where(builder.Eq{"label.repo_id": repoID}) + + if _, err := db.GetEngine(ctx).In("label_id", deleteCond). + Delete(&IssueLabel{}); err != nil { + return err + } + + _, err := db.DeleteByBean(ctx, &Label{RepoID: repoID}) + return err +} + +// CountOrphanedLabels return count of labels witch are broken and not accessible via ui anymore +func CountOrphanedLabels(ctx context.Context) (int64, error) { + noref, err := db.GetEngine(ctx).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Count() + if err != nil { + return 0, err + } + + norepo, err := db.GetEngine(ctx).Table("label"). + Where(builder.And( + builder.Gt{"repo_id": 0}, + builder.NotIn("repo_id", builder.Select("id").From("`repository`")), + )). + Count() + if err != nil { + return 0, err + } + + noorg, err := db.GetEngine(ctx).Table("label"). + Where(builder.And( + builder.Gt{"org_id": 0}, + builder.NotIn("org_id", builder.Select("id").From("`user`")), + )). + Count() + if err != nil { + return 0, err + } + + return noref + norepo + noorg, nil +} + +// DeleteOrphanedLabels delete labels witch are broken and not accessible via ui anymore +func DeleteOrphanedLabels(ctx context.Context) error { + // delete labels with no reference + if _, err := db.GetEngine(ctx).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Delete(new(Label)); err != nil { + return err + } + + // delete labels with none existing repos + if _, err := db.GetEngine(ctx). + Where(builder.And( + builder.Gt{"repo_id": 0}, + builder.NotIn("repo_id", builder.Select("id").From("`repository`")), + )). + Delete(Label{}); err != nil { + return err + } + + // delete labels with none existing orgs + if _, err := db.GetEngine(ctx). + Where(builder.And( + builder.Gt{"org_id": 0}, + builder.NotIn("org_id", builder.Select("id").From("`user`")), + )). + Delete(Label{}); err != nil { + return err + } + + return nil +} + +// CountOrphanedIssueLabels return count of IssueLabels witch have no label behind anymore +func CountOrphanedIssueLabels(ctx context.Context) (int64, error) { + return db.GetEngine(ctx).Table("issue_label"). + NotIn("label_id", builder.Select("id").From("label")). + Count() +} + +// DeleteOrphanedIssueLabels delete IssueLabels witch have no label behind anymore +func DeleteOrphanedIssueLabels(ctx context.Context) error { + _, err := db.GetEngine(ctx). + NotIn("label_id", builder.Select("id").From("label")). + Delete(IssueLabel{}) + return err +} + +// CountIssueLabelWithOutsideLabels count label comments with outside label +func CountIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) { + return db.GetEngine(ctx).Where(builder.Expr("(label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id)")). + Table("issue_label"). + Join("inner", "label", "issue_label.label_id = label.id "). + Join("inner", "issue", "issue.id = issue_label.issue_id "). + Join("inner", "repository", "issue.repo_id = repository.id"). + Count(new(IssueLabel)) +} + +// FixIssueLabelWithOutsideLabels fix label comments with outside label +func FixIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) { + res, err := db.GetEngine(ctx).Exec(`DELETE FROM issue_label WHERE issue_label.id IN ( + SELECT il_too.id FROM ( + SELECT il_too_too.id + FROM issue_label AS il_too_too + INNER JOIN label ON il_too_too.label_id = label.id + INNER JOIN issue on issue.id = il_too_too.issue_id + INNER JOIN repository on repository.id = issue.repo_id + WHERE + (label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id) + ) AS il_too )`) + if err != nil { + return 0, err + } + + return res.RowsAffected() +} + +// LoadLabels loads labels +func (issue *Issue) LoadLabels(ctx context.Context) (err error) { + if issue.Labels == nil && issue.ID != 0 { + issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID) + if err != nil { + return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err) + } + } + return nil +} + +// GetLabelsByIssueID returns all labels that belong to given issue by ID. +func GetLabelsByIssueID(ctx context.Context, issueID int64) ([]*Label, error) { + var labels []*Label + return labels, db.GetEngine(ctx).Where("issue_label.issue_id = ?", issueID). + Join("LEFT", "issue_label", "issue_label.label_id = label.id"). + Asc("label.name"). + Find(&labels) +} + +func clearIssueLabels(ctx context.Context, issue *Issue, doer *user_model.User) (err error) { + if err = issue.LoadLabels(ctx); err != nil { + return fmt.Errorf("getLabels: %w", err) + } + + for i := range issue.Labels { + if err = deleteIssueLabel(ctx, issue, issue.Labels[i], doer); err != nil { + return fmt.Errorf("removeLabel: %w", err) + } + } + + return nil +} + +// ClearIssueLabels removes all issue labels as the given user. +// Triggers appropriate WebHooks, if any. +func ClearIssueLabels(issue *Issue, doer *user_model.User) (err error) { + ctx, committer, err := db.TxContext(db.DefaultContext) + if err != nil { + return err + } + defer committer.Close() + + if err := issue.LoadRepo(ctx); err != nil { + return err + } else if err = issue.LoadPullRequest(ctx); err != nil { + return err + } + + perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) + if err != nil { + return err + } + if !perm.CanWriteIssuesOrPulls(issue.IsPull) { + return ErrRepoLabelNotExist{} + } + + if err = clearIssueLabels(ctx, issue, doer); err != nil { + return err + } + + if err = committer.Commit(); err != nil { + return fmt.Errorf("Commit: %w", err) + } + + return nil +} + +type labelSorter []*Label + +func (ts labelSorter) Len() int { + return len([]*Label(ts)) +} + +func (ts labelSorter) Less(i, j int) bool { + return []*Label(ts)[i].ID < []*Label(ts)[j].ID +} + +func (ts labelSorter) Swap(i, j int) { + []*Label(ts)[i], []*Label(ts)[j] = []*Label(ts)[j], []*Label(ts)[i] +} + +// Ensure only one label of a given scope exists, with labels at the end of the +// array getting preference over earlier ones. +func RemoveDuplicateExclusiveLabels(labels []*Label) []*Label { + validLabels := make([]*Label, 0, len(labels)) + + for i, label := range labels { + scope := label.ExclusiveScope() + if scope != "" { + foundOther := false + for _, otherLabel := range labels[i+1:] { + if otherLabel.ExclusiveScope() == scope { + foundOther = true + break + } + } + if foundOther { + continue + } + } + validLabels = append(validLabels, label) + } + + return validLabels +} + +// ReplaceIssueLabels removes all current labels and add new labels to the issue. +// Triggers appropriate WebHooks, if any. +func ReplaceIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (err error) { + ctx, committer, err := db.TxContext(db.DefaultContext) + if err != nil { + return err + } + defer committer.Close() + + if err = issue.LoadRepo(ctx); err != nil { + return err + } + + if err = issue.LoadLabels(ctx); err != nil { + return err + } + + labels = RemoveDuplicateExclusiveLabels(labels) + + sort.Sort(labelSorter(labels)) + sort.Sort(labelSorter(issue.Labels)) + + var toAdd, toRemove []*Label + + addIndex, removeIndex := 0, 0 + for addIndex < len(labels) && removeIndex < len(issue.Labels) { + addLabel := labels[addIndex] + removeLabel := issue.Labels[removeIndex] + if addLabel.ID == removeLabel.ID { + // Silently drop invalid labels + if removeLabel.RepoID != issue.RepoID && removeLabel.OrgID != issue.Repo.OwnerID { + toRemove = append(toRemove, removeLabel) + } + + addIndex++ + removeIndex++ + } else if addLabel.ID < removeLabel.ID { + // Only add if the label is valid + if addLabel.RepoID == issue.RepoID || addLabel.OrgID == issue.Repo.OwnerID { + toAdd = append(toAdd, addLabel) + } + addIndex++ + } else { + toRemove = append(toRemove, removeLabel) + removeIndex++ + } + } + toAdd = append(toAdd, labels[addIndex:]...) + toRemove = append(toRemove, issue.Labels[removeIndex:]...) + + if len(toAdd) > 0 { + if err = newIssueLabels(ctx, issue, toAdd, doer); err != nil { + return fmt.Errorf("addLabels: %w", err) + } + } + + for _, l := range toRemove { + if err = deleteIssueLabel(ctx, issue, l, doer); err != nil { + return fmt.Errorf("removeLabel: %w", err) + } + } + + issue.Labels = nil + if err = issue.LoadLabels(ctx); err != nil { + return err + } + + return committer.Commit() +} diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go new file mode 100644 index 0000000000..9fd13f0995 --- /dev/null +++ b/models/issues/issue_search.go @@ -0,0 +1,491 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package issues + +import ( + "context" + "fmt" + "strings" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" + "xorm.io/xorm" +) + +// IssuesOptions represents options of an issue. +type IssuesOptions struct { //nolint + db.ListOptions + RepoIDs []int64 // overwrites RepoCond if the length is not 0 + RepoCond builder.Cond + AssigneeID int64 + PosterID int64 + MentionedID int64 + ReviewRequestedID int64 + ReviewedID int64 + SubscriberID int64 + MilestoneIDs []int64 + ProjectID int64 + ProjectBoardID int64 + IsClosed util.OptionalBool + IsPull util.OptionalBool + LabelIDs []int64 + IncludedLabelNames []string + ExcludedLabelNames []string + IncludeMilestones []string + SortType string + IssueIDs []int64 + UpdatedAfterUnix int64 + UpdatedBeforeUnix int64 + // prioritize issues from this repo + PriorityRepoID int64 + IsArchived util.OptionalBool + Org *organization.Organization // issues permission scope + Team *organization.Team // issues permission scope + User *user_model.User // issues permission scope +} + +// applySorts sort an issues-related session based on the provided +// sortType string +func applySorts(sess *xorm.Session, sortType string, priorityRepoID int64) { + switch sortType { + case "oldest": + sess.Asc("issue.created_unix").Asc("issue.id") + case "recentupdate": + sess.Desc("issue.updated_unix").Desc("issue.created_unix").Desc("issue.id") + case "leastupdate": + sess.Asc("issue.updated_unix").Asc("issue.created_unix").Asc("issue.id") + case "mostcomment": + sess.Desc("issue.num_comments").Desc("issue.created_unix").Desc("issue.id") + case "leastcomment": + sess.Asc("issue.num_comments").Desc("issue.created_unix").Desc("issue.id") + case "priority": + sess.Desc("issue.priority").Desc("issue.created_unix").Desc("issue.id") + case "nearduedate": + // 253370764800 is 01/01/9999 @ 12:00am (UTC) + sess.Join("LEFT", "milestone", "issue.milestone_id = milestone.id"). + OrderBy("CASE " + + "WHEN issue.deadline_unix = 0 AND (milestone.deadline_unix = 0 OR milestone.deadline_unix IS NULL) THEN 253370764800 " + + "WHEN milestone.deadline_unix = 0 OR milestone.deadline_unix IS NULL THEN issue.deadline_unix " + + "WHEN milestone.deadline_unix < issue.deadline_unix OR issue.deadline_unix = 0 THEN milestone.deadline_unix " + + "ELSE issue.deadline_unix END ASC"). + Desc("issue.created_unix"). + Desc("issue.id") + case "farduedate": + sess.Join("LEFT", "milestone", "issue.milestone_id = milestone.id"). + OrderBy("CASE " + + "WHEN milestone.deadline_unix IS NULL THEN issue.deadline_unix " + + "WHEN milestone.deadline_unix < issue.deadline_unix OR issue.deadline_unix = 0 THEN milestone.deadline_unix " + + "ELSE issue.deadline_unix END DESC"). + Desc("issue.created_unix"). + Desc("issue.id") + case "priorityrepo": + sess.OrderBy("CASE "+ + "WHEN issue.repo_id = ? THEN 1 "+ + "ELSE 2 END ASC", priorityRepoID). + Desc("issue.created_unix"). + Desc("issue.id") + case "project-column-sorting": + sess.Asc("project_issue.sorting").Desc("issue.created_unix").Desc("issue.id") + default: + sess.Desc("issue.created_unix").Desc("issue.id") + } +} + +func applyLimit(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { + if opts.Page >= 0 && opts.PageSize > 0 { + var start int + if opts.Page == 0 { + start = 0 + } else { + start = (opts.Page - 1) * opts.PageSize + } + sess.Limit(opts.PageSize, start) + } + return sess +} + +func applyLabelsCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { + if len(opts.LabelIDs) > 0 { + if opts.LabelIDs[0] == 0 { + sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_label)") + } else { + for i, labelID := range opts.LabelIDs { + if labelID > 0 { + sess.Join("INNER", fmt.Sprintf("issue_label il%d", i), + fmt.Sprintf("issue.id = il%[1]d.issue_id AND il%[1]d.label_id = %[2]d", i, labelID)) + } else if labelID < 0 { // 0 is not supported here, so just ignore it + sess.Where("issue.id not in (select issue_id from issue_label where label_id = ?)", -labelID) + } + } + } + } + + if len(opts.IncludedLabelNames) > 0 { + sess.In("issue.id", BuildLabelNamesIssueIDsCondition(opts.IncludedLabelNames)) + } + + if len(opts.ExcludedLabelNames) > 0 { + sess.And(builder.NotIn("issue.id", BuildLabelNamesIssueIDsCondition(opts.ExcludedLabelNames))) + } + + return sess +} + +func applyMilestoneCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { + if len(opts.MilestoneIDs) == 1 && opts.MilestoneIDs[0] == db.NoConditionID { + sess.And("issue.milestone_id = 0") + } else if len(opts.MilestoneIDs) > 0 { + sess.In("issue.milestone_id", opts.MilestoneIDs) + } + + if len(opts.IncludeMilestones) > 0 { + sess.In("issue.milestone_id", + builder.Select("id"). + From("milestone"). + Where(builder.In("name", opts.IncludeMilestones))) + } + + return sess +} + +func applyRepoConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { + if len(opts.RepoIDs) == 1 { + opts.RepoCond = builder.Eq{"issue.repo_id": opts.RepoIDs[0]} + } else if len(opts.RepoIDs) > 1 { + opts.RepoCond = builder.In("issue.repo_id", opts.RepoIDs) + } + if opts.RepoCond != nil { + sess.And(opts.RepoCond) + } + return sess +} + +func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { + if len(opts.IssueIDs) > 0 { + sess.In("issue.id", opts.IssueIDs) + } + + applyRepoConditions(sess, opts) + + if !opts.IsClosed.IsNone() { + sess.And("issue.is_closed=?", opts.IsClosed.IsTrue()) + } + + if opts.AssigneeID > 0 { + applyAssigneeCondition(sess, opts.AssigneeID) + } else if opts.AssigneeID == db.NoConditionID { + sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)") + } + + if opts.PosterID > 0 { + applyPosterCondition(sess, opts.PosterID) + } + + if opts.MentionedID > 0 { + applyMentionedCondition(sess, opts.MentionedID) + } + + if opts.ReviewRequestedID > 0 { + applyReviewRequestedCondition(sess, opts.ReviewRequestedID) + } + + if opts.ReviewedID > 0 { + applyReviewedCondition(sess, opts.ReviewedID) + } + + if opts.SubscriberID > 0 { + applySubscribedCondition(sess, opts.SubscriberID) + } + + applyMilestoneCondition(sess, opts) + + if opts.UpdatedAfterUnix != 0 { + sess.And(builder.Gte{"issue.updated_unix": opts.UpdatedAfterUnix}) + } + if opts.UpdatedBeforeUnix != 0 { + sess.And(builder.Lte{"issue.updated_unix": opts.UpdatedBeforeUnix}) + } + + if opts.ProjectID > 0 { + sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id"). + And("project_issue.project_id=?", opts.ProjectID) + } else if opts.ProjectID == db.NoConditionID { // show those that are in no project + sess.And(builder.NotIn("issue.id", builder.Select("issue_id").From("project_issue"))) + } + + if opts.ProjectBoardID != 0 { + if opts.ProjectBoardID > 0 { + sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectBoardID})) + } else { + sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0})) + } + } + + switch opts.IsPull { + case util.OptionalBoolTrue: + sess.And("issue.is_pull=?", true) + case util.OptionalBoolFalse: + sess.And("issue.is_pull=?", false) + } + + if opts.IsArchived != util.OptionalBoolNone { + sess.And(builder.Eq{"repository.is_archived": opts.IsArchived.IsTrue()}) + } + + applyLabelsCondition(sess, opts) + + if opts.User != nil { + sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.IsTrue())) + } + + return sess +} + +// teamUnitsRepoCond returns query condition for those repo id in the special org team with special units access +func teamUnitsRepoCond(id string, userID, orgID, teamID int64, units ...unit.Type) builder.Cond { + return builder.In(id, + builder.Select("repo_id").From("team_repo").Where( + builder.Eq{ + "team_id": teamID, + }.And( + builder.Or( + // Check if the user is member of the team. + builder.In( + "team_id", builder.Select("team_id").From("team_user").Where( + builder.Eq{ + "uid": userID, + }, + ), + ), + // Check if the user is in the owner team of the organisation. + builder.Exists(builder.Select("team_id").From("team_user"). + Where(builder.Eq{ + "org_id": orgID, + "team_id": builder.Select("id").From("team").Where( + builder.Eq{ + "org_id": orgID, + "lower_name": strings.ToLower(organization.OwnerTeamName), + }), + "uid": userID, + }), + ), + )).And( + builder.In( + "team_id", builder.Select("team_id").From("team_unit").Where( + builder.Eq{ + "`team_unit`.org_id": orgID, + }.And( + builder.In("`team_unit`.type", units), + ), + ), + ), + ), + )) +} + +// issuePullAccessibleRepoCond userID must not be zero, this condition require join repository table +func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *organization.Organization, team *organization.Team, isPull bool) builder.Cond { + cond := builder.NewCond() + unitType := unit.TypeIssues + if isPull { + unitType = unit.TypePullRequests + } + if org != nil { + if team != nil { + cond = cond.And(teamUnitsRepoCond(repoIDstr, userID, org.ID, team.ID, unitType)) // special team member repos + } else { + cond = cond.And( + builder.Or( + repo_model.UserOrgUnitRepoCond(repoIDstr, userID, org.ID, unitType), // team member repos + repo_model.UserOrgPublicUnitRepoCond(userID, org.ID), // user org public non-member repos, TODO: check repo has issues + ), + ) + } + } else { + cond = cond.And( + builder.Or( + repo_model.UserOwnedRepoCond(userID), // owned repos + repo_model.UserAccessRepoCond(repoIDstr, userID), // user can access repo in a unit independent way + repo_model.UserAssignedRepoCond(repoIDstr, userID), // user has been assigned accessible public repos + repo_model.UserMentionedRepoCond(repoIDstr, userID), // user has been mentioned accessible public repos + repo_model.UserCreateIssueRepoCond(repoIDstr, userID, isPull), // user has created issue/pr accessible public repos + ), + ) + } + return cond +} + +func applyAssigneeCondition(sess *xorm.Session, assigneeID int64) *xorm.Session { + return sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). + And("issue_assignees.assignee_id = ?", assigneeID) +} + +func applyPosterCondition(sess *xorm.Session, posterID int64) *xorm.Session { + return sess.And("issue.poster_id=?", posterID) +} + +func applyMentionedCondition(sess *xorm.Session, mentionedID int64) *xorm.Session { + return sess.Join("INNER", "issue_user", "issue.id = issue_user.issue_id"). + And("issue_user.is_mentioned = ?", true). + And("issue_user.uid = ?", mentionedID) +} + +func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64) *xorm.Session { + return sess.Join("INNER", []string{"review", "r"}, "issue.id = r.issue_id"). + And("issue.poster_id <> ?", reviewRequestedID). + And("r.type = ?", ReviewTypeRequest). + And("r.reviewer_id = ? and r.id in (select max(id) from review where issue_id = r.issue_id and reviewer_id = r.reviewer_id and type in (?, ?, ?))"+ + " or r.reviewer_team_id in (select team_id from team_user where uid = ?)", + reviewRequestedID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, reviewRequestedID) +} + +func applyReviewedCondition(sess *xorm.Session, reviewedID int64) *xorm.Session { + // Query for pull requests where you are a reviewer or commenter, excluding + // any pull requests already returned by the the review requested filter. + notPoster := builder.Neq{"issue.poster_id": reviewedID} + reviewed := builder.In("issue.id", builder. + Select("issue_id"). + From("review"). + Where(builder.And( + builder.Neq{"type": ReviewTypeRequest}, + builder.Or( + builder.Eq{"reviewer_id": reviewedID}, + builder.In("reviewer_team_id", builder. + Select("team_id"). + From("team_user"). + Where(builder.Eq{"uid": reviewedID}), + ), + ), + )), + ) + commented := builder.In("issue.id", builder. + Select("issue_id"). + From("comment"). + Where(builder.And( + builder.Eq{"poster_id": reviewedID}, + builder.In("type", CommentTypeComment, CommentTypeCode, CommentTypeReview), + )), + ) + return sess.And(notPoster, builder.Or(reviewed, commented)) +} + +func applySubscribedCondition(sess *xorm.Session, subscriberID int64) *xorm.Session { + return sess.And( + builder. + NotIn("issue.id", + builder.Select("issue_id"). + From("issue_watch"). + Where(builder.Eq{"is_watching": false, "user_id": subscriberID}), + ), + ).And( + builder.Or( + builder.In("issue.id", builder. + Select("issue_id"). + From("issue_watch"). + Where(builder.Eq{"is_watching": true, "user_id": subscriberID}), + ), + builder.In("issue.id", builder. + Select("issue_id"). + From("comment"). + Where(builder.Eq{"poster_id": subscriberID}), + ), + builder.Eq{"issue.poster_id": subscriberID}, + builder.In("issue.repo_id", builder. + Select("id"). + From("watch"). + Where(builder.And(builder.Eq{"user_id": subscriberID}, + builder.In("mode", repo_model.WatchModeNormal, repo_model.WatchModeAuto))), + ), + ), + ) +} + +// GetRepoIDsForIssuesOptions find all repo ids for the given options +func GetRepoIDsForIssuesOptions(opts *IssuesOptions, user *user_model.User) ([]int64, error) { + repoIDs := make([]int64, 0, 5) + e := db.GetEngine(db.DefaultContext) + + sess := e.Join("INNER", "repository", "`issue`.repo_id = `repository`.id") + + applyConditions(sess, opts) + + accessCond := repo_model.AccessibleRepositoryCondition(user, unit.TypeInvalid) + if err := sess.Where(accessCond). + Distinct("issue.repo_id"). + Table("issue"). + Find(&repoIDs); err != nil { + return nil, fmt.Errorf("unable to GetRepoIDsForIssuesOptions: %w", err) + } + + return repoIDs, nil +} + +// Issues returns a list of issues by given conditions. +func Issues(ctx context.Context, opts *IssuesOptions) ([]*Issue, error) { + sess := db.GetEngine(ctx). + Join("INNER", "repository", "`issue`.repo_id = `repository`.id") + applyLimit(sess, opts) + applyConditions(sess, opts) + applySorts(sess, opts.SortType, opts.PriorityRepoID) + + issues := make(IssueList, 0, opts.ListOptions.PageSize) + if err := sess.Find(&issues); err != nil { + return nil, fmt.Errorf("unable to query Issues: %w", err) + } + + if err := issues.LoadAttributes(); err != nil { + return nil, fmt.Errorf("unable to LoadAttributes for Issues: %w", err) + } + + return issues, nil +} + +// SearchIssueIDsByKeyword search issues on database +func SearchIssueIDsByKeyword(ctx context.Context, kw string, repoIDs []int64, limit, start int) (int64, []int64, error) { + repoCond := builder.In("repo_id", repoIDs) + subQuery := builder.Select("id").From("issue").Where(repoCond) + cond := builder.And( + repoCond, + builder.Or( + db.BuildCaseInsensitiveLike("name", kw), + db.BuildCaseInsensitiveLike("content", kw), + builder.In("id", builder.Select("issue_id"). + From("comment"). + Where(builder.And( + builder.Eq{"type": CommentTypeComment}, + builder.In("issue_id", subQuery), + db.BuildCaseInsensitiveLike("content", kw), + )), + ), + ), + ) + + ids := make([]int64, 0, limit) + res := make([]struct { + ID int64 + UpdatedUnix int64 + }, 0, limit) + err := db.GetEngine(ctx).Distinct("id", "updated_unix").Table("issue").Where(cond). + OrderBy("`updated_unix` DESC").Limit(limit, start). + Find(&res) + if err != nil { + return 0, nil, err + } + for _, r := range res { + ids = append(ids, r.ID) + } + + total, err := db.GetEngine(ctx).Distinct("id").Table("issue").Where(cond).Count() + if err != nil { + return 0, nil, err + } + + return total, ids, nil +} diff --git a/models/issues/issue_stats.go b/models/issues/issue_stats.go new file mode 100644 index 0000000000..9b9562ebdd --- /dev/null +++ b/models/issues/issue_stats.go @@ -0,0 +1,383 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package issues + +import ( + "context" + "errors" + "fmt" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" + "xorm.io/xorm" +) + +// IssueStats represents issue statistic information. +type IssueStats struct { + OpenCount, ClosedCount int64 + YourRepositoriesCount int64 + AssignCount int64 + CreateCount int64 + MentionCount int64 + ReviewRequestedCount int64 + ReviewedCount int64 +} + +// Filter modes. +const ( + FilterModeAll = iota + FilterModeAssign + FilterModeCreate + FilterModeMention + FilterModeReviewRequested + FilterModeReviewed + FilterModeYourRepositories +) + +const ( + // MaxQueryParameters represents the max query parameters + // When queries are broken down in parts because of the number + // of parameters, attempt to break by this amount + MaxQueryParameters = 300 +) + +// CountIssuesByRepo map from repoID to number of issues matching the options +func CountIssuesByRepo(ctx context.Context, opts *IssuesOptions) (map[int64]int64, error) { + sess := db.GetEngine(ctx). + Join("INNER", "repository", "`issue`.repo_id = `repository`.id") + + applyConditions(sess, opts) + + countsSlice := make([]*struct { + RepoID int64 + Count int64 + }, 0, 10) + if err := sess.GroupBy("issue.repo_id"). + Select("issue.repo_id AS repo_id, COUNT(*) AS count"). + Table("issue"). + Find(&countsSlice); err != nil { + return nil, fmt.Errorf("unable to CountIssuesByRepo: %w", err) + } + + countMap := make(map[int64]int64, len(countsSlice)) + for _, c := range countsSlice { + countMap[c.RepoID] = c.Count + } + return countMap, nil +} + +// CountIssues number return of issues by given conditions. +func CountIssues(ctx context.Context, opts *IssuesOptions) (int64, error) { + sess := db.GetEngine(ctx). + Select("COUNT(issue.id) AS count"). + Table("issue"). + Join("INNER", "repository", "`issue`.repo_id = `repository`.id") + applyConditions(sess, opts) + + return sess.Count() +} + +// GetIssueStats returns issue statistic information by given conditions. +func GetIssueStats(opts *IssuesOptions) (*IssueStats, error) { + if len(opts.IssueIDs) <= MaxQueryParameters { + return getIssueStatsChunk(opts, opts.IssueIDs) + } + + // If too long a list of IDs is provided, we get the statistics in + // smaller chunks and get accumulates. Note: this could potentially + // get us invalid results. The alternative is to insert the list of + // ids in a temporary table and join from them. + accum := &IssueStats{} + for i := 0; i < len(opts.IssueIDs); { + chunk := i + MaxQueryParameters + if chunk > len(opts.IssueIDs) { + chunk = len(opts.IssueIDs) + } + stats, err := getIssueStatsChunk(opts, opts.IssueIDs[i:chunk]) + if err != nil { + return nil, err + } + accum.OpenCount += stats.OpenCount + accum.ClosedCount += stats.ClosedCount + accum.YourRepositoriesCount += stats.YourRepositoriesCount + accum.AssignCount += stats.AssignCount + accum.CreateCount += stats.CreateCount + accum.OpenCount += stats.MentionCount + accum.ReviewRequestedCount += stats.ReviewRequestedCount + accum.ReviewedCount += stats.ReviewedCount + i = chunk + } + return accum, nil +} + +func getIssueStatsChunk(opts *IssuesOptions, issueIDs []int64) (*IssueStats, error) { + stats := &IssueStats{} + + countSession := func(opts *IssuesOptions, issueIDs []int64) *xorm.Session { + sess := db.GetEngine(db.DefaultContext). + Join("INNER", "repository", "`issue`.repo_id = `repository`.id") + if len(opts.RepoIDs) > 1 { + sess.In("issue.repo_id", opts.RepoIDs) + } else if len(opts.RepoIDs) == 1 { + sess.And("issue.repo_id = ?", opts.RepoIDs[0]) + } + + if len(issueIDs) > 0 { + sess.In("issue.id", issueIDs) + } + + applyLabelsCondition(sess, opts) + + applyMilestoneCondition(sess, opts) + + if opts.ProjectID > 0 { + sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id"). + And("project_issue.project_id=?", opts.ProjectID) + } + + if opts.AssigneeID > 0 { + applyAssigneeCondition(sess, opts.AssigneeID) + } else if opts.AssigneeID == db.NoConditionID { + sess.Where("id NOT IN (SELECT issue_id FROM issue_assignees)") + } + + if opts.PosterID > 0 { + applyPosterCondition(sess, opts.PosterID) + } + + if opts.MentionedID > 0 { + applyMentionedCondition(sess, opts.MentionedID) + } + + if opts.ReviewRequestedID > 0 { + applyReviewRequestedCondition(sess, opts.ReviewRequestedID) + } + + if opts.ReviewedID > 0 { + applyReviewedCondition(sess, opts.ReviewedID) + } + + switch opts.IsPull { + case util.OptionalBoolTrue: + sess.And("issue.is_pull=?", true) + case util.OptionalBoolFalse: + sess.And("issue.is_pull=?", false) + } + + return sess + } + + var err error + stats.OpenCount, err = countSession(opts, issueIDs). + And("issue.is_closed = ?", false). + Count(new(Issue)) + if err != nil { + return stats, err + } + stats.ClosedCount, err = countSession(opts, issueIDs). + And("issue.is_closed = ?", true). + Count(new(Issue)) + return stats, err +} + +// GetUserIssueStats returns issue statistic information for dashboard by given conditions. +func GetUserIssueStats(filterMode int, opts IssuesOptions) (*IssueStats, error) { + if opts.User == nil { + return nil, errors.New("issue stats without user") + } + if opts.IsPull.IsNone() { + return nil, errors.New("unaccepted ispull option") + } + + var err error + stats := &IssueStats{} + + cond := builder.NewCond() + + cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.IsTrue()}) + + if len(opts.RepoIDs) > 0 { + cond = cond.And(builder.In("issue.repo_id", opts.RepoIDs)) + } + if len(opts.IssueIDs) > 0 { + cond = cond.And(builder.In("issue.id", opts.IssueIDs)) + } + if opts.RepoCond != nil { + cond = cond.And(opts.RepoCond) + } + + if opts.User != nil { + cond = cond.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.IsTrue())) + } + + sess := func(cond builder.Cond) *xorm.Session { + s := db.GetEngine(db.DefaultContext). + Join("INNER", "repository", "`issue`.repo_id = `repository`.id"). + Where(cond) + if len(opts.LabelIDs) > 0 { + s.Join("INNER", "issue_label", "issue_label.issue_id = issue.id"). + In("issue_label.label_id", opts.LabelIDs) + } + + if opts.IsArchived != util.OptionalBoolNone { + s.And(builder.Eq{"repository.is_archived": opts.IsArchived.IsTrue()}) + } + return s + } + + switch filterMode { + case FilterModeAll, FilterModeYourRepositories: + stats.OpenCount, err = sess(cond). + And("issue.is_closed = ?", false). + Count(new(Issue)) + if err != nil { + return nil, err + } + stats.ClosedCount, err = sess(cond). + And("issue.is_closed = ?", true). + Count(new(Issue)) + if err != nil { + return nil, err + } + case FilterModeAssign: + stats.OpenCount, err = applyAssigneeCondition(sess(cond), opts.User.ID). + And("issue.is_closed = ?", false). + Count(new(Issue)) + if err != nil { + return nil, err + } + stats.ClosedCount, err = applyAssigneeCondition(sess(cond), opts.User.ID). + And("issue.is_closed = ?", true). + Count(new(Issue)) + if err != nil { + return nil, err + } + case FilterModeCreate: + stats.OpenCount, err = applyPosterCondition(sess(cond), opts.User.ID). + And("issue.is_closed = ?", false). + Count(new(Issue)) + if err != nil { + return nil, err + } + stats.ClosedCount, err = applyPosterCondition(sess(cond), opts.User.ID). + And("issue.is_closed = ?", true). + Count(new(Issue)) + if err != nil { + return nil, err + } + case FilterModeMention: + stats.OpenCount, err = applyMentionedCondition(sess(cond), opts.User.ID). + And("issue.is_closed = ?", false). + Count(new(Issue)) + if err != nil { + return nil, err + } + stats.ClosedCount, err = applyMentionedCondition(sess(cond), opts.User.ID). + And("issue.is_closed = ?", true). + Count(new(Issue)) + if err != nil { + return nil, err + } + case FilterModeReviewRequested: + stats.OpenCount, err = applyReviewRequestedCondition(sess(cond), opts.User.ID). + And("issue.is_closed = ?", false). + Count(new(Issue)) + if err != nil { + return nil, err + } + stats.ClosedCount, err = applyReviewRequestedCondition(sess(cond), opts.User.ID). + And("issue.is_closed = ?", true). + Count(new(Issue)) + if err != nil { + return nil, err + } + case FilterModeReviewed: + stats.OpenCount, err = applyReviewedCondition(sess(cond), opts.User.ID). + And("issue.is_closed = ?", false). + Count(new(Issue)) + if err != nil { + return nil, err + } + stats.ClosedCount, err = applyReviewedCondition(sess(cond), opts.User.ID). + And("issue.is_closed = ?", true). + Count(new(Issue)) + if err != nil { + return nil, err + } + } + + cond = cond.And(builder.Eq{"issue.is_closed": opts.IsClosed.IsTrue()}) + stats.AssignCount, err = applyAssigneeCondition(sess(cond), opts.User.ID).Count(new(Issue)) + if err != nil { + return nil, err + } + + stats.CreateCount, err = applyPosterCondition(sess(cond), opts.User.ID).Count(new(Issue)) + if err != nil { + return nil, err + } + + stats.MentionCount, err = applyMentionedCondition(sess(cond), opts.User.ID).Count(new(Issue)) + if err != nil { + return nil, err + } + + stats.YourRepositoriesCount, err = sess(cond).Count(new(Issue)) + if err != nil { + return nil, err + } + + stats.ReviewRequestedCount, err = applyReviewRequestedCondition(sess(cond), opts.User.ID).Count(new(Issue)) + if err != nil { + return nil, err + } + + stats.ReviewedCount, err = applyReviewedCondition(sess(cond), opts.User.ID).Count(new(Issue)) + if err != nil { + return nil, err + } + + return stats, nil +} + +// GetRepoIssueStats returns number of open and closed repository issues by given filter mode. +func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen, numClosed int64) { + countSession := func(isClosed, isPull bool, repoID int64) *xorm.Session { + sess := db.GetEngine(db.DefaultContext). + Where("is_closed = ?", isClosed). + And("is_pull = ?", isPull). + And("repo_id = ?", repoID) + + return sess + } + + openCountSession := countSession(false, isPull, repoID) + closedCountSession := countSession(true, isPull, repoID) + + switch filterMode { + case FilterModeAssign: + applyAssigneeCondition(openCountSession, uid) + applyAssigneeCondition(closedCountSession, uid) + case FilterModeCreate: + applyPosterCondition(openCountSession, uid) + applyPosterCondition(closedCountSession, uid) + } + + openResult, _ := openCountSession.Count(new(Issue)) + closedResult, _ := closedCountSession.Count(new(Issue)) + + return openResult, closedResult +} + +// CountOrphanedIssues count issues without a repo +func CountOrphanedIssues(ctx context.Context) (int64, error) { + return db.GetEngine(ctx). + Table("issue"). + Join("LEFT", "repository", "issue.repo_id=repository.id"). + Where(builder.IsNull{"repository.id"}). + Select("COUNT(`issue`.`id`)"). + Count() +} diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go index 3a83d8d2b7..80699a57b4 100644 --- a/models/issues/issue_test.go +++ b/models/issues/issue_test.go @@ -17,6 +17,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" "xorm.io/builder" @@ -204,14 +205,16 @@ func TestIssues(t *testing.T) { func TestGetUserIssueStats(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) for _, test := range []struct { - Opts issues_model.UserIssueStatsOptions + FilterMode int + Opts issues_model.IssuesOptions ExpectedIssueStats issues_model.IssueStats }{ { - issues_model.UserIssueStatsOptions{ - UserID: 1, - RepoIDs: []int64{1}, - FilterMode: issues_model.FilterModeAll, + issues_model.FilterModeAll, + issues_model.IssuesOptions{ + User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}), + RepoIDs: []int64{1}, + IsPull: util.OptionalBoolFalse, }, issues_model.IssueStats{ YourRepositoriesCount: 1, // 6 @@ -222,11 +225,12 @@ func TestGetUserIssueStats(t *testing.T) { }, }, { - issues_model.UserIssueStatsOptions{ - UserID: 1, - RepoIDs: []int64{1}, - FilterMode: issues_model.FilterModeAll, - IsClosed: true, + issues_model.FilterModeAll, + issues_model.IssuesOptions{ + User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}), + RepoIDs: []int64{1}, + IsPull: util.OptionalBoolFalse, + IsClosed: util.OptionalBoolTrue, }, issues_model.IssueStats{ YourRepositoriesCount: 1, // 6 @@ -237,9 +241,10 @@ func TestGetUserIssueStats(t *testing.T) { }, }, { - issues_model.UserIssueStatsOptions{ - UserID: 1, - FilterMode: issues_model.FilterModeAssign, + issues_model.FilterModeAssign, + issues_model.IssuesOptions{ + User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}), + IsPull: util.OptionalBoolFalse, }, issues_model.IssueStats{ YourRepositoriesCount: 1, // 6 @@ -250,9 +255,10 @@ func TestGetUserIssueStats(t *testing.T) { }, }, { - issues_model.UserIssueStatsOptions{ - UserID: 1, - FilterMode: issues_model.FilterModeCreate, + issues_model.FilterModeCreate, + issues_model.IssuesOptions{ + User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}), + IsPull: util.OptionalBoolFalse, }, issues_model.IssueStats{ YourRepositoriesCount: 1, // 6 @@ -263,9 +269,10 @@ func TestGetUserIssueStats(t *testing.T) { }, }, { - issues_model.UserIssueStatsOptions{ - UserID: 1, - FilterMode: issues_model.FilterModeMention, + issues_model.FilterModeMention, + issues_model.IssuesOptions{ + User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}), + IsPull: util.OptionalBoolFalse, }, issues_model.IssueStats{ YourRepositoriesCount: 1, // 6 @@ -277,10 +284,11 @@ func TestGetUserIssueStats(t *testing.T) { }, }, { - issues_model.UserIssueStatsOptions{ - UserID: 1, - FilterMode: issues_model.FilterModeCreate, - IssueIDs: []int64{1}, + issues_model.FilterModeCreate, + issues_model.IssuesOptions{ + User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}), + IssueIDs: []int64{1}, + IsPull: util.OptionalBoolFalse, }, issues_model.IssueStats{ YourRepositoriesCount: 1, // 1 @@ -291,11 +299,12 @@ func TestGetUserIssueStats(t *testing.T) { }, }, { - issues_model.UserIssueStatsOptions{ - UserID: 2, - Org: unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}), - Team: unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 7}), - FilterMode: issues_model.FilterModeAll, + issues_model.FilterModeAll, + issues_model.IssuesOptions{ + User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}), + Org: unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}), + Team: unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 7}), + IsPull: util.OptionalBoolFalse, }, issues_model.IssueStats{ YourRepositoriesCount: 2, @@ -306,7 +315,7 @@ func TestGetUserIssueStats(t *testing.T) { }, } { t.Run(fmt.Sprintf("%#v", test.Opts), func(t *testing.T) { - stats, err := issues_model.GetUserIssueStats(test.Opts) + stats, err := issues_model.GetUserIssueStats(test.FilterMode, test.Opts) if !assert.NoError(t, err) { return } @@ -494,8 +503,8 @@ func TestCorrectIssueStats(t *testing.T) { // Now we will call the GetIssueStats with these IDs and if working, // get the correct stats back. - issueStats, err := issues_model.GetIssueStats(&issues_model.IssueStatsOptions{ - RepoID: 1, + issueStats, err := issues_model.GetIssueStats(&issues_model.IssuesOptions{ + RepoIDs: []int64{1}, IssueIDs: ids, }) diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go new file mode 100644 index 0000000000..b6fd720fe5 --- /dev/null +++ b/models/issues/issue_update.go @@ -0,0 +1,815 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package issues + +import ( + "context" + "fmt" + "strings" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" + project_model "code.gitea.io/gitea/models/project" + repo_model "code.gitea.io/gitea/models/repo" + system_model "code.gitea.io/gitea/models/system" + "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/references" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/builder" +) + +// UpdateIssueCols updates cols of issue +func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error { + if _, err := db.GetEngine(ctx).ID(issue.ID).Cols(cols...).Update(issue); err != nil { + return err + } + return nil +} + +func changeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed, isMergePull bool) (*Comment, error) { + // Reload the issue + currentIssue, err := GetIssueByID(ctx, issue.ID) + if err != nil { + return nil, err + } + + // Nothing should be performed if current status is same as target status + if currentIssue.IsClosed == isClosed { + if !issue.IsPull { + return nil, ErrIssueWasClosed{ + ID: issue.ID, + } + } + return nil, ErrPullWasClosed{ + ID: issue.ID, + } + } + + issue.IsClosed = isClosed + return doChangeIssueStatus(ctx, issue, doer, isMergePull) +} + +func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isMergePull bool) (*Comment, error) { + // Check for open dependencies + if issue.IsClosed && issue.Repo.IsDependenciesEnabled(ctx) { + // only check if dependencies are enabled and we're about to close an issue, otherwise reopening an issue would fail when there are unsatisfied dependencies + noDeps, err := IssueNoDependenciesLeft(ctx, issue) + if err != nil { + return nil, err + } + + if !noDeps { + return nil, ErrDependenciesLeft{issue.ID} + } + } + + if issue.IsClosed { + issue.ClosedUnix = timeutil.TimeStampNow() + } else { + issue.ClosedUnix = 0 + } + + if err := UpdateIssueCols(ctx, issue, "is_closed", "closed_unix"); err != nil { + return nil, err + } + + // Update issue count of labels + if err := issue.LoadLabels(ctx); err != nil { + return nil, err + } + for idx := range issue.Labels { + if err := updateLabelCols(ctx, issue.Labels[idx], "num_issues", "num_closed_issue"); err != nil { + return nil, err + } + } + + // Update issue count of milestone + if issue.MilestoneID > 0 { + if err := UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil { + return nil, err + } + } + + // update repository's issue closed number + if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, true); err != nil { + return nil, err + } + + // New action comment + cmtType := CommentTypeClose + if !issue.IsClosed { + cmtType = CommentTypeReopen + } else if isMergePull { + cmtType = CommentTypeMergePull + } + + return CreateComment(ctx, &CreateCommentOptions{ + Type: cmtType, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + }) +} + +// ChangeIssueStatus changes issue status to open or closed. +func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed bool) (*Comment, error) { + if err := issue.LoadRepo(ctx); err != nil { + return nil, err + } + if err := issue.LoadPoster(ctx); err != nil { + return nil, err + } + + return changeIssueStatus(ctx, issue, doer, isClosed, false) +} + +// ChangeIssueTitle changes the title of this issue, as the given user. +func ChangeIssueTitle(ctx context.Context, issue *Issue, doer *user_model.User, oldTitle string) (err error) { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + + if err = UpdateIssueCols(ctx, issue, "name"); err != nil { + return fmt.Errorf("updateIssueCols: %w", err) + } + + if err = issue.LoadRepo(ctx); err != nil { + return fmt.Errorf("loadRepo: %w", err) + } + + opts := &CreateCommentOptions{ + Type: CommentTypeChangeTitle, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + OldTitle: oldTitle, + NewTitle: issue.Title, + } + if _, err = CreateComment(ctx, opts); err != nil { + return fmt.Errorf("createComment: %w", err) + } + if err = issue.AddCrossReferences(ctx, doer, true); err != nil { + return err + } + + return committer.Commit() +} + +// ChangeIssueRef changes the branch of this issue, as the given user. +func ChangeIssueRef(issue *Issue, doer *user_model.User, oldRef string) (err error) { + ctx, committer, err := db.TxContext(db.DefaultContext) + if err != nil { + return err + } + defer committer.Close() + + if err = UpdateIssueCols(ctx, issue, "ref"); err != nil { + return fmt.Errorf("updateIssueCols: %w", err) + } + + if err = issue.LoadRepo(ctx); err != nil { + return fmt.Errorf("loadRepo: %w", err) + } + oldRefFriendly := strings.TrimPrefix(oldRef, git.BranchPrefix) + newRefFriendly := strings.TrimPrefix(issue.Ref, git.BranchPrefix) + + opts := &CreateCommentOptions{ + Type: CommentTypeChangeIssueRef, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + OldRef: oldRefFriendly, + NewRef: newRefFriendly, + } + if _, err = CreateComment(ctx, opts); err != nil { + return fmt.Errorf("createComment: %w", err) + } + + return committer.Commit() +} + +// AddDeletePRBranchComment adds delete branch comment for pull request issue +func AddDeletePRBranchComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issueID int64, branchName string) error { + issue, err := GetIssueByID(ctx, issueID) + if err != nil { + return err + } + opts := &CreateCommentOptions{ + Type: CommentTypeDeleteBranch, + Doer: doer, + Repo: repo, + Issue: issue, + OldRef: branchName, + } + _, err = CreateComment(ctx, opts) + return err +} + +// UpdateIssueAttachments update attachments by UUIDs for the issue +func UpdateIssueAttachments(issueID int64, uuids []string) (err error) { + ctx, committer, err := db.TxContext(db.DefaultContext) + if err != nil { + return err + } + defer committer.Close() + attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids) + if err != nil { + return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err) + } + for i := 0; i < len(attachments); i++ { + attachments[i].IssueID = issueID + if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { + return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err) + } + } + return committer.Commit() +} + +// ChangeIssueContent changes issue content, as the given user. +func ChangeIssueContent(issue *Issue, doer *user_model.User, content string) (err error) { + ctx, committer, err := db.TxContext(db.DefaultContext) + if err != nil { + return err + } + defer committer.Close() + + hasContentHistory, err := HasIssueContentHistory(ctx, issue.ID, 0) + if err != nil { + return fmt.Errorf("HasIssueContentHistory: %w", err) + } + if !hasContentHistory { + if err = SaveIssueContentHistory(ctx, issue.PosterID, issue.ID, 0, + issue.CreatedUnix, issue.Content, true); err != nil { + return fmt.Errorf("SaveIssueContentHistory: %w", err) + } + } + + issue.Content = content + + if err = UpdateIssueCols(ctx, issue, "content"); err != nil { + return fmt.Errorf("UpdateIssueCols: %w", err) + } + + if err = SaveIssueContentHistory(ctx, doer.ID, issue.ID, 0, + timeutil.TimeStampNow(), issue.Content, false); err != nil { + return fmt.Errorf("SaveIssueContentHistory: %w", err) + } + + if err = issue.AddCrossReferences(ctx, doer, true); err != nil { + return fmt.Errorf("addCrossReferences: %w", err) + } + + return committer.Commit() +} + +// NewIssueOptions represents the options of a new issue. +type NewIssueOptions struct { + Repo *repo_model.Repository + Issue *Issue + LabelIDs []int64 + Attachments []string // In UUID format. + IsPull bool +} + +// NewIssueWithIndex creates issue with given index +func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssueOptions) (err error) { + e := db.GetEngine(ctx) + opts.Issue.Title = strings.TrimSpace(opts.Issue.Title) + + if opts.Issue.MilestoneID > 0 { + milestone, err := GetMilestoneByRepoID(ctx, opts.Issue.RepoID, opts.Issue.MilestoneID) + if err != nil && !IsErrMilestoneNotExist(err) { + return fmt.Errorf("getMilestoneByID: %w", err) + } + + // Assume milestone is invalid and drop silently. + opts.Issue.MilestoneID = 0 + if milestone != nil { + opts.Issue.MilestoneID = milestone.ID + opts.Issue.Milestone = milestone + } + } + + if opts.Issue.Index <= 0 { + return fmt.Errorf("no issue index provided") + } + if opts.Issue.ID > 0 { + return fmt.Errorf("issue exist") + } + + if _, err := e.Insert(opts.Issue); err != nil { + return err + } + + if opts.Issue.MilestoneID > 0 { + if err := UpdateMilestoneCounters(ctx, opts.Issue.MilestoneID); err != nil { + return err + } + + opts := &CreateCommentOptions{ + Type: CommentTypeMilestone, + Doer: doer, + Repo: opts.Repo, + Issue: opts.Issue, + OldMilestoneID: 0, + MilestoneID: opts.Issue.MilestoneID, + } + if _, err = CreateComment(ctx, opts); err != nil { + return err + } + } + + if err := repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.IsPull, false); err != nil { + return err + } + + if len(opts.LabelIDs) > 0 { + // During the session, SQLite3 driver cannot handle retrieve objects after update something. + // So we have to get all needed labels first. + labels := make([]*Label, 0, len(opts.LabelIDs)) + if err = e.In("id", opts.LabelIDs).Find(&labels); err != nil { + return fmt.Errorf("find all labels [label_ids: %v]: %w", opts.LabelIDs, err) + } + + if err = opts.Issue.LoadPoster(ctx); err != nil { + return err + } + + for _, label := range labels { + // Silently drop invalid labels. + if label.RepoID != opts.Repo.ID && label.OrgID != opts.Repo.OwnerID { + continue + } + + if err = newIssueLabel(ctx, opts.Issue, label, opts.Issue.Poster); err != nil { + return fmt.Errorf("addLabel [id: %d]: %w", label.ID, err) + } + } + } + + if err = NewIssueUsers(ctx, opts.Repo, opts.Issue); err != nil { + return err + } + + if len(opts.Attachments) > 0 { + attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments) + if err != nil { + return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err) + } + + for i := 0; i < len(attachments); i++ { + attachments[i].IssueID = opts.Issue.ID + if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil { + return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err) + } + } + } + if err = opts.Issue.LoadAttributes(ctx); err != nil { + return err + } + + return opts.Issue.AddCrossReferences(ctx, doer, false) +} + +// NewIssue creates new issue with labels for repository. +func NewIssue(repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) { + ctx, committer, err := db.TxContext(db.DefaultContext) + if err != nil { + return err + } + defer committer.Close() + + idx, err := db.GetNextResourceIndex(ctx, "issue_index", repo.ID) + if err != nil { + return fmt.Errorf("generate issue index failed: %w", err) + } + + issue.Index = idx + + if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ + Repo: repo, + Issue: issue, + LabelIDs: labelIDs, + Attachments: uuids, + }); err != nil { + if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { + return err + } + return fmt.Errorf("newIssue: %w", err) + } + + if err = committer.Commit(); err != nil { + return fmt.Errorf("Commit: %w", err) + } + + return nil +} + +// UpdateIssueMentions updates issue-user relations for mentioned users. +func UpdateIssueMentions(ctx context.Context, issueID int64, mentions []*user_model.User) error { + if len(mentions) == 0 { + return nil + } + ids := make([]int64, len(mentions)) + for i, u := range mentions { + ids[i] = u.ID + } + if err := UpdateIssueUsersByMentions(ctx, issueID, ids); err != nil { + return fmt.Errorf("UpdateIssueUsersByMentions: %w", err) + } + return nil +} + +// UpdateIssueByAPI updates all allowed fields of given issue. +// If the issue status is changed a statusChangeComment is returned +// similarly if the title is changed the titleChanged bool is set to true +func UpdateIssueByAPI(issue *Issue, doer *user_model.User) (statusChangeComment *Comment, titleChanged bool, err error) { + ctx, committer, err := db.TxContext(db.DefaultContext) + if err != nil { + return nil, false, err + } + defer committer.Close() + + if err := issue.LoadRepo(ctx); err != nil { + return nil, false, fmt.Errorf("loadRepo: %w", err) + } + + // Reload the issue + currentIssue, err := GetIssueByID(ctx, issue.ID) + if err != nil { + return nil, false, err + } + + if _, err := db.GetEngine(ctx).ID(issue.ID).Cols( + "name", "content", "milestone_id", "priority", + "deadline_unix", "updated_unix", "is_locked"). + Update(issue); err != nil { + return nil, false, err + } + + titleChanged = currentIssue.Title != issue.Title + if titleChanged { + opts := &CreateCommentOptions{ + Type: CommentTypeChangeTitle, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + OldTitle: currentIssue.Title, + NewTitle: issue.Title, + } + _, err := CreateComment(ctx, opts) + if err != nil { + return nil, false, fmt.Errorf("createComment: %w", err) + } + } + + if currentIssue.IsClosed != issue.IsClosed { + statusChangeComment, err = doChangeIssueStatus(ctx, issue, doer, false) + if err != nil { + return nil, false, err + } + } + + if err := issue.AddCrossReferences(ctx, doer, true); err != nil { + return nil, false, err + } + return statusChangeComment, titleChanged, committer.Commit() +} + +// UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it. +func UpdateIssueDeadline(issue *Issue, deadlineUnix timeutil.TimeStamp, doer *user_model.User) (err error) { + // if the deadline hasn't changed do nothing + if issue.DeadlineUnix == deadlineUnix { + return nil + } + ctx, committer, err := db.TxContext(db.DefaultContext) + if err != nil { + return err + } + defer committer.Close() + + // Update the deadline + if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, DeadlineUnix: deadlineUnix}, "deadline_unix"); err != nil { + return err + } + + // Make the comment + if _, err = createDeadlineComment(ctx, doer, issue, deadlineUnix); err != nil { + return fmt.Errorf("createRemovedDueDateComment: %w", err) + } + + return committer.Commit() +} + +// DeleteInIssue delete records in beans with external key issue_id = ? +func DeleteInIssue(ctx context.Context, issueID int64, beans ...interface{}) error { + e := db.GetEngine(ctx) + for _, bean := range beans { + if _, err := e.In("issue_id", issueID).Delete(bean); err != nil { + return err + } + } + return nil +} + +// FindAndUpdateIssueMentions finds users mentioned in the given content string, and saves them in the database. +func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_model.User, content string) (mentions []*user_model.User, err error) { + rawMentions := references.FindAllMentionsMarkdown(content) + mentions, err = ResolveIssueMentionsByVisibility(ctx, issue, doer, rawMentions) + if err != nil { + return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err) + } + if err = UpdateIssueMentions(ctx, issue.ID, mentions); err != nil { + return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err) + } + return mentions, err +} + +// ResolveIssueMentionsByVisibility returns the users mentioned in an issue, removing those that +// don't have access to reading it. Teams are expanded into their users, but organizations are ignored. +func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *user_model.User, mentions []string) (users []*user_model.User, err error) { + if len(mentions) == 0 { + return + } + if err = issue.LoadRepo(ctx); err != nil { + return + } + + resolved := make(map[string]bool, 10) + var mentionTeams []string + + if err := issue.Repo.LoadOwner(ctx); err != nil { + return nil, err + } + + repoOwnerIsOrg := issue.Repo.Owner.IsOrganization() + if repoOwnerIsOrg { + mentionTeams = make([]string, 0, 5) + } + + resolved[doer.LowerName] = true + for _, name := range mentions { + name := strings.ToLower(name) + if _, ok := resolved[name]; ok { + continue + } + if repoOwnerIsOrg && strings.Contains(name, "/") { + names := strings.Split(name, "/") + if len(names) < 2 || names[0] != issue.Repo.Owner.LowerName { + continue + } + mentionTeams = append(mentionTeams, names[1]) + resolved[name] = true + } else { + resolved[name] = false + } + } + + if issue.Repo.Owner.IsOrganization() && len(mentionTeams) > 0 { + teams := make([]*organization.Team, 0, len(mentionTeams)) + if err := db.GetEngine(ctx). + Join("INNER", "team_repo", "team_repo.team_id = team.id"). + Where("team_repo.repo_id=?", issue.Repo.ID). + In("team.lower_name", mentionTeams). + Find(&teams); err != nil { + return nil, fmt.Errorf("find mentioned teams: %w", err) + } + if len(teams) != 0 { + checked := make([]int64, 0, len(teams)) + unittype := unit.TypeIssues + if issue.IsPull { + unittype = unit.TypePullRequests + } + for _, team := range teams { + if team.AccessMode >= perm.AccessModeAdmin { + checked = append(checked, team.ID) + resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true + continue + } + has, err := db.GetEngine(ctx).Get(&organization.TeamUnit{OrgID: issue.Repo.Owner.ID, TeamID: team.ID, Type: unittype}) + if err != nil { + return nil, fmt.Errorf("get team units (%d): %w", team.ID, err) + } + if has { + checked = append(checked, team.ID) + resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true + } + } + if len(checked) != 0 { + teamusers := make([]*user_model.User, 0, 20) + if err := db.GetEngine(ctx). + Join("INNER", "team_user", "team_user.uid = `user`.id"). + In("`team_user`.team_id", checked). + And("`user`.is_active = ?", true). + And("`user`.prohibit_login = ?", false). + Find(&teamusers); err != nil { + return nil, fmt.Errorf("get teams users: %w", err) + } + if len(teamusers) > 0 { + users = make([]*user_model.User, 0, len(teamusers)) + for _, user := range teamusers { + if already, ok := resolved[user.LowerName]; !ok || !already { + users = append(users, user) + resolved[user.LowerName] = true + } + } + } + } + } + } + + // Remove names already in the list to avoid querying the database if pending names remain + mentionUsers := make([]string, 0, len(resolved)) + for name, already := range resolved { + if !already { + mentionUsers = append(mentionUsers, name) + } + } + if len(mentionUsers) == 0 { + return + } + + if users == nil { + users = make([]*user_model.User, 0, len(mentionUsers)) + } + + unchecked := make([]*user_model.User, 0, len(mentionUsers)) + if err := db.GetEngine(ctx). + Where("`user`.is_active = ?", true). + And("`user`.prohibit_login = ?", false). + In("`user`.lower_name", mentionUsers). + Find(&unchecked); err != nil { + return nil, fmt.Errorf("find mentioned users: %w", err) + } + for _, user := range unchecked { + if already := resolved[user.LowerName]; already || user.IsOrganization() { + continue + } + // Normal users must have read access to the referencing issue + perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, user) + if err != nil { + return nil, fmt.Errorf("GetUserRepoPermission [%d]: %w", user.ID, err) + } + if !perm.CanReadIssuesOrPulls(issue.IsPull) { + continue + } + users = append(users, user) + } + + return users, err +} + +// UpdateIssuesMigrationsByType updates all migrated repositories' issues from gitServiceType to replace originalAuthorID to posterID +func UpdateIssuesMigrationsByType(gitServiceType api.GitServiceType, originalAuthorID string, posterID int64) error { + _, err := db.GetEngine(db.DefaultContext).Table("issue"). + Where("repo_id IN (SELECT id FROM repository WHERE original_service_type = ?)", gitServiceType). + And("original_author_id = ?", originalAuthorID). + Update(map[string]interface{}{ + "poster_id": posterID, + "original_author": "", + "original_author_id": 0, + }) + return err +} + +// UpdateReactionsMigrationsByType updates all migrated repositories' reactions from gitServiceType to replace originalAuthorID to posterID +func UpdateReactionsMigrationsByType(gitServiceType api.GitServiceType, originalAuthorID string, userID int64) error { + _, err := db.GetEngine(db.DefaultContext).Table("reaction"). + Where("original_author_id = ?", originalAuthorID). + And(migratedIssueCond(gitServiceType)). + Update(map[string]interface{}{ + "user_id": userID, + "original_author": "", + "original_author_id": 0, + }) + return err +} + +// DeleteIssuesByRepoID deletes issues by repositories id +func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) { + deleteCond := builder.Select("id").From("issue").Where(builder.Eq{"issue.repo_id": repoID}) + + sess := db.GetEngine(ctx) + // Delete content histories + if _, err = sess.In("issue_id", deleteCond). + Delete(&ContentHistory{}); err != nil { + return + } + + // Delete comments and attachments + if _, err = sess.In("issue_id", deleteCond). + Delete(&Comment{}); err != nil { + return + } + + // Dependencies for issues in this repository + if _, err = sess.In("issue_id", deleteCond). + Delete(&IssueDependency{}); err != nil { + return + } + + // Delete dependencies for issues in other repositories + if _, err = sess.In("dependency_id", deleteCond). + Delete(&IssueDependency{}); err != nil { + return + } + + if _, err = sess.In("issue_id", deleteCond). + Delete(&IssueUser{}); err != nil { + return + } + + if _, err = sess.In("issue_id", deleteCond). + Delete(&Reaction{}); err != nil { + return + } + + if _, err = sess.In("issue_id", deleteCond). + Delete(&IssueWatch{}); err != nil { + return + } + + if _, err = sess.In("issue_id", deleteCond). + Delete(&Stopwatch{}); err != nil { + return + } + + if _, err = sess.In("issue_id", deleteCond). + Delete(&TrackedTime{}); err != nil { + return + } + + if _, err = sess.In("issue_id", deleteCond). + Delete(&project_model.ProjectIssue{}); err != nil { + return + } + + if _, err = sess.In("dependent_issue_id", deleteCond). + Delete(&Comment{}); err != nil { + return + } + + var attachments []*repo_model.Attachment + if err = sess.In("issue_id", deleteCond). + Find(&attachments); err != nil { + return + } + + for j := range attachments { + attachmentPaths = append(attachmentPaths, attachments[j].RelativePath()) + } + + if _, err = sess.In("issue_id", deleteCond). + Delete(&repo_model.Attachment{}); err != nil { + return + } + + if _, err = db.DeleteByBean(ctx, &Issue{RepoID: repoID}); err != nil { + return + } + + return attachmentPaths, err +} + +// DeleteOrphanedIssues delete issues without a repo +func DeleteOrphanedIssues(ctx context.Context) error { + var attachmentPaths []string + err := db.WithTx(ctx, func(ctx context.Context) error { + var ids []int64 + + if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id"). + Join("LEFT", "repository", "issue.repo_id=repository.id"). + Where(builder.IsNull{"repository.id"}).GroupBy("issue.repo_id"). + Find(&ids); err != nil { + return err + } + + for i := range ids { + paths, err := DeleteIssuesByRepoID(ctx, ids[i]) + if err != nil { + return err + } + attachmentPaths = append(attachmentPaths, paths...) + } + + return nil + }) + if err != nil { + return err + } + + // Remove issue attachment files. + for i := range attachmentPaths { + system_model.RemoveAllWithNotice(db.DefaultContext, "Delete issue attachment", attachmentPaths[i]) + } + return nil +} diff --git a/models/issues/label.go b/models/issues/label.go index 9c22dcdd2d..8f2cf05a28 100644 --- a/models/issues/label.go +++ b/models/issues/label.go @@ -11,7 +11,6 @@ import ( "strings" "code.gitea.io/gitea/models/db" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/label" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -113,7 +112,7 @@ func (l *Label) CalOpenIssues() { // CalOpenOrgIssues calculates the open issues of a label for a specific repo func (l *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) { counts, _ := CountIssuesByRepo(ctx, &IssuesOptions{ - RepoID: repoID, + RepoIDs: []int64{repoID}, LabelIDs: []int64{labelID}, IsClosed: util.OptionalBoolFalse, }) @@ -282,13 +281,6 @@ func GetLabelsByIDs(labelIDs []int64) ([]*Label, error) { Find(&labels) } -// __________ .__ __ -// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__. -// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | | -// | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ | -// |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____| -// \/ \/|__| \/ \/ - // GetLabelInRepoByName returns a label by name in given repository. func GetLabelInRepoByName(ctx context.Context, repoID int64, labelName string) (*Label, error) { if len(labelName) == 0 || repoID <= 0 { @@ -393,13 +385,6 @@ func CountLabelsByRepoID(repoID int64) (int64, error) { return db.GetEngine(db.DefaultContext).Where("repo_id = ?", repoID).Count(&Label{}) } -// ________ -// \_____ \_______ ____ -// / | \_ __ \/ ___\ -// / | \ | \/ /_/ > -// \_______ /__| \___ / -// \/ /_____/ - // GetLabelInOrgByName returns a label by name in given organization. func GetLabelInOrgByName(ctx context.Context, orgID int64, labelName string) (*Label, error) { if len(labelName) == 0 || orgID <= 0 { @@ -496,22 +481,6 @@ func CountLabelsByOrgID(orgID int64) (int64, error) { return db.GetEngine(db.DefaultContext).Where("org_id = ?", orgID).Count(&Label{}) } -// .___ -// | | ______ ________ __ ____ -// | |/ ___// ___/ | \_/ __ \ -// | |\___ \ \___ \| | /\ ___/ -// |___/____ >____ >____/ \___ | -// \/ \/ \/ - -// GetLabelsByIssueID returns all labels that belong to given issue by ID. -func GetLabelsByIssueID(ctx context.Context, issueID int64) ([]*Label, error) { - var labels []*Label - return labels, db.GetEngine(ctx).Where("issue_label.issue_id = ?", issueID). - Join("LEFT", "issue_label", "issue_label.label_id = label.id"). - Asc("label.name"). - Find(&labels) -} - func updateLabelCols(ctx context.Context, l *Label, cols ...string) error { _, err := db.GetEngine(ctx).ID(l.ID). SetExpr("num_issues", @@ -529,307 +498,3 @@ func updateLabelCols(ctx context.Context, l *Label, cols ...string) error { Cols(cols...).Update(l) return err } - -// .___ .____ ___. .__ -// | | ______ ________ __ ____ | | _____ \_ |__ ____ | | -// | |/ ___// ___/ | \_/ __ \| | \__ \ | __ \_/ __ \| | -// | |\___ \ \___ \| | /\ ___/| |___ / __ \| \_\ \ ___/| |__ -// |___/____ >____ >____/ \___ >_______ (____ /___ /\___ >____/ -// \/ \/ \/ \/ \/ \/ \/ - -// IssueLabel represents an issue-label relation. -type IssueLabel struct { - ID int64 `xorm:"pk autoincr"` - IssueID int64 `xorm:"UNIQUE(s)"` - LabelID int64 `xorm:"UNIQUE(s)"` -} - -// HasIssueLabel returns true if issue has been labeled. -func HasIssueLabel(ctx context.Context, issueID, labelID int64) bool { - has, _ := db.GetEngine(ctx).Where("issue_id = ? AND label_id = ?", issueID, labelID).Get(new(IssueLabel)) - return has -} - -// newIssueLabel this function creates a new label it does not check if the label is valid for the issue -// YOU MUST CHECK THIS BEFORE THIS FUNCTION -func newIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) { - if err = db.Insert(ctx, &IssueLabel{ - IssueID: issue.ID, - LabelID: label.ID, - }); err != nil { - return err - } - - if err = issue.LoadRepo(ctx); err != nil { - return - } - - opts := &CreateCommentOptions{ - Type: CommentTypeLabel, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - Label: label, - Content: "1", - } - if _, err = CreateComment(ctx, opts); err != nil { - return err - } - - return updateLabelCols(ctx, label, "num_issues", "num_closed_issue") -} - -// Remove all issue labels in the given exclusive scope -func RemoveDuplicateExclusiveIssueLabels(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) { - scope := label.ExclusiveScope() - if scope == "" { - return nil - } - - var toRemove []*Label - for _, issueLabel := range issue.Labels { - if label.ID != issueLabel.ID && issueLabel.ExclusiveScope() == scope { - toRemove = append(toRemove, issueLabel) - } - } - - for _, issueLabel := range toRemove { - if err = deleteIssueLabel(ctx, issue, issueLabel, doer); err != nil { - return err - } - } - - return nil -} - -// NewIssueLabel creates a new issue-label relation. -func NewIssueLabel(issue *Issue, label *Label, doer *user_model.User) (err error) { - if HasIssueLabel(db.DefaultContext, issue.ID, label.ID) { - return nil - } - - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return err - } - defer committer.Close() - - if err = issue.LoadRepo(ctx); err != nil { - return err - } - - // Do NOT add invalid labels - if issue.RepoID != label.RepoID && issue.Repo.OwnerID != label.OrgID { - return nil - } - - if err = RemoveDuplicateExclusiveIssueLabels(ctx, issue, label, doer); err != nil { - return nil - } - - if err = newIssueLabel(ctx, issue, label, doer); err != nil { - return err - } - - issue.Labels = nil - if err = issue.LoadLabels(ctx); err != nil { - return err - } - - return committer.Commit() -} - -// newIssueLabels add labels to an issue. It will check if the labels are valid for the issue -func newIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *user_model.User) (err error) { - if err = issue.LoadRepo(ctx); err != nil { - return err - } - for _, l := range labels { - // Don't add already present labels and invalid labels - if HasIssueLabel(ctx, issue.ID, l.ID) || - (l.RepoID != issue.RepoID && l.OrgID != issue.Repo.OwnerID) { - continue - } - - if err = newIssueLabel(ctx, issue, l, doer); err != nil { - return fmt.Errorf("newIssueLabel: %w", err) - } - } - - return nil -} - -// NewIssueLabels creates a list of issue-label relations. -func NewIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (err error) { - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return err - } - defer committer.Close() - - if err = newIssueLabels(ctx, issue, labels, doer); err != nil { - return err - } - - issue.Labels = nil - if err = issue.LoadLabels(ctx); err != nil { - return err - } - - return committer.Commit() -} - -func deleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) { - if count, err := db.DeleteByBean(ctx, &IssueLabel{ - IssueID: issue.ID, - LabelID: label.ID, - }); err != nil { - return err - } else if count == 0 { - return nil - } - - if err = issue.LoadRepo(ctx); err != nil { - return - } - - opts := &CreateCommentOptions{ - Type: CommentTypeLabel, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - Label: label, - } - if _, err = CreateComment(ctx, opts); err != nil { - return err - } - - return updateLabelCols(ctx, label, "num_issues", "num_closed_issue") -} - -// DeleteIssueLabel deletes issue-label relation. -func DeleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) error { - if err := deleteIssueLabel(ctx, issue, label, doer); err != nil { - return err - } - - issue.Labels = nil - return issue.LoadLabels(ctx) -} - -// DeleteLabelsByRepoID deletes labels of some repository -func DeleteLabelsByRepoID(ctx context.Context, repoID int64) error { - deleteCond := builder.Select("id").From("label").Where(builder.Eq{"label.repo_id": repoID}) - - if _, err := db.GetEngine(ctx).In("label_id", deleteCond). - Delete(&IssueLabel{}); err != nil { - return err - } - - _, err := db.DeleteByBean(ctx, &Label{RepoID: repoID}) - return err -} - -// CountOrphanedLabels return count of labels witch are broken and not accessible via ui anymore -func CountOrphanedLabels(ctx context.Context) (int64, error) { - noref, err := db.GetEngine(ctx).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Count() - if err != nil { - return 0, err - } - - norepo, err := db.GetEngine(ctx).Table("label"). - Where(builder.And( - builder.Gt{"repo_id": 0}, - builder.NotIn("repo_id", builder.Select("id").From("`repository`")), - )). - Count() - if err != nil { - return 0, err - } - - noorg, err := db.GetEngine(ctx).Table("label"). - Where(builder.And( - builder.Gt{"org_id": 0}, - builder.NotIn("org_id", builder.Select("id").From("`user`")), - )). - Count() - if err != nil { - return 0, err - } - - return noref + norepo + noorg, nil -} - -// DeleteOrphanedLabels delete labels witch are broken and not accessible via ui anymore -func DeleteOrphanedLabels(ctx context.Context) error { - // delete labels with no reference - if _, err := db.GetEngine(ctx).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Delete(new(Label)); err != nil { - return err - } - - // delete labels with none existing repos - if _, err := db.GetEngine(ctx). - Where(builder.And( - builder.Gt{"repo_id": 0}, - builder.NotIn("repo_id", builder.Select("id").From("`repository`")), - )). - Delete(Label{}); err != nil { - return err - } - - // delete labels with none existing orgs - if _, err := db.GetEngine(ctx). - Where(builder.And( - builder.Gt{"org_id": 0}, - builder.NotIn("org_id", builder.Select("id").From("`user`")), - )). - Delete(Label{}); err != nil { - return err - } - - return nil -} - -// CountOrphanedIssueLabels return count of IssueLabels witch have no label behind anymore -func CountOrphanedIssueLabels(ctx context.Context) (int64, error) { - return db.GetEngine(ctx).Table("issue_label"). - NotIn("label_id", builder.Select("id").From("label")). - Count() -} - -// DeleteOrphanedIssueLabels delete IssueLabels witch have no label behind anymore -func DeleteOrphanedIssueLabels(ctx context.Context) error { - _, err := db.GetEngine(ctx). - NotIn("label_id", builder.Select("id").From("label")). - Delete(IssueLabel{}) - return err -} - -// CountIssueLabelWithOutsideLabels count label comments with outside label -func CountIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) { - return db.GetEngine(ctx).Where(builder.Expr("(label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id)")). - Table("issue_label"). - Join("inner", "label", "issue_label.label_id = label.id "). - Join("inner", "issue", "issue.id = issue_label.issue_id "). - Join("inner", "repository", "issue.repo_id = repository.id"). - Count(new(IssueLabel)) -} - -// FixIssueLabelWithOutsideLabels fix label comments with outside label -func FixIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) { - res, err := db.GetEngine(ctx).Exec(`DELETE FROM issue_label WHERE issue_label.id IN ( - SELECT il_too.id FROM ( - SELECT il_too_too.id - FROM issue_label AS il_too_too - INNER JOIN label ON il_too_too.label_id = label.id - INNER JOIN issue on issue.id = il_too_too.issue_id - INNER JOIN repository on repository.id = issue.repo_id - WHERE - (label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id) - ) AS il_too )`) - if err != nil { - return 0, err - } - - return res.RowsAffected() -} diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go index f2adac0701..c443928344 100644 --- a/models/issues/pull_list.go +++ b/models/issues/pull_list.go @@ -141,7 +141,7 @@ func PullRequests(baseRepoID int64, opts *PullRequestsOptions) ([]*PullRequest, } findSession, err := listPullRequestStatement(baseRepoID, opts) - sortIssuesSession(findSession, opts.SortType, 0) + applySorts(findSession, opts.SortType, 0) if err != nil { log.Error("listPullRequestStatement: %v", err) return nil, maxResults, err diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index b1d3eb1b03..173dee4967 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -491,6 +491,8 @@ var migrations = []Migration{ NewMigration("Add ArchivedUnix Column", v1_20.AddArchivedUnixToRepository), // v256 -> v257 NewMigration("Add is_internal column to package", v1_20.AddIsInternalColumnToPackage), + // v257 -> v258 + NewMigration("Add Actions Artifact table", v1_20.CreateActionArtifactTable), // to modify later NewMigration("Add size limit on repository", v1_20.AddSizeLimitOnRepo), } diff --git a/models/migrations/v1_20/v257.go b/models/migrations/v1_20/v257.go new file mode 100644 index 0000000000..6c6ca4c748 --- /dev/null +++ b/models/migrations/v1_20/v257.go @@ -0,0 +1,33 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_20 //nolint + +import ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func CreateActionArtifactTable(x *xorm.Engine) error { + // ActionArtifact is a file that is stored in the artifact storage. + type ActionArtifact struct { + ID int64 `xorm:"pk autoincr"` + RunID int64 `xorm:"index UNIQUE(runid_name)"` // The run id of the artifact + RunnerID int64 + RepoID int64 `xorm:"index"` + OwnerID int64 + CommitSHA string + StoragePath string // The path to the artifact in the storage + FileSize int64 // The size of the artifact in bytes + FileCompressedSize int64 // The size of the artifact in bytes after gzip compression + ContentEncoding string // The content encoding of the artifact + ArtifactPath string // The path to the artifact when runner uploads it + ArtifactName string `xorm:"UNIQUE(runid_name)"` // The name of the artifact when runner uploads it + Status int64 `xorm:"index"` // The status of the artifact + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated index"` + } + + return x.Sync(new(ActionArtifact)) +} diff --git a/models/repo.go b/models/repo.go index 5903132862..2e0e8af16c 100644 --- a/models/repo.go +++ b/models/repo.go @@ -59,6 +59,12 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error { return fmt.Errorf("find actions tasks of repo %v: %w", repoID, err) } + // Query the artifacts of this repo, they will be needed after they have been deleted to remove artifacts files in ObjectStorage + artifacts, err := actions_model.ListArtifactsByRepoID(ctx, repoID) + if err != nil { + return fmt.Errorf("list actions artifacts of repo %v: %w", repoID, err) + } + // In case is a organization. org, err := user_model.GetUserByID(ctx, uid) if err != nil { @@ -164,6 +170,7 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error { &actions_model.ActionRunJob{RepoID: repoID}, &actions_model.ActionRun{RepoID: repoID}, &actions_model.ActionRunner{RepoID: repoID}, + &actions_model.ActionArtifact{RepoID: repoID}, ); err != nil { return fmt.Errorf("deleteBeans: %w", err) } @@ -336,6 +343,14 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error { } } + // delete actions artifacts in ObjectStorage after the repo have already been deleted + for _, art := range artifacts { + if err := storage.ActionsArtifacts.Delete(art.StoragePath); err != nil { + log.Error("remove artifact file %q: %v", art.StoragePath, err) + // go on + } + } + return nil } diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 10a70ad9f8..5351ff1139 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -126,7 +126,7 @@ func MainTest(m *testing.M, testOpts *TestOptions) { setting.Packages.Storage.Path = filepath.Join(setting.AppDataPath, "packages") - setting.Actions.Storage.Path = filepath.Join(setting.AppDataPath, "actions_log") + setting.Actions.LogStorage.Path = filepath.Join(setting.AppDataPath, "actions_log") setting.Git.HomePath = filepath.Join(setting.AppDataPath, "home") diff --git a/modules/context/api.go b/modules/context/api.go index e263dcbe8d..092ad73f31 100644 --- a/modules/context/api.go +++ b/modules/context/api.go @@ -13,18 +13,32 @@ import ( "code.gitea.io/gitea/models/auth" repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/modules/cache" + "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" + mc "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/web/middleware" + + "gitea.com/go-chi/cache" ) // APIContext is a specific context for API service type APIContext struct { - *Context - Org *APIOrganization + *Base + + Cache cache.Cache + + Doer *user_model.User // current signed-in user + IsSigned bool + IsBasicAuth bool + + ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer + + Repo *Repository + Org *APIOrganization + Package *Package } // Currently, we have the following common fields in error response: @@ -128,11 +142,6 @@ type apiContextKeyType struct{} var apiContextKey = apiContextKeyType{} -// WithAPIContext set up api context in request -func WithAPIContext(req *http.Request, ctx *APIContext) *http.Request { - return req.WithContext(context.WithValue(req.Context(), apiContextKey, ctx)) -} - // GetAPIContext returns a context for API routes func GetAPIContext(req *http.Request) *APIContext { return req.Context().Value(apiContextKey).(*APIContext) @@ -195,21 +204,21 @@ func (ctx *APIContext) CheckForOTP() { } otpHeader := ctx.Req.Header.Get("X-Gitea-OTP") - twofa, err := auth.GetTwoFactorByUID(ctx.Context.Doer.ID) + twofa, err := auth.GetTwoFactorByUID(ctx.Doer.ID) if err != nil { if auth.IsErrTwoFactorNotEnrolled(err) { return // No 2FA enrollment for this user } - ctx.Context.Error(http.StatusInternalServerError) + ctx.Error(http.StatusInternalServerError, "GetTwoFactorByUID", err) return } ok, err := twofa.ValidateTOTP(otpHeader) if err != nil { - ctx.Context.Error(http.StatusInternalServerError) + ctx.Error(http.StatusInternalServerError, "ValidateTOTP", err) return } if !ok { - ctx.Context.Error(http.StatusUnauthorized) + ctx.Error(http.StatusUnauthorized, "", nil) return } } @@ -218,23 +227,17 @@ func (ctx *APIContext) CheckForOTP() { func APIContexter() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - locale := middleware.Locale(w, req) - ctx := APIContext{ - Context: &Context{ - Resp: NewResponse(w), - Data: middleware.GetContextData(req.Context()), - Locale: locale, - Cache: cache.GetCache(), - Repo: &Repository{ - PullRequest: &PullRequest{}, - }, - Org: &Organization{}, - }, - Org: &APIOrganization{}, + base, baseCleanUp := NewBaseContext(w, req) + ctx := &APIContext{ + Base: base, + Cache: mc.GetCache(), + Repo: &Repository{PullRequest: &PullRequest{}}, + Org: &APIOrganization{}, } - defer ctx.Close() + defer baseCleanUp() - ctx.Req = WithAPIContext(WithContext(req, ctx.Context), &ctx) + ctx.Base.AppendContextValue(apiContextKey, ctx) + ctx.Base.AppendContextValueFunc(git.RepositoryContextKey, func() any { return ctx.Repo.GitRepo }) // If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid. if ctx.Req.Method == "POST" && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") { @@ -247,8 +250,6 @@ func APIContexter() func(http.Handler) http.Handler { httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform") ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions) - ctx.Data["Context"] = &ctx - next.ServeHTTP(ctx.Resp, ctx.Req) }) } @@ -301,7 +302,7 @@ func ReferencesGitRepo(allowEmpty ...bool) func(ctx *APIContext) (cancel context return func() { // If it's been set to nil then assume someone else has closed it. if ctx.Repo.GitRepo != nil { - ctx.Repo.GitRepo.Close() + _ = ctx.Repo.GitRepo.Close() } } } @@ -337,7 +338,7 @@ func RepoRefForAPI(next http.Handler) http.Handler { } var err error - refName := getRefName(ctx.Context, RepoRefAny) + refName := getRefName(ctx.Base, ctx.Repo, RepoRefAny) if ctx.Repo.GitRepo.IsBranchExist(refName) { ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName) @@ -368,3 +369,53 @@ func RepoRefForAPI(next http.Handler) http.Handler { next.ServeHTTP(w, req) }) } + +// HasAPIError returns true if error occurs in form validation. +func (ctx *APIContext) HasAPIError() bool { + hasErr, ok := ctx.Data["HasError"] + if !ok { + return false + } + return hasErr.(bool) +} + +// GetErrMsg returns error message in form validation. +func (ctx *APIContext) GetErrMsg() string { + msg, _ := ctx.Data["ErrorMsg"].(string) + if msg == "" { + msg = "invalid form data" + } + return msg +} + +// NotFoundOrServerError use error check function to determine if the error +// is about not found. It responds with 404 status code for not found error, +// or error context description for logging purpose of 500 server error. +func (ctx *APIContext) NotFoundOrServerError(logMsg string, errCheck func(error) bool, logErr error) { + if errCheck(logErr) { + ctx.JSON(http.StatusNotFound, nil) + return + } + ctx.Error(http.StatusInternalServerError, "NotFoundOrServerError", logMsg) +} + +// IsUserSiteAdmin returns true if current user is a site admin +func (ctx *APIContext) IsUserSiteAdmin() bool { + return ctx.IsSigned && ctx.Doer.IsAdmin +} + +// IsUserRepoAdmin returns true if current user is admin in current repo +func (ctx *APIContext) IsUserRepoAdmin() bool { + return ctx.Repo.IsAdmin() +} + +// IsUserRepoWriter returns true if current user has write privilege in current repo +func (ctx *APIContext) IsUserRepoWriter(unitTypes []unit.Type) bool { + for _, unitType := range unitTypes { + if ctx.Repo.CanWrite(unitType) { + return true + } + } + + return false +} diff --git a/modules/context/base.go b/modules/context/base.go new file mode 100644 index 0000000000..ac9b52d51c --- /dev/null +++ b/modules/context/base.go @@ -0,0 +1,300 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package context + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "code.gitea.io/gitea/modules/httplib" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/translation" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/web/middleware" + + "github.com/go-chi/chi/v5" +) + +type contextValuePair struct { + key any + valueFn func() any +} + +type Base struct { + originCtx context.Context + contextValues []contextValuePair + + Resp ResponseWriter + Req *http.Request + + // Data is prepared by ContextDataStore middleware, this field only refers to the pre-created/prepared ContextData. + // Although it's mainly used for MVC templates, sometimes it's also used to pass data between middlewares/handler + Data middleware.ContextData + + // Locale is mainly for Web context, although the API context also uses it in some cases: message response, form validation + Locale translation.Locale +} + +func (b *Base) Deadline() (deadline time.Time, ok bool) { + return b.originCtx.Deadline() +} + +func (b *Base) Done() <-chan struct{} { + return b.originCtx.Done() +} + +func (b *Base) Err() error { + return b.originCtx.Err() +} + +func (b *Base) Value(key any) any { + for _, pair := range b.contextValues { + if pair.key == key { + return pair.valueFn() + } + } + return b.originCtx.Value(key) +} + +func (b *Base) AppendContextValueFunc(key any, valueFn func() any) any { + b.contextValues = append(b.contextValues, contextValuePair{key, valueFn}) + return b +} + +func (b *Base) AppendContextValue(key, value any) any { + b.contextValues = append(b.contextValues, contextValuePair{key, func() any { return value }}) + return b +} + +func (b *Base) GetData() middleware.ContextData { + return b.Data +} + +// AppendAccessControlExposeHeaders append headers by name to "Access-Control-Expose-Headers" header +func (b *Base) AppendAccessControlExposeHeaders(names ...string) { + val := b.RespHeader().Get("Access-Control-Expose-Headers") + if len(val) != 0 { + b.RespHeader().Set("Access-Control-Expose-Headers", fmt.Sprintf("%s, %s", val, strings.Join(names, ", "))) + } else { + b.RespHeader().Set("Access-Control-Expose-Headers", strings.Join(names, ", ")) + } +} + +// SetTotalCountHeader set "X-Total-Count" header +func (b *Base) SetTotalCountHeader(total int64) { + b.RespHeader().Set("X-Total-Count", fmt.Sprint(total)) + b.AppendAccessControlExposeHeaders("X-Total-Count") +} + +// Written returns true if there are something sent to web browser +func (b *Base) Written() bool { + return b.Resp.Status() > 0 +} + +// Status writes status code +func (b *Base) Status(status int) { + b.Resp.WriteHeader(status) +} + +// Write writes data to web browser +func (b *Base) Write(bs []byte) (int, error) { + return b.Resp.Write(bs) +} + +// RespHeader returns the response header +func (b *Base) RespHeader() http.Header { + return b.Resp.Header() +} + +// Error returned an error to web browser +func (b *Base) Error(status int, contents ...string) { + v := http.StatusText(status) + if len(contents) > 0 { + v = contents[0] + } + http.Error(b.Resp, v, status) +} + +// JSON render content as JSON +func (b *Base) JSON(status int, content interface{}) { + b.Resp.Header().Set("Content-Type", "application/json;charset=utf-8") + b.Resp.WriteHeader(status) + if err := json.NewEncoder(b.Resp).Encode(content); err != nil { + log.Error("Render JSON failed: %v", err) + } +} + +// RemoteAddr returns the client machine ip address +func (b *Base) RemoteAddr() string { + return b.Req.RemoteAddr +} + +// Params returns the param on route +func (b *Base) Params(p string) string { + s, _ := url.PathUnescape(chi.URLParam(b.Req, strings.TrimPrefix(p, ":"))) + return s +} + +// ParamsInt64 returns the param on route as int64 +func (b *Base) ParamsInt64(p string) int64 { + v, _ := strconv.ParseInt(b.Params(p), 10, 64) + return v +} + +// SetParams set params into routes +func (b *Base) SetParams(k, v string) { + chiCtx := chi.RouteContext(b) + chiCtx.URLParams.Add(strings.TrimPrefix(k, ":"), url.PathEscape(v)) +} + +// FormString returns the first value matching the provided key in the form as a string +func (b *Base) FormString(key string) string { + return b.Req.FormValue(key) +} + +// FormStrings returns a string slice for the provided key from the form +func (b *Base) FormStrings(key string) []string { + if b.Req.Form == nil { + if err := b.Req.ParseMultipartForm(32 << 20); err != nil { + return nil + } + } + if v, ok := b.Req.Form[key]; ok { + return v + } + return nil +} + +// FormTrim returns the first value for the provided key in the form as a space trimmed string +func (b *Base) FormTrim(key string) string { + return strings.TrimSpace(b.Req.FormValue(key)) +} + +// FormInt returns the first value for the provided key in the form as an int +func (b *Base) FormInt(key string) int { + v, _ := strconv.Atoi(b.Req.FormValue(key)) + return v +} + +// FormInt64 returns the first value for the provided key in the form as an int64 +func (b *Base) FormInt64(key string) int64 { + v, _ := strconv.ParseInt(b.Req.FormValue(key), 10, 64) + return v +} + +// FormBool returns true if the value for the provided key in the form is "1", "true" or "on" +func (b *Base) FormBool(key string) bool { + s := b.Req.FormValue(key) + v, _ := strconv.ParseBool(s) + v = v || strings.EqualFold(s, "on") + return v +} + +// FormOptionalBool returns an OptionalBoolTrue or OptionalBoolFalse if the value +// for the provided key exists in the form else it returns OptionalBoolNone +func (b *Base) FormOptionalBool(key string) util.OptionalBool { + value := b.Req.FormValue(key) + if len(value) == 0 { + return util.OptionalBoolNone + } + s := b.Req.FormValue(key) + v, _ := strconv.ParseBool(s) + v = v || strings.EqualFold(s, "on") + return util.OptionalBoolOf(v) +} + +func (b *Base) SetFormString(key, value string) { + _ = b.Req.FormValue(key) // force parse form + b.Req.Form.Set(key, value) +} + +// PlainTextBytes renders bytes as plain text +func (b *Base) plainTextInternal(skip, status int, bs []byte) { + statusPrefix := status / 100 + if statusPrefix == 4 || statusPrefix == 5 { + log.Log(skip, log.TRACE, "plainTextInternal (status=%d): %s", status, string(bs)) + } + b.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8") + b.Resp.Header().Set("X-Content-Type-Options", "nosniff") + b.Resp.WriteHeader(status) + if _, err := b.Resp.Write(bs); err != nil { + log.ErrorWithSkip(skip, "plainTextInternal (status=%d): write bytes failed: %v", status, err) + } +} + +// PlainTextBytes renders bytes as plain text +func (b *Base) PlainTextBytes(status int, bs []byte) { + b.plainTextInternal(2, status, bs) +} + +// PlainText renders content as plain text +func (b *Base) PlainText(status int, text string) { + b.plainTextInternal(2, status, []byte(text)) +} + +// Redirect redirects the request +func (b *Base) Redirect(location string, status ...int) { + code := http.StatusSeeOther + if len(status) == 1 { + code = status[0] + } + + if strings.Contains(location, "://") || strings.HasPrefix(location, "//") { + // Some browsers (Safari) have buggy behavior for Cookie + Cache + External Redirection, eg: /my-path => https://other/path + // 1. the first request to "/my-path" contains cookie + // 2. some time later, the request to "/my-path" doesn't contain cookie (caused by Prevent web tracking) + // 3. Gitea's Sessioner doesn't see the session cookie, so it generates a new session id, and returns it to browser + // 4. then the browser accepts the empty session, then the user is logged out + // So in this case, we should remove the session cookie from the response header + removeSessionCookieHeader(b.Resp) + } + http.Redirect(b.Resp, b.Req, location, code) +} + +type ServeHeaderOptions httplib.ServeHeaderOptions + +func (b *Base) SetServeHeaders(opt *ServeHeaderOptions) { + httplib.ServeSetHeaders(b.Resp, (*httplib.ServeHeaderOptions)(opt)) +} + +// ServeContent serves content to http request +func (b *Base) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) { + httplib.ServeSetHeaders(b.Resp, (*httplib.ServeHeaderOptions)(opts)) + http.ServeContent(b.Resp, b.Req, opts.Filename, opts.LastModified, r) +} + +// Close frees all resources hold by Context +func (b *Base) cleanUp() { + if b.Req != nil && b.Req.MultipartForm != nil { + _ = b.Req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory + } +} + +func (b *Base) Tr(msg string, args ...any) string { + return b.Locale.Tr(msg, args...) +} + +func (b *Base) TrN(cnt any, key1, keyN string, args ...any) string { + return b.Locale.TrN(cnt, key1, keyN, args...) +} + +func NewBaseContext(resp http.ResponseWriter, req *http.Request) (b *Base, closeFunc func()) { + b = &Base{ + originCtx: req.Context(), + Req: req, + Resp: WrapResponseWriter(resp), + Locale: middleware.Locale(resp, req), + Data: middleware.GetContextData(req.Context()), + } + b.AppendContextValue(translation.ContextKey, b.Locale) + b.Req = b.Req.WithContext(b) + return b, b.cleanUp +} diff --git a/modules/context/context.go b/modules/context/context.go index 9ba1985f36..1e15081479 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -5,7 +5,6 @@ package context import ( - "context" "html" "html/template" "io" @@ -36,38 +35,27 @@ type Render interface { // Context represents context of a request. type Context struct { - Resp ResponseWriter - Req *http.Request - Render Render + *Base - Data middleware.ContextData // data used by MVC templates - PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData` + Render Render + PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData` - Locale translation.Locale Cache cache.Cache Csrf CSRFProtector Flash *middleware.Flash Session session.Store - Link string // current request URL (without query string) - Doer *user_model.User + Link string // current request URL (without query string) + + Doer *user_model.User // current signed-in user IsSigned bool IsBasicAuth bool - ContextUser *user_model.User - Repo *Repository - Org *Organization - Package *Package -} + ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer -// Close frees all resources hold by Context -func (ctx *Context) Close() error { - var err error - if ctx.Req != nil && ctx.Req.MultipartForm != nil { - err = ctx.Req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory - } - // TODO: close opened repo, and more - return err + Repo *Repository + Org *Organization + Package *Package } // TrHTMLEscapeArgs runs ".Locale.Tr()" but pre-escapes all arguments with html.EscapeString. @@ -80,55 +68,30 @@ func (ctx *Context) TrHTMLEscapeArgs(msg string, args ...string) string { return ctx.Locale.Tr(msg, trArgs...) } -func (ctx *Context) Tr(msg string, args ...any) string { - return ctx.Locale.Tr(msg, args...) -} - -func (ctx *Context) TrN(cnt any, key1, keyN string, args ...any) string { - return ctx.Locale.TrN(cnt, key1, keyN, args...) -} - -// Deadline is part of the interface for context.Context and we pass this to the request context -func (ctx *Context) Deadline() (deadline time.Time, ok bool) { - return ctx.Req.Context().Deadline() -} - -// Done is part of the interface for context.Context and we pass this to the request context -func (ctx *Context) Done() <-chan struct{} { - return ctx.Req.Context().Done() -} - -// Err is part of the interface for context.Context and we pass this to the request context -func (ctx *Context) Err() error { - return ctx.Req.Context().Err() -} - -// Value is part of the interface for context.Context and we pass this to the request context -func (ctx *Context) Value(key interface{}) interface{} { - if key == git.RepositoryContextKey && ctx.Repo != nil { - return ctx.Repo.GitRepo - } - if key == translation.ContextKey && ctx.Locale != nil { - return ctx.Locale - } - return ctx.Req.Context().Value(key) -} - type contextKeyType struct{} var contextKey interface{} = contextKeyType{} -// WithContext set up install context in request -func WithContext(req *http.Request, ctx *Context) *http.Request { - return req.WithContext(context.WithValue(req.Context(), contextKey, ctx)) +func GetContext(req *http.Request) *Context { + ctx, _ := req.Context().Value(contextKey).(*Context) + return ctx } -// GetContext retrieves install context from request -func GetContext(req *http.Request) *Context { - if ctx, ok := req.Context().Value(contextKey).(*Context); ok { - return ctx +// ValidateContext is a special context for form validation middleware. It may be different from other contexts. +type ValidateContext struct { + *Base +} + +// GetValidateContext gets a context for middleware form validation +func GetValidateContext(req *http.Request) (ctx *ValidateContext) { + if ctxAPI, ok := req.Context().Value(apiContextKey).(*APIContext); ok { + ctx = &ValidateContext{Base: ctxAPI.Base} + } else if ctxWeb, ok := req.Context().Value(contextKey).(*Context); ok { + ctx = &ValidateContext{Base: ctxWeb.Base} + } else { + panic("invalid context, expect either APIContext or Context") } - return nil + return ctx } // Contexter initializes a classic context for a request. @@ -150,20 +113,17 @@ func Contexter() func(next http.Handler) http.Handler { } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - ctx := Context{ - Resp: NewResponse(resp), + base, baseCleanUp := NewBaseContext(resp, req) + ctx := &Context{ + Base: base, Cache: mc.GetCache(), - Locale: middleware.Locale(resp, req), Link: setting.AppSubURL + strings.TrimSuffix(req.URL.EscapedPath(), "/"), Render: rnd, Session: session.GetSession(req), - Repo: &Repository{ - PullRequest: &PullRequest{}, - }, - Org: &Organization{}, - Data: middleware.GetContextData(req.Context()), + Repo: &Repository{PullRequest: &PullRequest{}}, + Org: &Organization{}, } - defer ctx.Close() + defer baseCleanUp() ctx.Data.MergeFrom(middleware.CommonTemplateContextData()) ctx.Data["Context"] = &ctx @@ -175,15 +135,17 @@ func Contexter() func(next http.Handler) http.Handler { ctx.PageData = map[string]any{} ctx.Data["PageData"] = ctx.PageData - ctx.Req = WithContext(req, &ctx) - ctx.Csrf = PrepareCSRFProtector(csrfOpts, &ctx) + ctx.Base.AppendContextValue(contextKey, ctx) + ctx.Base.AppendContextValueFunc(git.RepositoryContextKey, func() any { return ctx.Repo.GitRepo }) + + ctx.Csrf = PrepareCSRFProtector(csrfOpts, ctx) // Get the last flash message from cookie lastFlashCookie := middleware.GetSiteCookie(ctx.Req, CookieNameFlash) if vals, _ := url.ParseQuery(lastFlashCookie); len(vals) > 0 { // store last Flash message into the template data, to render it ctx.Data["Flash"] = &middleware.Flash{ - DataStore: &ctx, + DataStore: ctx, Values: vals, ErrorMsg: vals.Get("error"), SuccessMsg: vals.Get("success"), @@ -193,7 +155,7 @@ func Contexter() func(next http.Handler) http.Handler { } // prepare an empty Flash message for current request - ctx.Flash = &middleware.Flash{DataStore: &ctx, Values: url.Values{}} + ctx.Flash = &middleware.Flash{DataStore: ctx, Values: url.Values{}} ctx.Resp.Before(func(resp ResponseWriter) { if val := ctx.Flash.Encode(); val != "" { middleware.SetSiteCookie(ctx.Resp, CookieNameFlash, val, 0) @@ -235,3 +197,24 @@ func Contexter() func(next http.Handler) http.Handler { }) } } + +// HasError returns true if error occurs in form validation. +// Attention: this function changes ctx.Data and ctx.Flash +func (ctx *Context) HasError() bool { + hasErr, ok := ctx.Data["HasError"] + if !ok { + return false + } + ctx.Flash.ErrorMsg = ctx.GetErrMsg() + ctx.Data["Flash"] = ctx.Flash + return hasErr.(bool) +} + +// GetErrMsg returns error message in form validation. +func (ctx *Context) GetErrMsg() string { + msg, _ := ctx.Data["ErrorMsg"].(string) + if msg == "" { + msg = "invalid form data" + } + return msg +} diff --git a/modules/context/context_data.go b/modules/context/context_data.go deleted file mode 100644 index cdf4ff9afe..0000000000 --- a/modules/context/context_data.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package context - -import "code.gitea.io/gitea/modules/web/middleware" - -// GetData returns the data -func (ctx *Context) GetData() middleware.ContextData { - return ctx.Data -} - -// HasAPIError returns true if error occurs in form validation. -func (ctx *Context) HasAPIError() bool { - hasErr, ok := ctx.Data["HasError"] - if !ok { - return false - } - return hasErr.(bool) -} - -// GetErrMsg returns error message -func (ctx *Context) GetErrMsg() string { - return ctx.Data["ErrorMsg"].(string) -} - -// HasError returns true if error occurs in form validation. -// Attention: this function changes ctx.Data and ctx.Flash -func (ctx *Context) HasError() bool { - hasErr, ok := ctx.Data["HasError"] - if !ok { - return false - } - ctx.Flash.ErrorMsg = ctx.Data["ErrorMsg"].(string) - ctx.Data["Flash"] = ctx.Flash - return hasErr.(bool) -} - -// HasValue returns true if value of given name exists. -func (ctx *Context) HasValue(name string) bool { - _, ok := ctx.Data[name] - return ok -} diff --git a/modules/context/context_form.go b/modules/context/context_form.go deleted file mode 100644 index 5c02152582..0000000000 --- a/modules/context/context_form.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package context - -import ( - "strconv" - "strings" - - "code.gitea.io/gitea/modules/util" -) - -// FormString returns the first value matching the provided key in the form as a string -func (ctx *Context) FormString(key string) string { - return ctx.Req.FormValue(key) -} - -// FormStrings returns a string slice for the provided key from the form -func (ctx *Context) FormStrings(key string) []string { - if ctx.Req.Form == nil { - if err := ctx.Req.ParseMultipartForm(32 << 20); err != nil { - return nil - } - } - if v, ok := ctx.Req.Form[key]; ok { - return v - } - return nil -} - -// FormTrim returns the first value for the provided key in the form as a space trimmed string -func (ctx *Context) FormTrim(key string) string { - return strings.TrimSpace(ctx.Req.FormValue(key)) -} - -// FormInt returns the first value for the provided key in the form as an int -func (ctx *Context) FormInt(key string) int { - v, _ := strconv.Atoi(ctx.Req.FormValue(key)) - return v -} - -// FormInt64 returns the first value for the provided key in the form as an int64 -func (ctx *Context) FormInt64(key string) int64 { - v, _ := strconv.ParseInt(ctx.Req.FormValue(key), 10, 64) - return v -} - -// FormBool returns true if the value for the provided key in the form is "1", "true" or "on" -func (ctx *Context) FormBool(key string) bool { - s := ctx.Req.FormValue(key) - v, _ := strconv.ParseBool(s) - v = v || strings.EqualFold(s, "on") - return v -} - -// FormOptionalBool returns an OptionalBoolTrue or OptionalBoolFalse if the value -// for the provided key exists in the form else it returns OptionalBoolNone -func (ctx *Context) FormOptionalBool(key string) util.OptionalBool { - value := ctx.Req.FormValue(key) - if len(value) == 0 { - return util.OptionalBoolNone - } - s := ctx.Req.FormValue(key) - v, _ := strconv.ParseBool(s) - v = v || strings.EqualFold(s, "on") - return util.OptionalBoolOf(v) -} - -func (ctx *Context) SetFormString(key, value string) { - _ = ctx.Req.FormValue(key) // force parse form - ctx.Req.Form.Set(key, value) -} diff --git a/modules/context/context_request.go b/modules/context/context_request.go index 0b87552c08..984b9ac793 100644 --- a/modules/context/context_request.go +++ b/modules/context/context_request.go @@ -6,36 +6,9 @@ package context import ( "io" "net/http" - "net/url" - "strconv" "strings" - - "github.com/go-chi/chi/v5" ) -// RemoteAddr returns the client machine ip address -func (ctx *Context) RemoteAddr() string { - return ctx.Req.RemoteAddr -} - -// Params returns the param on route -func (ctx *Context) Params(p string) string { - s, _ := url.PathUnescape(chi.URLParam(ctx.Req, strings.TrimPrefix(p, ":"))) - return s -} - -// ParamsInt64 returns the param on route as int64 -func (ctx *Context) ParamsInt64(p string) int64 { - v, _ := strconv.ParseInt(ctx.Params(p), 10, 64) - return v -} - -// SetParams set params into routes -func (ctx *Context) SetParams(k, v string) { - chiCtx := chi.RouteContext(ctx) - chiCtx.URLParams.Add(strings.TrimPrefix(k, ":"), url.PathEscape(v)) -} - // UploadStream returns the request body or the first form file // Only form files need to get closed. func (ctx *Context) UploadStream() (rd io.ReadCloser, needToClose bool, err error) { diff --git a/modules/context/context_response.go b/modules/context/context_response.go index 8adff96994..aeeb51ba37 100644 --- a/modules/context/context_response.go +++ b/modules/context/context_response.go @@ -16,49 +16,17 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" - "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web/middleware" ) -// SetTotalCountHeader set "X-Total-Count" header -func (ctx *Context) SetTotalCountHeader(total int64) { - ctx.RespHeader().Set("X-Total-Count", fmt.Sprint(total)) - ctx.AppendAccessControlExposeHeaders("X-Total-Count") -} - -// AppendAccessControlExposeHeaders append headers by name to "Access-Control-Expose-Headers" header -func (ctx *Context) AppendAccessControlExposeHeaders(names ...string) { - val := ctx.RespHeader().Get("Access-Control-Expose-Headers") - if len(val) != 0 { - ctx.RespHeader().Set("Access-Control-Expose-Headers", fmt.Sprintf("%s, %s", val, strings.Join(names, ", "))) - } else { - ctx.RespHeader().Set("Access-Control-Expose-Headers", strings.Join(names, ", ")) - } -} - -// Written returns true if there are something sent to web browser -func (ctx *Context) Written() bool { - return ctx.Resp.Status() > 0 -} - -// Status writes status code -func (ctx *Context) Status(status int) { - ctx.Resp.WriteHeader(status) -} - -// Write writes data to web browser -func (ctx *Context) Write(bs []byte) (int, error) { - return ctx.Resp.Write(bs) -} - // RedirectToUser redirect to a differently-named user -func RedirectToUser(ctx *Context, userName string, redirectUserID int64) { +func RedirectToUser(ctx *Base, userName string, redirectUserID int64) { user, err := user_model.GetUserByID(ctx, redirectUserID) if err != nil { - ctx.ServerError("GetUserByID", err) + ctx.Error(http.StatusInternalServerError, "unable to get user") return } @@ -211,69 +179,3 @@ func (ctx *Context) NotFoundOrServerError(logMsg string, errCheck func(error) bo } ctx.serverErrorInternal(logMsg, logErr) } - -// PlainTextBytes renders bytes as plain text -func (ctx *Context) plainTextInternal(skip, status int, bs []byte) { - statusPrefix := status / 100 - if statusPrefix == 4 || statusPrefix == 5 { - log.Log(skip, log.TRACE, "plainTextInternal (status=%d): %s", status, string(bs)) - } - ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8") - ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff") - ctx.Resp.WriteHeader(status) - if _, err := ctx.Resp.Write(bs); err != nil { - log.ErrorWithSkip(skip, "plainTextInternal (status=%d): write bytes failed: %v", status, err) - } -} - -// PlainTextBytes renders bytes as plain text -func (ctx *Context) PlainTextBytes(status int, bs []byte) { - ctx.plainTextInternal(2, status, bs) -} - -// PlainText renders content as plain text -func (ctx *Context) PlainText(status int, text string) { - ctx.plainTextInternal(2, status, []byte(text)) -} - -// RespHeader returns the response header -func (ctx *Context) RespHeader() http.Header { - return ctx.Resp.Header() -} - -// Error returned an error to web browser -func (ctx *Context) Error(status int, contents ...string) { - v := http.StatusText(status) - if len(contents) > 0 { - v = contents[0] - } - http.Error(ctx.Resp, v, status) -} - -// JSON render content as JSON -func (ctx *Context) JSON(status int, content interface{}) { - ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8") - ctx.Resp.WriteHeader(status) - if err := json.NewEncoder(ctx.Resp).Encode(content); err != nil { - ctx.ServerError("Render JSON failed", err) - } -} - -// Redirect redirects the request -func (ctx *Context) Redirect(location string, status ...int) { - code := http.StatusSeeOther - if len(status) == 1 { - code = status[0] - } - - if strings.Contains(location, "://") || strings.HasPrefix(location, "//") { - // Some browsers (Safari) have buggy behavior for Cookie + Cache + External Redirection, eg: /my-path => https://other/path - // 1. the first request to "/my-path" contains cookie - // 2. some time later, the request to "/my-path" doesn't contain cookie (caused by Prevent web tracking) - // 3. Gitea's Sessioner doesn't see the session cookie, so it generates a new session id, and returns it to browser - // 4. then the browser accepts the empty session, then the user is logged out - // So in this case, we should remove the session cookie from the response header - removeSessionCookieHeader(ctx.Resp) - } - http.Redirect(ctx.Resp, ctx.Req, location, code) -} diff --git a/modules/context/context_serve.go b/modules/context/context_serve.go deleted file mode 100644 index 5569efbc7e..0000000000 --- a/modules/context/context_serve.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package context - -import ( - "io" - "net/http" - - "code.gitea.io/gitea/modules/httplib" -) - -type ServeHeaderOptions httplib.ServeHeaderOptions - -func (ctx *Context) SetServeHeaders(opt *ServeHeaderOptions) { - httplib.ServeSetHeaders(ctx.Resp, (*httplib.ServeHeaderOptions)(opt)) -} - -// ServeContent serves content to http request -func (ctx *Context) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) { - httplib.ServeSetHeaders(ctx.Resp, (*httplib.ServeHeaderOptions)(opts)) - http.ServeContent(ctx.Resp, ctx.Req, opts.Filename, opts.LastModified, r) -} diff --git a/modules/context/org.go b/modules/context/org.go index 39a3038f91..355ba0ebd0 100644 --- a/modules/context/org.go +++ b/modules/context/org.go @@ -47,7 +47,7 @@ func GetOrganizationByParams(ctx *Context) { if organization.IsErrOrgNotExist(err) { redirectUserID, err := user_model.LookupUserRedirect(orgName) if err == nil { - RedirectToUser(ctx, orgName, redirectUserID) + RedirectToUser(ctx.Base, orgName, redirectUserID) } else if user_model.IsErrUserRedirectNotExist(err) { ctx.NotFound("GetUserByName", err) } else { diff --git a/modules/context/package.go b/modules/context/package.go index fe5bdac19d..b1fd7088dd 100644 --- a/modules/context/package.go +++ b/modules/context/package.go @@ -4,7 +4,6 @@ package context import ( - gocontext "context" "fmt" "net/http" @@ -16,7 +15,6 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/templates" - "code.gitea.io/gitea/modules/web/middleware" ) // Package contains owner, access mode and optional the package descriptor @@ -26,10 +24,16 @@ type Package struct { Descriptor *packages_model.PackageDescriptor } +type packageAssignmentCtx struct { + *Base + Doer *user_model.User + ContextUser *user_model.User +} + // PackageAssignment returns a middleware to handle Context.Package assignment func PackageAssignment() func(ctx *Context) { return func(ctx *Context) { - packageAssignment(ctx, func(status int, title string, obj interface{}) { + errorFn := func(status int, title string, obj interface{}) { err, ok := obj.(error) if !ok { err = fmt.Errorf("%s", obj) @@ -39,68 +43,72 @@ func PackageAssignment() func(ctx *Context) { } else { ctx.ServerError(title, err) } - }) + } + paCtx := &packageAssignmentCtx{Base: ctx.Base, Doer: ctx.Doer, ContextUser: ctx.ContextUser} + ctx.Package = packageAssignment(paCtx, errorFn) } } // PackageAssignmentAPI returns a middleware to handle Context.Package assignment func PackageAssignmentAPI() func(ctx *APIContext) { return func(ctx *APIContext) { - packageAssignment(ctx.Context, ctx.Error) + paCtx := &packageAssignmentCtx{Base: ctx.Base, Doer: ctx.Doer, ContextUser: ctx.ContextUser} + ctx.Package = packageAssignment(paCtx, ctx.Error) } } -func packageAssignment(ctx *Context, errCb func(int, string, interface{})) { - ctx.Package = &Package{ +func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, string, interface{})) *Package { + pkg := &Package{ Owner: ctx.ContextUser, } - var err error - ctx.Package.AccessMode, err = determineAccessMode(ctx) + pkg.AccessMode, err = determineAccessMode(ctx.Base, pkg, ctx.Doer) if err != nil { errCb(http.StatusInternalServerError, "determineAccessMode", err) - return + return pkg } packageType := ctx.Params("type") name := ctx.Params("name") version := ctx.Params("version") if packageType != "" && name != "" && version != "" { - pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.Type(packageType), name, version) + pv, err := packages_model.GetVersionByNameAndVersion(ctx, pkg.Owner.ID, packages_model.Type(packageType), name, version) if err != nil { if err == packages_model.ErrPackageNotExist { errCb(http.StatusNotFound, "GetVersionByNameAndVersion", err) } else { errCb(http.StatusInternalServerError, "GetVersionByNameAndVersion", err) } - return + return pkg } - ctx.Package.Descriptor, err = packages_model.GetPackageDescriptor(ctx, pv) + pkg.Descriptor, err = packages_model.GetPackageDescriptor(ctx, pv) if err != nil { errCb(http.StatusInternalServerError, "GetPackageDescriptor", err) - return + return pkg } } + + return pkg } -func determineAccessMode(ctx *Context) (perm.AccessMode, error) { - if setting.Service.RequireSignInView && ctx.Doer == nil { +func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.AccessMode, error) { + if setting.Service.RequireSignInView && doer == nil { return perm.AccessModeNone, nil } - if ctx.Doer != nil && !ctx.Doer.IsGhost() && (!ctx.Doer.IsActive || ctx.Doer.ProhibitLogin) { + if doer != nil && !doer.IsGhost() && (!doer.IsActive || doer.ProhibitLogin) { return perm.AccessModeNone, nil } // TODO: ActionUser permission check accessMode := perm.AccessModeNone - if ctx.Package.Owner.IsOrganization() { - org := organization.OrgFromUser(ctx.Package.Owner) + if pkg.Owner.IsOrganization() { + org := organization.OrgFromUser(pkg.Owner) - if ctx.Doer != nil && !ctx.Doer.IsGhost() { + if doer != nil && !doer.IsGhost() { // 1. If user is logged in, check all team packages permissions - teams, err := organization.GetUserOrgTeams(ctx, org.ID, ctx.Doer.ID) + teams, err := organization.GetUserOrgTeams(ctx, org.ID, doer.ID) if err != nil { return accessMode, err } @@ -110,19 +118,19 @@ func determineAccessMode(ctx *Context) (perm.AccessMode, error) { accessMode = perm } } - } else if organization.HasOrgOrUserVisible(ctx, ctx.Package.Owner, ctx.Doer) { + } else if organization.HasOrgOrUserVisible(ctx, pkg.Owner, doer) { // 2. If user is non-login, check if org is visible to non-login user accessMode = perm.AccessModeRead } } else { - if ctx.Doer != nil && !ctx.Doer.IsGhost() { + if doer != nil && !doer.IsGhost() { // 1. Check if user is package owner - if ctx.Doer.ID == ctx.Package.Owner.ID { + if doer.ID == pkg.Owner.ID { accessMode = perm.AccessModeOwner - } else if ctx.Package.Owner.Visibility == structs.VisibleTypePublic || ctx.Package.Owner.Visibility == structs.VisibleTypeLimited { // 2. Check if package owner is public or limited + } else if pkg.Owner.Visibility == structs.VisibleTypePublic || pkg.Owner.Visibility == structs.VisibleTypeLimited { // 2. Check if package owner is public or limited accessMode = perm.AccessModeRead } - } else if ctx.Package.Owner.Visibility == structs.VisibleTypePublic { // 3. Check if package owner is public + } else if pkg.Owner.Visibility == structs.VisibleTypePublic { // 3. Check if package owner is public accessMode = perm.AccessModeRead } } @@ -131,19 +139,18 @@ func determineAccessMode(ctx *Context) (perm.AccessMode, error) { } // PackageContexter initializes a package context for a request. -func PackageContexter(ctx gocontext.Context) func(next http.Handler) http.Handler { - rnd := templates.HTMLRenderer() +func PackageContexter() func(next http.Handler) http.Handler { + renderer := templates.HTMLRenderer() return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - ctx := Context{ - Resp: NewResponse(resp), - Data: middleware.GetContextData(req.Context()), - Render: rnd, + base, baseCleanUp := NewBaseContext(resp, req) + ctx := &Context{ + Base: base, + Render: renderer, // it is still needed when rendering 500 page in a package handler } - defer ctx.Close() - - ctx.Req = WithContext(req, &ctx) + defer baseCleanUp() + ctx.Base.AppendContextValue(contextKey, ctx) next.ServeHTTP(ctx.Resp, ctx.Req) }) } diff --git a/modules/context/private.go b/modules/context/private.go index f621dd6839..41ca8a4709 100644 --- a/modules/context/private.go +++ b/modules/context/private.go @@ -11,13 +11,14 @@ import ( "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/process" - "code.gitea.io/gitea/modules/web/middleware" ) // PrivateContext represents a context for private routes type PrivateContext struct { - *Context + *Base Override context.Context + + Repo *Repository } // Deadline is part of the interface for context.Context and we pass this to the request context @@ -25,7 +26,7 @@ func (ctx *PrivateContext) Deadline() (deadline time.Time, ok bool) { if ctx.Override != nil { return ctx.Override.Deadline() } - return ctx.Req.Context().Deadline() + return ctx.Base.Deadline() } // Done is part of the interface for context.Context and we pass this to the request context @@ -33,7 +34,7 @@ func (ctx *PrivateContext) Done() <-chan struct{} { if ctx.Override != nil { return ctx.Override.Done() } - return ctx.Req.Context().Done() + return ctx.Base.Done() } // Err is part of the interface for context.Context and we pass this to the request context @@ -41,16 +42,11 @@ func (ctx *PrivateContext) Err() error { if ctx.Override != nil { return ctx.Override.Err() } - return ctx.Req.Context().Err() + return ctx.Base.Err() } var privateContextKey interface{} = "default_private_context" -// WithPrivateContext set up private context in request -func WithPrivateContext(req *http.Request, ctx *PrivateContext) *http.Request { - return req.WithContext(context.WithValue(req.Context(), privateContextKey, ctx)) -} - // GetPrivateContext returns a context for Private routes func GetPrivateContext(req *http.Request) *PrivateContext { return req.Context().Value(privateContextKey).(*PrivateContext) @@ -60,16 +56,11 @@ func GetPrivateContext(req *http.Request) *PrivateContext { func PrivateContexter() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - ctx := &PrivateContext{ - Context: &Context{ - Resp: NewResponse(w), - Data: middleware.GetContextData(req.Context()), - }, - } - defer ctx.Close() + base, baseCleanUp := NewBaseContext(w, req) + ctx := &PrivateContext{Base: base} + defer baseCleanUp() + ctx.Base.AppendContextValue(privateContextKey, ctx) - ctx.Req = WithPrivateContext(req, ctx) - ctx.Data["Context"] = ctx next.ServeHTTP(ctx.Resp, ctx.Req) }) } diff --git a/modules/context/repo.go b/modules/context/repo.go index 5e90e8aec0..fd5f208576 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -331,13 +331,14 @@ func EarlyResponseForGoGetMeta(ctx *Context) { } // RedirectToRepo redirect to a differently-named repository -func RedirectToRepo(ctx *Context, redirectRepoID int64) { +func RedirectToRepo(ctx *Base, redirectRepoID int64) { ownerName := ctx.Params(":username") previousRepoName := ctx.Params(":reponame") repo, err := repo_model.GetRepositoryByID(ctx, redirectRepoID) if err != nil { - ctx.ServerError("GetRepositoryByID", err) + log.Error("GetRepositoryByID: %v", err) + ctx.Error(http.StatusInternalServerError, "GetRepositoryByID") return } @@ -456,7 +457,7 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) { } if redirectUserID, err := user_model.LookupUserRedirect(userName); err == nil { - RedirectToUser(ctx, userName, redirectUserID) + RedirectToUser(ctx.Base, userName, redirectUserID) } else if user_model.IsErrUserRedirectNotExist(err) { ctx.NotFound("GetUserByName", nil) } else { @@ -498,7 +499,7 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) { if repo_model.IsErrRepoNotExist(err) { redirectRepoID, err := repo_model.LookupRedirect(owner.ID, repoName) if err == nil { - RedirectToRepo(ctx, redirectRepoID) + RedirectToRepo(ctx.Base, redirectRepoID) } else if repo_model.IsErrRedirectNotExist(err) { if ctx.FormString("go-get") == "1" { EarlyResponseForGoGetMeta(ctx) @@ -781,46 +782,46 @@ func (rt RepoRefType) RefTypeIncludesTags() bool { return false } -func getRefNameFromPath(ctx *Context, path string, isExist func(string) bool) string { +func getRefNameFromPath(ctx *Base, repo *Repository, path string, isExist func(string) bool) string { refName := "" parts := strings.Split(path, "/") for i, part := range parts { refName = strings.TrimPrefix(refName+"/"+part, "/") if isExist(refName) { - ctx.Repo.TreePath = strings.Join(parts[i+1:], "/") + repo.TreePath = strings.Join(parts[i+1:], "/") return refName } } return "" } -func getRefName(ctx *Context, pathType RepoRefType) string { +func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string { path := ctx.Params("*") switch pathType { case RepoRefLegacy, RepoRefAny: - if refName := getRefName(ctx, RepoRefBranch); len(refName) > 0 { + if refName := getRefName(ctx, repo, RepoRefBranch); len(refName) > 0 { return refName } - if refName := getRefName(ctx, RepoRefTag); len(refName) > 0 { + if refName := getRefName(ctx, repo, RepoRefTag); len(refName) > 0 { return refName } // For legacy and API support only full commit sha parts := strings.Split(path, "/") if len(parts) > 0 && len(parts[0]) == git.SHAFullLength { - ctx.Repo.TreePath = strings.Join(parts[1:], "/") + repo.TreePath = strings.Join(parts[1:], "/") return parts[0] } - if refName := getRefName(ctx, RepoRefBlob); len(refName) > 0 { + if refName := getRefName(ctx, repo, RepoRefBlob); len(refName) > 0 { return refName } - ctx.Repo.TreePath = path - return ctx.Repo.Repository.DefaultBranch + repo.TreePath = path + return repo.Repository.DefaultBranch case RepoRefBranch: - ref := getRefNameFromPath(ctx, path, ctx.Repo.GitRepo.IsBranchExist) + ref := getRefNameFromPath(ctx, repo, path, repo.GitRepo.IsBranchExist) if len(ref) == 0 { // maybe it's a renamed branch - return getRefNameFromPath(ctx, path, func(s string) bool { - b, exist, err := git_model.FindRenamedBranch(ctx, ctx.Repo.Repository.ID, s) + return getRefNameFromPath(ctx, repo, path, func(s string) bool { + b, exist, err := git_model.FindRenamedBranch(ctx, repo.Repository.ID, s) if err != nil { log.Error("FindRenamedBranch", err) return false @@ -839,15 +840,15 @@ func getRefName(ctx *Context, pathType RepoRefType) string { return ref case RepoRefTag: - return getRefNameFromPath(ctx, path, ctx.Repo.GitRepo.IsTagExist) + return getRefNameFromPath(ctx, repo, path, repo.GitRepo.IsTagExist) case RepoRefCommit: parts := strings.Split(path, "/") if len(parts) > 0 && len(parts[0]) >= 7 && len(parts[0]) <= git.SHAFullLength { - ctx.Repo.TreePath = strings.Join(parts[1:], "/") + repo.TreePath = strings.Join(parts[1:], "/") return parts[0] } case RepoRefBlob: - _, err := ctx.Repo.GitRepo.GetBlob(path) + _, err := repo.GitRepo.GetBlob(path) if err != nil { return "" } @@ -922,7 +923,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context } ctx.Repo.IsViewBranch = true } else { - refName = getRefName(ctx, refType) + refName = getRefName(ctx.Base, ctx.Repo, refType) ctx.Repo.RefName = refName isRenamedBranch, has := ctx.Data["IsRenamedBranch"].(bool) if isRenamedBranch && has { diff --git a/modules/context/response.go b/modules/context/response.go index 40eb5c0d35..ca52ea137d 100644 --- a/modules/context/response.go +++ b/modules/context/response.go @@ -10,10 +10,9 @@ import ( // ResponseWriter represents a response writer for HTTP type ResponseWriter interface { http.ResponseWriter - Flush() + http.Flusher Status() int Before(func(ResponseWriter)) - Size() int } var _ ResponseWriter = &Response{} @@ -27,11 +26,6 @@ type Response struct { beforeExecuted bool } -// Size return written size -func (r *Response) Size() int { - return r.written -} - // Write writes bytes to HTTP endpoint func (r *Response) Write(bs []byte) (int, error) { if !r.beforeExecuted { @@ -65,7 +59,7 @@ func (r *Response) WriteHeader(statusCode int) { } } -// Flush flush cached data +// Flush flushes cached data func (r *Response) Flush() { if f, ok := r.ResponseWriter.(http.Flusher); ok { f.Flush() @@ -83,8 +77,7 @@ func (r *Response) Before(f func(ResponseWriter)) { r.befores = append(r.befores, f) } -// NewResponse creates a response -func NewResponse(resp http.ResponseWriter) *Response { +func WrapResponseWriter(resp http.ResponseWriter) *Response { if v, ok := resp.(*Response); ok { return v } diff --git a/modules/context/utils.go b/modules/context/utils.go index 1fa99953a2..c0f619aa23 100644 --- a/modules/context/utils.go +++ b/modules/context/utils.go @@ -10,7 +10,7 @@ import ( ) // GetQueryBeforeSince return parsed time (unix format) from URL query's before and since -func GetQueryBeforeSince(ctx *Context) (before, since int64, err error) { +func GetQueryBeforeSince(ctx *Base) (before, since int64, err error) { qCreatedBefore, err := prepareQueryArg(ctx, "before") if err != nil { return 0, 0, err @@ -48,7 +48,7 @@ func parseTime(value string) (int64, error) { } // prepareQueryArg unescape and trim a query arg -func prepareQueryArg(ctx *Context, name string) (value string, err error) { +func prepareQueryArg(ctx *Base, name string) (value string, err error) { value, err = url.PathUnescape(ctx.FormString(name)) value = strings.TrimSpace(value) return value, err diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go index e88b1b2bef..76ff80ffca 100644 --- a/modules/indexer/issues/indexer.go +++ b/modules/indexer/issues/indexer.go @@ -302,7 +302,7 @@ func populateIssueIndexer(ctx context.Context) { // UpdateRepoIndexer add/update all issues of the repositories func UpdateRepoIndexer(ctx context.Context, repo *repo_model.Repository) { is, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{ - RepoID: repo.ID, + RepoIDs: []int64{repo.ID}, IsClosed: util.OptionalBoolNone, IsPull: util.OptionalBoolNone, }) diff --git a/modules/markup/html.go b/modules/markup/html.go index 11888b8536..da16bcd3cb 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -630,7 +630,7 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) { } mentionedUsername := mention[1:] - if processorHelper.IsUsernameMentionable != nil && processorHelper.IsUsernameMentionable(ctx.Ctx, mentionedUsername) { + if DefaultProcessorHelper.IsUsernameMentionable != nil && DefaultProcessorHelper.IsUsernameMentionable(ctx.Ctx, mentionedUsername) { replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, mentionedUsername), mention, "mention")) node = node.NextSibling.NextSibling } else { diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index 816e93b700..f03a780900 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -47,6 +47,12 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa tocMode = rc.TOC } + applyElementDir := func(n ast.Node) { + if markup.DefaultProcessorHelper.ElementDir != "" { + n.SetAttributeString("dir", []byte(markup.DefaultProcessorHelper.ElementDir)) + } + } + attentionMarkedBlockquotes := make(container.Set[*ast.Blockquote]) _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { if !entering { @@ -69,6 +75,9 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa header.ID = util.BytesToReadOnlyString(id.([]byte)) } tocList = append(tocList, header) + applyElementDir(v) + case *ast.Paragraph: + applyElementDir(v) case *ast.Image: // Images need two things: // @@ -171,6 +180,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa v.AppendChild(v, newChild) } } + applyElementDir(v) case *ast.Text: if v.SoftLineBreak() && !v.HardLineBreak() { renderMetas := pc.Get(renderMetasKey).(map[string]string) diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go index f2477f1e9e..0331c3742a 100644 --- a/modules/markup/renderer.go +++ b/modules/markup/renderer.go @@ -30,14 +30,16 @@ const ( type ProcessorHelper struct { IsUsernameMentionable func(ctx context.Context, username string) bool + + ElementDir string // the direction of the elements, eg: "ltr", "rtl", "auto", default to no direction attribute } -var processorHelper ProcessorHelper +var DefaultProcessorHelper ProcessorHelper // Init initialize regexps for markdown parsing func Init(ph *ProcessorHelper) { if ph != nil { - processorHelper = *ph + DefaultProcessorHelper = *ph } NewSanitizer() diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go index 600ccbf3c6..a0c9ee171f 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -22,7 +22,10 @@ type Sanitizer struct { init sync.Once } -var sanitizer = &Sanitizer{} +var ( + sanitizer = &Sanitizer{} + allowAllRegex = regexp.MustCompile(".+") +) // NewSanitizer initializes sanitizer with allowed attributes based on settings. // Multiple calls to this function will only create one instance of Sanitizer during @@ -74,6 +77,8 @@ func createDefaultPolicy() *bluemonday.Policy { // Custom URL-Schemes if len(setting.Markdown.CustomURLSchemes) > 0 { policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...) + } else { + policy.AllowURLSchemesMatching(allowAllRegex) } // Allow classes for anchors diff --git a/modules/markup/sanitizer_test.go b/modules/markup/sanitizer_test.go index c792ec2dc6..0c22ce3ba0 100644 --- a/modules/markup/sanitizer_test.go +++ b/modules/markup/sanitizer_test.go @@ -52,6 +52,10 @@ func Test_Sanitizer(t *testing.T) { `Hello World`, `Hello World`, `
Hello World
`, `Hello World
`, `Hello World`, `Hello World`,
+
+ // URLs
+ `[my custom URL scheme](cbthunderlink://somebase64string)`, `[my custom URL scheme](cbthunderlink://somebase64string)`,
+ `[my custom URL scheme](matrix:roomid/psumPMeAfzgAeQpXMG:feneas.org?action=join)`, `[my custom URL scheme](matrix:roomid/psumPMeAfzgAeQpXMG:feneas.org?action=join)`,
}
for i := 0; i < len(testCases); i += 2 {
diff --git a/modules/setting/actions.go b/modules/setting/actions.go
index b11500dab4..eb1b637a1c 100644
--- a/modules/setting/actions.go
+++ b/modules/setting/actions.go
@@ -10,7 +10,8 @@ import (
// Actions settings
var (
Actions = struct {
- Storage // how the created logs should be stored
+ LogStorage Storage // how the created logs should be stored
+ ArtifactStorage Storage // how the created artifacts should be stored
Enabled bool
DefaultActionsURL string `ini:"DEFAULT_ACTIONS_URL"`
}{
@@ -25,5 +26,9 @@ func loadActionsFrom(rootCfg ConfigProvider) {
log.Fatal("Failed to map Actions settings: %v", err)
}
- Actions.Storage = getStorage(rootCfg, "actions_log", "", nil)
+ actionsSec := rootCfg.Section("actions.artifacts")
+ storageType := actionsSec.Key("STORAGE_TYPE").MustString("")
+
+ Actions.LogStorage = getStorage(rootCfg, "actions_log", "", nil)
+ Actions.ArtifactStorage = getStorage(rootCfg, "actions_artifacts", storageType, actionsSec)
}
diff --git a/modules/setting/repository.go b/modules/setting/repository.go
index 0adf6a95a0..f9dc8fbe6b 100644
--- a/modules/setting/repository.go
+++ b/modules/setting/repository.go
@@ -335,11 +335,11 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
log.Fatal("Failed to map Repository.PullRequest settings: %v", err)
}
- if !rootCfg.Section("packages").Key("ENABLED").MustBool(true) {
+ if !rootCfg.Section("packages").Key("ENABLED").MustBool(Packages.Enabled) {
Repository.DisabledRepoUnits = append(Repository.DisabledRepoUnits, "repo.packages")
}
- if !rootCfg.Section("actions").Key("ENABLED").MustBool(true) {
+ if !rootCfg.Section("actions").Key("ENABLED").MustBool(Actions.Enabled) {
Repository.DisabledRepoUnits = append(Repository.DisabledRepoUnits, "repo.actions")
}
diff --git a/modules/storage/minio_test.go b/modules/storage/minio_test.go
index 861eedfe12..bfddf33032 100644
--- a/modules/storage/minio_test.go
+++ b/modules/storage/minio_test.go
@@ -10,7 +10,7 @@ import (
func TestMinioStorageIterator(t *testing.T) {
if os.Getenv("CI") == "" {
- t.Skip("minioStorage not present on CI")
+ t.Skip("minioStorage not present outside of CI")
return
}
testStorageIterator(t, string(MinioStorageType), MinioStorageConfig{
diff --git a/modules/storage/storage.go b/modules/storage/storage.go
index caecab306e..5b6efccb6a 100644
--- a/modules/storage/storage.go
+++ b/modules/storage/storage.go
@@ -128,6 +128,8 @@ var (
// Actions represents actions storage
Actions ObjectStorage = uninitializedStorage
+ // Actions Artifacts represents actions artifacts storage
+ ActionsArtifacts ObjectStorage = uninitializedStorage
)
// Init init the stoarge
@@ -212,9 +214,14 @@ func initPackages() (err error) {
func initActions() (err error) {
if !setting.Actions.Enabled {
Actions = discardStorage("Actions isn't enabled")
+ ActionsArtifacts = discardStorage("ActionsArtifacts isn't enabled")
return nil
}
- log.Info("Initialising Actions storage with type: %s", setting.Actions.Storage.Type)
- Actions, err = NewStorage(setting.Actions.Storage.Type, &setting.Actions.Storage)
+ log.Info("Initialising Actions storage with type: %s", setting.Actions.LogStorage.Type)
+ if Actions, err = NewStorage(setting.Actions.LogStorage.Type, &setting.Actions.LogStorage); err != nil {
+ return err
+ }
+ log.Info("Initialising ActionsArtifacts storage with type: %s", setting.Actions.ArtifactStorage.Type)
+ ActionsArtifacts, err = NewStorage(setting.Actions.ArtifactStorage.Type, &setting.Actions.ArtifactStorage)
return err
}
diff --git a/modules/test/context_tests.go b/modules/test/context_tests.go
index 5ba2126126..349c7e3e80 100644
--- a/modules/test/context_tests.go
+++ b/modules/test/context_tests.go
@@ -4,7 +4,7 @@
package test
import (
- scontext "context"
+ gocontext "context"
"io"
"net/http"
"net/http/httptest"
@@ -28,18 +28,7 @@ import (
// MockContext mock context for unit tests
// TODO: move this function to other packages, because it depends on "models" package
func MockContext(t *testing.T, path string) *context.Context {
- resp := &mockResponseWriter{}
- ctx := context.Context{
- Render: &mockRender{},
- Data: make(middleware.ContextData),
- Flash: &middleware.Flash{
- Values: make(url.Values),
- },
- Resp: context.NewResponse(resp),
- Locale: &translation.MockLocale{},
- }
- defer ctx.Close()
-
+ resp := httptest.NewRecorder()
requestURL, err := url.Parse(path)
assert.NoError(t, err)
req := &http.Request{
@@ -47,41 +36,105 @@ func MockContext(t *testing.T, path string) *context.Context {
Form: url.Values{},
}
+ base, baseCleanUp := context.NewBaseContext(resp, req)
+ base.Data = middleware.ContextData{}
+ base.Locale = &translation.MockLocale{}
+ ctx := &context.Context{
+ Base: base,
+ Render: &mockRender{},
+ Flash: &middleware.Flash{Values: url.Values{}},
+ }
+ _ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later
+
chiCtx := chi.NewRouteContext()
- req = req.WithContext(scontext.WithValue(req.Context(), chi.RouteCtxKey, chiCtx))
- ctx.Req = context.WithContext(req, &ctx)
- return &ctx
+ ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx)
+ return ctx
+}
+
+// MockAPIContext mock context for unit tests
+// TODO: move this function to other packages, because it depends on "models" package
+func MockAPIContext(t *testing.T, path string) *context.APIContext {
+ resp := httptest.NewRecorder()
+ requestURL, err := url.Parse(path)
+ assert.NoError(t, err)
+ req := &http.Request{
+ URL: requestURL,
+ Form: url.Values{},
+ }
+
+ base, baseCleanUp := context.NewBaseContext(resp, req)
+ base.Data = middleware.ContextData{}
+ base.Locale = &translation.MockLocale{}
+ ctx := &context.APIContext{Base: base}
+ _ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later
+
+ chiCtx := chi.NewRouteContext()
+ ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx)
+ return ctx
}
// LoadRepo load a repo into a test context.
-func LoadRepo(t *testing.T, ctx *context.Context, repoID int64) {
- ctx.Repo = &context.Repository{}
- ctx.Repo.Repository = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
+func LoadRepo(t *testing.T, ctx gocontext.Context, repoID int64) {
+ var doer *user_model.User
+ repo := &context.Repository{}
+ switch ctx := ctx.(type) {
+ case *context.Context:
+ ctx.Repo = repo
+ doer = ctx.Doer
+ case *context.APIContext:
+ ctx.Repo = repo
+ doer = ctx.Doer
+ default:
+ assert.Fail(t, "context is not *context.Context or *context.APIContext")
+ return
+ }
+
+ repo.Repository = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
var err error
- ctx.Repo.Owner, err = user_model.GetUserByID(ctx, ctx.Repo.Repository.OwnerID)
+ repo.Owner, err = user_model.GetUserByID(ctx, repo.Repository.OwnerID)
assert.NoError(t, err)
- ctx.Repo.RepoLink = ctx.Repo.Repository.Link()
- ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, ctx.Repo.Repository, ctx.Doer)
+ repo.RepoLink = repo.Repository.Link()
+ repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo.Repository, doer)
assert.NoError(t, err)
}
// LoadRepoCommit loads a repo's commit into a test context.
-func LoadRepoCommit(t *testing.T, ctx *context.Context) {
- gitRepo, err := git.OpenRepository(ctx, ctx.Repo.Repository.RepoPath())
+func LoadRepoCommit(t *testing.T, ctx gocontext.Context) {
+ var repo *context.Repository
+ switch ctx := ctx.(type) {
+ case *context.Context:
+ repo = ctx.Repo
+ case *context.APIContext:
+ repo = ctx.Repo
+ default:
+ assert.Fail(t, "context is not *context.Context or *context.APIContext")
+ return
+ }
+
+ gitRepo, err := git.OpenRepository(ctx, repo.Repository.RepoPath())
assert.NoError(t, err)
defer gitRepo.Close()
branch, err := gitRepo.GetHEADBranch()
assert.NoError(t, err)
assert.NotNil(t, branch)
if branch != nil {
- ctx.Repo.Commit, err = gitRepo.GetBranchCommit(branch.Name)
+ repo.Commit, err = gitRepo.GetBranchCommit(branch.Name)
assert.NoError(t, err)
}
}
// LoadUser load a user into a test context.
-func LoadUser(t *testing.T, ctx *context.Context, userID int64) {
- ctx.Doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID})
+func LoadUser(t *testing.T, ctx gocontext.Context, userID int64) {
+ doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID})
+ switch ctx := ctx.(type) {
+ case *context.Context:
+ ctx.Doer = doer
+ case *context.APIContext:
+ ctx.Doer = doer
+ default:
+ assert.Fail(t, "context is not *context.Context or *context.APIContext")
+ return
+ }
}
// LoadGitRepo load a git repo into a test context. Requires that ctx.Repo has
@@ -93,32 +146,6 @@ func LoadGitRepo(t *testing.T, ctx *context.Context) {
assert.NoError(t, err)
}
-type mockResponseWriter struct {
- httptest.ResponseRecorder
- size int
-}
-
-func (rw *mockResponseWriter) Write(b []byte) (int, error) {
- rw.size += len(b)
- return rw.ResponseRecorder.Write(b)
-}
-
-func (rw *mockResponseWriter) Status() int {
- return rw.ResponseRecorder.Code
-}
-
-func (rw *mockResponseWriter) Written() bool {
- return rw.ResponseRecorder.Code > 0
-}
-
-func (rw *mockResponseWriter) Size() int {
- return rw.size
-}
-
-func (rw *mockResponseWriter) Push(target string, opts *http.PushOptions) error {
- return nil
-}
-
type mockRender struct{}
func (tr *mockRender) TemplateLookup(tmpl string) (templates.TemplateExecutor, error) {
diff --git a/modules/translation/translation.go b/modules/translation/translation.go
index 49dfa84d1b..dba4de6607 100644
--- a/modules/translation/translation.go
+++ b/modules/translation/translation.go
@@ -38,10 +38,12 @@ type LangType struct {
}
var (
- lock *sync.RWMutex
+ lock *sync.RWMutex
+
+ allLangs []*LangType
+ allLangMap map[string]*LangType
+
matcher language.Matcher
- allLangs []*LangType
- allLangMap map[string]*LangType
supportedTags []language.Tag
)
@@ -251,3 +253,9 @@ func (l *locale) PrettyNumber(v any) string {
}
return l.msgPrinter.Sprintf("%v", number.Decimal(v))
}
+
+func init() {
+ // prepare a default matcher, especially for tests
+ supportedTags = []language.Tag{language.English}
+ matcher = language.NewMatcher(supportedTags)
+}
diff --git a/modules/web/handler.go b/modules/web/handler.go
index bfb83820c8..5013bac93f 100644
--- a/modules/web/handler.go
+++ b/modules/web/handler.go
@@ -10,6 +10,7 @@ import (
"reflect"
"code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/web/routing"
)
@@ -25,6 +26,10 @@ var argTypeProvider = map[reflect.Type]func(req *http.Request) ResponseStatusPro
reflect.TypeOf(&context.PrivateContext{}): func(req *http.Request) ResponseStatusProvider { return context.GetPrivateContext(req) },
}
+func RegisterHandleTypeProvider[T any](fn func(req *http.Request) ResponseStatusProvider) {
+ argTypeProvider[reflect.TypeOf((*T)(nil)).Elem()] = fn
+}
+
// responseWriter is a wrapper of http.ResponseWriter, to check whether the response has been written
type responseWriter struct {
respWriter http.ResponseWriter
@@ -78,7 +83,13 @@ func preCheckHandler(fn reflect.Value, argsIn []reflect.Value) {
}
}
-func prepareHandleArgsIn(resp http.ResponseWriter, req *http.Request, fn reflect.Value) []reflect.Value {
+func prepareHandleArgsIn(resp http.ResponseWriter, req *http.Request, fn reflect.Value, fnInfo *routing.FuncInfo) []reflect.Value {
+ defer func() {
+ if err := recover(); err != nil {
+ log.Error("unable to prepare handler arguments for %s: %v", fnInfo.String(), err)
+ panic(err)
+ }
+ }()
isPreCheck := req == nil
argsIn := make([]reflect.Value, fn.Type().NumIn())
@@ -155,7 +166,7 @@ func toHandlerProvider(handler any) func(next http.Handler) http.Handler {
}
// prepare the arguments for the handler and do pre-check
- argsIn := prepareHandleArgsIn(resp, req, fn)
+ argsIn := prepareHandleArgsIn(resp, req, fn, funcInfo)
if req == nil {
preCheckHandler(fn, argsIn)
return // it's doing pre-check, just return
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 23348b482e..070812661a 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -114,6 +114,8 @@ unknown = Unknown
rss_feed = RSS Feed
+artifacts = Artifacts
+
concept_system_global = Global
concept_user_individual = Individual
concept_code_repository = Repository
diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini
index ef18cb2af7..8eed831848 100644
--- a/options/locale/locale_fr-FR.ini
+++ b/options/locale/locale_fr-FR.ini
@@ -318,6 +318,7 @@ repo_no_results=Aucun dépôt correspondant n'a été trouvé.
user_no_results=Aucun utilisateur correspondant n'a été trouvé.
org_no_results=Aucune organisation correspondante n'a été trouvée.
code_no_results=Aucun code source correspondant à votre terme de recherche n'a été trouvé.
+code_search_results=Résultats de la recherche pour « %s »
code_last_indexed_at=Dernière indexation %s
relevant_repositories_tooltip=Les dépôts qui sont des forks ou qui n'ont aucun sujet, aucune icône et aucune description sont cachés.
relevant_repositories=Seuls les dépôts pertinents sont affichés, afficher les résultats non filtrés.
@@ -394,6 +395,7 @@ password_pwned_err=Impossible d'envoyer la demande à HaveIBeenPwned
[mail]
view_it_on=Voir sur %s
+reply=ou répondez directement à cet e-mail
link_not_working_do_paste=Le lien ne fonctionne pas ? Essayez de le copier-coller dans votre navigateur.
hi_user_x=Bonjour %s,
@@ -495,6 +497,7 @@ size_error=` doit être à la taille de %s.`
min_size_error=` %s caractères minimum `
max_size_error=` %s caractères maximum `
email_error=` adresse e-mail invalide `
+url_error=`"%s" n'est pas une URL valide.`
include_error=` doit contenir la sous-chaîne "%s".`
glob_pattern_error=` le motif de développement est invalide : %s.`
regex_pattern_error=` le motif regex est invalide : %s.`
@@ -540,8 +543,10 @@ organization_leave_success=Vous avez quitté l'organisation %s avec succès.
invalid_ssh_key=Impossible de vérifier votre clé SSH : %s
invalid_gpg_key=Impossible de vérifier votre clé GPG : %s
invalid_ssh_principal=Principal invalide : %s
+must_use_public_key=La clé que vous avez fournie est une clé privée. Veuillez ne pas divulguer votre clé privée. Utilisez votre clé publique à la place.
auth_failed=Échec d'authentification : %v
+still_own_repo=Votre compte possède toujours un ou plusieurs dépôts, vous devez d’abord les supprimer ou les transférer.
still_has_org=Votre compte est un membre d’une ou plusieurs organisations, veuillez d’abord les quitter.
still_own_packages=Votre compte possède toujours un ou plusieurs paquets, vous devez d’abord les supprimer ou les transférer.
org_still_own_repo=Cette organisation possède encore un ou plusieurs dépôts. Vous devez d’abord les supprimer ou les transférer.
@@ -569,6 +574,7 @@ email_visibility.limited=Votre adresse e-mail est visible pour tous les utilisat
email_visibility.private=Votre adresse e-mail n'est visible que pour vous et les administrateurs
form.name_reserved=Le nom d’utilisateur "%s" est réservé.
+form.name_pattern_not_allowed=Le motif "%s" n'est pas autorisé dans un nom de d'utilisateur.
form.name_chars_not_allowed=Le nom d'utilisateur "%s" contient des caractères non valides.
[settings]
@@ -578,6 +584,7 @@ appearance=Apparence
password=Mot de passe
security=Sécurité
avatar=Avatar
+ssh_gpg_keys=Clés SSH / GPG
social=Réseaux Sociaux
applications=Applications
orgs=Gérer les organisations
@@ -586,13 +593,19 @@ delete=Supprimer le compte
twofa=Authentification à deux facteurs
account_link=Comptes liés
organization=Organisations
+uid=Uid
+webauthn=Clés de sécurité
public_profile=Profil public
biography_placeholder=Parlez-nous un peu de vous.
profile_desc=Votre adresse e-mail sera utilisée pour les notifications et d'autres opérations.
+password_username_disabled=Les utilisateurs externes ne sont pas autorisés à modifier leur nom d'utilisateur. Veuillez contacter l'administrateur de votre site pour plus de détails.
full_name=Non Complet
website=Site Web
location=Localisation
+update_theme=Modifier le thème
+update_profile=Modifier le profil
+update_language=Modifier la langue
update_language_not_found=La langue "%s" n'est pas disponible.
update_language_success=La langue a été mise à jour.
update_profile_success=Votre profil a été mis à jour.
@@ -604,12 +617,19 @@ cancel=Annuler
language=Langue
ui=Thème
hidden_comment_types=Types de commentaires masqués
+hidden_comment_types.issue_ref_tooltip=Commentaires où l’utilisateur change la branche/étiquette associée au ticket
comment_type_group_reference=Référence
+comment_type_group_label=Libellé
comment_type_group_milestone=Jalon
comment_type_group_assignee=Assigné à
comment_type_group_title=Titre
comment_type_group_branch=Branche
+comment_type_group_time_tracking=Minuteur
+comment_type_group_deadline=Échéance
comment_type_group_dependency=Dépendance
+comment_type_group_lock=Verrouiller le statut
+comment_type_group_review_request=Demande de revue
+comment_type_group_pull_request_push=Révisions ajoutées
comment_type_group_project=Projet
comment_type_group_issue_ref=Référence du ticket
saved_successfully=Vos paramètres ont été enregistrés avec succès.
@@ -618,53 +638,219 @@ keep_activity_private=Masquer l'activité de la page de profil
keep_activity_private_popup=Rend l'activité visible uniquement pour vous et les administrateurs
lookup_avatar_by_mail=Rechercher un avatar par adresse e-mail
+federated_avatar_lookup=Recherche d'avatars fédérés
enable_custom_avatar=Utiliser un avatar personnalisé
choose_new_avatar=Sélectionner un nouvel avatar
+update_avatar=Modifier l’avatar
delete_current_avatar=Supprimer l'avatar actuel
uploaded_avatar_not_a_image=Le fichier téléchargé n'est pas une image.
+uploaded_avatar_is_too_big=Le fichier téléchargé dépasse la taille limite.
update_avatar_success=Votre avatar a été mis à jour.
update_user_avatar_success=L'avatar de l'utilisateur a été mis à jour.
+old_password=Mot de passe actuel
+new_password=Nouveau mot de passe
+retype_new_password=Retapez le nouveau mot de passe
+password_incorrect=Le mot de passe actuel est incorrect.
+change_password_success=Votre mot de passe a été mis à jour. Désormais, connectez-vous avec votre nouveau mot de passe.
password_change_disabled=Les mots de passes des comptes utilisateurs externes ne peuvent pas être modifiées depuis l'interface web Gitea.
+emails=Adresses e-mail
+manage_emails=Gérer les adresses e-mail
+manage_themes=Sélectionner le thème par défaut
+manage_openid=Gérer les adresses OpenID
+email_desc=Votre adresse e-mail principale sera utilisée pour les notifications et d'autres opérations.
+theme_desc=Ce sera votre thème par défaut sur le site.
primary=Principale
activated=Activé
+requires_activation=Nécessite une activation
+primary_email=Faire de cette adresse votre adresse principale
+activate_email=Envoyer l’activation
+activations_pending=Activations en attente
delete_email=Exclure
+email_deletion=Supprimer l'adresse e-mail
+email_deletion_desc=L’adresse e-mail et les informations associées seront retirées de votre compte. Les révisions Git effectuées par cette adresse resteront inchangées. Continuer ?
+email_deletion_success=L'adresse e-mail a été supprimée.
+theme_update_success=Votre thème a été mis à jour.
+theme_update_error=Le thème sélectionné n'existe pas.
+openid_deletion=Supprimer l’adresse OpenID
+openid_deletion_desc=Supprimer cette adresse OpenID de votre compte vous empêchera de vous connecter avec. Continuer ?
openid_deletion_success=L'adresse OpenID a été supprimée.
+add_new_email=Ajouter une nouvelle adresse e-mail
add_new_openid=Ajouter une nouvelle URI OpenID
+add_email=Ajouter une adresse e-mail
+add_openid=Ajouter une URI OpenID
+add_email_confirmation_sent=Un e-mail de confirmation a été envoyé à "%s". Veuillez vérifier votre boîte de réception dans les %s suivants pour confirmer votre adresse e-mail.
+add_email_success=La nouvelle adresse e-mail a été ajoutée.
+email_preference_set_success=L'e-mail de préférence a été défini avec succès.
+add_openid_success=La nouvelle adresse OpenID a été ajoutée.
+keep_email_private=Cacher l'adresse e-mail
+keep_email_private_popup=Votre adresse e-mail ne sera visible que de vous et des administrateurs
+openid_desc=OpenID vous permet de confier l'authentification à une tierce partie.
+manage_ssh_keys=Gérer les clés SSH
+manage_ssh_principals=Gérer les certificats principaux SSH
+manage_gpg_keys=Gérer les clés GPG
+add_key=Ajouter une clé
+ssh_desc=Ces clefs SSH publiques sont associées à votre compte. Les clefs privées correspondantes permettent l'accès complet à vos repos.
+principal_desc=Ces Principaux de certificats SSH sont associés à votre compte et permettent un accès complet à vos dépôts.
+gpg_desc=Ces clefs GPG sont associées avec votre compte. Conservez-les en lieu sûr, car elles permettent la vérification de vos commits.
+ssh_helper=Besoin d'aide ? Consultez le guide de GitHub pour créer vos propres clés SSH ou résoudre les problèmes courants que vous pourriez rencontrer en utilisant SSH.
+gpg_helper=Besoin d'aide ? Consultez le guide de GitHub sur GPG.
+add_new_key=Ajouter une clé SSH
+add_new_gpg_key=Ajouter une clé GPG
+key_content_ssh_placeholder=Commence par 'ssh-ed25519', 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'sk-ecdsa-sha2-nistp256@openssh.com' ou par 'sk-ssh-ed25519@openssh.com'
+key_content_gpg_placeholder=Commence par '-----BEGIN PGP PUBLIC KEY BLOCK-----'
+add_new_principal=Ajouter le principal
+ssh_key_been_used=Cette clé SSH a déjà été ajoutée au serveur.
+ssh_key_name_used=Une clé SSH avec le même nom existe déjà sur votre compte.
+ssh_principal_been_used=Ce principal a déjà été ajouté au serveur.
+gpg_key_id_used=Une clé publique GPG avec le même ID existe déjà.
+gpg_no_key_email_found=Cette clé GPG ne correspond à aucune adresse e-mail activée associée à votre compte. Elle peut toujours être ajoutée si vous signez le jeton fourni.
+gpg_key_matched_identities=Identités correspondantes :
+gpg_key_matched_identities_long=Les identités intégrées dans cette clé correspondent aux adresses e-mail activées suivantes pour cet utilisateur. Les commits correspondant à ces adresses e-mail peuvent être vérifiés avec cette clé.
+gpg_key_verified=Clé vérifiée
+gpg_key_verified_long=La clé a été vérifiée avec un jeton et peut être utilisée pour vérifier les révisions correspondant à toutes les adresses e-mails pour cet utilisateur en plus de toutes les identités pour cette clé.
gpg_key_verify=Vérifier
+gpg_invalid_token_signature=La clé GPG, la signature et le jeton fournis ne correspondent pas ou le jeton n'est pas à jour.
+gpg_token_required=Vous devez fournir une signature pour le jeton ci-dessous
gpg_token=Jeton
+gpg_token_help=Vous pouvez générer une signature en utilisant :
+gpg_token_signature=Signature GPG renforcée
+key_signature_gpg_placeholder=Commence par '-----BEGIN PGP SIGNATURE-----'
+verify_gpg_key_success=La clé GPG "%s" a été vérifiée.
+ssh_key_verified=Clé vérifiée
+ssh_key_verified_long=La clé a été vérifiée avec un jeton et peut être utilisée pour vérifier les commits correspondant à toutes les adresses mail activées pour cet utilisateur.
ssh_key_verify=Vérifier
+ssh_invalid_token_signature=La clé SSH, la signature ou le jeton fournis ne correspondent pas ou le jeton est périmé.
+ssh_token_required=Vous devez fournir une signature pour le jeton ci-dessous
ssh_token=Jeton
+ssh_token_help=Vous pouvez générer une signature en utilisant :
+ssh_token_signature=Signature SSH renforcée
+key_signature_ssh_placeholder=Commence par '-----BEGIN SSH SIGNATURE-----'
+verify_ssh_key_success=La clé SSH "%s" a été vérifiée.
+subkeys=Sous-clés
+key_id=ID de la clé
+key_name=Nom de la Clé
key_content=Contenu
principal_content=Contenu
+add_key_success=La clé SSH "%s" a été ajoutée.
+add_gpg_key_success=La clé GPG "%s" a été ajoutée.
+add_principal_success=Le Principal de certificat SSH "%s" a été ajouté.
delete_key=Exclure
+ssh_key_deletion=Retirer la clé SSH
+gpg_key_deletion=Retirer la clé GPG
+ssh_principal_deletion=Retirer le Principal de certificat SSH
+ssh_key_deletion_desc=Le retrait d'une clé SSH révoque son accès à votre compte. Continuer ?
+gpg_key_deletion_desc=Supprimer une clé GPG renie les révisions signées par celle-ci. Continuer ?
+ssh_principal_deletion_desc=Le retrait d'un Principal de certificat SSH révoque son accès à votre compte. Poursuivre ?
+ssh_key_deletion_success=La clé SSH a été retirée.
+gpg_key_deletion_success=La clé GPG a été retirée.
+ssh_principal_deletion_success=Le Principal a été retiré.
+add_on=Ajouté le
+valid_until=Valable jusqu'au
+last_used=Dernière utilisation le
+no_activity=Aucune activité récente
can_read_info=Lue(s)
+can_write_info=Écriture
+key_state_desc=Cette clé a été utilisée au cours des 7 derniers jours
+token_state_desc=Ce jeton a été utilisé au cours des 7 derniers jours
+principal_state_desc=Ce Principal a été utilisé au cours des 7 derniers jours
+show_openid=Afficher sur le profil
+hide_openid=Masquer du profil
+ssh_disabled=SSH désactivé
+ssh_signonly=Le SSH est actuellement désactivé, donc ces clés ne sont utilisées que pour la vérification de la signature de livraison.
+unbind=Dissocier
+manage_access_token=Gérer les jetons d'accès
+generate_new_token=Générer un nouveau jeton
+token_name=Nom du jeton
+generate_token=Générer un jeton
+generate_token_success=Votre nouveau jeton a été généré. Copiez-le maintenant car il ne sera plus affiché.
+generate_token_name_duplicate=%s a déjà été utilisé comme nom d'application. Veuillez en utiliser un autre.
delete_token=Supprimer
+access_token_deletion=Supprimer le jeton d'accès
access_token_deletion_cancel_action=Annuler
access_token_deletion_confirm_action=Supprimer
+access_token_deletion_desc=Supprimer un jeton révoquera l'accès à votre compte pour toutes les applications l'utilisant. Cette action est irréversible. Continuer ?
+delete_token_success=Ce jeton a été supprimé. Les applications l'utilisant n'ont plus accès à votre compte.
+select_scopes=Sélectionner les périmètres
+scopes_list=Périmètres :
+manage_oauth2_applications=Gérer les applications OAuth2
+edit_oauth2_application=Modifier l'application OAuth2
+oauth2_applications_desc=Les applications OAuth2 permettent à votre application tierce d'authentifier en toute sécurité les utilisateurs de cette instance Gitea.
+remove_oauth2_application=Supprimer l'application OAuth2
+remove_oauth2_application_desc=La suppression d'une application OAuth2 révoquera l'accès à tous les jetons d'accès signés. Continuer ?
+remove_oauth2_application_success=L'application a été supprimée.
+create_oauth2_application=Créer une nouvelle application OAuth2
+create_oauth2_application_button=Créer une application
+create_oauth2_application_success=Vous avez créé une nouvelle application OAuth2 avec succès.
+update_oauth2_application_success=Vous avez mis à jour l'application OAuth2 avec succès.
+oauth2_application_name=Nom de l'Application
+oauth2_redirect_uri=URI de redirection
save_application=Enregistrer
+oauth2_client_id=ID du client
oauth2_client_secret=Secret du client
+oauth2_regenerate_secret=Regénérer le secret
+oauth2_regenerate_secret_hint=Avez-vous perdu votre secret ?
+oauth2_client_secret_hint=Le secret ne sera plus visible si vous revenez sur cette page. Veuillez sauvegarder votre secret.
oauth2_application_edit=Éditer
+oauth2_application_create_description=Les applications OAuth2 permettent à votre application tierce d'accéder aux comptes d'utilisateurs de cette instance.
+oauth2_application_remove_description=La suppression d'une application OAuth2 l'empêchera d'accéder aux comptes d'utilisateurs autorisés sur cette instance. Poursuivre ?
+authorized_oauth2_applications=Applications OAuth2 autorisées
+revoke_key=Révoquer
+revoke_oauth2_grant=Révoquer l'accès
+revoke_oauth2_grant_description=La révocation de l'accès à cette application tierce l'empêchera d'accéder à vos données. Vous êtes sûr ?
+revoke_oauth2_grant_success=Vous avez révoqué l'accès avec succès.
+twofa_desc=L'authentification à deux facteurs améliore la sécurité de votre compte.
+twofa_scratch_token_regenerate=Régénérer un jeton de secours
+twofa_scratch_token_regenerated=Votre jeton de secours est maintenant %s. Gardez-le en lieu sûr.
+twofa_enroll=Activer l'authentification à deux facteurs
+twofa_disable_note=Vous pouvez désactiver l'authentification à deux facteurs si nécessaire.
+twofa_disable_desc=Désactiver l'authentification à deux facteurs rendra votre compte plus vulnérable. Confirmer ?
+twofa_disabled=L'authentification à deux facteurs a été désactivée.
+twofa_failed_get_secret=Impossible d'obtenir le secret.
+webauthn_register_key=Ajouter une clé de sécurité
+webauthn_nickname=Pseudonyme
+webauthn_delete_key=Retirer la clé de sécurité
+manage_account_links=Gérer les comptes liés
+manage_account_links_desc=Ces comptes externes sont liés à votre compte Gitea.
+account_links_not_available=Il n'y a pour l'instant pas de compte externe connecté à votre compte Gitea.
link_account=Lier un Compte
+remove_account_link=Supprimer un compte lié
+remove_account_link_desc=La suppression d'un compte lié révoquera son accès à votre compte Gitea. Continuer ?
+remove_account_link_success=Le compte lié a été supprimé.
+hooks.desc=Ajoute des déclencheurs qui seront activés pour tous les dépôts de cet utilisateur.
+orgs_none=Vous n'êtes membre d'aucune organisation.
+repos_none=Vous ne possédez aucun dépôt
+delete_account=Supprimer votre compte
+delete_prompt=Cette opération supprimera définitivement votre compte d'utilisateur. Cette action est IRRÉVERSIBLE.
+delete_with_all_comments=Votre compte est plus jeune que %s. Afin d'éviter les commentaires fantômes, tous les commentaires sur les tickets/PR seront aussi supprimés.
confirm_delete_account=Confirmez la suppression
delete_account_title=Supprimer cet utilisateur
+delete_account_desc=Êtes-vous sûr de vouloir supprimer définitivement ce compte d'utilisateur ?
email_notifications.enable=Activer les notifications par e-mail
+email_notifications.onmention=N'envoyer un e-mail que si vous êtes mentionné
+email_notifications.disable=Désactiver les notifications par e-mail
+email_notifications.submit=Définir les préférences d'e-mail
+email_notifications.andyourown=Et vos propres notifications
+visibility=Visibilité de l'utilisateur
visibility.public=Public
+visibility.public_tooltip=Visible par tout le monde
visibility.limited=Limité
+visibility.limited_tooltip=Visible uniquement pour les utilisateurs authentifiés
visibility.private=Privé
+visibility.private_tooltip=Visible uniquement aux membres de l'organisation
[repo]
new_repo_helper=Un dépôt contient tous les fichiers du projet, y compris l'historique des révisions. Vous l'avez déjà ailleurs ? Migrer le dépôt.
@@ -751,6 +937,7 @@ delete_preexisting=Supprimer les fichiers préexistants
delete_preexisting_content=Supprimer les fichiers dans %s
delete_preexisting_success=Supprimer les fichiers non adoptés dans %s
blame_prior=Voir le blame avant cette modification
+author_search_tooltip=Affiche un maximum de 30 utilisateurs
transfer.accept=Accepter le transfert
transfer.accept_desc=`Transférer à "%s"`
@@ -784,7 +971,13 @@ archive.pull.nocomment=Ce dépôt est archivé. Vous ne pouvez pas commenter de
form.reach_limit_of_creation_1=Vous avez déjà atteint la limite d'%d dépôt.
form.reach_limit_of_creation_n=Vous avez déjà atteint la limite de %d dépôts.
+form.name_reserved=Ce nom de dépôt "%s" est réservé.
+form.name_pattern_not_allowed=Le motif "%s" n'est pas autorisé dans un nom de dépôt.
+need_auth=Autorisation
+migrate_options=Options de migration
+migrate_service=Service de migration
+migrate_options_lfs=Migrer les fichiers LFS
migrate_options_lfs_endpoint.label=Point d'accès LFS
migrate_options_lfs_endpoint.description.local=Un chemin de serveur local est également pris en charge.
migrate_options_lfs_endpoint.placeholder=Laisser vide pour dériver de l'URL de clonage
@@ -842,10 +1035,12 @@ unstar=Retirer des favoris
star=Ajouter aux favoris
fork=Bifurcation
download_archive=Télécharger ce dépôt
+more_operations=Plus d'opérations
no_desc=Aucune description
quick_guide=Introduction rapide
clone_this_repo=Cloner ce dépôt
+cite_this_repo=Citer ce dépôt
create_new_repo_command=Création d'un nouveau dépôt en ligne de commande
push_exist_repo=Soumission d'un dépôt existant par ligne de commande
empty_message=Ce dépôt n'a pas de contenu.
@@ -864,6 +1059,7 @@ issues=Tickets
pulls=Demandes d'ajout
project_board=Projets
packages=Paquets
+actions=Actions
labels=Étiquettes
org_labels_desc=Les étiquettes de niveau d'une organisation peuvent être utilisés avec tous les dépôts de cette organisation
org_labels_desc_manage=gérer
@@ -875,6 +1071,7 @@ release=Versions
releases=Versions
tag=Tag
released_this=a publié ceci
+tagged_this=a étiqueté
file.title=%s sur %s
file_raw=Brut
file_history=Historique
@@ -886,6 +1083,7 @@ file_too_large=Le fichier est trop gros pour être affiché.
invisible_runes_header=`Ce fichier contient des caractères Unicode invisibles !`
invisible_runes_description=`Ce fichier contient des caractères Unicode invisibles qui pourraient être affichés différemment de ce qui apparaît ci-dessous. Si votre cas d'utilisation est intentionnel et légitime, vous pouvez ignorer en toute sécurité cet avertissement. Utilisez le bouton Échapper pour mettre en évidence ces caractères invisbles.`
ambiguous_runes_header=`Ce fichier contient des caractères Unicode ambigus !`
+ambiguous_runes_description=`Ce fichier contient des caractères Unicode ambigus qui peuvent être confondus avec d'autres dans votre locale actuelle. Si votre cas d'utilisation est intentionnel et légitime, vous pouvez ignorer cet avertissement. Utilisez le bouton Echap pour mettre en évidence ces caractères.`
invisible_runes_line=`Cette ligne contient des caractères Unicode invisibles`
ambiguous_runes_line=`Cette ligne contient des caractères Unicode ambigus`
ambiguous_character=`%[1]c [U+%04[1]X] peut être confondu avec %[2]c [U+%04[2]X]`
@@ -893,6 +1091,7 @@ ambiguous_character=`%[1]c [U+%04[1]X] peut être confondu avec %[2]c [U+%04[2]X
escape_control_characters=Échapper
unescape_control_characters=Annuler l'échappement
file_copy_permalink=Copier le lien permanent
+view_git_blame=Voir Git Blâme
video_not_supported_in_browser=Votre navigateur ne supporte pas le tag HTML5 "video".
audio_not_supported_in_browser=Votre navigateur ne supporte pas la balise « audio » HTML5.
stored_lfs=Stocké avec Git LFS
@@ -907,6 +1106,7 @@ download_file=Télécharger le fichier
normal_view=Vue normale
line=ligne
lines=lignes
+from_comment=(commentaire)
editor.add_file=Ajouter un fichier
editor.new_file=Nouveau fichier
@@ -921,6 +1121,7 @@ editor.must_be_on_a_branch=Vous devez être sur une branche pour appliquer ou pr
editor.fork_before_edit=Vous devez faire bifurquer ce dépôt pour appliquer ou proposer des modifications à ce fichier.
editor.delete_this_file=Supprimer le fichier
editor.must_have_write_access=Vous devez avoir un accès en écriture pour appliquer ou proposer des modifications à ce fichier.
+editor.file_delete_success=Le fichier "%s" a été supprimé.
editor.name_your_file=Nommez votre fichier…
editor.filename_help=Ajoutez un dossier en entrant son nom suivi d'une barre oblique ('/'). Supprimez un dossier avec un retour arrière au début du champ.
editor.or=ou
@@ -928,8 +1129,12 @@ editor.cancel_lower=Annuler
editor.commit_signed_changes=Valider les révisions signées
editor.commit_changes=Enregistrer les modifications
editor.add_tmpl=Ajouter 'alt + clic/entrée pour exclure les étiquettes`
@@ -1078,6 +1315,7 @@ issues.filter_label_no_select=Toutes les étiquettes
issues.filter_milestone=Jalon
issues.filter_milestone_no_select=Tous les jalons
issues.filter_project=Projet
+issues.filter_project_all=Tous les projets
issues.filter_project_none=Pas de projet
issues.filter_assignee=Assigné
issues.filter_assginee_no_select=Toutes les affectations
@@ -1089,6 +1327,7 @@ issues.filter_type.assigned_to_you=Qui vous sont assignés
issues.filter_type.created_by_you=Créés par vous
issues.filter_type.mentioning_you=Vous mentionnant
issues.filter_type.review_requested=Revue demandée
+issues.filter_type.reviewed_by_you=Revu par vous
issues.filter_sort=Trier
issues.filter_sort.latest=Plus récent
issues.filter_sort.oldest=Plus ancien/ne
@@ -1102,6 +1341,7 @@ issues.filter_sort.moststars=Favoris (décroissant)
issues.filter_sort.feweststars=Favoris (croissant)
issues.filter_sort.mostforks=Bifurcations (décroissant)
issues.filter_sort.fewestforks=Bifurcations (croissant)
+issues.keyword_search_unavailable=La recherche par mot clé n'est pas disponible. Veuillez contacter l'administrateur de votre instance.
issues.action_open=Ouvrir
issues.action_close=Fermer
issues.action_label=Étiquette
@@ -1109,6 +1349,8 @@ issues.action_milestone=Jalon
issues.action_milestone_no_select=Aucun jalon
issues.action_assignee=Assigné à
issues.action_assignee_no_select=Pas d'assignataire
+issues.action_check=Cocher/Décocher
+issues.action_check_all=Cocher/Décocher tous les éléments
issues.opened_by=créé %[1]s par %[3]s
issues.opened_by_fake=%[1]s ouvert par %[2]s
issues.previous=Page Précédente
@@ -1125,6 +1367,7 @@ issues.context.reference_issue=Référencer dans un nouveau ticket
issues.context.edit=Éditer
issues.context.delete=Supprimer
issues.no_content=Il n'existe pas encore de contenu.
+issues.close=Fermer le ticket
issues.pull_merged_at=`révision fusionnée %[2]s dans %[3]s %[4]s`
issues.manually_pull_merged_at=`révision fusionnée %[2]s dans %[3]s manuellement %[4]s`
issues.close_comment_issue=Commenter et Fermer
@@ -1157,6 +1400,9 @@ issues.save=Enregistrer
issues.label_title=Nom de l'étiquette
issues.label_description=Description de l’étiquette
issues.label_color=Couleur de l'étiquette
+issues.label_exclusive=Exclusif
+issues.label_exclusive_desc=Nommez le libellé périmètre/élément pour qu'il soit mutuellement exclusif avec d'autres libellés du périmètre.
+issues.label_exclusive_warning=Tout libellé conflictuel sera supprimé lors de l'édition des libellés d'un ticket ou d'une demande de tirage.
issues.label_count=%d étiquettes
issues.label_open_issues=%d tickets ouverts
issues.label_edit=Éditer
@@ -1194,6 +1440,8 @@ issues.lock.title=Verrouiller la conversation sur ce ticket.
issues.unlock.title=Déverrouiller la conversation sur ce ticket.
issues.comment_on_locked=Vous ne pouvez pas commenter un ticket verrouillé.
issues.delete=Supprimer
+issues.delete.title=Supprimer ce ticket ?
+issues.delete.text=Voulez-vous vraiment supprimer ce ticket ? (Cette opération supprimera définitivement tout le contenu. Envisagez plutôt de le fermer si vous avez l'intention de l'archiver)
issues.tracker=Suivi du temps
issues.start_tracking_short=Démarrer le suivi de temps
issues.start_tracking=Démarrer le suivi du temps
@@ -1230,10 +1478,16 @@ issues.due_date_form_remove=Supprimer
issues.due_date_not_writer=Vous devez avoir accès au dépôt en écriture pour mettre à jour l'échéance d'un ticket.
issues.due_date_not_set=Aucune échéance n'a été définie.
issues.due_date_added=a ajouté l'échéance %s %s
+issues.due_date_modified=a modifié l'échéance de %[2]s à %[1]s %[3]s
issues.due_date_remove=a supprimé l'échéance %s %s
issues.due_date_overdue=En retard
issues.due_date_invalid=La date d’échéance est invalide ou hors plage. Veuillez utiliser le format 'aaaa-mm-dd'.
issues.dependency.title=Dépendances
+issues.dependency.issue_no_dependencies=Aucune dépendance définie.
+issues.dependency.pr_no_dependencies=Aucune dépendance définie.
+issues.dependency.no_permission_1=Vous n’avez pas la permission de voir la dépendance de %d
+issues.dependency.no_permission_n=Vous n’avez pas la permission de voir les dépendances de %d
+issues.dependency.no_permission.can_remove=Vous n'avez pas la permission de voir cette dépendance, mais vous pouvez la supprimer
issues.dependency.add=Ajouter une dépendance…
issues.dependency.cancel=Annuler
issues.dependency.remove=Supprimer
@@ -1272,6 +1526,7 @@ issues.review.add_review_request=a demandé une révision de %s %s
issues.review.remove_review_request=a supprimé la demande de révision pour %s %s
issues.review.remove_review_request_self=a refusé la revue %s
issues.review.pending=En attente
+issues.review.pending.tooltip=Ce commentaire n'est pas encore visible par les autres utilisateurs. Pour soumettre vos commentaires en attente, sélectionnez "%s" → "%s/%s/%s" en haut de la page.
issues.review.review=Révision
issues.review.reviewers=Relecteurs
issues.review.outdated=Périmé
@@ -1284,11 +1539,13 @@ issues.review.un_resolve_conversation=Conversation non résolue
issues.review.resolved_by=marquer cette conversation comme résolue
issues.assignee.error=Tous les assignés n'ont pas été ajoutés en raison d'une erreur inattendue.
issues.reference_issue.body=Corps
+issues.content_history.deleted=supprimé
issues.content_history.edited=édité
issues.content_history.created=créé
issues.content_history.delete_from_history=Supprimé de l’historique
issues.content_history.delete_from_history_confirm=Supprimer de l’historique ?
issues.content_history.options=Options
+issues.reference_link=Référence : %s
compare.compare_base=base
compare.compare_head=comparer
@@ -1297,10 +1554,19 @@ pulls.desc=Activer les demandes de fusion et la revue de code.
pulls.new=Nouvelle demande d'ajout
pulls.view=Voir la demande d'ajout
pulls.compare_changes=Nouvelle demande de fusion
+pulls.allow_edits_from_maintainers=Autoriser les modifications des mainteneurs
+pulls.allow_edits_from_maintainers_desc=Les utilisateurs ayant un accès en écriture à la branche de base peuvent également pousser vers cette branche
+pulls.allow_edits_from_maintainers_err=La mise à jour à échoué
pulls.compare_changes_desc=Sélectionnez la branche dans laquelle fusionner et la branche depuis laquelle tirer les modifications.
+pulls.has_viewed_file=Consulté
+pulls.has_changed_since_last_review=Modifiée depuis votre dernière revue
+pulls.viewed_files_label=%[1]d / %[2]d fichiers vus
+pulls.expand_files=Développer tous les fichiers
+pulls.collapse_files=Réduire tous les fichiers
pulls.compare_base=fusionner dans
pulls.compare_compare=tirer les modifications depuis
pulls.switch_comparison_type=Changer le type de comparaison
+pulls.switch_head_and_base=Passez de head à base
pulls.filter_branch=Filtre de branche
pulls.no_results=Aucun résultat trouvé.
pulls.nothing_to_compare=Ces branches sont identiques. Il n'y a pas besoin de créer une demande de fusion.
@@ -1309,14 +1575,17 @@ pulls.has_pull_request='Il existe déjà une demande d'ajout entre ces deux bran
pulls.create=Créer une demande d'ajout
pulls.title_desc=veut fusionner %[1]d révision(s) depuis %[2]s vers %[3]s
pulls.merged_title_desc=a fusionné %[1]d révision(s) à partir de %[2]s vers %[3]s %[4]s
-pulls.change_target_branch_at=`a changé la branche cible de %s à %s %s`
+pulls.change_target_branch_at=`a modifié la branche cible %s pour %s %s`
pulls.tab_conversation=Discussion
pulls.tab_commits=Révisions
pulls.tab_files=Fichiers Modifiés
pulls.reopen_to_merge=Veuillez rouvrir cette demande d'ajout pour effectuer l'opération de fusion.
pulls.cant_reopen_deleted_branch=Cette demande d'ajout ne peut pas être rouverte car la branche a été supprimée.
pulls.merged=Fusionnée
+pulls.merged_success=Demande d’ajout fusionnée et fermée avec succès
+pulls.closed=Demande d’ajout fermée
pulls.manually_merged=Fusionné manuellement
+pulls.merged_info_text=La branche %s peut maintenant être supprimée.
pulls.is_closed=La demande de fusion a été fermée.
pulls.title_wip_desc=`Préfixer le titre par %s pour empêcher cette demande d'ajout d'être fusionnée par erreur.`
pulls.cannot_merge_work_in_progress=Cette demande d'ajout est marquée comme en cours de chantier.
@@ -1326,6 +1595,8 @@ pulls.remove_prefix=Enlever le préfixe %s
pulls.data_broken=Cette demande de fusion est impossible par manque d'informations de bifurcation.
pulls.files_conflicted=Cette demande d'ajout contient des modifications en conflit avec la branche ciblée.
pulls.is_checking=Vérification des conflits de fusion en cours. Réessayez dans quelques instants.
+pulls.is_ancestor=Cette branche est déjà présente dans la branche ciblée. Il n'y a rien à fusionner.
+pulls.is_empty=Les changements sur cette branche sont déjà sur la branche cible. Cette révision sera vide.
pulls.required_status_check_failed=Certains contrôles requis n'ont pas réussi.
pulls.required_status_check_missing=Certains contrôles requis sont manquants.
pulls.required_status_check_administrator=En tant qu'administrateur, vous pouvez toujours fusionner cette requête de pull.
@@ -1378,18 +1649,24 @@ pulls.status_checks_failure=Certaines vérifications ont échoué
pulls.status_checks_error=Quelques vérifications ont signalé des erreurs
pulls.status_checks_requested=Requis
pulls.status_checks_details=Détails
+pulls.update_branch=Actualiser la branche par fusion
+pulls.update_branch_rebase=Actualiser la branche par rebasage
pulls.update_branch_success=La mise à jour de la branche a réussi
pulls.update_not_allowed=Vous n'êtes pas autorisé à mettre à jour la branche
pulls.outdated_with_base_branch=Cette branche est désynchronisée avec la branche de base
+pulls.close=Fermer la demande d’ajout
pulls.closed_at=`a fermé cette pull request %[2]s`
pulls.reopened_at=`a réouvert cette pull request %[2]s`
pulls.merge_instruction_hint=`Vous pouvez également voir les instructions en ligne de commande.`
pulls.merge_instruction_step1_desc=Depuis le dépôt de votre projet, sélectionnez une nouvelle branche et testez les modifications.
pulls.merge_instruction_step2_desc=Fusionner les modifications et mettre à jour sur Gitea.
+pulls.clear_merge_message=Effacer le message de fusion
+pulls.auto_merge_button_when_succeed=(Lorsque les vérifications ont réussi)
pulls.auto_merge_newly_scheduled=La demande d'ajout était programmée pour fusionner lorsque toutes les vérifications aurait réussi.
pulls.auto_merge_has_pending_schedule=%[1]s Ont planifié cette demande d'ajout pour fusionner automatiquement lorsque toutes les vérifications réussissent %[2]s.
+pulls.auto_merge_cancel_schedule=Annuler la fusion automatique
pulls.auto_merge_not_scheduled=Cette demande d'ajout n'est pas planifiée pour fusionner automatiquement.
pulls.auto_merge_canceled_schedule=La fusion automatique a été annulée pour cette demande d'ajout.
@@ -1401,6 +1678,7 @@ pulls.delete.text=Voulez-vous vraiment supprimer cet demande d'ajout ? (Cela sup
milestones.new=Nouveau jalon
milestones.closed=%s fermé
+milestones.update_ago=Actualisé il y a %s
milestones.no_due_date=Aucune date d'échéance
milestones.open=Ouvrir
milestones.close=Fermer
@@ -1412,10 +1690,12 @@ milestones.desc=Description
milestones.due_date=Date d'échéance (facultatif)
milestones.clear=Effacer
milestones.invalid_due_date_format=Le format de la date d'échéance est invalide, il doit être comme suit 'aaaa-mm-jj'.
+milestones.create_success=Le jalon "%s" a été créé.
milestones.edit=Éditer le Jalon
milestones.edit_subheader=Les jalons organisent les tickets et le suivi d'avancement.
milestones.cancel=Annuler
milestones.modify=Mettre à jour un jalon
+milestones.edit_success=Le jalon "%s" a été mis à jour.
milestones.deletion=Supprimer un Jalon
milestones.deletion_desc=Supprimer un jalon le retire de tous les tickets. Continuer ?
milestones.deletion_success=Le jalon a été supprimé.
@@ -1426,6 +1706,7 @@ milestones.filter_sort.most_complete=Le plus complété
milestones.filter_sort.most_issues=Le plus de tickets
milestones.filter_sort.least_issues=Le moins de tickets
+signing.will_sign=`Cette révision sera signée avec la clé "%s"`
signing.wont_sign.error=Une erreur s'est produite lors de la vérification de la signature de la révision
signing.wont_sign.nokey=Il n'y a aucune clé disponible pour signer cette révision
signing.wont_sign.never=Les révisions ne sont jamais signées
@@ -1439,6 +1720,7 @@ signing.wont_sign.commitssigned=La fusion ne sera pas signée car toutes les ré
signing.wont_sign.approved=La fusion ne sera pas signée car la PR n'a pas approuvée
signing.wont_sign.not_signed_in=Vous n'êtes pas authentifié
+ext_wiki=Accès au wiki externe
ext_wiki.desc=Lier un wiki externe.
wiki=Wiki
@@ -1449,6 +1731,8 @@ wiki.create_first_page=Créer la première page
wiki.page=Page
wiki.filter_page=Filtrer la page
wiki.new_page=Page
+wiki.page_title=Titre de la page
+wiki.page_content=Contenu de la page
wiki.default_commit_message=Écrire une note concernant cette mise à jour (optionnel).
wiki.save_page=Enregistrer la page
wiki.last_commit_info=%s a édité cette page %s
@@ -1458,7 +1742,9 @@ wiki.file_revision=Révisions de la page
wiki.wiki_page_revisions=Révisions de la page wiki
wiki.back_to_wiki=Retour à la page wiki
wiki.delete_page_button=Supprimer la page
+wiki.delete_page_notice_1=Supprimer la page de wiki "%s" est irréversible. Continuer ?
wiki.page_already_exists=Une page de wiki avec le même nom existe déjà.
+wiki.reserved_page=Le nom de page de wiki "%s" est réservé.
wiki.pages=Pages
wiki.last_updated=Dernière mise à jour: %s
@@ -1552,6 +1838,7 @@ settings.hooks=Webhooks
settings.githooks=Déclencheurs Git
settings.basic_settings=Paramètres de base
settings.mirror_settings=Réglages Miroir
+settings.mirror_settings.docs=Configurez votre projet pour automatiquement pousser ou tirer les modifications d'un autre dépôt. Les branches, étiquettes et révisions seront automatiquement synchronisées. Comment mettre en miroir les dépôts ?
settings.mirror_settings.mirrored_repository=Dépôt en miroir
settings.mirror_settings.direction=Direction
settings.mirror_settings.direction.push=Pousser
@@ -1588,20 +1875,28 @@ settings.pulls.ignore_whitespace=Ignorer les espaces lors des conflits
settings.pulls.enable_autodetect_manual_merge=Activer la détection automatique de la fusion manuelle (Remarque : dans certains cas particuliers, des erreurs de détection peuvent se produire)
settings.pulls.allow_rebase_update=Activer la mise à jour de demande d'ajout par rebase
settings.pulls.default_delete_branch_after_merge=Supprimer la branche après la fusion par default
+settings.packages_desc=Activer le registre des paquets du dépôt
settings.projects_desc=Activer les projets de dépôt
+settings.actions_desc=Activer les actions du dépôt
settings.admin_settings=Paramètres administrateur
settings.admin_enable_health_check=Activer les vérifications de santé du dépôt (git fsck)
+settings.admin_code_indexer=Indexeur de code
+settings.admin_stats_indexer=Indexeur des statistiques de code
+settings.admin_indexer_commit_sha=Dernier SHA indexé
+settings.admin_indexer_unindexed=Non indexé
+settings.reindex_button=Ajouter à queue de réindexation
+settings.reindex_requested=Réindexation demandée
settings.admin_enable_close_issues_via_commit_in_any_branch=Fermer un ticket via une révision faite sur une branche non par défaut
settings.danger_zone=Zone de danger
settings.new_owner_has_same_repo=Le nouveau propriétaire a déjà un dépôt nommé ainsi.
settings.convert=Convertir en dépôt standard
-settings.convert_desc=Vous pouvez convertir ce miroir en dépôt standard. Ceci ne peut pas être annulé.
-settings.convert_notices_1=Cette opération convertira le miroir en dépôt standard et ne peut être annulée.
+settings.convert_desc=Vous pouvez convertir ce miroir en dépôt standard. Cette action est irréversible.
+settings.convert_notices_1=Cette opération convertira le miroir en dépôt standard. Cette action est irréversible.
settings.convert_confirm=Convertir le dépôt
settings.convert_succeed=Le miroir a été converti en dépôt standard.
settings.convert_fork=Convertir en dépôt standard
-settings.convert_fork_desc=Vous pouvez convertir ce miroir en dépôt standard. Ceci ne peut pas être annulé.
-settings.convert_fork_notices_1=Cette opération convertira le miroir en dépôt standard et ne peut être annulée.
+settings.convert_fork_desc=Vous pouvez convertir ce miroir en dépôt standard. Cette action est irréversible.
+settings.convert_fork_notices_1=Cette opération convertira le miroir en dépôt standard. Cette action est irréversible.
settings.convert_fork_confirm=Convertir le dépôt
settings.convert_fork_succeed=Le miroir a été converti en dépôt standard.
settings.transfer=Changer de propriétaire
@@ -1633,13 +1928,13 @@ settings.trust_model.collaboratorcommitter=Collaborateur+Committer
settings.trust_model.collaboratorcommitter.long=Collaborateur+Committer: Faire confiance aux signatures des collaborateurs qui correspondent à l'auteur
settings.trust_model.collaboratorcommitter.desc=Les signatures valides des des collaborateurs de ce dépôt seront marquées "de confiance" si elles correspondent à l'expéditeur. Dans le cas contraire, les signatures valides seront marquées "non fiables" si la signature correspond au validateur et "sans correspondance" pour les autres cas. Cela forcera Gitea à être marqué comme le committer sur les commits signés avec le committer réel marqué comme Co-Authored-By: et Co-Committed-By: inclus dans la livraison. La clé par défaut de Gitea doit correspondre à un utilisateur dans la base de données.
settings.wiki_delete=Supprimer les données du Wiki
-settings.wiki_delete_desc=Supprimer les données du wiki d'un dépôt est permanent et ne peut être annulé.
+settings.wiki_delete_desc=Supprimer les données du wiki d'un dépôt est permanent. Cette action est irréversible.
settings.wiki_delete_notices_1=- Ceci supprimera de manière permanente et désactivera le wiki de dépôt pour %s.
settings.confirm_wiki_delete=Supprimer les données du Wiki
settings.wiki_deletion_success=Les données du wiki de ce dépôt ont été effacées.
settings.delete=Supprimer ce dépôt
-settings.delete_desc=Supprimer un dépôt est permanent et ne peut être annulé.
-settings.delete_notices_1=- Cette opération ne peut pas être annulée.
+settings.delete_desc=Supprimer un dépôt est permanent et irréversible.
+settings.delete_notices_1=- Cette opération est irréversible.
settings.delete_notices_2=- Cette opération supprimera définitivement le dépôt %s, y compris le code, les tickets, les commentaires, les données de wiki et les accès des collaborateurs.
settings.delete_notices_fork_1=- Les bifurcations de ce dépôt deviendront indépendants après suppression.
settings.deletion_success=Le dépôt a été supprimé.
@@ -1678,6 +1973,7 @@ settings.webhook.response=Réponse
settings.webhook.headers=Entêtes
settings.webhook.payload=Contenu
settings.webhook.body=Corps
+settings.webhook.replay.description=Rejouer ce déclencheur.
settings.githook_edit_desc=Si un Hook est inactif, un exemple de contenu vous sera proposé. Un contenu laissé vide signifie un Hook inactif.
settings.githook_name=Nom du Hook
settings.githook_content=Contenu du Hook
@@ -1736,18 +2032,39 @@ settings.event_pull_request_review=Demande d'ajout révisée
settings.event_pull_request_review_desc=Demande d'ajout approvée, rejetée ou commentaire de révision.
settings.event_pull_request_sync=Demande d'ajout synchronisée
settings.event_pull_request_sync_desc=Demande d'ajout synchronisée.
+settings.event_package=Paquet
settings.branch_filter=Filtre de branche
+settings.branch_filter_desc=Liste blanche pour les poussées, la création et la suppression de branches, spécifiées par motif de glob. Si vide ou *, les événements pour toutes les branches sont signalés. Voir la documentation github.com/gobwas/glob pour la syntaxe. Exemples: master, {master,release*}.
+settings.authorization_header=En-tête « Authorization »
+settings.authorization_header_desc=Si présent, sera ajouté aux requêtes comme en-tête d’authentification. Exemples : %s.
settings.active=Actif
settings.active_helper=Les informations sur les événements déclenchés seront envoyées à cette url de Webhook.
settings.add_hook_success=Nouveau Webhook ajouté.
-settings.update_webhook=Mettre à jour le Webhook
-settings.update_hook_success=Webhook mis à jour.
+settings.update_webhook=Actualiser le déclencheur
+settings.update_hook_success=Déclencheur Web actualisé.
settings.delete_webhook=Retirer le Webhook
settings.recent_deliveries=Livraisons récentes
settings.hook_type=Type de Hook
settings.slack_token=Jeton
settings.slack_domain=Domaine
settings.slack_channel=Canal
+settings.add_web_hook_desc=Intégrez %s dans votre dépôt.
+settings.web_hook_name_gitea=Gitea
+settings.web_hook_name_gogs=Gogs
+settings.web_hook_name_slack=Slack
+settings.web_hook_name_discord=Discord
+settings.web_hook_name_dingtalk=DingTalk
+settings.web_hook_name_telegram=Telegram
+settings.web_hook_name_matrix=Matrix
+settings.web_hook_name_msteams=Microsoft Teams
+settings.web_hook_name_feishu_or_larksuite=Feishu / Lark Suite
+settings.web_hook_name_feishu=Feishu
+settings.web_hook_name_larksuite=Lark Suite
+settings.web_hook_name_wechatwork=WeCom (Wechat Work)
+settings.web_hook_name_packagist=Packagist
+settings.packagist_username=Nom d'utilisateur Packagist
+settings.packagist_api_token=Jeton API
+settings.packagist_package_url=URL du paquet Packagist
settings.deploy_keys=Clés de déploiement
settings.add_deploy_key=Ajouter une clé de déploiement
settings.deploy_key_desc=Les clefs de déploiement ont un accès en lecture seule au dépôt.
@@ -1758,11 +2075,14 @@ settings.title=Titre
settings.deploy_key_content=Contenu
settings.key_been_used=Une clef de déploiement identique est déjà en cours d'utilisation.
settings.key_name_used=Une clef de déploiement du même nom existe déjà.
+settings.add_key_success=La clé de déploiement "%s" a été ajoutée.
settings.deploy_key_deletion=Supprimer une clef de déploiement
settings.deploy_key_deletion_desc=La suppression d'une clef de déploiement révoque son accès à ce dépôt. Continuer ?
settings.deploy_key_deletion_success=La clé de déploiement a été supprimée.
settings.branches=Branches
settings.protected_branch=Protection de branche
+settings.protected_branch.save_rule=Enregistrer la règle
+settings.protected_branch.delete_rule=Supprimer la règle
settings.protected_branch_can_push=Autoriser la poussée ?
settings.protected_branch_can_push_yes=Vous pouvez pousser
settings.protected_branch_can_push_no=Vous ne pouvez pas pousser
@@ -1797,8 +2117,16 @@ settings.dismiss_stale_approvals=Rejeter les approbations obsolètes
settings.dismiss_stale_approvals_desc=Quand de nouvelles révisions qui changent le contenu de la demande d'ajout sont poussées vers la branche, les anciennes approbations seront rejetées.
settings.require_signed_commits=Exiger des révisions signées
settings.require_signed_commits_desc=Rejeter les pushs vers cette branche s’ils ne sont pas signés ou vérifiables.
+settings.protect_branch_name_pattern=Motif de nom de branche protégé
+settings.protect_protected_file_patterns=Motifs de fichiers protégés (séparés par un point-virgule ';') :
+settings.protect_protected_file_patterns_desc=Les fichiers protégés ne sont pas autorisés à être modifiés directement même si l'utilisateur a les droits pour ajouter, modifier ou supprimer des fichiers dans cette branche. Plusieurs motifs peuvent être séparés en utilisant un point-virgule (';'). Voir la documentation github.com/gobwas/glob pour la syntaxe du motif. Exemples: .drone.yml, /docs/**/*.txt.
+settings.protect_unprotected_file_patterns=Motifs de fichiers non protégés (séparés par un point-virgule ’;’) :
+settings.protect_unprotected_file_patterns_desc=Fichiers non protégés qui sont autorisés à être modifiés directement si l'utilisateur a des droits d’écriture, contournant ainsi les restrictions de poussée. Plusieurs motifs peuvent être séparés en utilisant un point-virgule (';'). Voir la documentation github.com/gobwas/glob pour la syntaxe des motifs. Exemples: .drone.yml, /docs/**/*.txt.
settings.add_protected_branch=Activer la protection
settings.delete_protected_branch=Désactiver la protection
+settings.update_protect_branch_success=La règle de protection de branche "%s" a été mise à jour.
+settings.remove_protected_branch_success=La règle de protection de branche "%s" a été retirée.
+settings.remove_protected_branch_failed=Impossible de retirer la règle de protection de branche "%s".
settings.protected_branch_deletion=Désactiver la protection de branche
settings.protected_branch_deletion_desc=Désactiver la protection de branche permet aux utilisateurs ayant accès en écriture de pousser des modifications sur la branche. Continuer ?
settings.block_rejected_reviews=Bloquer la fusion quand il y a des avis de rejet
@@ -1808,12 +2136,24 @@ settings.block_on_official_review_requests_desc=La fusion ne sera pas possible q
settings.block_outdated_branch=Bloquer la fusion si la demande d'ajout est obsolète
settings.block_outdated_branch_desc=La fusion ne sera pas possible lorsque la branche principale est derrière la branche de base.
settings.default_branch_desc=Sélectionnez une branche par défaut pour les demandes de fusion et les révisions :
+settings.merge_style_desc=Fusionner les styles
settings.default_merge_style_desc=Style de fusion par défaut pour les demandes d'ajouts :
settings.choose_branch=Choisissez une branche…
settings.no_protected_branch=Il n'y a pas de branche protégée.
settings.edit_protected_branch=Éditer
+settings.protected_branch_required_rule_name=Nom de la règle requise
+settings.protected_branch_duplicate_rule_name=Nom de la règle en double
settings.protected_branch_required_approvals_min=Le nombre de revues nécessaires ne peut être négatif.
-settings.tags=Tags
+settings.tags=Étiquettes
+settings.tags.protection=Protection d'étiquette
+settings.tags.protection.pattern=Motif d'étiquette
+settings.tags.protection.allowed=Autorisé
+settings.tags.protection.allowed.users=Utilisateurs autorisés
+settings.tags.protection.allowed.teams=Équipes autorisées
+settings.tags.protection.allowed.noone=Personne
+settings.tags.protection.create=Protéger l'étiquette
+settings.tags.protection.none=Il n'y a pas d'étiquettes protégées.
+settings.tags.protection.pattern.description=Vous pouvez utiliser soit un nom unique, soit un motif de glob ou une expression régulière qui correspondront à plusieurs étiquettes. Pour plus d'informations, veuillez vous reporter au guide sur les étiquettes protégées.
settings.bot_token=Jeton de Bot
settings.chat_id=ID de conversation
settings.matrix.homeserver_url=URL du serveur d'accueil
@@ -1826,6 +2166,7 @@ settings.archive.success=Ce dépôt a été archivé avec succès.
settings.archive.error=Une erreur s'est produite lors de l'archivage du dépôt. Voir le journal pour plus de détails.
settings.archive.error_ismirror=Vous ne pouvez pas archiver un dépôt en miroir.
settings.archive.branchsettings_unavailable=Le paramétrage des branches n'est pas disponible quand le dépôt est archivé.
+settings.archive.tagsettings_unavailable=Le paramétrage des étiquettes n'est pas disponible si le dépôt est archivé.
settings.unarchive.button=Désarchiver ce dépôt
settings.unarchive.header=Désarchiver ce dépôt
settings.unarchive.text=Désarchiver le dépôt lui permettra de recevoir des révisions, ainsi que des nouveaux tickets ou demandes d'ajout.
@@ -1857,6 +2198,12 @@ settings.lfs_pointers.inRepo=Dans le dépôt
settings.lfs_pointers.exists=Existe en magasin
settings.lfs_pointers.accessible=Accessible à l'utilisateur
settings.lfs_pointers.associateAccessible=Associer %d OID accessibles
+settings.rename_branch_failed_exist=Impossible de renommer la branche car la branche %s existe déjà.
+settings.rename_branch_failed_not_exist=Impossible de renommer la branche %s car elle n’existe pas.
+settings.rename_branch_success=La branche %s à été renommée avec succès en %s.
+settings.rename_branch_from=ancien nom de la branche
+settings.rename_branch_to=nouveau nom de la branche
+settings.rename_branch=Renommer la branche
diff.browse_source=Parcourir la source
diff.parent=Parent
@@ -1873,7 +2220,7 @@ diff.whitespace_button=Espace
diff.whitespace_show_everything=Afficher toutes les modifications
diff.whitespace_ignore_all_whitespace=Ignorer les espaces lors de la comparaison des lignes
diff.whitespace_ignore_amount_changes=Ignorer les changements quand ce sont des espaces
-diff.whitespace_ignore_at_eol=Ignorer les changements quand ce sont des espaces à la fin de la ligne
+diff.whitespace_ignore_at_eol=Ignorer les blancs en fin de ligne
diff.stats_desc= %d fichiers modifiés avec %d ajouts et %d suppressions
diff.stats_desc_file=%d modifications: %d ajouts et %d suppressions
diff.bin=BIN
@@ -1886,6 +2233,11 @@ diff.file_image_height=Hauteur
diff.file_byte_size=Taille
diff.file_suppressed=Fichier diff supprimé car celui-ci est trop grand
diff.file_suppressed_line_too_long=Diff de fichier supprimé car une ou plusieurs lignes sont trop longues
+diff.too_many_files=Certains fichiers ne sont pas affichés car ce diff contient trop de modifications
+diff.show_more=Voir plus
+diff.load=Voir la Diff
+diff.generated=générée
+diff.vendored=externe
diff.comment.placeholder=Laisser un commentaire
diff.comment.markdown_info=Mise en page avec markdown est prise en charge.
diff.comment.add_single_comment=Ajouter un commentaire
@@ -1897,12 +2249,17 @@ diff.review.header=Soumettre une révision
diff.review.placeholder=Commentaire de révision
diff.review.comment=Commenter
diff.review.approve=Approuver
+diff.review.self_reject=Les auteurs d’une demande d’ajout ne peuvent pas demander des changements sur leur propre demande d’ajout
diff.review.reject=Demander des changements
+diff.review.self_approve=Les auteurs d’une demande d’ajout ne peuvent pas approuver leur propre demande d’ajout
diff.committed_by=commité par
diff.protected=Protégé
diff.image.side_by_side=Côte à côte
diff.image.swipe=Glisser
diff.image.overlay=Superposition
+diff.has_escaped=Cette ligne contient des caractères Unicode cachés
+diff.show_file_tree=Afficher l’arborescence des fichiers
+diff.hide_file_tree=Masquer l’arborescence des fichiers
releases.desc=Suivi des versions et des téléchargements.
release.releases=Versions
@@ -1914,61 +2271,101 @@ release.prerelease=Pré-publication
release.stable=Stable
release.compare=Comparer
release.edit=Éditer
+release.ahead.commits=%d révisions
release.ahead.target=à %s depuis cette livraison
+tag.ahead.target=à %s depuis cette étiquette
release.source_code=Code source
release.new_subheader=Les versions organisent les versions publiées du projet.
release.edit_subheader=Les versions organisent les versions publiées du projet.
release.tag_name=Nom du tag
release.target=Cible
release.tag_helper=Choisissez une étiquette existante ou créez une nouvelle étiquette.
+release.tag_helper_new=Nouvelle étiquette. Cette étiquette sera créée à partir de la cible.
+release.tag_helper_existing=Étiquette existante.
+release.title_empty=Le titre ne peut pas être vide.
release.prerelease_desc=Marquer comme pré-version
release.prerelease_helper=Marquer cette version comme impropre à la production.
release.cancel=Annuler
release.publish=Publier
release.save_draft=Sauvegarder le Brouillon
-release.edit_release=Modifier la version
+release.edit_release=Actualiser la version
release.delete_release=Supprimer cette version
release.delete_tag=Supprimer l'étiquette
release.deletion=Supprimer cette version
+release.deletion_desc=La suppression d'une version la supprime seulement de Gitea. Les étiquettes Git, le contenu du dépôt et l'historique restent inchangés. Voulez vous continuer ?
release.deletion_success=Cette livraison a été supprimée.
+release.deletion_tag_desc=Ceci supprimera cette étiquette du dépôt. Le contenu du dépôt et l'historique resteront inchangés. Continuer ?
release.deletion_tag_success=L'étiquette a été supprimée.
release.tag_name_already_exist=Une version avec ce nom d'étiquette existe déjà.
release.tag_name_invalid=Le nom de l'étiquette est invalide.
+release.tag_name_protected=Ce nom d'étiquette est protégé.
release.tag_already_exist=Ce nom d'étiquette existe déjà.
release.downloads=Téléchargements
release.download_count=Télécharger: %s
+release.add_tag_msg=Utiliser le titre et le contenu de la version comme message d'étiquette.
+release.add_tag=Créer uniquement une Balise
+release.tags_for=Étiquettes pour %s
branch.name=Nom de la branche
branch.search=Rechercher des branches
+branch.already_exists=Une branche nommée "%s" existe déjà.
branch.delete_head=Supprimer
+branch.delete=`Supprimer la branche "%s"`
branch.delete_html=Supprimer la branche
-branch.delete_desc=Supprimer une branche est permanent. Cela NE PEUVENT être annulées. Continuer ?
+branch.delete_desc=Supprimer une branche est permanent. Cette action est IRRÉVERSIBLE. Continuer ?
+branch.deletion_success=La branche "%s" a été supprimée.
+branch.deletion_failed=Impossible de supprimer la branche "%s".
+branch.delete_branch_has_new_commits=La branche "%s" ne peut être supprimé, car de nouvelles révisions ont été ajoutées après la fusion.
branch.create_branch=Créer la branche %s
+branch.create_from=`de "%s"`
+branch.create_success=La branche "%s" a été créée.
+branch.branch_already_exists=La branche "%s" existe déjà dans ce dépôt.
+branch.branch_name_conflict=Le nom de la branche "%s" entre en conflit avec la branche déjà existante "%s".
+branch.tag_collision=La branche "%s" ne peut être créée car une étiquette avec un nom identique existe déjà dans le dépôt.
branch.deleted_by=Supprimée par %s
+branch.restore_success=La branche "%s" a été restaurée.
+branch.restore_failed=Impossible de restaurer la branche "%s".
+branch.protected_deletion_failed=La branche "%s" est protégé. Elle ne peut pas être supprimée.
+branch.default_deletion_failed=La branche "%s" est la branche par défaut. Elle ne peut pas être supprimée.
+branch.restore=`Restaurer la branche "%s"`
+branch.download=`Télécharger la branche "%s"`
branch.included_desc=Cette branche fait partie de la branche par défaut
branch.included=Incluses
branch.create_new_branch=Créer une branche à partir de la branche :
branch.confirm_create_branch=Créer une branche
branch.create_branch_operation=Créer une branche
branch.new_branch=Créer une nouvelle branche
+branch.new_branch_from=`Créer une nouvelle branche à partir de "%s"`
+branch.renamed=La branche %s à été renommée en %s.
+tag.create_tag=Créer l'étiquette %s
+tag.create_tag_operation=Créer une étiquette
+tag.confirm_create_tag=Créer une étiquette
+tag.create_tag_from=`Créer une nouvelle étiquette à partir de "%s"`
+tag.create_success=L'étiquette "%s" a été créée.
topic.manage_topics=Gérer les sujets
topic.done=Terminé
topic.count_prompt=Vous ne pouvez pas sélectionner plus de 25 sujets
topic.format_prompt=Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
+find_file.go_to_file=Aller au fichier
+find_file.no_matching=Aucun fichier correspondant trouvé
+error.csv.too_large=Impossible de visualiser le fichier car il est trop volumineux.
+error.csv.unexpected=Impossible de visualiser ce fichier car il contient un caractère inattendu ligne %d, colonne %d.
+error.csv.invalid_field_count=Impossible de visualiser ce fichier car il contient un nombre de champs incorrect à la ligne %d.
[org]
org_name_holder=Nom de l'organisation
org_full_name_holder=Nom complet de l'organisation
org_name_helper=Le nom de l'organisation doit être court et mémorable.
create_org=Créer une organisation
-repo_updated=Mis à jour
+repo_updated=Actualisé
members=Membres
teams=Équipes
+code=Code
lower_members=Membres
lower_repositories=dépôts
create_new_team=Nouvelle équipe
@@ -1983,6 +2380,8 @@ team_permission_desc=Autorisation
team_unit_desc=Permettre l’accès aux Sections du dépôt
team_unit_disabled=(Désactivé)
+form.name_reserved=Le nom d'organisation "%s" est réservé.
+form.name_pattern_not_allowed=Le motif "%s" n'est pas autorisé dans un nom d'organisation.
form.create_org_not_allowed=Vous n'êtes pas autorisé à créer une organisation.
settings=Paramètres
@@ -1994,18 +2393,19 @@ settings.permission=Autorisations
settings.repoadminchangeteam=L'administrateur de dépôt peut ajouter et supprimer l'accès aux équipes
settings.visibility=Visibilité
settings.visibility.public=Public
+settings.visibility.limited=Limité (Visible uniquement aux utilisateurs authentifiés)
settings.visibility.limited_shortname=Limité
settings.visibility.private=Privé (Visible uniquement aux membres de l’organisation)
settings.visibility.private_shortname=Privé
-settings.update_settings=Valider
+settings.update_settings=Appliquer les paramètres
settings.update_setting_success=Les paramètres de l'organisation ont été mis à jour.
settings.change_orgname_prompt=NB: changer le nom de l'organisation changera aussi son URL.
settings.change_orgname_redirect_prompt=L'ancien nom d'utilisateur redirigera jusqu'à ce qu'il soit réclamé.
settings.update_avatar_success=L'avatar de l'organisation a été mis à jour.
settings.delete=Supprimer l'organisation
settings.delete_account=Supprimer cette organisation
-settings.delete_prompt=Cette organisation sera supprimée définitivement. Cette opération est IRRÉVERSIBLE !
+settings.delete_prompt=Cette organisation sera supprimée définitivement. Cette action est IRRÉVERSIBLE !
settings.confirm_delete_account=Confirmez la suppression
settings.delete_org_title=Supprimer l'organisation
settings.delete_org_desc=Cette organisation sera supprimée définitivement. Voulez-vous continuer ?
@@ -2033,8 +2433,13 @@ teams.leave=Quitter
teams.leave.detail=Quitter %s?
teams.can_create_org_repo=Créer des dépôts
teams.can_create_org_repo_helper=Les membres peuvent créer de nouveaux dépôts dans l'organisation. Le créateur obtiendra l'accès administrateur au nouveau dépôt.
-teams.read_access=Lue(s)
+teams.none_access=Aucun accès
+teams.none_access_helper=Les membres ne peuvent voir ou faire quoi que ce soit sur cette partie. Sans effet pour les dépôts publics.
+teams.general_access=Accès général
+teams.general_access_helper=Les permissions des membres seront déterminées par la table des permissions ci-dessous.
+teams.read_access=Lecture
teams.read_access_helper=Les membres peuvent voir et cloner les dépôts de l'équipe.
+teams.write_access=Écriture
teams.write_access_helper=Les membres peuvent voir et pousser dans les dépôts de l'équipe.
teams.admin_access=Accès Administrateur
teams.admin_access_helper=Les membres peuvent tirer et pousser des modifications vers les dépôts de l'équipe, et y ajouter des collaborateurs.
@@ -2042,9 +2447,11 @@ teams.no_desc=Aucune description
teams.settings=Paramètres
teams.owners_permission_desc=Les propriétaires ont un accès complet à tous les dépôts et disposent d'un accès administrateur de l'organisation.
teams.members=Membres de L'Équipe
-teams.update_settings=Valider
+teams.update_settings=Appliquer les paramètres
teams.delete_team=Supprimer l'équipe
teams.add_team_member=Ajouter un Membre
+teams.invite_team_member=Inviter à %s
+teams.invite_team_member.list=Invitations en attente
teams.delete_team_title=Supprimer l'équipe
teams.delete_team_desc=Supprimer une équipe supprime l'accès aux dépôts à ses membres. Continuer ?
teams.delete_team_success=L’équipe a été supprimée.
@@ -2058,6 +2465,7 @@ teams.remove_all_repos_title=Supprimer tous les dépôts de l'équipe
teams.remove_all_repos_desc=Ceci supprimera tous les dépôts de l'équipe.
teams.add_all_repos_title=Ajouter tous les dépôts
teams.add_all_repos_desc=Ceci ajoutera tous les dépôts de l'organisation à l'équipe.
+teams.add_nonexistent_repo=Le dépôt que vous essayez d'ajouter n'existe pas, veuillez le créer d'abord.
teams.add_duplicate_users=L’utilisateur est déjà un membre de l’équipe.
teams.repos.none=Aucun dépôt n'est accessible par cette équipe.
teams.members.none=Aucun membre dans cette équipe.
@@ -2068,12 +2476,16 @@ teams.all_repositories_helper=L'équipe a accès à tous les dépôts. Sélectio
teams.all_repositories_read_permission_desc=Cette équipe accorde l'accès en lecture à tous les dépôts : les membres peuvent voir et cloner les dépôts.
teams.all_repositories_write_permission_desc=Cette équipe accorde l'accès en écriture à tous les dépôts : les membres peuvent lire et écrire dans les dépôts.
teams.all_repositories_admin_permission_desc=Cette équipe accorde l'accès administrateur à tous les dépôts : les membres peuvent lire, écrire dans et ajouter des collaborateurs aux dépôts.
+teams.invite.title=Vous avez été invité à rejoindre l’équipe %s dans l’organisation %s.
+teams.invite.by=Invité par %s
+teams.invite.description=Veuillez cliquer sur le bouton ci-dessous pour rejoindre l’équipe.
[admin]
dashboard=Tableau de bord
users=Comptes utilisateurs
organizations=Organisations
repositories=Dépôts
+hooks=Déclencheurs web
authentication=Sources d'authentification
emails=Courriels de l'utilisateur
config=Configuration
@@ -2083,6 +2495,7 @@ first_page=Première
last_page=Dernière
total=Total : %d
+dashboard.new_version_hint=Gitea %s est maintenant disponible, vous utilisez %s. Consultez le blog pour plus de détails.
dashboard.statistic=Résumé
dashboard.operations=Opérations de maintenance
dashboard.system_status=État du système
@@ -2097,6 +2510,7 @@ dashboard.task.cancelled=Tâche: %[1]s a annulé: %[3]s
dashboard.task.error=Erreur dans la tâche: %[1]s: %[3]s
dashboard.task.finished=Tâche: %[1]s démarrée par %[2]s est terminée
dashboard.task.unknown=Tâche inconnue: %[1]s
+dashboard.cron.started=Tâche planifiée démarrée : %[1]s
dashboard.cron.process=Tâche planifiée: %[1]s
dashboard.cron.cancelled=Tâche planifiée : %s annulée : %[3]s
dashboard.cron.error=Erreur dans la tâche planifiée : %s: %[3]s
@@ -2108,7 +2522,7 @@ dashboard.delete_repo_archives.started=Tâche de suppression de toutes les archi
dashboard.delete_missing_repos=Supprimer tous les dépôts dont les fichiers Git sont manquants
dashboard.delete_missing_repos.started=Tâche de suppression de tous les dépôts sans fichiers Git démarrée.
dashboard.delete_generated_repository_avatars=Supprimer les avatars de dépôt générés
-dashboard.update_mirrors=Mettre à jour les miroirs
+dashboard.update_mirrors=Actualiser les miroirs
dashboard.repo_health_check=Vérifier l'état de santé de tous les dépôts
dashboard.check_repo_stats=Voir les statistiques de tous les dépôts
dashboard.archive_cleanup=Supprimer les archives des vieux dépôts
@@ -2120,6 +2534,7 @@ dashboard.resync_all_hooks=Re-synchroniser les déclencheurs Git pre-receive, up
dashboard.reinit_missing_repos=Réinitialiser tous les dépôts Git manquants pour lesquels un enregistrement existe
dashboard.sync_external_users=Synchroniser les données de l’utilisateur externe
dashboard.cleanup_hook_task_table=Nettoyer la table hook_task
+dashboard.cleanup_packages=Nettoyer des paquets expirés
dashboard.server_uptime=Uptime du serveur
dashboard.current_goroutine=Goroutines actuelles
dashboard.current_memory_usage=Utilisation Mémoire actuelle
@@ -2150,6 +2565,12 @@ dashboard.total_gc_pause=Pause GC
dashboard.last_gc_pause=Dernière Pause GC
dashboard.gc_times=Nombres de GC
dashboard.delete_old_actions=Supprimer toutes les anciennes actions de la base de données
+dashboard.delete_old_actions.started=Suppression de toutes les anciennes actions de la base de données démarrée.
+dashboard.update_checker=Vérificateur de mise à jour
+dashboard.delete_old_system_notices=Supprimer toutes les anciennes observations de la base de données
+dashboard.stop_zombie_tasks=Arrêter les tâches zombies
+dashboard.stop_endless_tasks=Arrêter les tâches sans fin
+dashboard.cancel_abandoned_jobs=Annuler les jobs abandonnés
users.user_manage_panel=Gestion du compte utilisateur
users.new_account=Créer un compte
@@ -2164,6 +2585,7 @@ users.created=Créés
users.last_login=Dernière connexion
users.never_login=Jamais connecté
users.send_register_notify=Envoyer une notification d'inscription
+users.new_success=Le compte "%s" a bien été créé.
users.edit=Éditer
users.auth_source=Sources d'authentification
users.local=Locales
@@ -2178,17 +2600,31 @@ users.prohibit_login=Désactiver la connexion
users.is_admin=Est Administrateur
users.is_restricted=Est restreint
users.allow_git_hook=Autoriser la création de Git Hooks
+users.allow_git_hook_tooltip=Les Déclencheurs Git sont exécutés par le même utilisateur que Gitea, qui a des privilèges systèmes élevés. Les utilisateurs ayant ce droit peuvent altérer touts les dépôts, compromettre la base de données applicative, et se promouvoir administrateurs de Gitea.
users.allow_import_local=Autoriser l'importation de dépôts locaux
users.allow_create_organization=Autoriser la création d'organisations
users.update_profile=Modifier un compte
users.delete_account=Supprimer cet utilisateur
+users.cannot_delete_self=Vous ne pouvez pas vous supprimer vous-même
users.still_own_repo=Cet utilisateur possède un ou plusieurs dépôts. Veuillez les supprimer ou les transférer à un autre utilisateur.
users.still_has_org=Cet utilisateur est membre d'une organisation. Veuillez le retirer de toutes les organisations dont il est membre au préalable.
+users.purge=Purger l'utilisateur
+users.purge_help=Éradique l'utilisateur et tous ses dépôts, organisations, commentaires et paquets.
+users.still_own_packages=Cet utilisateur possède encore un ou plusieurs paquets. Supprimez d’abord ces paquets.
users.deletion_success=Le compte a été supprimé.
users.reset_2fa=Réinitialiser l'authentification à deux facteurs
+users.list_status_filter.menu_text=Filtrer
+users.list_status_filter.reset=Réinitialiser
users.list_status_filter.is_active=Actif
+users.list_status_filter.not_active=Inactif
users.list_status_filter.is_admin=Administrateur
+users.list_status_filter.not_admin=Non Administrateur
users.list_status_filter.is_restricted=Restreint
+users.list_status_filter.not_restricted=Non restraint
+users.list_status_filter.is_prohibit_login=Interdit de connexion
+users.list_status_filter.not_prohibit_login=Autorisé à se connecter
+users.list_status_filter.is_2fa_enabled=2FA Activé
+users.list_status_filter.not_2fa_enabled=2FA désactivé
emails.email_manage_panel=Gestion des courriels des utilisateurs
emails.primary=Principale
@@ -2220,13 +2656,22 @@ repos.forks=Bifurcations
repos.issues=Tickets
repos.size=Taille
+packages.package_manage_panel=Gestion des paquets
+packages.total_size=Taille totale : %s
+packages.unreferenced_size=Taille non référencée : %s
packages.owner=Propriétaire
+packages.creator=Créateur
packages.name=Nom
+packages.version=Version
packages.type=Type
packages.repository=Dépôt
packages.size=Taille
packages.published=Publiés
+defaulthooks=Déclencheurs web par défaut
+defaulthooks.desc=Les Déclencheurs Web font des requêtes HTTP POST à un serveur lorsque certains événements Gitea se produisent. Les Déclencheurs déclarés ici seront prédéfinit dans tous nouveaux dépôts. Consultez le guide sur les Déclencheurs Web.
+defaulthooks.add_webhook=Ajouter un déclencheur web par défaut
+defaulthooks.update_webhook=Mettre à jour le déclencheur web par défaut
systemhooks=Rappels système
systemhooks.desc=Les Webhooks font automatiquement des requêtes HTTP POST à un serveur lorsque certains événements Gitea se déclenchent. Les Webhooks définis ici agiront sur tous les dépots du système, donc veuillez prendre en compte les implications en termes de performances que cela peut avoir. Lire la suite dans le guide des Webhooks.
@@ -2256,6 +2701,7 @@ auths.attribute_name=Attribut prénom
auths.attribute_surname=Attribut nom de famille
auths.attribute_mail=Attribut e-mail
auths.attribute_ssh_public_key=Attribut clef SSH publique
+auths.attribute_avatar=Attribut de l'avatar
auths.attributes_in_bind=Aller chercher les attributs dans le contexte de liaison DN
auths.allow_deactivate_all=Permettre à un résultat de recherche vide de désactiver tous les utilisateurs
auths.use_paged_search=Utiliser la recherche paginée
@@ -2264,6 +2710,8 @@ auths.filter=Filtre utilisateur
auths.admin_filter=Filtre administrateur
auths.restricted_filter=Filtre restrictif
auths.restricted_filter_helper=Laisser vide pour ne définir aucun utilisateur comme restreint. Utilisez un astérisque ('*') pour définir tous les utilisateurs qui ne correspondent pas au filtre Admin comme restreint.
+auths.map_group_to_team_removal=Retirer les utilisateurs des équipes synchronisées si l'utilisateur n'appartient pas au groupe LDAP correspondant
+auths.enable_ldap_groups=Activer les groupes LDAP
auths.ms_ad_sa=Rechercher les attributs MS AD
auths.smtp_auth=Type d'authentification SMTP
auths.smtphost=Hôte SMTP
@@ -2272,9 +2720,14 @@ auths.allowed_domains=Domaines autorisés
auths.allowed_domains_helper=Laisser ce champ vide autorise tous les domaines. Separez les domaines multiples avec une virgule (",").
auths.skip_tls_verify=Ne pas vérifier TLS
auths.force_smtps=Forcer SMTPS
+auths.force_smtps_helper=SMTPS est toujours utilisé sur le port 465. Définissez ceci pour forcer SMTPS sur d'autres ports. (STARTTLS sera utilisé sur d'autres ports si cela est supporté par l'hôte.)
auths.helo_hostname=Nom d’hôte HELO
+auths.helo_hostname_helper=Nom d’hôte envoyé avec HELO. Laisser vide pour envoyer le nom d’hôte actuel.
+auths.disable_helo=Désactiver HELO
auths.pam_service_name=Nom du Service PAM
+auths.pam_email_domain=Domaine de messagerie PAM (optionnel)
auths.oauth2_provider=Fournisseur OAuth2
+auths.oauth2_icon_url=URL de l'icône
auths.oauth2_clientID=ID du client (clé)
auths.oauth2_clientSecret=Secret du client
auths.openIdConnectAutoDiscoveryURL=URL de découverte OpenID Connect
@@ -2283,6 +2736,10 @@ auths.oauth2_tokenURL=URL du jeton
auths.oauth2_authURL=URL d'autorisation
auths.oauth2_profileURL=URL du profil
auths.oauth2_emailURL=URL de l'e-mail
+auths.skip_local_two_fa=Ignorer l’authentification à deux facteurs locale
+auths.skip_local_two_fa_helper=Laisser indéfini signifie que les utilisateurs locaux avec l’authentification à deux facteurs activée devront tout de même s’y soumettre pour se connecter
+auths.oauth2_tenant=Locataire
+auths.oauth2_required_claim_name_helper=Définissez ce nom pour restreindre la connexion depuis cette source aux utilisateurs ayant une réclamation avec ce nom
auths.enable_auto_register=Connexion Automatique
auths.sspi_auto_create_users=Créer automatiquement des utilisateurs
auths.sspi_auto_create_users_helper=Autoriser la méthode d'authentification SSPI à créer automatiquement de nouveaux comptes pour les utilisateurs qui se connectent pour la première fois
@@ -2313,6 +2770,7 @@ auths.tip.yandex=`Créez une nouvelle application sur https://oauth.yandex.com/c
auths.tip.mastodon=Entrez une URL d'instance personnalisée pour l'instance mastodon avec laquelle vous voulez vous authentifier (ou utiliser celle par défaut)
auths.edit=Mettre à jour la source d'authentification
auths.activated=Cette source d'authentification est activée
+auths.new_success=L'authentification "%s" a été ajoutée.
auths.update_success=La source d'authentification a été mise à jour.
auths.update=Mettre à jour la source d'authentification
auths.delete=Supprimer la source d'authentification
@@ -2320,7 +2778,10 @@ auths.delete_auth_title=Suppression de la source d'authentification
auths.delete_auth_desc=La suppression d'une source d'authentification empêche les utilisateurs de l'utiliser pour se connecter. Continuer ?
auths.still_in_used=Cette source d'authentification est utilisée par un ou plusieurs utilisateurs, veuillez convertir ou supprimer ces comptes utilisateurs avant toute action.
auths.deletion_success=La source d'authentification a été supprimée.
+auths.login_source_exist=La source d'authentification "%s" existe déjà.
auths.login_source_of_type_exist=Une source d'authentification de ce type existe déjà.
+auths.unable_to_initialize_openid=Impossible d'initialiser le fournisseur OpenID Connect : %s
+auths.invalid_openIdConnectAutoDiscoveryURL=URL de découverte automatique invalide (une URL valide commence par http:// ou https://)
config.server_config=Configuration du serveur
config.app_name=Titre du site
@@ -2328,6 +2789,7 @@ config.app_ver=Version de Gitea
config.app_url=URL de base de Gitea
config.custom_conf=Chemin du fichier de configuration
config.custom_file_root_path=Emplacement personnalisé du fichier racine
+config.domain=Domaine du serveur
config.offline_mode=Mode hors-ligne
config.disable_router_log=Désactiver la Journalisation du Routeur
config.run_user=Exécuter avec l'utilisateur
@@ -2343,6 +2805,7 @@ config.reverse_auth_user=Annuler l'Authentification de l'Utilisateur
config.ssh_config=Configuration SSH
config.ssh_enabled=Activé
config.ssh_start_builtin_server=Utiliser le serveur incorporé
+config.ssh_domain=Domaine du serveur SSH
config.ssh_port=Port
config.ssh_listen_port=Port d'écoute
config.ssh_root_path=Emplacement racine
@@ -2393,16 +2856,23 @@ config.queue_length=Longueur de la file d'attente
config.deliver_timeout=Expiration d'Envoi
config.skip_tls_verify=Passer la vérification TLS
+config.mailer_config=Configuration du service SMTP
config.mailer_enabled=Activé
+config.mailer_enable_helo=Activer HELO
config.mailer_name=Nom
+config.mailer_protocol=Protocole
+config.mailer_smtp_addr=Adresse SMTP
config.mailer_smtp_port=Port SMTP
config.mailer_user=Utilisateur
config.mailer_use_sendmail=Utiliser Sendmail
config.mailer_sendmail_path=Chemin d’accès à Sendmail
config.mailer_sendmail_args=Arguments supplémentaires pour Sendmail
config.mailer_sendmail_timeout=Délai d’attente de Sendmail
+config.mailer_use_dummy=Factice
config.test_email_placeholder=E-mail (ex: test@example.com)
config.send_test_mail=Envoyer un e-mail de test
+config.test_mail_failed=Impossible d'envoyer un email de test à "%s" : %v
+config.test_mail_sent=Un e-mail de test a été envoyé à "%s".
config.oauth_config=Configuration OAuth
config.oauth_enabled=Activé
@@ -2432,6 +2902,8 @@ config.git_disable_diff_highlight=Désactiver la surbrillance syntaxique de Diff
config.git_max_diff_lines=Lignes de Diff Max (pour un seul fichier)
config.git_max_diff_line_characters=Nombre max de caractères de Diff (pour une seule ligne)
config.git_max_diff_files=Nombre max de fichiers de Diff (à afficher)
+config.git_enable_reflogs=Activer les reflogs
+config.git_reflog_expiry_time=Délai d'expiration
config.git_gc_args=Arguments de GC
config.git_migrate_timeout=Délai imparti pour une migration
config.git_mirror_timeout=Délai imparti pour mettre à jour le miroir
@@ -2451,6 +2923,8 @@ config.access_log_template=Modèle
config.xorm_log_mode=Mode de journalisation de XORM
config.xorm_log_sql=Activer la journalisation SQL
+config.get_setting_failed=Impossible d'obtenir le paramètre %s
+config.set_setting_failed=Impossible de définir le paramètre %s
monitor.cron=Tâches récurrentes
monitor.name=Nom
@@ -2459,12 +2933,16 @@ monitor.next=Suivant
monitor.previous=Précédent
monitor.execute_times=Exécutions
monitor.process=Processus en cours d'exécution
+monitor.stacktrace=Piles d'execution
+monitor.goroutines=%d Goroutines
monitor.desc=Description
monitor.start=Heure de démarrage
monitor.execute_time=Heure d'Éxécution
+monitor.last_execution_result=Résultat
monitor.process.cancel=Annuler le processus
monitor.process.cancel_desc=L'annulation d'un processus peut entraîner une perte de données
monitor.process.cancel_notices=Annuler : %s?
+monitor.process.children=Enfant
monitor.queues=Files d'attente
monitor.queue=File d'attente : %s
monitor.queue.name=Nom
@@ -2472,6 +2950,7 @@ monitor.queue.type=Type
monitor.queue.exemplar=Type d'exemple
monitor.queue.numberworkers=Nombre de processus
monitor.queue.maxnumberworkers=Nombre maximale de processus
+monitor.queue.numberinqueue=Position dans la queue
monitor.queue.review=Revoir la configuration
monitor.queue.review_add=Réviser/Ajouter des processus
monitor.queue.configuration=Configuration initiale
@@ -2479,6 +2958,7 @@ monitor.queue.nopool.title=Pas de réservoir de processus
monitor.queue.nopool.desc=Cette file d'attente contient d'autres files d'attente et ne possède pas de réservoir de processus.
monitor.queue.wrapped.desc=Une file d'attente enveloppée, enveloppe une file d'attente de démarrage lente, mettant en mémoire tampon les requêtes dans un canal. Elle n'a pas de pool de processus elle-même.
monitor.queue.persistable-channel.desc=Un canal persistant enveloppe deux files d'attente, une file d'attente de canal qui a son propre pool de processus et une file d'attente de niveau pour les requêtes persistantes des arrêts précédents. Il ne dispose pas d'un pool de travailleurs lui-même.
+monitor.queue.flush=Évacuer la queue
monitor.queue.pool.timeout=Expiration du délai
monitor.queue.pool.addworkers.title=Ajouter un processus
monitor.queue.pool.addworkers.submit=Ajouter un processus
@@ -2491,6 +2971,12 @@ monitor.queue.pool.flush.title=Vider la file d'attente
monitor.queue.pool.flush.desc=Vider va ajouter un processus qui se terminera une fois que la file d'attente est vide, ou qu'elle est en délai d'attente dépassé.
monitor.queue.pool.flush.submit=Ajouter un processus de vidage
monitor.queue.pool.flush.added=Processus de vidage ajouté pour %[1]s
+monitor.queue.pool.pause.title=Suspendre la queue
+monitor.queue.pool.pause.desc=Suspendre une queue l'empêchera de traiter les données
+monitor.queue.pool.pause.submit=Suspendre la queue
+monitor.queue.pool.resume.title=Reprendre la queue
+monitor.queue.pool.resume.desc=Définit cette queue pour reprendre le travail
+monitor.queue.pool.resume.submit=Reprendre la queue
monitor.queue.settings.title=Paramètres du réservoir
monitor.queue.settings.desc=Les pools se développent dynamiquement en réponse au blocage de la file d'attente des processus. Ces changements n'affecteront pas les groupes de processus actuels.
@@ -2520,6 +3006,7 @@ monitor.queue.pool.cancel_desc=Quitter une file d'attente sans aucun groupe de p
notices.system_notice_list=Informations
notices.view_detail_header=Voir les détails de l'information système
+notices.operations=Opérations
notices.select_all=Tout Sélectionner
notices.deselect_all=Tout désélectionner
notices.inverse_selection=Inverser la sélection
@@ -2535,12 +3022,15 @@ notices.delete_success=Les informations systèmes ont été supprimées.
[action]
create_repo=a créé le dépôt %s
rename_repo=a rebaptisé le dépôt de %[1]s vers %[3]s
+create_issue=`ticket ouvert %[3]s#%[2]s`
+close_issue=`ticket fermé %[3]s#%[2]s`
create_pull_request=`a créé la demande d'ajout %[3]s#%[2]s`
close_pull_request=`a fermé la demande d'ajout %[3]s#%[2]s`
reopen_pull_request=`a réouvert la demande d'ajout %[3]s#%[2]s`
comment_pull=`a commenté la demande d'ajout %[3]s#%[2]s`
merge_pull_request=`a fusionné la demande d'ajout %[3]s#%[2]s`
transfer_repo=a transféré le dépôt %s à %s
+push_tag=a poussé l'étiquette %[3]s vers %[4]s
delete_tag=étiquette supprimée %[2]s de %[3]s
delete_branch=branche %[2]s supprimée de %[3]s
compare_branch=Comparer
@@ -2588,6 +3078,9 @@ pin=Epingler la notification
mark_as_read=Marquer comme lu
mark_as_unread=Marquer comme non lue
mark_all_as_read=Tout marquer comme lu
+subscriptions=Abonnements
+watching=Suivi
+no_subscriptions=Pas d'abonnements
[gpg]
default_key=Signé avec la clé par défaut
@@ -2601,35 +3094,226 @@ error.probable_bad_signature=AVERTISSEMENT ! Bien qu'il y ait une clé avec cet
error.probable_bad_default_signature=AVERTISSEMENT ! Bien que la clé par défaut ait cet ID, elle ne vérifie pas cette livraison ! Cette livraison est SUSPECTE.
[units]
+unit=Unité
error.no_unit_allowed_repo=Vous n'êtes pas autorisé à accéder à n'importe quelle section de ce dépôt.
error.unit_not_allowed=Vous n'êtes pas autorisé à accéder à cette section du dépôt.
[packages]
+title=Paquets
+desc=Gérer les paquets du dépôt.
+empty=Il n'y pas de paquet pour le moment.
+empty.documentation=Pour plus d'informations sur le registre de paquets, voir la documentation.
empty.repo=Avez-vous téléchargé un paquet, mais il n'est pas affiché ici? Allez dans les paramètres du paquet et liez le à ce dépôt.
filter.type=Type
+filter.type.all=Tous
+filter.no_result=Votre filtre n'affiche aucun résultat.
+filter.container.tagged=Balisé
+filter.container.untagged=Débalisé
+published_by=%[1]s publié par %[3]s
published_by_in=%[1]s publié par %[3]s en %[5]s
+installation=Installation
+about=À propos de ce paquet
+requirements=Exigences
+dependencies=Dépendances
+keywords=Mots-clés
+details=Détails
+details.author=Auteur
+details.project_site=Site du projet
+details.repository_site=Site du dépôt
+details.documentation_site=Site de documentation
+details.license=Licence
+assets=Ressources
+versions=Versions
+versions.view_all=Voir tout
+dependency.id=ID
+dependency.version=Version
+cargo.install=Pour installer le paquet en utilisant Cargo, exécutez la commande suivante :
+cargo.documentation=Pour plus d'informations sur le registre Cargo, voir la documentation.
+cargo.details.repository_site=Site du dépôt
+cargo.details.documentation_site=Site de documentation
+chef.registry=Configurer ce registre dans votre fichier ~/.chef/config.rb:
+chef.install=Pour installer le paquet, exécutez la commande suivante :
+chef.documentation=Pour plus d'informations sur le registre Chef, voir la documentation.
+composer.registry=Configurez ce registre dans votre fichier ~/.composer/config.json :
+composer.install=Pour installer le paquet en utilisant Composer, exécutez la commande suivante :
+composer.documentation=Pour plus d'informations sur le registre Composer voir la documentation.
+composer.dependencies=Dépendances
+composer.dependencies.development=Dépendances de développement
conan.details.repository=Dépôt
+conan.registry=Configurez ce registre à partir d'un terminal :
+conan.install=Pour installer le paquet en utilisant Conan, exécutez la commande suivante :
+conan.documentation=Pour plus d'informations sur le registre Conan, voir la documentation.
+conda.documentation=Pour plus d'informations sur le registre Conda, voir la documentation.
+conda.details.repository_site=Site du dépôt
+conda.details.documentation_site=Site de documentation
+container.details.type=Type d'image
+container.details.platform=Plateforme
+container.pull=Tirez l'image depuis un terminal :
+container.digest=Empreinte :
+container.documentation=Pour plus d'informations sur le registre Container, voir la documentation.
+container.multi_arch=SE / Arch
+container.layers=Calques d'image
+container.labels=Étiquettes
+container.labels.key=Clé
+container.labels.value=Valeur
+generic.download=Télécharger le paquet depuis un terminal :
+generic.documentation=Pour plus d'informations sur le registre générique, voir la documentation.
+helm.registry=Configurer ce registre à partir d'un terminal :
+helm.install=Pour installer le paquet, exécutez la commande suivante :
+helm.documentation=Pour plus d'informations sur le registre Helm, voir la documentation.
+maven.install2=Exécuter dans un terminal :
+maven.documentation=Pour plus d'informations sur le registre Maven, voir la documentation.
+nuget.registry=Configurer ce registre à partir d'un terminal :
+nuget.documentation=Pour plus d'informations sur le registre NuGet, voir la documentation.
+nuget.dependency.framework=Cadriciel cible
+npm.registry=Configurez ce registre dans le fichier .npmrc de votre projet :
+npm.install=Pour installer le paquet en utilisant npm, exécutez la commande suivante :
+npm.install2=ou ajoutez-le au fichier package.json :
+npm.documentation=Pour plus d'informations sur le registre npm, voir la documentation.
+npm.dependencies=Dépendances
+npm.dependencies.development=Dépendances de développement
+npm.dependencies.peer=Dépendances de pairs
+npm.dependencies.optional=Dépendances optionnelles
+npm.details.tag=Balise
+pub.install=Pour installer le paquet en utilisant Dart, exécutez la commande suivante :
+pub.documentation=Pour davantage d'informations sur le registre Pub, voir la documentation.
+pypi.requires=Nécessite Python
+pypi.install=Pour installer le paquet en utilisant pip, exécutez la commande suivante :
+pypi.documentation=Pour plus d'informations sur le registre PyPI, voir la documentation.
+rubygems.install=Pour installer le paquet en utilisant gem, exécutez la commande suivante :
+rubygems.install2=ou ajoutez-le au Gemfile :
+rubygems.dependencies.runtime=Dépendances d'exécution
+rubygems.dependencies.development=Dépendances de développement
rubygems.required.ruby=Nécessite la version de Ruby
rubygems.required.rubygems=Nécessite la version de RubyGem
+rubygems.documentation=Pour plus d'informations sur le registre RubyGems, consulter la documentation.
+swift.registry=Configurez ce registre à partir d'un terminal :
+swift.install=Ajoutez le paquet dans votre fichier Package.swift:
+swift.install2=et exécutez la commande suivante :
+swift.documentation=Pour plus d'informations sur le registre Swift, voir la documentation.
+vagrant.install=Pour ajouter une machine Vagrant, exécutez la commande suivante :
+vagrant.documentation=Pour plus d'informations sur le registre Vagrant, voir la documentation.
+settings.link=Lier ce paquet à un dépôt
+settings.link.description=Si vous liez un paquet à dépôt, le paquet sera inclus dans sa liste des paquets.
+settings.link.select=Sélectionner un dépôt
+settings.link.button=Actualiser le lien du dépôt
+settings.link.success=Le lien du dépôt a été mis à jour avec succès.
+settings.link.error=Impossible de mettre à jour le lien du dépôt.
+settings.delete=Supprimer le paquet
+settings.delete.description=Supprimer un paquet est permanent et irréversible.
+settings.delete.notice=Vous êtes sur le point de supprimer %s (%s). Cette opération est irréversible, êtes-vous sûr ?
+settings.delete.success=Le paquet a été supprimé.
+settings.delete.error=Impossible de supprimer le paquet.
+owner.settings.cargo.title=Index du Registre Cargo
+owner.settings.cargo.initialize=Initialiser l'index
+owner.settings.cargo.initialize.description=Pour utiliser le registre Cargo, un dépôt git d'index spécial est nécessaire. Ici, vous pouvez le (re)créer avec la configuration requise.
+owner.settings.cargo.initialize.error=Impossible d'initialiser l'index de Cargo : %v
+owner.settings.cargo.initialize.success=L'index Cargo a été créé avec succès.
+owner.settings.cargo.rebuild=Reconstruire l'index
+owner.settings.cargo.rebuild.description=Si l’index est désynchronisé avec les paquets cargo stockés, vous pouvez le reconstruire ici.
+owner.settings.cargo.rebuild.error=Impossible de reconstruire l'index Cargo : %v
+owner.settings.cargo.rebuild.success=L'index Cargo a été reconstruit avec succès.
+owner.settings.cleanuprules.title=Gérer les règles de nettoyage
+owner.settings.cleanuprules.add=Ajouter une règle de nettoyage
+owner.settings.cleanuprules.edit=Modifier la règle de nettoyage
+owner.settings.cleanuprules.none=Aucune règle de nettoyage disponible. Consultez la documentation pour en savoir plus.
+owner.settings.cleanuprules.preview=Aperçu des règles de nettoyage
+owner.settings.cleanuprules.preview.overview=%d paquets sont programmés pour être supprimés.
+owner.settings.cleanuprules.preview.none=La règle de nettoyage ne correspond à aucun paquet.
owner.settings.cleanuprules.enabled=Activé
+owner.settings.cleanuprules.pattern_full_match=Appliquer le modèle au nom complet du paquet
+owner.settings.cleanuprules.keep.title=Les versions qui correspondent à ces règles sont conservées, même si elles correspondent à une règle de suppression ci-dessous.
+owner.settings.cleanuprules.keep.count=Garder le plus récent
+owner.settings.cleanuprules.keep.count.1=1 version par paquet
+owner.settings.cleanuprules.keep.count.n=%d versions par paquet
+owner.settings.cleanuprules.keep.pattern=Garder les versions correspondantes
+owner.settings.cleanuprules.keep.pattern.container=La version la plus récente est toujours conservée pour les paquets Container.
+owner.settings.cleanuprules.remove.title=Les versions qui correspondent à ces règles sont supprimées, sauf si une règle ci-dessus dit de les garder.
+owner.settings.cleanuprules.remove.days=Supprimer les versions antérieures à
+owner.settings.cleanuprules.remove.pattern=Supprimer les versions correspondantes
+owner.settings.cleanuprules.success.update=La règle de nettoyage a été mise à jour.
+owner.settings.cleanuprules.success.delete=La règle de nettoyage a été supprimée.
+owner.settings.chef.title=Dépôt Chef
+owner.settings.chef.keypair=Générer une paire de clés
+owner.settings.chef.keypair.description=Génère une paire de clés utilisée pour s’authentifier auprès du registre Chef. La précédente clé ne pourra plus être utilisée.
[secrets]
+secrets=Secrets
+description=Les secrets seront transmis à certaines actions et ne pourront pas être lus autrement.
+none=Il n'y a pas encore de secrets.
+value=Valeur
name=Nom
+creation=Ajouter un secret
+creation.name_placeholder=Caractères alphanumériques ou tirets bas uniquement, insensibles à la casse, ne peut commencer par GITEA_ ou GITHUB_
+creation.value_placeholder=Entrez n'importe quoi. Les blancs cernant seront taillés.
+creation.success=Le secret "%s" a été ajouté.
+creation.failed=Impossible d'ajouter le secret.
+deletion=Supprimer le secret
+deletion.description=La suppression d'un secret est permanente et irréversible. Continuer ?
+deletion.success=Le secret a été supprimé.
+deletion.failed=Impossible de supprimer le secret.
[actions]
+actions=Actions
+unit.desc=Gérer les actions
+status.unknown=Inconnu
+status.waiting=En attente
+status.running=En cours d'exécution
+status.success=Succès
+status.failure=Échec
+status.cancelled=Annulé
+status.skipped=Ignoré
+status.blocked=Bloqué
+runners=Exécuteurs
+runners.runner_manage_panel=Gestion des exécuteurs
+runners.new=Créer un nouvel exécuteur
+runners.new_notice=Comment démarrer un exécuteur
+runners.status=Statut
+runners.id=ID
runners.name=Nom
runners.owner_type=Type
runners.description=Description
+runners.labels=Étiquettes
+runners.last_online=Dernière fois en ligne
+runners.agent_labels=Étiquettes de l'agent
+runners.custom_labels=Étiquettes personnalisées
+runners.custom_labels_helper=Les libellés personnalisés sont des libellés ajoutés manuellement par un administrateur. Ils sont séparés par des virgules et les blancs les cernant seront taillés.
+runners.runner_title=Exécuteur
+runners.task_list=Tâches récentes sur cet exécuteur
runners.task_list.run=Exécuter
+runners.task_list.status=Statut
runners.task_list.repository=Dépôt
runners.task_list.commit=Commit
+runners.task_list.done_at=Fait à
+runners.edit_runner=Éditer l'Exécuteur
+runners.update_runner=Appliquer les modifications
+runners.update_runner_success=Exécuteur mis à jour avec succès
+runners.update_runner_failed=Impossible d'actualiser l'Exécuteur
+runners.delete_runner=Supprimer cet exécuteur
+runners.delete_runner_success=Exécuteur supprimé avec succès
+runners.delete_runner_failed=Impossible de supprimer l'Exécuteur
+runners.delete_runner_header=Êtes-vous sûr de vouloir supprimer cet exécuteur ?
+runners.none=Aucun exécuteur disponible
+runners.status.unspecified=Inconnu
+runners.status.idle=Inactif
runners.status.active=Actif
+runners.status.offline=Hors-ligne
+runners.version=Version
+runners.reset_registration_token_success=Le jeton d’inscription de l’exécuteur a été réinitialisé avec succès
+runs.open_tab=%d Ouvert
+runs.closed_tab=%d Fermé
runs.commit=Commit
+runs.pushed_by=Poussée par
+runs.no_matching_runner_helper=Aucun exécuteur correspondant : %s
+need_approval_desc=Besoin d'approbation pour exécuter des workflows pour une demande de fusion de bifurcation.
[projects]
+type-1.display_name=Projet personnel
+type-2.display_name=Projet de dépôt
+type-3.display_name=Projet d’organisation
diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini
index 4b8c1d51a3..76c52da678 100644
--- a/options/locale/locale_tr-TR.ini
+++ b/options/locale/locale_tr-TR.ini
@@ -462,6 +462,7 @@ team_invite.text_3=Not: Bu davet %[1] içindi. Bu daveti beklemiyorsanız, e-pos
[modal]
yes=Evet
no=Hayır
+confirm=Onayla
cancel=İptal
modify=Güncelle
@@ -1618,7 +1619,10 @@ pulls.tab_files=Değiştirilen Dosyalar
pulls.reopen_to_merge=Lütfen birleştirme gerçekleştirmek için bu değişiklik isteğini yeniden açın.
pulls.cant_reopen_deleted_branch=Dal silindiğinden bu değişiklik isteği yeniden açılamaz.
pulls.merged=Birleştirildi
+pulls.merged_success=Değişiklik isteği başarıyla birleştirildi ve kapatıldı
+pulls.closed=Değişiklik isteği kapatıldı
pulls.manually_merged=Elle birleştirildi
+pulls.merged_info_text=%s dalı şimdi silinebilir.
pulls.is_closed=Değişiklik isteği kapatıldı.
pulls.title_wip_desc=`Değişiklik isteğinin yanlışlıkla birleştirilmesini önlemek için, başlığı %s ile başlatın`
pulls.cannot_merge_work_in_progress=Bu değişiklik isteği, devam eden bir çalışma olarak işaretlendi.
@@ -3221,6 +3225,7 @@ cargo.details.repository_site=Depo Sitesi
cargo.details.documentation_site=Belge Sitesi
chef.registry=Bu kütüğü ~/.chef/config.rb dosyasında ayarlayın:
chef.install=Paketi kurmak için, aşağıdaki komutu çalıştırın:
+chef.documentation=Chef kütüğü hakkında daha fazla bilgi için, belgeye bakabilirsiniz.
composer.registry=Bu kütüğü ~/.composer/config.json dosyasında ayarlayın:
composer.install=Paketi Composer ile kurmak için, şu komutu çalıştırın:
composer.documentation=Composer kütüğü hakkında daha fazla bilgi için, belgeye bakabilirsiniz.
@@ -3232,6 +3237,7 @@ conan.install=Conan ile paket kurmak için aşağıdaki komutu çalıştırın:
conan.documentation=Conan kütüğü hakkında daha fazla bilgi için, belgeye bakabilirsiniz.
conda.registry=Bu kütüğü .condarc dosyasında bir Conda deposu olarak ayarlayın:
conda.install=Conda ile paket kurmak için aşağıdaki komutu çalıştırın:
+conda.documentation=Conda kütüğü hakkında daha fazla bilgi için, belgeye bakabilirsiniz.
conda.details.repository_site=Depo Sitesi
conda.details.documentation_site=Belge Sitesi
container.details.type=Görüntü Türü
@@ -3282,6 +3288,7 @@ rubygems.documentation=RubyGems kütüğü hakkında daha fazla bilgi için, Package.swift dosyanıza ekleyin:
swift.install2=ve şu komutu çalıştırın:
+swift.documentation=Swift kütüğü hakkında daha fazla bilgi için, belgeye bakabilirsiniz.
vagrant.install=Vagrant paketi eklemek için aşağıdaki komutu çalıştırın:
vagrant.documentation=Vagrant kütüğü hakkında daha fazla bilgi için, belgeye bakabilirsiniz.
settings.link=Bu paketi bir depoya bağlayın
diff --git a/package-lock.json b/package-lock.json
index 7f30fe5ebc..a28539c571 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,13 +17,12 @@
"@github/text-expander-element": "2.3.0",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "19.1.0",
- "@vue/compiler-sfc": "3.3.2",
"@webcomponents/custom-elements": "1.6.0",
"add-asset-webpack-plugin": "2.0.1",
"ansi-to-html": "0.7.2",
- "asciinema-player": "3.3.0",
- "clippie": "3.1.4",
- "css-loader": "6.7.3",
+ "asciinema-player": "3.4.0",
+ "clippie": "4.0.1",
+ "css-loader": "6.7.4",
"dropzone": "6.0.0-beta.2",
"easymde": "2.18.0",
"esbuild-loader": "3.0.1",
@@ -34,7 +33,7 @@
"katex": "0.16.7",
"license-checker-webpack-plugin": "0.2.1",
"mermaid": "10.1.0",
- "mini-css-extract-plugin": "2.7.5",
+ "mini-css-extract-plugin": "2.7.6",
"minimatch": "9.0.0",
"monaco-editor": "0.38.0",
"monaco-editor-webpack-plugin": "7.0.1",
@@ -45,11 +44,11 @@
"tippy.js": "6.3.7",
"tributejs": "5.1.3",
"uint8-to-base64": "0.2.0",
- "vue": "3.3.2",
+ "vue": "3.3.4",
"vue-bar-graph": "2.0.0",
"vue-loader": "17.1.1",
"vue3-calendar-heatmap": "2.0.5",
- "webpack": "5.82.1",
+ "webpack": "5.83.1",
"webpack-cli": "5.1.1",
"workbox-routing": "6.5.4",
"workbox-strategies": "6.5.4",
@@ -62,22 +61,24 @@
"@stoplight/spectral-cli": "6.6.0",
"@vitejs/plugin-vue": "4.2.3",
"eslint": "8.40.0",
+ "eslint-plugin-array-func": "3.1.8",
"eslint-plugin-custom-elements": "0.0.8",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-jquery": "1.5.1",
"eslint-plugin-no-jquery": "2.7.0",
+ "eslint-plugin-no-use-extend-native": "0.5.0",
"eslint-plugin-regexp": "1.15.0",
"eslint-plugin-sonarjs": "0.19.0",
"eslint-plugin-unicorn": "47.0.0",
- "eslint-plugin-vue": "9.12.0",
+ "eslint-plugin-vue": "9.13.0",
"eslint-plugin-wc": "1.5.0",
"jsdom": "22.0.0",
"markdownlint-cli": "0.34.0",
- "stylelint": "15.6.1",
+ "stylelint": "15.6.2",
"stylelint-declaration-strict-value": "1.9.2",
"svgo": "3.0.2",
"updates": "14.1.0",
- "vitest": "0.31.0"
+ "vitest": "0.31.1"
},
"engines": {
"node": ">= 16.0.0"
@@ -168,15 +169,6 @@
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
- "node_modules/@babel/highlight/node_modules/escape-string-regexp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
- "dev": true,
- "engines": {
- "node": ">=0.8.0"
- }
- },
"node_modules/@babel/highlight/node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
@@ -439,9 +431,9 @@
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.17.18",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.18.tgz",
- "integrity": "sha512-EmwL+vUBZJ7mhFCs5lA4ZimpUH3WMAoqvOIYhVQwdIgSpHC8ImHdsRyhHAVxpDYUSm0lWvd63z0XH1IlImS2Qw==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
+ "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==",
"cpu": [
"arm"
],
@@ -454,9 +446,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.17.18",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.18.tgz",
- "integrity": "sha512-/iq0aK0eeHgSC3z55ucMAHO05OIqmQehiGay8eP5l/5l+iEr4EIbh4/MI8xD9qRFjqzgkc0JkX0LculNC9mXBw==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz",
+ "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==",
"cpu": [
"arm64"
],
@@ -469,9 +461,9 @@
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.17.18",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.18.tgz",
- "integrity": "sha512-x+0efYNBF3NPW2Xc5bFOSFW7tTXdAcpfEg2nXmxegm4mJuVeS+i109m/7HMiOQ6M12aVGGFlqJX3RhNdYM2lWg==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz",
+ "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==",
"cpu": [
"x64"
],
@@ -484,9 +476,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.17.18",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.18.tgz",
- "integrity": "sha512-6tY+djEAdF48M1ONWnQb1C+6LiXrKjmqjzPNPWXhu/GzOHTHX2nh8Mo2ZAmBFg0kIodHhciEgUBtcYCAIjGbjQ==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz",
+ "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==",
"cpu": [
"arm64"
],
@@ -499,9 +491,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.17.18",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.18.tgz",
- "integrity": "sha512-Qq84ykvLvya3dO49wVC9FFCNUfSrQJLbxhoQk/TE1r6MjHo3sFF2tlJCwMjhkBVq3/ahUisj7+EpRSz0/+8+9A==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz",
+ "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==",
"cpu": [
"x64"
],
@@ -514,9 +506,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.17.18",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.18.tgz",
- "integrity": "sha512-fw/ZfxfAzuHfaQeMDhbzxp9mc+mHn1Y94VDHFHjGvt2Uxl10mT4CDavHm+/L9KG441t1QdABqkVYwakMUeyLRA==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz",
+ "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==",
"cpu": [
"arm64"
],
@@ -529,9 +521,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.17.18",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.18.tgz",
- "integrity": "sha512-FQFbRtTaEi8ZBi/A6kxOC0V0E9B/97vPdYjY9NdawyLd4Qk5VD5g2pbWN2VR1c0xhzcJm74HWpObPszWC+qTew==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz",
+ "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==",
"cpu": [
"x64"
],
@@ -544,9 +536,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.17.18",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.18.tgz",
- "integrity": "sha512-jW+UCM40LzHcouIaqv3e/oRs0JM76JfhHjCavPxMUti7VAPh8CaGSlS7cmyrdpzSk7A+8f0hiedHqr/LMnfijg==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz",
+ "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==",
"cpu": [
"arm"
],
@@ -559,9 +551,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.17.18",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.18.tgz",
- "integrity": "sha512-R7pZvQZFOY2sxUG8P6A21eq6q+eBv7JPQYIybHVf1XkQYC+lT7nDBdC7wWKTrbvMXKRaGudp/dzZCwL/863mZQ==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz",
+ "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==",
"cpu": [
"arm64"
],
@@ -574,9 +566,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.17.18",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.18.tgz",
- "integrity": "sha512-ygIMc3I7wxgXIxk6j3V00VlABIjq260i967Cp9BNAk5pOOpIXmd1RFQJQX9Io7KRsthDrQYrtcx7QCof4o3ZoQ==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz",
+ "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==",
"cpu": [
"ia32"
],
@@ -589,9 +581,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.17.18",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.18.tgz",
- "integrity": "sha512-bvPG+MyFs5ZlwYclCG1D744oHk1Pv7j8psF5TfYx7otCVmcJsEXgFEhQkbhNW8otDHL1a2KDINW20cfCgnzgMQ==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz",
+ "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==",
"cpu": [
"loong64"
],
@@ -604,9 +596,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.17.18",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.18.tgz",
- "integrity": "sha512-oVqckATOAGuiUOa6wr8TXaVPSa+6IwVJrGidmNZS1cZVx0HqkTMkqFGD2HIx9H1RvOwFeWYdaYbdY6B89KUMxA==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz",
+ "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==",
"cpu": [
"mips64el"
],
@@ -619,9 +611,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.17.18",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.18.tgz",
- "integrity": "sha512-3dLlQO+b/LnQNxgH4l9rqa2/IwRJVN9u/bK63FhOPB4xqiRqlQAU0qDU3JJuf0BmaH0yytTBdoSBHrb2jqc5qQ==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz",
+ "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==",
"cpu": [
"ppc64"
],
@@ -634,9 +626,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.17.18",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.18.tgz",
- "integrity": "sha512-/x7leOyDPjZV3TcsdfrSI107zItVnsX1q2nho7hbbQoKnmoeUWjs+08rKKt4AUXju7+3aRZSsKrJtaRmsdL1xA==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz",
+ "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==",
"cpu": [
"riscv64"
],
@@ -649,9 +641,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.17.18",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.18.tgz",
- "integrity": "sha512-cX0I8Q9xQkL/6F5zWdYmVf5JSQt+ZfZD2bJudZrWD+4mnUvoZ3TDDXtDX2mUaq6upMFv9FlfIh4Gfun0tbGzuw==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz",
+ "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==",
"cpu": [
"s390x"
],
@@ -664,9 +656,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.17.18",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.18.tgz",
- "integrity": "sha512-66RmRsPlYy4jFl0vG80GcNRdirx4nVWAzJmXkevgphP1qf4dsLQCpSKGM3DUQCojwU1hnepI63gNZdrr02wHUA==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz",
+ "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==",
"cpu": [
"x64"
],
@@ -679,9 +671,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.17.18",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.18.tgz",
- "integrity": "sha512-95IRY7mI2yrkLlTLb1gpDxdC5WLC5mZDi+kA9dmM5XAGxCME0F8i4bYH4jZreaJ6lIZ0B8hTrweqG1fUyW7jbg==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz",
+ "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==",
"cpu": [
"x64"
],
@@ -694,9 +686,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.17.18",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.18.tgz",
- "integrity": "sha512-WevVOgcng+8hSZ4Q3BKL3n1xTv5H6Nb53cBrtzzEjDbbnOmucEVcZeGCsCOi9bAOcDYEeBZbD2SJNBxlfP3qiA==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz",
+ "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==",
"cpu": [
"x64"
],
@@ -709,9 +701,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.17.18",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.18.tgz",
- "integrity": "sha512-Rzf4QfQagnwhQXVBS3BYUlxmEbcV7MY+BH5vfDZekU5eYpcffHSyjU8T0xucKVuOcdCsMo+Ur5wmgQJH2GfNrg==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz",
+ "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==",
"cpu": [
"x64"
],
@@ -724,9 +716,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.17.18",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.18.tgz",
- "integrity": "sha512-Kb3Ko/KKaWhjeAm2YoT/cNZaHaD1Yk/pa3FTsmqo9uFh1D1Rfco7BBLIPdDOozrObj2sahslFuAQGvWbgWldAg==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz",
+ "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==",
"cpu": [
"arm64"
],
@@ -739,9 +731,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.17.18",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.18.tgz",
- "integrity": "sha512-0/xUMIdkVHwkvxfbd5+lfG7mHOf2FRrxNbPiKWg9C4fFrB8H0guClmaM3BFiRUYrznVoyxTIyC/Ou2B7QQSwmw==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz",
+ "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==",
"cpu": [
"ia32"
],
@@ -754,9 +746,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.17.18",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.18.tgz",
- "integrity": "sha512-qU25Ma1I3NqTSHJUOKi9sAH1/Mzuvlke0ioMJRthLXKm7JiSKVwFghlGbDLOO2sARECGhja4xYfRAZNPAkooYg==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz",
+ "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==",
"cpu": [
"x64"
],
@@ -784,15 +776,6 @@
"eslint": ">=4.19.1"
}
},
- "node_modules/@eslint-community/eslint-plugin-eslint-comments/node_modules/escape-string-regexp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
- "dev": true,
- "engines": {
- "node": ">=0.8.0"
- }
- },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -1616,9 +1599,9 @@
}
},
"node_modules/@stoplight/spectral-ruleset-bundler": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/@stoplight/spectral-ruleset-bundler/-/spectral-ruleset-bundler-1.5.1.tgz",
- "integrity": "sha512-gvlBXkyxLBNdslN/5HEYvCqMr0dvKQwJbbJCKbOvvRTZhhPVzmLb7yavWXjOnRhYn6IAhGAs7u4yhxn7cX/FtQ==",
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/@stoplight/spectral-ruleset-bundler/-/spectral-ruleset-bundler-1.5.2.tgz",
+ "integrity": "sha512-4QUVUFAU+S7IQ9XeCu+0TQMYxKFpKnkOAfa9unRQ1iPL2cviaipEN6witpbAptdHJD3UUjx4OnwlX8WwmXSq9w==",
"dev": true,
"dependencies": {
"@rollup/plugin-commonjs": "~22.0.2",
@@ -1847,9 +1830,9 @@
"dev": true
},
"node_modules/@types/marked": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.0.tgz",
- "integrity": "sha512-zK4gSFMjgslsv5Lyvr3O1yCjgmnE4pr8jbG8qVn4QglMwtpvPCf4YT2Wma7Nk95OxUUJI8Z+kzdXohbM7mVpGw=="
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.1.tgz",
+ "integrity": "sha512-vSSbKZFbNktrQ15v7o1EaH78EbWV+sPQbPjHG+Cp8CaNcPFUEfjZ0Iml/V0bFDwsTlYe8o6XC5Hfdp91cqPV2g=="
},
"node_modules/@types/minimist": {
"version": "1.2.2",
@@ -1858,9 +1841,9 @@
"dev": true
},
"node_modules/@types/node": {
- "version": "20.1.3",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.3.tgz",
- "integrity": "sha512-NP2yfZpgmf2eDRPmgGq+fjGjSwFgYbihA8/gK+ey23qT9RkxsgNTZvGOEpXgzIGqesTYkElELLgtKoMQTys5vA=="
+ "version": "20.2.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.1.tgz",
+ "integrity": "sha512-DqJociPbZP1lbZ5SQPk4oag6W7AyaGMO6gSfRwq3PWl4PXTwJpRQJhDq4W0kzrg3w6tJ1SwlvGZ5uKFHY13LIg=="
},
"node_modules/@types/normalize-package-data": {
"version": "2.4.1",
@@ -1916,13 +1899,13 @@
}
},
"node_modules/@vitest/expect": {
- "version": "0.31.0",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.31.0.tgz",
- "integrity": "sha512-Jlm8ZTyp6vMY9iz9Ny9a0BHnCG4fqBa8neCF6Pk/c/6vkUk49Ls6UBlgGAU82QnzzoaUs9E/mUhq/eq9uMOv/g==",
+ "version": "0.31.1",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.31.1.tgz",
+ "integrity": "sha512-BV1LyNvhnX+eNYzJxlHIGPWZpwJFZaCcOIzp2CNG0P+bbetenTupk6EO0LANm4QFt0TTit+yqx7Rxd1qxi/SQA==",
"dev": true,
"dependencies": {
- "@vitest/spy": "0.31.0",
- "@vitest/utils": "0.31.0",
+ "@vitest/spy": "0.31.1",
+ "@vitest/utils": "0.31.1",
"chai": "^4.3.7"
},
"funding": {
@@ -1930,12 +1913,12 @@
}
},
"node_modules/@vitest/runner": {
- "version": "0.31.0",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.31.0.tgz",
- "integrity": "sha512-H1OE+Ly7JFeBwnpHTrKyCNm/oZgr+16N4qIlzzqSG/YRQDATBYmJb/KUn3GrZaiQQyL7GwpNHVZxSQd6juLCgw==",
+ "version": "0.31.1",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.31.1.tgz",
+ "integrity": "sha512-imWuc82ngOtxdCUpXwtEzZIuc1KMr+VlQ3Ondph45VhWoQWit5yvG/fFcldbnCi8DUuFi+NmNx5ehMUw/cGLUw==",
"dev": true,
"dependencies": {
- "@vitest/utils": "0.31.0",
+ "@vitest/utils": "0.31.1",
"concordance": "^5.0.4",
"p-limit": "^4.0.0",
"pathe": "^1.1.0"
@@ -1972,9 +1955,9 @@
}
},
"node_modules/@vitest/snapshot": {
- "version": "0.31.0",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.31.0.tgz",
- "integrity": "sha512-5dTXhbHnyUMTMOujZPB0wjFjQ6q5x9c8TvAsSPUNKjp1tVU7i9pbqcKPqntyu2oXtmVxKbuHCqrOd+Ft60r4tg==",
+ "version": "0.31.1",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.31.1.tgz",
+ "integrity": "sha512-L3w5uU9bMe6asrNzJ8WZzN+jUTX4KSgCinEJPXyny0o90fG4FPQMV0OWsq7vrCWfQlAilMjDnOF9nP8lidsJ+g==",
"dev": true,
"dependencies": {
"magic-string": "^0.30.0",
@@ -1998,9 +1981,9 @@
}
},
"node_modules/@vitest/spy": {
- "version": "0.31.0",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.31.0.tgz",
- "integrity": "sha512-IzCEQ85RN26GqjQNkYahgVLLkULOxOm5H/t364LG0JYb3Apg0PsYCHLBYGA006+SVRMWhQvHlBBCyuByAMFmkg==",
+ "version": "0.31.1",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.31.1.tgz",
+ "integrity": "sha512-1cTpt2m9mdo3hRLDyCG2hDQvRrePTDgEJBFQQNz1ydHHZy03EiA6EpFxY+7ODaY7vMRCie+WlFZBZ0/dQWyssQ==",
"dev": true,
"dependencies": {
"tinyspy": "^2.1.0"
@@ -2010,9 +1993,9 @@
}
},
"node_modules/@vitest/utils": {
- "version": "0.31.0",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.31.0.tgz",
- "integrity": "sha512-kahaRyLX7GS1urekRXN2752X4gIgOGVX4Wo8eDUGUkTWlGpXzf5ZS6N9RUUS+Re3XEE8nVGqNyxkSxF5HXlGhQ==",
+ "version": "0.31.1",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.31.1.tgz",
+ "integrity": "sha512-yFyRD5ilwojsZfo3E0BnH72pSVSuLg2356cN1tCEe/0RtDzxTPYwOomIC+eQbot7m6DRy4tPZw+09mB7NkbMmA==",
"dev": true,
"dependencies": {
"concordance": "^5.0.4",
@@ -2024,36 +2007,36 @@
}
},
"node_modules/@vue/compiler-core": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.2.tgz",
- "integrity": "sha512-CKZWo1dzsQYTNTft7whzjL0HsrEpMfiK7pjZ2WFE3bC1NA7caUjWioHSK+49y/LK7Bsm4poJZzAMnvZMQ7OTeg==",
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz",
+ "integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==",
"dependencies": {
"@babel/parser": "^7.21.3",
- "@vue/shared": "3.3.2",
+ "@vue/shared": "3.3.4",
"estree-walker": "^2.0.2",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-dom": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.2.tgz",
- "integrity": "sha512-6gS3auANuKXLw0XH6QxkWqyPYPunziS2xb6VRenM3JY7gVfZcJvkCBHkb5RuNY1FCbBO3lkIi0CdXUCW1c7SXw==",
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz",
+ "integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==",
"dependencies": {
- "@vue/compiler-core": "3.3.2",
- "@vue/shared": "3.3.2"
+ "@vue/compiler-core": "3.3.4",
+ "@vue/shared": "3.3.4"
}
},
"node_modules/@vue/compiler-sfc": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.2.tgz",
- "integrity": "sha512-jG4jQy28H4BqzEKsQqqW65BZgmo3vzdLHTBjF+35RwtDdlFE+Fk1VWJYUnDMMqkFBo6Ye1ltSKVOMPgkzYj7SQ==",
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz",
+ "integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==",
"dependencies": {
"@babel/parser": "^7.20.15",
- "@vue/compiler-core": "3.3.2",
- "@vue/compiler-dom": "3.3.2",
- "@vue/compiler-ssr": "3.3.2",
- "@vue/reactivity-transform": "3.3.2",
- "@vue/shared": "3.3.2",
+ "@vue/compiler-core": "3.3.4",
+ "@vue/compiler-dom": "3.3.4",
+ "@vue/compiler-ssr": "3.3.4",
+ "@vue/reactivity-transform": "3.3.4",
+ "@vue/shared": "3.3.4",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.0",
"postcss": "^8.1.10",
@@ -2072,30 +2055,30 @@
}
},
"node_modules/@vue/compiler-ssr": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.2.tgz",
- "integrity": "sha512-K8OfY5FQtZaSOJHHe8xhEfIfLrefL/Y9frv4k4NsyQL3+0lRKxr9QuJhfdBDjkl7Fhz8CzKh63mULvmOfx3l2w==",
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz",
+ "integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==",
"dependencies": {
- "@vue/compiler-dom": "3.3.2",
- "@vue/shared": "3.3.2"
+ "@vue/compiler-dom": "3.3.4",
+ "@vue/shared": "3.3.4"
}
},
"node_modules/@vue/reactivity": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.2.tgz",
- "integrity": "sha512-yX8C4uTgg2Tdj+512EEMnMKbLveoITl7YdQX35AYgx8vBvQGszKiiCN46g4RY6/deeo/5DLbeUUGxCq1qWMf5g==",
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.4.tgz",
+ "integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==",
"dependencies": {
- "@vue/shared": "3.3.2"
+ "@vue/shared": "3.3.4"
}
},
"node_modules/@vue/reactivity-transform": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.2.tgz",
- "integrity": "sha512-iu2WaQvlJHdnONrsyv4ibIEnSsuKF+aHFngGj/y1lwpHQtalpVhKg9wsKMoiKXS9zPNjG9mNKzJS9vudvjzvyg==",
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz",
+ "integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==",
"dependencies": {
"@babel/parser": "^7.20.15",
- "@vue/compiler-core": "3.3.2",
- "@vue/shared": "3.3.2",
+ "@vue/compiler-core": "3.3.4",
+ "@vue/shared": "3.3.4",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.0"
}
@@ -2112,40 +2095,40 @@
}
},
"node_modules/@vue/runtime-core": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.2.tgz",
- "integrity": "sha512-qSl95qj0BvKfcsO+hICqFEoLhJn6++HtsPxmTkkadFbuhe3uQfJ8HmQwvEr7xbxBd2rcJB6XOJg7nWAn/ymC5A==",
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.4.tgz",
+ "integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==",
"dependencies": {
- "@vue/reactivity": "3.3.2",
- "@vue/shared": "3.3.2"
+ "@vue/reactivity": "3.3.4",
+ "@vue/shared": "3.3.4"
}
},
"node_modules/@vue/runtime-dom": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.2.tgz",
- "integrity": "sha512-+drStsJT+0mtgHdarT7cXZReCcTFfm6ptxMrz0kAW5hms6UNBd8Q1pi4JKlncAhu+Ld/TevsSp7pqAZxBBoGng==",
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz",
+ "integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==",
"dependencies": {
- "@vue/runtime-core": "3.3.2",
- "@vue/shared": "3.3.2",
+ "@vue/runtime-core": "3.3.4",
+ "@vue/shared": "3.3.4",
"csstype": "^3.1.1"
}
},
"node_modules/@vue/server-renderer": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.2.tgz",
- "integrity": "sha512-QCwh6OGwJg6GDLE0fbQhRTR6tnU+XDJ1iCsTYHXBiezCXAhqMygFRij7BiLF4ytvvHcg5kX9joX5R5vP85++wg==",
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz",
+ "integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==",
"dependencies": {
- "@vue/compiler-ssr": "3.3.2",
- "@vue/shared": "3.3.2"
+ "@vue/compiler-ssr": "3.3.4",
+ "@vue/shared": "3.3.4"
},
"peerDependencies": {
- "vue": "3.3.2"
+ "vue": "3.3.4"
}
},
"node_modules/@vue/shared": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.2.tgz",
- "integrity": "sha512-0rFu3h8JbclbnvvKrs7Fe5FNGV9/5X2rPD7KmOzhLSUAiQH5//Hq437Gv0fR5Mev3u/nbtvmLl8XgwCU20/ZfQ=="
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
+ "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ=="
},
"node_modules/@webassemblyjs/ast": {
"version": "1.11.6",
@@ -2625,9 +2608,9 @@
}
},
"node_modules/asciinema-player": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.3.0.tgz",
- "integrity": "sha512-4uyCGe83+5gZ06jgIGyV4vl0TS3egBgW0NXyCpDuDDvmzDGHEG8OICChrTecmTvajgLyq8YQet9nI7SYkVt8vQ==",
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.4.0.tgz",
+ "integrity": "sha512-dX6jt5S3K6daItsVWzyY9mRDK+ivC2QgqCxFkdSiNslo0vY/ZqA4upcTzqIKZqBtxppovOZk44ltg9VnHG9QVg==",
"dependencies": {
"@babel/runtime": "^7.21.0",
"solid-js": "^1.3.0"
@@ -2893,9 +2876,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001486",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001486.tgz",
- "integrity": "sha512-uv7/gXuHi10Whlj0pp5q/tsK/32J2QSqVRKQhs2j8VsDCjgyruAh/eEXHF822VqO9yT6iZKw3nRwZRSPBE9OQg==",
+ "version": "1.0.30001488",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001488.tgz",
+ "integrity": "sha512-NORIQuuL4xGpIy6iCCQGN4iFjlBXtfKWIenlUuyZJumLRIindLb7wXM+GO8erEhb7vXfcnf4BAg2PrSDN5TNLQ==",
"funding": [
{
"type": "opencollective",
@@ -2993,19 +2976,10 @@
"node": ">=4"
}
},
- "node_modules/clean-regexp/node_modules/escape-string-regexp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
- "dev": true,
- "engines": {
- "node": ">=0.8.0"
- }
- },
"node_modules/clippie": {
- "version": "3.1.4",
- "resolved": "https://registry.npmjs.org/clippie/-/clippie-3.1.4.tgz",
- "integrity": "sha512-jrW6sG1zcTEQr5MtCXJzszNmHWV9Fkaco8sAqFeuOApNFP/lRFcUi4JABMmxBJwFZLIvbw2BY3G5E+BjBqZMdQ=="
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/clippie/-/clippie-4.0.1.tgz",
+ "integrity": "sha512-glbCTIRm5gL/vOZ02Dvxa2DXNYrwn8tylBHP3Bz77O1rQ4GMzbzm2Vzv8NYBv1kQYQsHXaUxHBRof8c/f2NXrg=="
},
"node_modules/cliui": {
"version": "7.0.4",
@@ -3222,14 +3196,14 @@
}
},
"node_modules/css-loader": {
- "version": "6.7.3",
- "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.3.tgz",
- "integrity": "sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ==",
+ "version": "6.7.4",
+ "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.4.tgz",
+ "integrity": "sha512-0Y5uHtK5BswfaGJ+jrO+4pPg1msFBc0pwPIE1VqfpmVn6YbDfYfXMj8rfd7nt+4goAhJueO+H/I40VWJfcP1mQ==",
"dependencies": {
"icss-utils": "^5.1.0",
- "postcss": "^8.4.19",
+ "postcss": "^8.4.21",
"postcss-modules-extract-imports": "^3.0.0",
- "postcss-modules-local-by-default": "^4.0.0",
+ "postcss-modules-local-by-default": "^4.0.1",
"postcss-modules-scope": "^3.0.0",
"postcss-modules-values": "^4.0.0",
"postcss-value-parser": "^4.2.0",
@@ -3372,9 +3346,9 @@
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
},
"node_modules/cytoscape": {
- "version": "3.24.0",
- "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.24.0.tgz",
- "integrity": "sha512-W9fJMrAfr/zKFzDCpRR/wn6uoEQ7gfbJmxPK5DadXj69XyAhZYi1QXLOE+UXJfXVXxqGM1o1eeiIrtxrtB43zA==",
+ "version": "3.25.0",
+ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.25.0.tgz",
+ "integrity": "sha512-7MW3Iz57mCUo6JQCho6CmPBCbTlJr7LzyEtIkutG255HLVd4XuBg2I9BkTZLI/e4HoaOB/BiAzXuQybQ95+r9Q==",
"dependencies": {
"heap": "^0.2.6",
"lodash": "^4.17.21"
@@ -4142,9 +4116,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.4.392",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.392.tgz",
- "integrity": "sha512-TXQOMW9tnhIms3jGy/lJctLjICOgyueZFJ1KUtm6DTQ+QpxX3p7ZBwB6syuZ9KBuT5S4XX7bgY1ECPgfxKUdOg=="
+ "version": "1.4.402",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.402.tgz",
+ "integrity": "sha512-gWYvJSkohOiBE6ecVYXkrDgNaUjo47QEKK0kQzmWyhkH+yoYiG44bwuicTGNSIQRG3WDMsWVZJLRnJnLNkbWvA=="
},
"node_modules/elkjs": {
"version": "0.8.2",
@@ -4334,9 +4308,9 @@
}
},
"node_modules/esbuild": {
- "version": "0.17.18",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.18.tgz",
- "integrity": "sha512-z1lix43jBs6UKjcZVKOw2xx69ffE2aG0PygLL5qJ9OS/gy0Ewd1gW/PUQIOIQGXBHWNywSc0floSKoMFF8aK2w==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz",
+ "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==",
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
@@ -4345,28 +4319,28 @@
"node": ">=12"
},
"optionalDependencies": {
- "@esbuild/android-arm": "0.17.18",
- "@esbuild/android-arm64": "0.17.18",
- "@esbuild/android-x64": "0.17.18",
- "@esbuild/darwin-arm64": "0.17.18",
- "@esbuild/darwin-x64": "0.17.18",
- "@esbuild/freebsd-arm64": "0.17.18",
- "@esbuild/freebsd-x64": "0.17.18",
- "@esbuild/linux-arm": "0.17.18",
- "@esbuild/linux-arm64": "0.17.18",
- "@esbuild/linux-ia32": "0.17.18",
- "@esbuild/linux-loong64": "0.17.18",
- "@esbuild/linux-mips64el": "0.17.18",
- "@esbuild/linux-ppc64": "0.17.18",
- "@esbuild/linux-riscv64": "0.17.18",
- "@esbuild/linux-s390x": "0.17.18",
- "@esbuild/linux-x64": "0.17.18",
- "@esbuild/netbsd-x64": "0.17.18",
- "@esbuild/openbsd-x64": "0.17.18",
- "@esbuild/sunos-x64": "0.17.18",
- "@esbuild/win32-arm64": "0.17.18",
- "@esbuild/win32-ia32": "0.17.18",
- "@esbuild/win32-x64": "0.17.18"
+ "@esbuild/android-arm": "0.17.19",
+ "@esbuild/android-arm64": "0.17.19",
+ "@esbuild/android-x64": "0.17.19",
+ "@esbuild/darwin-arm64": "0.17.19",
+ "@esbuild/darwin-x64": "0.17.19",
+ "@esbuild/freebsd-arm64": "0.17.19",
+ "@esbuild/freebsd-x64": "0.17.19",
+ "@esbuild/linux-arm": "0.17.19",
+ "@esbuild/linux-arm64": "0.17.19",
+ "@esbuild/linux-ia32": "0.17.19",
+ "@esbuild/linux-loong64": "0.17.19",
+ "@esbuild/linux-mips64el": "0.17.19",
+ "@esbuild/linux-ppc64": "0.17.19",
+ "@esbuild/linux-riscv64": "0.17.19",
+ "@esbuild/linux-s390x": "0.17.19",
+ "@esbuild/linux-x64": "0.17.19",
+ "@esbuild/netbsd-x64": "0.17.19",
+ "@esbuild/openbsd-x64": "0.17.19",
+ "@esbuild/sunos-x64": "0.17.19",
+ "@esbuild/win32-arm64": "0.17.19",
+ "@esbuild/win32-ia32": "0.17.19",
+ "@esbuild/win32-x64": "0.17.19"
}
},
"node_modules/esbuild-loader": {
@@ -4406,15 +4380,12 @@
}
},
"node_modules/escape-string-regexp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true,
"engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": ">=0.8.0"
}
},
"node_modules/escodegen": {
@@ -4602,6 +4573,18 @@
"ms": "^2.1.1"
}
},
+ "node_modules/eslint-plugin-array-func": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-array-func/-/eslint-plugin-array-func-3.1.8.tgz",
+ "integrity": "sha512-BjnbJvw+knaHgVddIL3q5xYcoqAZoK8wOdT7QF+mkvSAjXdZCdhL0z71Y7oRtgXA8BpN9QLJ2uHgD3I6ymlbOw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6.8.0"
+ },
+ "peerDependencies": {
+ "eslint": ">=3.0.0"
+ }
+ },
"node_modules/eslint-plugin-custom-elements": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/eslint-plugin-custom-elements/-/eslint-plugin-custom-elements-0.0.8.tgz",
@@ -4710,6 +4693,21 @@
"eslint": ">=2.3.0"
}
},
+ "node_modules/eslint-plugin-no-use-extend-native": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-no-use-extend-native/-/eslint-plugin-no-use-extend-native-0.5.0.tgz",
+ "integrity": "sha512-dBNjs8hor8rJgeXLH4HTut5eD3RGWf9JUsadIfuL7UosVQ/dnvOKwxEcRrXrFxrMZ8llUVWT+hOimxJABsAUzQ==",
+ "dev": true,
+ "dependencies": {
+ "is-get-set-prop": "^1.0.0",
+ "is-js-type": "^2.0.0",
+ "is-obj-prop": "^1.0.0",
+ "is-proto-prop": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/eslint-plugin-regexp": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-1.15.0.tgz",
@@ -4778,9 +4776,9 @@
}
},
"node_modules/eslint-plugin-vue": {
- "version": "9.12.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.12.0.tgz",
- "integrity": "sha512-xH8PgpDW2WwmFSmRfs/3iWogef1CJzQqX264I65zz77jDuxF2yLy7+GA2diUM8ZNATuSl1+UehMQkb5YEyau5w==",
+ "version": "9.13.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.13.0.tgz",
+ "integrity": "sha512-aBz9A8WB4wmpnVv0pYUt86cmH9EkcwWzgEwecBxMoRNhQjTL5i4sqadnwShv/hOdr8Hbl8XANGV7dtX9UQIAyA==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.3.0",
@@ -4788,7 +4786,7 @@
"nth-check": "^2.0.1",
"postcss-selector-parser": "^6.0.9",
"semver": "^7.3.5",
- "vue-eslint-parser": "^9.0.1",
+ "vue-eslint-parser": "^9.3.0",
"xml-name-validator": "^4.0.0"
},
"engines": {
@@ -4865,6 +4863,18 @@
"concat-map": "0.0.1"
}
},
+ "node_modules/eslint/node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/eslint/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -4981,9 +4991,9 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-diff": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
- "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
+ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
"dev": true
},
"node_modules/fast-glob": {
@@ -5256,19 +5266,29 @@
}
},
"node_modules/get-intrinsic": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
- "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
+ "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
+ "has-proto": "^1.0.1",
"has-symbols": "^1.0.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-set-props": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/get-set-props/-/get-set-props-0.1.0.tgz",
+ "integrity": "sha512-7oKuKzAGKj0ag+eWZwcGw2fjiZ78tXnXQoBgY0aU7ZOxTu4bB7hSuQSDgtKy978EDH062P5FmD2EWiDpQS9K9Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/get-source": {
"version": "2.0.12",
"resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz",
@@ -5958,9 +5978,9 @@
}
},
"node_modules/is-core-module": {
- "version": "2.12.0",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz",
- "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==",
+ "version": "2.12.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz",
+ "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==",
"dependencies": {
"has": "^1.0.3"
},
@@ -5999,6 +6019,16 @@
"node": ">=8"
}
},
+ "node_modules/is-get-set-prop": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-get-set-prop/-/is-get-set-prop-1.0.0.tgz",
+ "integrity": "sha512-DvAYZ1ZgGUz4lzxKMPYlt08qAUqyG9ckSg2pIjfvcQ7+pkVNUHk8yVLXOnCLe5WKXhLop8oorWFBJHpwWQpszQ==",
+ "dev": true,
+ "dependencies": {
+ "get-set-props": "^0.1.0",
+ "lowercase-keys": "^1.0.0"
+ }
+ },
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -6010,6 +6040,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-js-type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-js-type/-/is-js-type-2.0.0.tgz",
+ "integrity": "sha512-Aj13l47+uyTjlQNHtXBV8Cji3jb037vxwMWCgopRR8h6xocgBGW3qG8qGlIOEmbXQtkKShKuBM9e8AA1OeQ+xw==",
+ "dev": true,
+ "dependencies": {
+ "js-types": "^1.0.0"
+ }
+ },
"node_modules/is-negative-zero": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
@@ -6045,6 +6084,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-obj-prop": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-obj-prop/-/is-obj-prop-1.0.0.tgz",
+ "integrity": "sha512-5Idb61slRlJlsAzi0Wsfwbp+zZY+9LXKUAZpvT/1ySw+NxKLRWfa0Bzj+wXI3fX5O9hiddm5c3DAaRSNP/yl2w==",
+ "dev": true,
+ "dependencies": {
+ "lowercase-keys": "^1.0.0",
+ "obj-props": "^1.0.0"
+ }
+ },
"node_modules/is-path-inside": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
@@ -6078,6 +6127,16 @@
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
"dev": true
},
+ "node_modules/is-proto-prop": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-proto-prop/-/is-proto-prop-2.0.0.tgz",
+ "integrity": "sha512-jl3NbQ/fGLv5Jhan4uX+Ge9ohnemqyblWVVCpAvtTQzNFvV2xhJq+esnkIbYQ9F1nITXoLfDDQLp7LBw/zzncg==",
+ "dev": true,
+ "dependencies": {
+ "lowercase-keys": "^1.0.0",
+ "proto-props": "^2.0.0"
+ }
+ },
"node_modules/is-reference": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
@@ -6295,6 +6354,15 @@
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
+ "node_modules/js-types": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/js-types/-/js-types-1.0.0.tgz",
+ "integrity": "sha512-bfwqBW9cC/Lp7xcRpug7YrXm0IVw+T9e3g4mCYnv0Pjr3zIzU9PCQElYU9oSGAWzXlbdl9X5SAMPejO9sxkeUw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -6701,6 +6769,15 @@
"get-func-name": "^2.0.0"
}
},
+ "node_modules/lowercase-keys": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
+ "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -6805,15 +6882,15 @@
}
},
"node_modules/markdownlint-cli/node_modules/glob": {
- "version": "10.2.3",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.2.3.tgz",
- "integrity": "sha512-Kb4rfmBVE3eQTAimgmeqc2LwSnN0wIOkkUL6HmxEFxNJ4fHghYHVbFba/HcGcRjE6s9KoMNK3rSOwkL4PioZjg==",
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.2.5.tgz",
+ "integrity": "sha512-Gj+dFYPZ5hc5dazjXzB0iHg2jKWJZYMjITXYPBRQ/xc2Buw7H0BINknRTwURJ6IC6MEFpYbLvtgVb3qD+DwyuA==",
"dev": true,
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^2.0.3",
"minimatch": "^9.0.0",
- "minipass": "^5.0.0",
+ "minipass": "^5.0.0 || ^6.0.2",
"path-scurry": "^1.7.0"
},
"bin": {
@@ -7047,9 +7124,9 @@
}
},
"node_modules/mini-css-extract-plugin": {
- "version": "2.7.5",
- "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.5.tgz",
- "integrity": "sha512-9HaR++0mlgom81s95vvNjxkg52n2b5s//3ZTI1EtzFb98awsLSivs2LMsVqnQ3ay0PVhqWcGNyDaTE961FOcjQ==",
+ "version": "2.7.6",
+ "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz",
+ "integrity": "sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==",
"dependencies": {
"schema-utils": "^4.0.0"
},
@@ -7102,12 +7179,12 @@
}
},
"node_modules/minipass": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
- "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-6.0.2.tgz",
+ "integrity": "sha512-MzWSV5nYVT7mVyWCwn2o7JH13w2TBRmmSqSRCKzTw+lmft9X4z+3wjvs06Tzijo5z4W/kahUCDpRXTF+ZrmF/w==",
"dev": true,
"engines": {
- "node": ">=8"
+ "node": ">=16 || 14 >=14.17"
}
},
"node_modules/mlly": {
@@ -7312,6 +7389,15 @@
"integrity": "sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g==",
"dev": true
},
+ "node_modules/obj-props": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/obj-props/-/obj-props-1.4.0.tgz",
+ "integrity": "sha512-p7p/7ltzPDiBs6DqxOrIbtRdwxxVRBj5ROukeNb9RgA+fawhrz5n2hpNz8DDmYR//tviJSj7nUnlppGmONkjiQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -7588,13 +7674,13 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"node_modules/path-scurry": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.8.0.tgz",
- "integrity": "sha512-IjTrKseM404/UAWA8bBbL3Qp6O2wXkanuIE3seCxBH7ctRuvH1QRawy1N3nVDHGkdeZsjOsSe/8AQBL/VQCy2g==",
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.9.2.tgz",
+ "integrity": "sha512-qSDLy2aGFPm8i4rsbHd4MNyTcrzHFsLQykrtbuGRknZZCBBVXSv2tSCDN2Cg6Rt/GFRw8GoW9y9Ecw5rIPG1sg==",
"dev": true,
"dependencies": {
"lru-cache": "^9.1.1",
- "minipass": "^5.0.0"
+ "minipass": "^5.0.0 || ^6.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
@@ -7803,9 +7889,9 @@
}
},
"node_modules/postcss-modules-local-by-default": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz",
- "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==",
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.1.tgz",
+ "integrity": "sha512-Zr/dB+IlXaEqdoslLHhhqecwj73vc3rDmOpsBNBEVk7P2aqAlz+Ijy0fFbU5Ie9PtreDOIgGa9MsLWakVGl+fA==",
"dependencies": {
"icss-utils": "^5.0.0",
"postcss-selector-parser": "^6.0.2",
@@ -7869,9 +7955,9 @@
}
},
"node_modules/postcss-selector-parser": {
- "version": "6.0.12",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.12.tgz",
- "integrity": "sha512-NdxGCAZdRrwVI1sy59+Wzrh+pMMHxapGnpfenDVlMEXoOcvt4pGE0JLK9YY2F5dLxcFYA/YbVQKhcGU+FtSYQg==",
+ "version": "6.0.13",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz",
+ "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -7957,6 +8043,15 @@
"react-is": "^16.13.1"
}
},
+ "node_modules/proto-props": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/proto-props/-/proto-props-2.0.0.tgz",
+ "integrity": "sha512-2yma2tog9VaRZY2mn3Wq51uiSW4NcPYT1cQdBagwyrznrilKSZwIZ0UG3ZPL/mx+axEns0hE35T5ufOYZXEnBQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/proxy-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-5.0.0.tgz",
@@ -8611,9 +8706,9 @@
}
},
"node_modules/semver": {
- "version": "7.5.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz",
- "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==",
+ "version": "7.5.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz",
+ "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -9114,9 +9209,9 @@
"dev": true
},
"node_modules/stylelint": {
- "version": "15.6.1",
- "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.6.1.tgz",
- "integrity": "sha512-d8icFBlVl93Elf3Z5ABQNOCe4nx69is3D/NZhDLAie1eyYnpxfeKe7pCfqzT5W4F8vxHCLSDfV8nKNJzogvV2Q==",
+ "version": "15.6.2",
+ "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.6.2.tgz",
+ "integrity": "sha512-fjQWwcdUye4DU+0oIxNGwawIPC5DvG5kdObY5Sg4rc87untze3gC/5g/ikePqVjrAsBUZjwMN+pZsAYbDO6ArQ==",
"dev": true,
"dependencies": {
"@csstools/css-parser-algorithms": "^2.1.1",
@@ -9333,9 +9428,9 @@
}
},
"node_modules/terser": {
- "version": "5.17.3",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.3.tgz",
- "integrity": "sha512-AudpAZKmZHkG9jueayypz4duuCFJMMNGRMwaPvQKWfxKedh8Z2x3OCoDqIIi1xx5+iwx1u6Au8XQcc9Lke65Yg==",
+ "version": "5.17.4",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.4.tgz",
+ "integrity": "sha512-jcEKZw6UPrgugz/0Tuk/PVyLAPfMBJf5clnGueo45wTweoV8yh7Q7PEkhkJ5uuUbC7zAxEcG3tqNr1bstkQ8nw==",
"dependencies": {
"@jridgewell/source-map": "^0.3.2",
"acorn": "^8.5.0",
@@ -9350,9 +9445,9 @@
}
},
"node_modules/terser-webpack-plugin": {
- "version": "5.3.8",
- "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.8.tgz",
- "integrity": "sha512-WiHL3ElchZMsK27P8uIUh4604IgJyAW47LVXGbEoB21DbQcZ+OuMpGjVYnEUaqcWM6dO8uS2qUbA7LSCWqvsbg==",
+ "version": "5.3.9",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz",
+ "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.17",
"jest-worker": "^27.4.5",
@@ -9590,9 +9685,9 @@
}
},
"node_modules/tslib": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
- "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==",
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.2.tgz",
+ "integrity": "sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==",
"dev": true
},
"node_modules/type-check": {
@@ -9810,9 +9905,9 @@
}
},
"node_modules/vite": {
- "version": "4.3.5",
- "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.5.tgz",
- "integrity": "sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==",
+ "version": "4.3.8",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.8.tgz",
+ "integrity": "sha512-uYB8PwN7hbMrf4j1xzGDk/lqjsZvCDbt/JC5dyfxc19Pg8kRm14LinK/uq+HSLNswZEoKmweGdtpbnxRtrAXiQ==",
"dev": true,
"dependencies": {
"esbuild": "^0.17.5",
@@ -9858,9 +9953,9 @@
}
},
"node_modules/vite-node": {
- "version": "0.31.0",
- "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.31.0.tgz",
- "integrity": "sha512-8x1x1LNuPvE2vIvkSB7c1mApX5oqlgsxzHQesYF7l5n1gKrEmrClIiZuOFbFDQcjLsmcWSwwmrWrcGWm9Fxc/g==",
+ "version": "0.31.1",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.31.1.tgz",
+ "integrity": "sha512-BajE/IsNQ6JyizPzu9zRgHrBwczkAs0erQf/JRpgTIESpKvNj9/Gd0vxX905klLkb0I0SJVCKbdrl5c6FnqYKA==",
"dev": true,
"dependencies": {
"cac": "^6.7.14",
@@ -9881,9 +9976,9 @@
}
},
"node_modules/vite/node_modules/rollup": {
- "version": "3.21.6",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.6.tgz",
- "integrity": "sha512-SXIICxvxQxR3D4dp/3LDHZIJPC8a4anKMHd4E3Jiz2/JnY+2bEjqrOokAauc5ShGVNFHlEFjBXAXlaxkJqIqSg==",
+ "version": "3.22.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.22.0.tgz",
+ "integrity": "sha512-imsigcWor5Y/dC0rz2q0bBt9PabcL3TORry2hAa6O6BuMvY71bqHyfReAz5qyAqiQATD1m70qdntqBfBQjVWpQ==",
"dev": true,
"bin": {
"rollup": "dist/bin/rollup"
@@ -9897,19 +9992,19 @@
}
},
"node_modules/vitest": {
- "version": "0.31.0",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.31.0.tgz",
- "integrity": "sha512-JwWJS9p3GU9GxkG7eBSmr4Q4x4bvVBSswaCFf1PBNHiPx00obfhHRJfgHcnI0ffn+NMlIh9QGvG75FlaIBdKGA==",
+ "version": "0.31.1",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.31.1.tgz",
+ "integrity": "sha512-/dOoOgzoFk/5pTvg1E65WVaobknWREN15+HF+0ucudo3dDG/vCZoXTQrjIfEaWvQXmqScwkRodrTbM/ScMpRcQ==",
"dev": true,
"dependencies": {
- "@types/chai": "^4.3.4",
+ "@types/chai": "^4.3.5",
"@types/chai-subset": "^1.3.3",
"@types/node": "*",
- "@vitest/expect": "0.31.0",
- "@vitest/runner": "0.31.0",
- "@vitest/snapshot": "0.31.0",
- "@vitest/spy": "0.31.0",
- "@vitest/utils": "0.31.0",
+ "@vitest/expect": "0.31.1",
+ "@vitest/runner": "0.31.1",
+ "@vitest/snapshot": "0.31.1",
+ "@vitest/spy": "0.31.1",
+ "@vitest/utils": "0.31.1",
"acorn": "^8.8.2",
"acorn-walk": "^8.2.0",
"cac": "^6.7.14",
@@ -9922,10 +10017,10 @@
"picocolors": "^1.0.0",
"std-env": "^3.3.2",
"strip-literal": "^1.0.1",
- "tinybench": "^2.4.0",
+ "tinybench": "^2.5.0",
"tinypool": "^0.5.0",
"vite": "^3.0.0 || ^4.0.0",
- "vite-node": "0.31.0",
+ "vite-node": "0.31.1",
"why-is-node-running": "^2.2.2"
},
"bin": {
@@ -9987,9 +10082,9 @@
}
},
"node_modules/vm2": {
- "version": "3.9.18",
- "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.18.tgz",
- "integrity": "sha512-iM7PchOElv6Uv6Q+0Hq7dcgDtWWT6SizYqVcvol+1WQc+E9HlgTCnPozbQNSP3yDV9oXHQOEQu530w2q/BCVZg==",
+ "version": "3.9.19",
+ "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.19.tgz",
+ "integrity": "sha512-J637XF0DHDMV57R6JyVsTak7nIL8gy5KH4r1HiwWLf/4GBbb5MKL5y7LpmF4A8E2nR6XmzpmMFQ7V7ppPTmUQg==",
"dev": true,
"dependencies": {
"acorn": "^8.7.0",
@@ -10003,15 +10098,15 @@
}
},
"node_modules/vue": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.2.tgz",
- "integrity": "sha512-98hJcAhyDwZoOo2flAQBSPVYG/o0HA9ivIy2ktHshjE+6/q8IMQ+kvDKQzOZTFPxvnNMcGM+zS2A00xeZMA7tA==",
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz",
+ "integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==",
"dependencies": {
- "@vue/compiler-dom": "3.3.2",
- "@vue/compiler-sfc": "3.3.2",
- "@vue/runtime-dom": "3.3.2",
- "@vue/server-renderer": "3.3.2",
- "@vue/shared": "3.3.2"
+ "@vue/compiler-dom": "3.3.4",
+ "@vue/compiler-sfc": "3.3.4",
+ "@vue/runtime-dom": "3.3.4",
+ "@vue/server-renderer": "3.3.4",
+ "@vue/shared": "3.3.4"
}
},
"node_modules/vue-bar-graph": {
@@ -10024,9 +10119,9 @@
}
},
"node_modules/vue-eslint-parser": {
- "version": "9.2.1",
- "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.2.1.tgz",
- "integrity": "sha512-tPOex4n6jit4E7h68auOEbDMwE58XiP4dylfaVTCOVCouR45g+QFDBjgIdEU52EXJxKyjgh91dLfN2rxUcV0bQ==",
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.0.tgz",
+ "integrity": "sha512-48IxT9d0+wArT1+3wNIy0tascRoywqSUe2E1YalIC1L8jsUGe5aJQItWfRok7DVFGz3UYvzEI7n5wiTXsCMAcQ==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
@@ -10119,9 +10214,9 @@
}
},
"node_modules/webpack": {
- "version": "5.82.1",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.82.1.tgz",
- "integrity": "sha512-C6uiGQJ+Gt4RyHXXYt+v9f+SN1v83x68URwgxNQ98cvH8kxiuywWGP4XeNZ1paOzZ63aY3cTciCEQJNFUljlLw==",
+ "version": "5.83.1",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.83.1.tgz",
+ "integrity": "sha512-TNsG9jDScbNuB+Lb/3+vYolPplCS3bbEaJf+Bj0Gw4DhP3ioAflBb1flcRt9zsWITyvOhM96wMQNRWlSX52DgA==",
"dependencies": {
"@types/eslint-scope": "^3.7.3",
"@types/estree": "^1.0.0",
diff --git a/package.json b/package.json
index 6ec0d755dc..e33e013cf8 100644
--- a/package.json
+++ b/package.json
@@ -17,13 +17,12 @@
"@github/text-expander-element": "2.3.0",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "19.1.0",
- "@vue/compiler-sfc": "3.3.2",
"@webcomponents/custom-elements": "1.6.0",
"add-asset-webpack-plugin": "2.0.1",
"ansi-to-html": "0.7.2",
- "asciinema-player": "3.3.0",
- "clippie": "3.1.4",
- "css-loader": "6.7.3",
+ "asciinema-player": "3.4.0",
+ "clippie": "4.0.1",
+ "css-loader": "6.7.4",
"dropzone": "6.0.0-beta.2",
"easymde": "2.18.0",
"esbuild-loader": "3.0.1",
@@ -34,7 +33,7 @@
"katex": "0.16.7",
"license-checker-webpack-plugin": "0.2.1",
"mermaid": "10.1.0",
- "mini-css-extract-plugin": "2.7.5",
+ "mini-css-extract-plugin": "2.7.6",
"minimatch": "9.0.0",
"monaco-editor": "0.38.0",
"monaco-editor-webpack-plugin": "7.0.1",
@@ -45,11 +44,11 @@
"tippy.js": "6.3.7",
"tributejs": "5.1.3",
"uint8-to-base64": "0.2.0",
- "vue": "3.3.2",
+ "vue": "3.3.4",
"vue-bar-graph": "2.0.0",
"vue-loader": "17.1.1",
"vue3-calendar-heatmap": "2.0.5",
- "webpack": "5.82.1",
+ "webpack": "5.83.1",
"webpack-cli": "5.1.1",
"workbox-routing": "6.5.4",
"workbox-strategies": "6.5.4",
@@ -62,22 +61,24 @@
"@stoplight/spectral-cli": "6.6.0",
"@vitejs/plugin-vue": "4.2.3",
"eslint": "8.40.0",
+ "eslint-plugin-array-func": "3.1.8",
"eslint-plugin-custom-elements": "0.0.8",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-jquery": "1.5.1",
"eslint-plugin-no-jquery": "2.7.0",
+ "eslint-plugin-no-use-extend-native": "0.5.0",
"eslint-plugin-regexp": "1.15.0",
"eslint-plugin-sonarjs": "0.19.0",
"eslint-plugin-unicorn": "47.0.0",
- "eslint-plugin-vue": "9.12.0",
+ "eslint-plugin-vue": "9.13.0",
"eslint-plugin-wc": "1.5.0",
"jsdom": "22.0.0",
"markdownlint-cli": "0.34.0",
- "stylelint": "15.6.1",
+ "stylelint": "15.6.2",
"stylelint-declaration-strict-value": "1.9.2",
"svgo": "3.0.2",
"updates": "14.1.0",
- "vitest": "0.31.0"
+ "vitest": "0.31.1"
},
"browserslist": [
"defaults",
diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go
new file mode 100644
index 0000000000..4b10cd7ad1
--- /dev/null
+++ b/routers/api/actions/artifacts.go
@@ -0,0 +1,598 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+// GitHub Actions Artifacts API Simple Description
+//
+// 1. Upload artifact
+// 1.1. Post upload url
+// Post: /api/actions_pipeline/_apis/pipelines/workflows/{run_id}/artifacts?api-version=6.0-preview
+// Request:
+// {
+// "Type": "actions_storage",
+// "Name": "artifact"
+// }
+// Response:
+// {
+// "fileContainerResourceUrl":"/api/actions_pipeline/_apis/pipelines/workflows/{run_id}/artifacts/{artifact_id}/upload"
+// }
+// it acquires an upload url for artifact upload
+// 1.2. Upload artifact
+// PUT: /api/actions_pipeline/_apis/pipelines/workflows/{run_id}/artifacts/{artifact_id}/upload?itemPath=artifact%2Ffilename
+// it upload chunk with headers:
+// x-tfs-filelength: 1024 // total file length
+// content-length: 1024 // chunk length
+// x-actions-results-md5: md5sum // md5sum of chunk
+// content-range: bytes 0-1023/1024 // chunk range
+// we save all chunks to one storage directory after md5sum check
+// 1.3. Confirm upload
+// PATCH: /api/actions_pipeline/_apis/pipelines/workflows/{run_id}/artifacts/{artifact_id}/upload?itemPath=artifact%2Ffilename
+// it confirm upload and merge all chunks to one file, save this file to storage
+//
+// 2. Download artifact
+// 2.1 list artifacts
+// GET: /api/actions_pipeline/_apis/pipelines/workflows/{run_id}/artifacts?api-version=6.0-preview
+// Response:
+// {
+// "count": 1,
+// "value": [
+// {
+// "name": "artifact",
+// "fileContainerResourceUrl": "/api/actions_pipeline/_apis/pipelines/workflows/{run_id}/artifacts/{artifact_id}/path"
+// }
+// ]
+// }
+// 2.2 download artifact
+// GET: /api/actions_pipeline/_apis/pipelines/workflows/{run_id}/artifacts/{artifact_id}/path?api-version=6.0-preview
+// Response:
+// {
+// "value": [
+// {
+// "contentLocation": "/api/actions_pipeline/_apis/pipelines/workflows/{run_id}/artifacts/{artifact_id}/download",
+// "path": "artifact/filename",
+// "itemType": "file"
+// }
+// ]
+// }
+// 2.3 download artifact file
+// GET: /api/actions_pipeline/_apis/pipelines/workflows/{run_id}/artifacts/{artifact_id}/download?itemPath=artifact%2Ffilename
+// Response:
+// download file
+//
+
+import (
+ "compress/gzip"
+ "crypto/md5"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ "code.gitea.io/gitea/models/actions"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/storage"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/web"
+)
+
+const (
+ artifactXTfsFileLengthHeader = "x-tfs-filelength"
+ artifactXActionsResultsMD5Header = "x-actions-results-md5"
+)
+
+const artifactRouteBase = "/_apis/pipelines/workflows/{run_id}/artifacts"
+
+type artifactContextKeyType struct{}
+
+var artifactContextKey = artifactContextKeyType{}
+
+type ArtifactContext struct {
+ *context.Base
+
+ ActionTask *actions.ActionTask
+}
+
+func init() {
+ web.RegisterHandleTypeProvider[*ArtifactContext](func(req *http.Request) web.ResponseStatusProvider {
+ return req.Context().Value(artifactContextKey).(*ArtifactContext)
+ })
+}
+
+func ArtifactsRoutes(prefix string) *web.Route {
+ m := web.NewRoute()
+ m.Use(ArtifactContexter())
+
+ r := artifactRoutes{
+ prefix: prefix,
+ fs: storage.ActionsArtifacts,
+ }
+
+ m.Group(artifactRouteBase, func() {
+ // retrieve, list and confirm artifacts
+ m.Combo("").Get(r.listArtifacts).Post(r.getUploadArtifactURL).Patch(r.comfirmUploadArtifact)
+ // handle container artifacts list and download
+ m.Group("/{artifact_id}", func() {
+ m.Put("/upload", r.uploadArtifact)
+ m.Get("/path", r.getDownloadArtifactURL)
+ m.Get("/download", r.downloadArtifact)
+ })
+ })
+
+ return m
+}
+
+func ArtifactContexter() func(next http.Handler) http.Handler {
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
+ base, baseCleanUp := context.NewBaseContext(resp, req)
+ defer baseCleanUp()
+
+ ctx := &ArtifactContext{Base: base}
+ ctx.AppendContextValue(artifactContextKey, ctx)
+
+ // action task call server api with Bearer ACTIONS_RUNTIME_TOKEN
+ // we should verify the ACTIONS_RUNTIME_TOKEN
+ authHeader := req.Header.Get("Authorization")
+ if len(authHeader) == 0 || !strings.HasPrefix(authHeader, "Bearer ") {
+ ctx.Error(http.StatusUnauthorized, "Bad authorization header")
+ return
+ }
+
+ authToken := strings.TrimPrefix(authHeader, "Bearer ")
+ task, err := actions.GetRunningTaskByToken(req.Context(), authToken)
+ if err != nil {
+ log.Error("Error runner api getting task: %v", err)
+ ctx.Error(http.StatusInternalServerError, "Error runner api getting task")
+ return
+ }
+
+ if err := task.LoadJob(req.Context()); err != nil {
+ log.Error("Error runner api getting job: %v", err)
+ ctx.Error(http.StatusInternalServerError, "Error runner api getting job")
+ return
+ }
+
+ ctx.ActionTask = task
+ next.ServeHTTP(ctx.Resp, ctx.Req)
+ })
+ }
+}
+
+type artifactRoutes struct {
+ prefix string
+ fs storage.ObjectStorage
+}
+
+func (ar artifactRoutes) buildArtifactURL(runID, artifactID int64, suffix string) string {
+ uploadURL := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(ar.prefix, "/") +
+ strings.ReplaceAll(artifactRouteBase, "{run_id}", strconv.FormatInt(runID, 10)) +
+ "/" + strconv.FormatInt(artifactID, 10) + "/" + suffix
+ return uploadURL
+}
+
+type getUploadArtifactRequest struct {
+ Type string
+ Name string
+}
+
+type getUploadArtifactResponse struct {
+ FileContainerResourceURL string `json:"fileContainerResourceUrl"`
+}
+
+func (ar artifactRoutes) validateRunID(ctx *ArtifactContext) (*actions.ActionTask, int64, bool) {
+ task := ctx.ActionTask
+ runID := ctx.ParamsInt64("run_id")
+ if task.Job.RunID != runID {
+ log.Error("Error runID not match")
+ ctx.Error(http.StatusBadRequest, "run-id does not match")
+ return nil, 0, false
+ }
+ return task, runID, true
+}
+
+// getUploadArtifactURL generates a URL for uploading an artifact
+func (ar artifactRoutes) getUploadArtifactURL(ctx *ArtifactContext) {
+ task, runID, ok := ar.validateRunID(ctx)
+ if !ok {
+ return
+ }
+
+ var req getUploadArtifactRequest
+ if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
+ log.Error("Error decode request body: %v", err)
+ ctx.Error(http.StatusInternalServerError, "Error decode request body")
+ return
+ }
+
+ artifact, err := actions.CreateArtifact(ctx, task, req.Name)
+ if err != nil {
+ log.Error("Error creating artifact: %v", err)
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+ resp := getUploadArtifactResponse{
+ FileContainerResourceURL: ar.buildArtifactURL(runID, artifact.ID, "upload"),
+ }
+ log.Debug("[artifact] get upload url: %s, artifact id: %d", resp.FileContainerResourceURL, artifact.ID)
+ ctx.JSON(http.StatusOK, resp)
+}
+
+// getUploadFileSize returns the size of the file to be uploaded.
+// The raw size is the size of the file as reported by the header X-TFS-FileLength.
+func (ar artifactRoutes) getUploadFileSize(ctx *ArtifactContext) (int64, int64, error) {
+ contentLength := ctx.Req.ContentLength
+ xTfsLength, _ := strconv.ParseInt(ctx.Req.Header.Get(artifactXTfsFileLengthHeader), 10, 64)
+ if xTfsLength > 0 {
+ return xTfsLength, contentLength, nil
+ }
+ return contentLength, contentLength, nil
+}
+
+func (ar artifactRoutes) saveUploadChunk(ctx *ArtifactContext,
+ artifact *actions.ActionArtifact,
+ contentSize, runID int64,
+) (int64, error) {
+ contentRange := ctx.Req.Header.Get("Content-Range")
+ start, end, length := int64(0), int64(0), int64(0)
+ if _, err := fmt.Sscanf(contentRange, "bytes %d-%d/%d", &start, &end, &length); err != nil {
+ return -1, fmt.Errorf("parse content range error: %v", err)
+ }
+
+ storagePath := fmt.Sprintf("tmp%d/%d-%d-%d.chunk", runID, artifact.ID, start, end)
+
+ // use io.TeeReader to avoid reading all body to md5 sum.
+ // it writes data to hasher after reading end
+ // if hash is not matched, delete the read-end result
+ hasher := md5.New()
+ r := io.TeeReader(ctx.Req.Body, hasher)
+
+ // save chunk to storage
+ writtenSize, err := ar.fs.Save(storagePath, r, -1)
+ if err != nil {
+ return -1, fmt.Errorf("save chunk to storage error: %v", err)
+ }
+
+ // check md5
+ reqMd5String := ctx.Req.Header.Get(artifactXActionsResultsMD5Header)
+ chunkMd5String := base64.StdEncoding.EncodeToString(hasher.Sum(nil))
+ log.Debug("[artifact] check chunk md5, sum: %s, header: %s", chunkMd5String, reqMd5String)
+ if reqMd5String != chunkMd5String || writtenSize != contentSize {
+ if err := ar.fs.Delete(storagePath); err != nil {
+ log.Error("Error deleting chunk: %s, %v", storagePath, err)
+ }
+ return -1, fmt.Errorf("md5 not match")
+ }
+
+ log.Debug("[artifact] save chunk %s, size: %d, artifact id: %d, start: %d, end: %d",
+ storagePath, contentSize, artifact.ID, start, end)
+
+ return length, nil
+}
+
+// The rules are from https://github.com/actions/toolkit/blob/main/packages/artifact/src/internal/path-and-artifact-name-validation.ts#L32
+var invalidArtifactNameChars = strings.Join([]string{"\\", "/", "\"", ":", "<", ">", "|", "*", "?", "\r", "\n"}, "")
+
+func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) {
+ _, runID, ok := ar.validateRunID(ctx)
+ if !ok {
+ return
+ }
+ artifactID := ctx.ParamsInt64("artifact_id")
+
+ artifact, err := actions.GetArtifactByID(ctx, artifactID)
+ if errors.Is(err, util.ErrNotExist) {
+ log.Error("Error getting artifact: %v", err)
+ ctx.Error(http.StatusNotFound, err.Error())
+ return
+ } else if err != nil {
+ log.Error("Error getting artifact: %v", err)
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ // itemPath is generated from upload-artifact action
+ // it's formatted as {artifact_name}/{artfict_path_in_runner}
+ itemPath := util.PathJoinRel(ctx.Req.URL.Query().Get("itemPath"))
+ artifactName := strings.Split(itemPath, "/")[0]
+
+ // checkArtifactName checks if the artifact name contains invalid characters.
+ // If the name contains invalid characters, an error is returned.
+ if strings.ContainsAny(artifactName, invalidArtifactNameChars) {
+ log.Error("Error checking artifact name contains invalid character")
+ ctx.Error(http.StatusBadRequest, err.Error())
+ return
+ }
+
+ // get upload file size
+ fileSize, contentLength, err := ar.getUploadFileSize(ctx)
+ if err != nil {
+ log.Error("Error getting upload file size: %v", err)
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ // save chunk
+ chunkAllLength, err := ar.saveUploadChunk(ctx, artifact, contentLength, runID)
+ if err != nil {
+ log.Error("Error saving upload chunk: %v", err)
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ // if artifact name is not set, update it
+ if artifact.ArtifactName == "" {
+ artifact.ArtifactName = artifactName
+ artifact.ArtifactPath = itemPath // path in container
+ artifact.FileSize = fileSize // this is total size of all chunks
+ artifact.FileCompressedSize = chunkAllLength
+ artifact.ContentEncoding = ctx.Req.Header.Get("Content-Encoding")
+ if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil {
+ log.Error("Error updating artifact: %v", err)
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+ }
+
+ ctx.JSON(http.StatusOK, map[string]string{
+ "message": "success",
+ })
+}
+
+// comfirmUploadArtifact comfirm upload artifact.
+// if all chunks are uploaded, merge them to one file.
+func (ar artifactRoutes) comfirmUploadArtifact(ctx *ArtifactContext) {
+ _, runID, ok := ar.validateRunID(ctx)
+ if !ok {
+ return
+ }
+ if err := ar.mergeArtifactChunks(ctx, runID); err != nil {
+ log.Error("Error merging chunks: %v", err)
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ ctx.JSON(http.StatusOK, map[string]string{
+ "message": "success",
+ })
+}
+
+type chunkItem struct {
+ ArtifactID int64
+ Start int64
+ End int64
+ Path string
+}
+
+func (ar artifactRoutes) mergeArtifactChunks(ctx *ArtifactContext, runID int64) error {
+ storageDir := fmt.Sprintf("tmp%d", runID)
+ var chunks []*chunkItem
+ if err := ar.fs.IterateObjects(storageDir, func(path string, obj storage.Object) error {
+ item := chunkItem{Path: path}
+ if _, err := fmt.Sscanf(path, storageDir+"/%d-%d-%d.chunk", &item.ArtifactID, &item.Start, &item.End); err != nil {
+ return fmt.Errorf("parse content range error: %v", err)
+ }
+ chunks = append(chunks, &item)
+ return nil
+ }); err != nil {
+ return err
+ }
+ // group chunks by artifact id
+ chunksMap := make(map[int64][]*chunkItem)
+ for _, c := range chunks {
+ chunksMap[c.ArtifactID] = append(chunksMap[c.ArtifactID], c)
+ }
+
+ for artifactID, cs := range chunksMap {
+ // get artifact to handle merged chunks
+ artifact, err := actions.GetArtifactByID(ctx, cs[0].ArtifactID)
+ if err != nil {
+ return fmt.Errorf("get artifact error: %v", err)
+ }
+
+ sort.Slice(cs, func(i, j int) bool {
+ return cs[i].Start < cs[j].Start
+ })
+
+ allChunks := make([]*chunkItem, 0)
+ startAt := int64(-1)
+ // check if all chunks are uploaded and in order and clean repeated chunks
+ for _, c := range cs {
+ // startAt is -1 means this is the first chunk
+ // previous c.ChunkEnd + 1 == c.ChunkStart means this chunk is in order
+ // StartAt is not -1 and c.ChunkStart is not startAt + 1 means there is a chunk missing
+ if c.Start == (startAt + 1) {
+ allChunks = append(allChunks, c)
+ startAt = c.End
+ }
+ }
+
+ // if the last chunk.End + 1 is not equal to chunk.ChunkLength, means chunks are not uploaded completely
+ if startAt+1 != artifact.FileCompressedSize {
+ log.Debug("[artifact] chunks are not uploaded completely, artifact_id: %d", artifactID)
+ break
+ }
+
+ // use multiReader
+ readers := make([]io.Reader, 0, len(allChunks))
+ closeReaders := func() {
+ for _, r := range readers {
+ _ = r.(io.Closer).Close() // it guarantees to be io.Closer by the following loop's Open function
+ }
+ readers = nil
+ }
+ defer closeReaders()
+
+ for _, c := range allChunks {
+ var readCloser io.ReadCloser
+ if readCloser, err = ar.fs.Open(c.Path); err != nil {
+ return fmt.Errorf("open chunk error: %v, %s", err, c.Path)
+ }
+ readers = append(readers, readCloser)
+ }
+ mergedReader := io.MultiReader(readers...)
+
+ // if chunk is gzip, decompress it
+ if artifact.ContentEncoding == "gzip" {
+ var err error
+ mergedReader, err = gzip.NewReader(mergedReader)
+ if err != nil {
+ return fmt.Errorf("gzip reader error: %v", err)
+ }
+ }
+
+ // save merged file
+ storagePath := fmt.Sprintf("%d/%d/%d.chunk", runID%255, artifactID%255, time.Now().UnixNano())
+ written, err := ar.fs.Save(storagePath, mergedReader, -1)
+ if err != nil {
+ return fmt.Errorf("save merged file error: %v", err)
+ }
+ if written != artifact.FileSize {
+ return fmt.Errorf("merged file size is not equal to chunk length")
+ }
+
+ // save storage path to artifact
+ log.Debug("[artifact] merge chunks to artifact: %d, %s", artifact.ID, storagePath)
+ artifact.StoragePath = storagePath
+ artifact.Status = actions.ArtifactStatusUploadConfirmed
+ if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil {
+ return fmt.Errorf("update artifact error: %v", err)
+ }
+
+ closeReaders() // close before delete
+
+ // drop chunks
+ for _, c := range cs {
+ if err := ar.fs.Delete(c.Path); err != nil {
+ return fmt.Errorf("delete chunk file error: %v", err)
+ }
+ }
+ }
+ return nil
+}
+
+type (
+ listArtifactsResponse struct {
+ Count int64 `json:"count"`
+ Value []listArtifactsResponseItem `json:"value"`
+ }
+ listArtifactsResponseItem struct {
+ Name string `json:"name"`
+ FileContainerResourceURL string `json:"fileContainerResourceUrl"`
+ }
+)
+
+func (ar artifactRoutes) listArtifacts(ctx *ArtifactContext) {
+ _, runID, ok := ar.validateRunID(ctx)
+ if !ok {
+ return
+ }
+
+ artifacts, err := actions.ListArtifactsByRunID(ctx, runID)
+ if err != nil {
+ log.Error("Error getting artifacts: %v", err)
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ artficatsData := make([]listArtifactsResponseItem, 0, len(artifacts))
+ for _, a := range artifacts {
+ artficatsData = append(artficatsData, listArtifactsResponseItem{
+ Name: a.ArtifactName,
+ FileContainerResourceURL: ar.buildArtifactURL(runID, a.ID, "path"),
+ })
+ }
+ respData := listArtifactsResponse{
+ Count: int64(len(artficatsData)),
+ Value: artficatsData,
+ }
+ ctx.JSON(http.StatusOK, respData)
+}
+
+type (
+ downloadArtifactResponse struct {
+ Value []downloadArtifactResponseItem `json:"value"`
+ }
+ downloadArtifactResponseItem struct {
+ Path string `json:"path"`
+ ItemType string `json:"itemType"`
+ ContentLocation string `json:"contentLocation"`
+ }
+)
+
+func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) {
+ _, runID, ok := ar.validateRunID(ctx)
+ if !ok {
+ return
+ }
+
+ artifactID := ctx.ParamsInt64("artifact_id")
+ artifact, err := actions.GetArtifactByID(ctx, artifactID)
+ if errors.Is(err, util.ErrNotExist) {
+ log.Error("Error getting artifact: %v", err)
+ ctx.Error(http.StatusNotFound, err.Error())
+ return
+ } else if err != nil {
+ log.Error("Error getting artifact: %v", err)
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+ downloadURL := ar.buildArtifactURL(runID, artifact.ID, "download")
+ itemPath := util.PathJoinRel(ctx.Req.URL.Query().Get("itemPath"))
+ respData := downloadArtifactResponse{
+ Value: []downloadArtifactResponseItem{{
+ Path: util.PathJoinRel(itemPath, artifact.ArtifactPath),
+ ItemType: "file",
+ ContentLocation: downloadURL,
+ }},
+ }
+ ctx.JSON(http.StatusOK, respData)
+}
+
+func (ar artifactRoutes) downloadArtifact(ctx *ArtifactContext) {
+ _, runID, ok := ar.validateRunID(ctx)
+ if !ok {
+ return
+ }
+
+ artifactID := ctx.ParamsInt64("artifact_id")
+ artifact, err := actions.GetArtifactByID(ctx, artifactID)
+ if errors.Is(err, util.ErrNotExist) {
+ log.Error("Error getting artifact: %v", err)
+ ctx.Error(http.StatusNotFound, err.Error())
+ return
+ } else if err != nil {
+ log.Error("Error getting artifact: %v", err)
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+ if artifact.RunID != runID {
+ log.Error("Error dismatch runID and artifactID, task: %v, artifact: %v", runID, artifactID)
+ ctx.Error(http.StatusBadRequest, err.Error())
+ return
+ }
+
+ fd, err := ar.fs.Open(artifact.StoragePath)
+ if err != nil {
+ log.Error("Error opening file: %v", err)
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+ defer fd.Close()
+
+ if strings.HasSuffix(artifact.ArtifactPath, ".gz") {
+ ctx.Resp.Header().Set("Content-Encoding", "gzip")
+ }
+ ctx.ServeContent(fd, &context.ServeHeaderOptions{
+ Filename: artifact.ArtifactName,
+ LastModified: artifact.CreatedUnix.AsLocalTime(),
+ })
+}
diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go
index aaceb8a92b..e715997e82 100644
--- a/routers/api/packages/api.go
+++ b/routers/api/packages/api.go
@@ -98,7 +98,7 @@ func verifyAuth(r *web.Route, authMethods []auth.Method) {
func CommonRoutes(ctx gocontext.Context) *web.Route {
r := web.NewRoute()
- r.Use(context.PackageContexter(ctx))
+ r.Use(context.PackageContexter())
verifyAuth(r, []auth.Method{
&auth.OAuth2{},
@@ -574,7 +574,7 @@ func CommonRoutes(ctx gocontext.Context) *web.Route {
func ContainerRoutes(ctx gocontext.Context) *web.Route {
r := web.NewRoute()
- r.Use(context.PackageContexter(ctx))
+ r.Use(context.PackageContexter())
verifyAuth(r, []auth.Method{
&auth.Basic{},
diff --git a/routers/api/v1/admin/hooks.go b/routers/api/v1/admin/hooks.go
index 6a08aa5125..8a095a7def 100644
--- a/routers/api/v1/admin/hooks.go
+++ b/routers/api/v1/admin/hooks.go
@@ -4,6 +4,7 @@
package admin
import (
+ "errors"
"net/http"
"code.gitea.io/gitea/models/webhook"
@@ -74,7 +75,11 @@ func GetHook(ctx *context.APIContext) {
hookID := ctx.ParamsInt64(":id")
hook, err := webhook.GetSystemOrDefaultWebhook(ctx, hookID)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "GetSystemOrDefaultWebhook", err)
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.NotFound()
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetSystemOrDefaultWebhook", err)
+ }
return
}
h, err := webhook_service.ToHook("/admin/", hook)
@@ -160,7 +165,7 @@ func DeleteHook(ctx *context.APIContext) {
hookID := ctx.ParamsInt64(":id")
if err := webhook.DeleteDefaultSystemWebhook(ctx, hookID); err != nil {
- if webhook.IsErrWebhookNotExist(err) {
+ if errors.Is(err, util.ErrNotExist) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "DeleteDefaultSystemWebhook", err)
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index a67a5420ac..f1e1cf946a 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -149,7 +149,7 @@ func repoAssignment() func(ctx *context.APIContext) {
if err != nil {
if user_model.IsErrUserNotExist(err) {
if redirectUserID, err := user_model.LookupUserRedirect(userName); err == nil {
- context.RedirectToUser(ctx.Context, userName, redirectUserID)
+ context.RedirectToUser(ctx.Base, userName, redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) {
ctx.NotFound("GetUserByName", err)
} else {
@@ -170,7 +170,7 @@ func repoAssignment() func(ctx *context.APIContext) {
if repo_model.IsErrRepoNotExist(err) {
redirectRepoID, err := repo_model.LookupRedirect(owner.ID, repoName)
if err == nil {
- context.RedirectToRepo(ctx.Context, redirectRepoID)
+ context.RedirectToRepo(ctx.Base, redirectRepoID)
} else if repo_model.IsErrRedirectNotExist(err) {
ctx.NotFound()
} else {
@@ -274,7 +274,7 @@ func reqToken(requiredScope auth_model.AccessTokenScope) func(ctx *context.APICo
ctx.Error(http.StatusForbidden, "reqToken", "token does not have required scope: "+requiredScope)
return
}
- if ctx.Context.IsBasicAuth {
+ if ctx.IsBasicAuth {
ctx.CheckForOTP()
return
}
@@ -295,7 +295,7 @@ func reqExploreSignIn() func(ctx *context.APIContext) {
func reqBasicAuth() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
- if !ctx.Context.IsBasicAuth {
+ if !ctx.IsBasicAuth {
ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth required")
return
}
@@ -375,7 +375,7 @@ func reqAnyRepoReader() func(ctx *context.APIContext) {
// reqOrgOwnership user should be an organization owner, or a site admin
func reqOrgOwnership() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
- if ctx.Context.IsUserSiteAdmin() {
+ if ctx.IsUserSiteAdmin() {
return
}
@@ -407,7 +407,7 @@ func reqOrgOwnership() func(ctx *context.APIContext) {
// reqTeamMembership user should be an team member, or a site admin
func reqTeamMembership() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
- if ctx.Context.IsUserSiteAdmin() {
+ if ctx.IsUserSiteAdmin() {
return
}
if ctx.Org.Team == nil {
@@ -444,7 +444,7 @@ func reqTeamMembership() func(ctx *context.APIContext) {
// reqOrgMembership user should be an organization member, or a site admin
func reqOrgMembership() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
- if ctx.Context.IsUserSiteAdmin() {
+ if ctx.IsUserSiteAdmin() {
return
}
@@ -512,7 +512,7 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) {
if organization.IsErrOrgNotExist(err) {
redirectUserID, err := user_model.LookupUserRedirect(ctx.Params(":org"))
if err == nil {
- context.RedirectToUser(ctx.Context, ctx.Params(":org"), redirectUserID)
+ context.RedirectToUser(ctx.Base, ctx.Params(":org"), redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) {
ctx.NotFound("GetOrgByName", err)
} else {
diff --git a/routers/api/v1/misc/markup.go b/routers/api/v1/misc/markup.go
index 93d5754444..7b24b353b6 100644
--- a/routers/api/v1/misc/markup.go
+++ b/routers/api/v1/misc/markup.go
@@ -41,7 +41,7 @@ func Markup(ctx *context.APIContext) {
return
}
- common.RenderMarkup(ctx.Context, form.Mode, form.Text, form.Context, form.FilePath, form.Wiki)
+ common.RenderMarkup(ctx.Base, ctx.Repo, form.Mode, form.Text, form.Context, form.FilePath, form.Wiki)
}
// Markdown render markdown document to HTML
@@ -76,7 +76,7 @@ func Markdown(ctx *context.APIContext) {
mode = form.Mode
}
- common.RenderMarkup(ctx.Context, mode, form.Text, form.Context, "", form.Wiki)
+ common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, "", form.Wiki)
}
// MarkdownRaw render raw markdown HTML
diff --git a/routers/api/v1/misc/markup_test.go b/routers/api/v1/misc/markup_test.go
index 68776613b2..fdf540fd65 100644
--- a/routers/api/v1/misc/markup_test.go
+++ b/routers/api/v1/misc/markup_test.go
@@ -16,7 +16,6 @@ import (
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
@@ -30,26 +29,16 @@ const (
AppSubURL = AppURL + Repo + "/"
)
-func createContext(req *http.Request) (*context.Context, *httptest.ResponseRecorder) {
- rnd := templates.HTMLRenderer()
+func createAPIContext(req *http.Request) (*context.APIContext, *httptest.ResponseRecorder) {
resp := httptest.NewRecorder()
- c := &context.Context{
- Req: req,
- Resp: context.NewResponse(resp),
- Render: rnd,
- Data: make(middleware.ContextData),
- }
- defer c.Close()
+ base, baseCleanUp := context.NewBaseContext(resp, req)
+ base.Data = middleware.ContextData{}
+ c := &context.APIContext{Base: base}
+ _ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later
return c, resp
}
-func wrap(ctx *context.Context) *context.APIContext {
- return &context.APIContext{
- Context: ctx,
- }
-}
-
func testRenderMarkup(t *testing.T, mode, filePath, text, responseBody string, responseCode int) {
setting.AppURL = AppURL
@@ -65,8 +54,7 @@ func testRenderMarkup(t *testing.T, mode, filePath, text, responseBody string, r
Method: "POST",
URL: requrl,
}
- m, resp := createContext(req)
- ctx := wrap(m)
+ ctx, resp := createAPIContext(req)
options.Text = text
web.SetForm(ctx, &options)
@@ -90,8 +78,7 @@ func testRenderMarkdown(t *testing.T, mode, text, responseBody string, responseC
Method: "POST",
URL: requrl,
}
- m, resp := createContext(req)
- ctx := wrap(m)
+ ctx, resp := createAPIContext(req)
options.Text = text
web.SetForm(ctx, &options)
@@ -211,8 +198,7 @@ func TestAPI_RenderSimple(t *testing.T) {
Method: "POST",
URL: requrl,
}
- m, resp := createContext(req)
- ctx := wrap(m)
+ ctx, resp := createAPIContext(req)
for i := 0; i < len(simpleCases); i += 2 {
options.Text = simpleCases[i]
@@ -231,8 +217,7 @@ func TestAPI_RenderRaw(t *testing.T) {
Method: "POST",
URL: requrl,
}
- m, resp := createContext(req)
- ctx := wrap(m)
+ ctx, resp := createAPIContext(req)
for i := 0; i < len(simpleCases); i += 2 {
ctx.Req.Body = io.NopCloser(strings.NewReader(simpleCases[i]))
diff --git a/routers/api/v1/notify/notifications.go b/routers/api/v1/notify/notifications.go
index 3b6a9bfdc2..b22ea8a771 100644
--- a/routers/api/v1/notify/notifications.go
+++ b/routers/api/v1/notify/notifications.go
@@ -25,7 +25,7 @@ func NewAvailable(ctx *context.APIContext) {
}
func getFindNotificationOptions(ctx *context.APIContext) *activities_model.FindNotificationOptions {
- before, since, err := context.GetQueryBeforeSince(ctx.Context)
+ before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return nil
diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go
index eb63dda590..786407827c 100644
--- a/routers/api/v1/repo/file.go
+++ b/routers/api/v1/repo/file.go
@@ -80,7 +80,7 @@ func GetRawFile(ctx *context.APIContext) {
ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry)))
- if err := common.ServeBlob(ctx.Context, blob, lastModified); err != nil {
+ if err := common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified); err != nil {
ctx.Error(http.StatusInternalServerError, "ServeBlob", err)
}
}
@@ -137,7 +137,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
}
// OK not cached - serve!
- if err := common.ServeBlob(ctx.Context, blob, lastModified); err != nil {
+ if err := common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified); err != nil {
ctx.ServerError("ServeBlob", err)
}
return
@@ -159,7 +159,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
}
if err := dataRc.Close(); err != nil {
- log.Error("Error whilst closing blob %s reader in %-v. Error: %v", blob.ID, ctx.Context.Repo.Repository, err)
+ log.Error("Error whilst closing blob %s reader in %-v. Error: %v", blob.ID, ctx.Repo.Repository, err)
}
// Check if the blob represents a pointer
@@ -173,7 +173,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
}
// OK not cached - serve!
- common.ServeContentByReader(ctx.Context, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf))
+ common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf))
return
}
@@ -187,7 +187,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
return
}
- common.ServeContentByReader(ctx.Context, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf))
+ common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf))
return
} else if err != nil {
ctx.ServerError("GetLFSMetaObjectByOid", err)
@@ -215,7 +215,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
}
defer lfsDataRc.Close()
- common.ServeContentByReadSeeker(ctx.Context, ctx.Repo.TreePath, lastModified, lfsDataRc)
+ common.ServeContentByReadSeeker(ctx.Base, ctx.Repo.TreePath, lastModified, lfsDataRc)
}
func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEntry, lastModified time.Time) {
diff --git a/routers/api/v1/repo/hook_test.go b/routers/api/v1/repo/hook_test.go
index 34dc990c3d..56658b45d5 100644
--- a/routers/api/v1/repo/hook_test.go
+++ b/routers/api/v1/repo/hook_test.go
@@ -9,7 +9,6 @@ import (
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
@@ -18,12 +17,12 @@ import (
func TestTestHook(t *testing.T) {
unittest.PrepareTestEnv(t)
- ctx := test.MockContext(t, "user2/repo1/wiki/_pages")
+ ctx := test.MockAPIContext(t, "user2/repo1/wiki/_pages")
ctx.SetParams(":id", "1")
test.LoadRepo(t, ctx, 1)
test.LoadRepoCommit(t, ctx)
test.LoadUser(t, ctx, 2)
- TestHook(&context.APIContext{Context: ctx, Org: nil})
+ TestHook(ctx)
assert.EqualValues(t, http.StatusNoContent, ctx.Resp.Status())
unittest.AssertExistsAndLoadBean(t, &webhook.HookTask{
diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index 5bf5fc8c8b..49252f7a4b 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -116,7 +116,7 @@ func SearchIssues(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/IssueList"
- before, since, err := context.GetQueryBeforeSince(ctx.Context)
+ before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return
@@ -368,7 +368,7 @@ func ListIssues(ctx *context.APIContext) {
// responses:
// "200":
// "$ref": "#/responses/IssueList"
- before, since, err := context.GetQueryBeforeSince(ctx.Context)
+ before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return
@@ -470,7 +470,7 @@ func ListIssues(ctx *context.APIContext) {
if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
issuesOpt := &issues_model.IssuesOptions{
ListOptions: listOptions,
- RepoID: ctx.Repo.Repository.ID,
+ RepoIDs: []int64{ctx.Repo.Repository.ID},
IsClosed: isClosed,
IssueIDs: issueIDs,
LabelIDs: labelIDs,
diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go
index 6ae6063303..7c8f30f116 100644
--- a/routers/api/v1/repo/issue_comment.go
+++ b/routers/api/v1/repo/issue_comment.go
@@ -59,7 +59,7 @@ func ListIssueComments(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/CommentList"
- before, since, err := context.GetQueryBeforeSince(ctx.Context)
+ before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return
@@ -156,7 +156,7 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/TimelineList"
- before, since, err := context.GetQueryBeforeSince(ctx.Context)
+ before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return
@@ -259,7 +259,7 @@ func ListRepoIssueComments(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/CommentList"
- before, since, err := context.GetQueryBeforeSince(ctx.Context)
+ before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return
diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go
index 16bb8cb73d..1ff934950c 100644
--- a/routers/api/v1/repo/issue_tracked_time.go
+++ b/routers/api/v1/repo/issue_tracked_time.go
@@ -103,7 +103,7 @@ func ListTrackedTimes(ctx *context.APIContext) {
opts.UserID = user.ID
}
- if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Context); err != nil {
+ if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return
}
@@ -522,7 +522,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
}
var err error
- if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Context); err != nil {
+ if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return
}
@@ -596,7 +596,7 @@ func ListMyTrackedTimes(ctx *context.APIContext) {
}
var err error
- if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Context); err != nil {
+ if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return
}
diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go
index efce39e520..b458cd122b 100644
--- a/routers/api/v1/repo/migrate.go
+++ b/routers/api/v1/repo/migrate.go
@@ -79,7 +79,7 @@ func Migrate(ctx *context.APIContext) {
return
}
- if ctx.HasError() {
+ if ctx.HasAPIError() {
ctx.Error(http.StatusUnprocessableEntity, "", ctx.GetErrMsg())
return
}
diff --git a/routers/api/v1/repo/repo_test.go b/routers/api/v1/repo/repo_test.go
index 59c3bde819..e1bdea5c82 100644
--- a/routers/api/v1/repo/repo_test.go
+++ b/routers/api/v1/repo/repo_test.go
@@ -9,7 +9,6 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/web"
@@ -20,7 +19,7 @@ import (
func TestRepoEdit(t *testing.T) {
unittest.PrepareTestEnv(t)
- ctx := test.MockContext(t, "user2/repo1")
+ ctx := test.MockAPIContext(t, "user2/repo1")
test.LoadRepo(t, ctx, 1)
test.LoadUser(t, ctx, 2)
ctx.Repo.Owner = ctx.Doer
@@ -54,9 +53,8 @@ func TestRepoEdit(t *testing.T) {
Archived: &archived,
}
- apiCtx := &context.APIContext{Context: ctx, Org: nil}
- web.SetForm(apiCtx, &opts)
- Edit(apiCtx)
+ web.SetForm(ctx, &opts)
+ Edit(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
@@ -67,7 +65,7 @@ func TestRepoEdit(t *testing.T) {
func TestRepoEditNameChange(t *testing.T) {
unittest.PrepareTestEnv(t)
- ctx := test.MockContext(t, "user2/repo1")
+ ctx := test.MockAPIContext(t, "user2/repo1")
test.LoadRepo(t, ctx, 1)
test.LoadUser(t, ctx, 2)
ctx.Repo.Owner = ctx.Doer
@@ -76,9 +74,8 @@ func TestRepoEditNameChange(t *testing.T) {
Name: &name,
}
- apiCtx := &context.APIContext{Context: ctx, Org: nil}
- web.SetForm(apiCtx, &opts)
- Edit(apiCtx)
+ web.SetForm(ctx, &opts)
+ Edit(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
diff --git a/routers/api/v1/repo/status.go b/routers/api/v1/repo/status.go
index 5158f38e14..c1110ebce5 100644
--- a/routers/api/v1/repo/status.go
+++ b/routers/api/v1/repo/status.go
@@ -183,7 +183,7 @@ func getCommitStatuses(ctx *context.APIContext, sha string) {
ctx.Error(http.StatusBadRequest, "ref/sha not given", nil)
return
}
- sha = utils.MustConvertToSHA1(ctx.Context, sha)
+ sha = utils.MustConvertToSHA1(ctx.Base, ctx.Repo, sha)
repo := ctx.Repo.Repository
listOptions := utils.GetListOptions(ctx)
diff --git a/routers/api/v1/user/helper.go b/routers/api/v1/user/helper.go
index 28f600ad92..4b642910b1 100644
--- a/routers/api/v1/user/helper.go
+++ b/routers/api/v1/user/helper.go
@@ -17,7 +17,7 @@ func GetUserByParamsName(ctx *context.APIContext, name string) *user_model.User
if err != nil {
if user_model.IsErrUserNotExist(err) {
if redirectUserID, err2 := user_model.LookupUserRedirect(username); err2 == nil {
- context.RedirectToUser(ctx.Context, username, redirectUserID)
+ context.RedirectToUser(ctx.Base, username, redirectUserID)
} else {
ctx.NotFound("GetUserByName", err)
}
diff --git a/routers/api/v1/utils/git.go b/routers/api/v1/utils/git.go
index eaf0f5fd37..32f5c85319 100644
--- a/routers/api/v1/utils/git.go
+++ b/routers/api/v1/utils/git.go
@@ -4,6 +4,7 @@
package utils
import (
+ gocontext "context"
"fmt"
"net/http"
@@ -33,7 +34,7 @@ func ResolveRefOrSha(ctx *context.APIContext, ref string) string {
}
}
- sha = MustConvertToSHA1(ctx.Context, sha)
+ sha = MustConvertToSHA1(ctx, ctx.Repo, sha)
if ctx.Repo.GitRepo != nil {
err := ctx.Repo.GitRepo.AddLastCommitCache(ctx.Repo.Repository.GetCommitsCountCacheKey(ref, ref != sha), ctx.Repo.Repository.FullName(), sha)
@@ -69,7 +70,7 @@ func searchRefCommitByType(ctx *context.APIContext, refType, filter string) (str
}
// ConvertToSHA1 returns a full-length SHA1 from a potential ID string
-func ConvertToSHA1(ctx *context.Context, commitID string) (git.SHA1, error) {
+func ConvertToSHA1(ctx gocontext.Context, repo *context.Repository, commitID string) (git.SHA1, error) {
if len(commitID) == git.SHAFullLength && git.IsValidSHAPattern(commitID) {
sha1, err := git.NewIDFromString(commitID)
if err == nil {
@@ -77,7 +78,7 @@ func ConvertToSHA1(ctx *context.Context, commitID string) (git.SHA1, error) {
}
}
- gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, ctx.Repo.Repository.RepoPath())
+ gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repo.Repository.RepoPath())
if err != nil {
return git.SHA1{}, fmt.Errorf("RepositoryFromContextOrOpen: %w", err)
}
@@ -87,8 +88,8 @@ func ConvertToSHA1(ctx *context.Context, commitID string) (git.SHA1, error) {
}
// MustConvertToSHA1 returns a full-length SHA1 string from a potential ID string, or returns origin input if it can't convert to SHA1
-func MustConvertToSHA1(ctx *context.Context, commitID string) string {
- sha, err := ConvertToSHA1(ctx, commitID)
+func MustConvertToSHA1(ctx gocontext.Context, repo *context.Repository, commitID string) string {
+ sha, err := ConvertToSHA1(ctx, repo, commitID)
if err != nil {
return commitID
}
diff --git a/routers/common/markup.go b/routers/common/markup.go
index 3acd12721e..5f412014d7 100644
--- a/routers/common/markup.go
+++ b/routers/common/markup.go
@@ -19,7 +19,7 @@ import (
)
// RenderMarkup renders markup text for the /markup and /markdown endpoints
-func RenderMarkup(ctx *context.Context, mode, text, urlPrefix, filePath string, wiki bool) {
+func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPrefix, filePath string, wiki bool) {
var markupType string
relativePath := ""
@@ -63,11 +63,11 @@ func RenderMarkup(ctx *context.Context, mode, text, urlPrefix, filePath string,
}
meta := map[string]string{}
- if ctx.Repo != nil && ctx.Repo.Repository != nil {
+ if repo != nil && repo.Repository != nil {
if mode == "comment" {
- meta = ctx.Repo.Repository.ComposeMetas()
+ meta = repo.Repository.ComposeMetas()
} else {
- meta = ctx.Repo.Repository.ComposeDocumentMetas()
+ meta = repo.Repository.ComposeDocumentMetas()
}
}
if mode != "comment" {
diff --git a/routers/common/middleware.go b/routers/common/middleware.go
index c1ee9dd765..a25ff1ee00 100644
--- a/routers/common/middleware.go
+++ b/routers/common/middleware.go
@@ -42,7 +42,7 @@ func ProtocolMiddlewares() (handlers []any) {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
ctx, _, finished := process.GetManager().AddTypedContext(req.Context(), fmt.Sprintf("%s: %s", req.Method, req.RequestURI), process.RequestProcessType, true)
defer finished()
- next.ServeHTTP(context.NewResponse(resp), req.WithContext(cache.WithCacheContext(ctx)))
+ next.ServeHTTP(context.WrapResponseWriter(resp), req.WithContext(cache.WithCacheContext(ctx)))
})
})
diff --git a/routers/common/serve.go b/routers/common/serve.go
index 59b993328e..3094ee6a6e 100644
--- a/routers/common/serve.go
+++ b/routers/common/serve.go
@@ -15,7 +15,7 @@ import (
)
// ServeBlob download a git.Blob
-func ServeBlob(ctx *context.Context, blob *git.Blob, lastModified time.Time) error {
+func ServeBlob(ctx *context.Base, filePath string, blob *git.Blob, lastModified time.Time) error {
if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
return nil
}
@@ -30,14 +30,14 @@ func ServeBlob(ctx *context.Context, blob *git.Blob, lastModified time.Time) err
}
}()
- httplib.ServeContentByReader(ctx.Req, ctx.Resp, ctx.Repo.TreePath, blob.Size(), dataRc)
+ httplib.ServeContentByReader(ctx.Req, ctx.Resp, filePath, blob.Size(), dataRc)
return nil
}
-func ServeContentByReader(ctx *context.Context, filePath string, size int64, reader io.Reader) {
+func ServeContentByReader(ctx *context.Base, filePath string, size int64, reader io.Reader) {
httplib.ServeContentByReader(ctx.Req, ctx.Resp, filePath, size, reader)
}
-func ServeContentByReadSeeker(ctx *context.Context, filePath string, modTime time.Time, reader io.ReadSeeker) {
+func ServeContentByReadSeeker(ctx *context.Base, filePath string, modTime time.Time, reader io.ReadSeeker) {
httplib.ServeContentByReadSeeker(ctx.Req, ctx.Resp, filePath, modTime, reader)
}
diff --git a/routers/init.go b/routers/init.go
index 358922b1ae..5737ef3dc0 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -193,6 +193,12 @@ func NormalRoutes(ctx context.Context) *web.Route {
if setting.Actions.Enabled {
prefix := "/api/actions"
r.Mount(prefix, actions_router.Routes(ctx, prefix))
+
+ // TODO: Pipeline api used for runner internal communication with gitea server. but only artifact is used for now.
+ // In Github, it uses ACTIONS_RUNTIME_URL=https://pipelines.actions.githubusercontent.com/fLgcSHkPGySXeIFrg8W8OBSfeg3b5Fls1A1CwX566g8PayEGlg/
+ // TODO: this prefix should be generated with a token string with runner ?
+ prefix = "/api/actions_pipeline"
+ r.Mount(prefix, actions_router.ArtifactsRoutes(prefix))
}
return r
diff --git a/routers/install/install.go b/routers/install/install.go
index 714ddd5548..89b91a5a48 100644
--- a/routers/install/install.go
+++ b/routers/install/install.go
@@ -58,15 +58,14 @@ func Contexter() func(next http.Handler) http.Handler {
dbTypeNames := getSupportedDbTypeNames()
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
+ base, baseCleanUp := context.NewBaseContext(resp, req)
ctx := context.Context{
- Resp: context.NewResponse(resp),
+ Base: base,
Flash: &middleware.Flash{},
- Locale: middleware.Locale(resp, req),
Render: rnd,
- Data: middleware.GetContextData(req.Context()),
Session: session.GetSession(req),
}
- defer ctx.Close()
+ defer baseCleanUp()
ctx.Data.MergeFrom(middleware.CommonTemplateContextData())
ctx.Data.MergeFrom(middleware.ContextData{
@@ -78,7 +77,6 @@ func Contexter() func(next http.Handler) http.Handler {
"PasswordHashAlgorithms": hash.RecommendedHashAlgorithms,
})
- ctx.Req = context.WithContext(req, &ctx)
next.ServeHTTP(resp, ctx.Req)
})
}
@@ -249,15 +247,8 @@ func SubmitInstall(ctx *context.Context) {
ctx.Data["CurDbType"] = form.DbType
if ctx.HasError() {
- if ctx.HasValue("Err_SMTPUser") {
- ctx.Data["Err_SMTP"] = true
- }
- if ctx.HasValue("Err_AdminName") ||
- ctx.HasValue("Err_AdminPasswd") ||
- ctx.HasValue("Err_AdminEmail") {
- ctx.Data["Err_Admin"] = true
- }
-
+ ctx.Data["Err_SMTP"] = ctx.Data["Err_SMTPUser"] != nil
+ ctx.Data["Err_Admin"] = ctx.Data["Err_AdminName"] != nil || ctx.Data["Err_AdminPasswd"] != nil || ctx.Data["Err_AdminEmail"] != nil
ctx.HTML(http.StatusOK, tplInstall)
return
}
diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go
index a4e1ad94f6..0d1d261e3d 100644
--- a/routers/private/hook_pre_receive.go
+++ b/routers/private/hook_pre_receive.go
@@ -9,6 +9,7 @@ import (
"os"
"strconv"
"strings"
+ "time"
"code.gitea.io/gitea/models"
asymkey_model "code.gitea.io/gitea/models/asymkey"
@@ -104,70 +105,72 @@ func (ctx *preReceiveContext) AssertCreatePullRequest() bool {
return true
}
-// CalculateSizeOfAddedObjects calculates the total size of objects provided as output from rev-list command
-func CalculateSizeOfAddedObjects(ctx *gitea_context.PrivateContext, opts *git.RunOpts, revlistObjects string) int64 {
- // Calculate the size of added Objects.
- var totalSize int64
- for _, object := range strings.Split(revlistObjects, "\n") {
- if len(object) == 0 {
- continue
- }
- objectID := strings.Split(object, " ")[0]
- objectSizeStr, _, err := git.NewCommand(ctx, "cat-file", "-s").AddDynamicArguments(objectID).RunStdString(opts)
- if err != nil {
- log.Trace("CalculateSizeOfAddedObjects: Error during git cat-file -s on object %s", objectID)
- return totalSize
- }
- objectSize, _ := strconv.ParseInt(strings.TrimSpace(objectSizeStr), 10, 64)
- if err != nil {
- log.Trace("CalculateSizeOfAddedObjects: Error during ParseInt on string: '%s'", objectSizeStr)
- return totalSize
- }
- totalSize += objectSize
+// CalculateSizeOfObject calculates the size of one git object via git cat-file -s command
+func CalculateSizeOfObject(ctx *gitea_context.PrivateContext, opts *git.RunOpts, objectID string) (objectSize int64) {
+ objectSizeStr, _, err := git.NewCommand(ctx, "cat-file", "-s").AddDynamicArguments(objectID).RunStdString(opts)
+ if err != nil {
+ log.Trace("CalculateSizeOfRemovedObjects: Error during git cat-file -s on object: %s", objectID)
+ return
}
- return totalSize
+
+ objectSize, _ = strconv.ParseInt(strings.TrimSpace(objectSizeStr), 10, 64)
+ if err != nil {
+ log.Trace("CalculateSizeOfRemovedObjects: Error during ParseInt on string '%s'", objectID)
+ return
+ }
+ return
}
-// CalculateSizeOfRemovedObjects calculates the size of removed objects provided as output from rev-list command
-// and confirms that the object is not referenced anywhere
-func CalculateSizeOfRemovedObjects(ctx *gitea_context.PrivateContext, opts *git.RunOpts, revlistObjects string) int64 {
- var totalSize int64
- for _, object := range strings.Split(revlistObjects, "\n") {
+// CalculateSizeOfObjects calculates the size of objects added and removed from the repository by new commit
+func CalculateSizeOfObjects(ctx *gitea_context.PrivateContext, opts *git.RunOpts, newCommitObjects map[string]bool, oldCommitObjects map[string]bool, otherCommitObjects map[string]bool) (addedSize int64, removedSize int64) {
+
+ // Calculate size of objects that were added
+ for objectID := range newCommitObjects {
+ if _, exists := oldCommitObjects[objectID]; !exists {
+ // objectID is not referenced in the list of objects of old commit so it is a new object
+ // Calculate its size and add it to the addedSize
+ addedSize += CalculateSizeOfObject(ctx, opts, objectID)
+ }
+ // We might check here if new object is not already in the rest of repo to be precise
+ // However our goal is to prevent growth of repository so on determination of addedSize
+ // We can skip this preciseness, addedSize will be more then real addedSize
+ // TODO - do not count size of object that is referenced in other part of repo but not referenced neither in old nor new commit
+ // git will not add the object twice
+ }
+
+ // Calculate size of objects that were removed
+ for objectID := range oldCommitObjects {
+ if _, exists := newCommitObjects[objectID]; !exists {
+ // objectID is not referenced in the list of new commit objects so it was possibly removed
+ if _, exists := otherCommitObjects[objectID]; !exists {
+ // objectID is not referenced in rest of the objects of the repository so it was removed
+ // Calculate its size and add it to the addedSize
+ removedSize += CalculateSizeOfObject(ctx, opts, objectID)
+ }
+ }
+
+ }
+ return
+}
+
+// ConvertObjectsToMap takes a newline-separated string of git objects and
+// converts it into a map for efficient lookup.
+func ConvertObjectsToMap(objects string) map[string]bool {
+ objectsMap := make(map[string]bool)
+ for _, object := range strings.Split(objects, "\n") {
if len(object) == 0 {
continue
}
objectID := strings.Split(object, " ")[0]
-
- // Confirm that the object is still reachable from anywhere in the repository.
- isReachable, _, err := git.NewCommand(ctx, "rev-list", "--objects", "--all", "--").AddDynamicArguments(objectID).RunStdString(opts)
- if err != nil {
- log.Trace("CalculateSizeOfRemovedObjects: Error during git rev-list --objects --all on object: %s", objectID)
- return totalSize
- }
-
- if isReachable != "" {
- // The object is still reachable, therefore we don't add it's size
- continue
- }
-
- objectSizeStr, _, err := git.NewCommand(ctx, "cat-file", "-s").AddDynamicArguments(objectID).RunStdString(opts)
- if err != nil {
- log.Trace("CalculateSizeOfRemovedObjects: Error during git cat-file -s on object: %s", objectID)
- return totalSize
- }
-
- objectSize, _ := strconv.ParseInt(strings.TrimSpace(objectSizeStr), 10, 64)
- if err != nil {
- log.Trace("CalculateSizeOfRemovedObjects: Error during ParseInt on string '%s'", objectID)
- return totalSize
- }
- totalSize += objectSize
+ objectsMap[objectID] = true
}
- return totalSize
+ return objectsMap
}
// HookPreReceive checks whether a individual commit is acceptable
func HookPreReceive(ctx *gitea_context.PrivateContext) {
+ startTime := time.Now()
+
opts := web.GetForm(ctx).(*private.HookOptions)
ourCtx := &preReceiveContext{
@@ -178,8 +181,8 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
repo := ourCtx.Repo.Repository
- var removedSize int64
var addedSize int64
+ var removedSize int64
// Calculating total size of the push using git count-objects
pushSize, err := git.CountObjectsWithEnv(ctx, repo.RepoPath(), ourCtx.env)
@@ -191,7 +194,7 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
return
}
- // Cash whether the repository would breach the size limit after the operation
+ // Cache whether the repository would breach the size limit after the operation
isRepoOversized := repo.RepoSizeIsOversized(pushSize.Size)
log.Trace("Push size %d", pushSize.Size)
@@ -203,32 +206,41 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
// If operation is in potential breach of size limit prepare data for analysis
if isRepoOversized {
-
- // ctx.JSON(http.StatusForbidden, private.Response{
- // UserMsg: fmt.Sprintf("oldCommitID(%s) newCommitID(%s)", oldCommitID, newCommitID),
- // })
-
- // Objects that are in newCommitID but not in oldCommitID are added
- addedObjects, _, err := git.NewCommand(ctx, "rev-list", "--objects").AddDynamicArguments(newCommitID, "^"+oldCommitID).RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: ourCtx.env})
+ // Create cache of objects in old commit
+ gitObjects, _, err := git.NewCommand(ctx, "rev-list", "--objects").AddDynamicArguments(oldCommitID).RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: ourCtx.env})
if err != nil {
- log.Error("Unable to list objects added between commits: %s and %s in %-v Error: %v", oldCommitID, newCommitID, repo, err)
+ log.Error("Unable to list objects in old commit: %s in %-v Error: %v", oldCommitID, repo, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
- Err: fmt.Sprintf("Fail to list objects added: %v", err),
+ Err: fmt.Sprintf("Fail to list objects in old commit: %v", err),
})
return
}
- addedSize += CalculateSizeOfAddedObjects(ctx, &git.RunOpts{Dir: repo.RepoPath(), Env: ourCtx.env}, addedObjects)
+ oldCommitObjects := ConvertObjectsToMap(gitObjects)
- // Objects that are in oldCommitID but not in newCommitID are removed
- removedObjects, _, err := git.NewCommand(ctx, "rev-list", "--objects").AddDynamicArguments(oldCommitID, "^"+newCommitID).RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: ourCtx.env})
+ // Create cache of objects in new commit
+ gitObjects, _, err = git.NewCommand(ctx, "rev-list", "--objects").AddDynamicArguments(newCommitID).RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: ourCtx.env})
if err != nil {
- log.Error("Unable to list objects removed between commits: %s and %s in %-v Error: %v", oldCommitID, newCommitID, repo, err)
+ log.Error("Unable to list objects in new commit %s in %-v Error: %v", newCommitID, repo, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
- Err: fmt.Sprintf("Fail to list objects removed: %v", err),
+ Err: fmt.Sprintf("Fail to list objects in new commit: %v", err),
})
return
}
- removedSize += CalculateSizeOfRemovedObjects(ctx, &git.RunOpts{Dir: repo.RepoPath(), Env: ourCtx.env}, removedObjects)
+ newCommitObjects := ConvertObjectsToMap(gitObjects)
+
+ // Create cache of objects that are in the repository but not part of old or new commit
+ gitObjects, _, err = git.NewCommand(ctx, "rev-list", "--objects", "--all").AddDynamicArguments("^"+oldCommitID, "^"+newCommitID).RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: ourCtx.env})
+ if err != nil {
+ log.Error("Unable to list objects in the repo that are missing from both old %s and new %s commits in %-v Error: %v", oldCommitID, newCommitID, repo, err)
+ ctx.JSON(http.StatusInternalServerError, private.Response{
+ Err: fmt.Sprintf("Fail to list objects missing from both old and new commits: %v", err),
+ })
+ return
+ }
+ otherCommitObjects := ConvertObjectsToMap(gitObjects)
+
+ // Calculate size that was added and removed by the new commit
+ addedSize, removedSize = CalculateSizeOfObjects(ctx, &git.RunOpts{Dir: repo.RepoPath(), Env: ourCtx.env}, newCommitObjects, oldCommitObjects, otherCommitObjects)
}
switch {
@@ -246,9 +258,12 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
}
}
+ duration := time.Since(startTime)
+ log.Warn("Addition in size is: %d, removal in size is: %d, limit size: %s, push size: %d. Took %s seconds.", addedSize, removedSize, base.FileSize(repo.GetActualSizeLimit()), pushSize.Size, duration)
+
// If total of commits add more size then they remove and we are in a potential breach of size limit -- abort
- if (addedSize > removedSize) && isRepoOversized { // Check next size if we are not deleting a reference
- log.Warn("Forbidden: new repo size is over limitation: %s", base.FileSize(repo.GetActualSizeLimit()))
+ if (addedSize > removedSize) && isRepoOversized {
+ log.Warn("Forbidden: new repo size %s is over limitation of %s. Push size: %s. Took %s seconds.", base.FileSize(addedSize-removedSize), base.FileSize(repo.GetActualSizeLimit()), base.FileSize(pushSize.Size), duration)
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("Repository size is over limitation of %s addedSize(%d) removedSize(%d) repo(%s)", base.FileSize(repo.GetActualSizeLimit()), addedSize, removedSize, repo.RepoPath()),
})
diff --git a/routers/web/misc/markup.go b/routers/web/misc/markup.go
index 1690378945..c91da9a7f1 100644
--- a/routers/web/misc/markup.go
+++ b/routers/web/misc/markup.go
@@ -5,8 +5,6 @@
package misc
import (
- "net/http"
-
"code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
@@ -16,11 +14,5 @@ import (
// Markup render markup document to HTML
func Markup(ctx *context.Context) {
form := web.GetForm(ctx).(*api.MarkupOption)
-
- if ctx.HasAPIError() {
- ctx.Error(http.StatusUnprocessableEntity, "", ctx.GetErrMsg())
- return
- }
-
- common.RenderMarkup(ctx, form.Mode, form.Text, form.Context, form.FilePath, form.Wiki)
+ common.RenderMarkup(ctx.Base, ctx.Repo, form.Mode, form.Text, form.Context, form.FilePath, form.Wiki)
}
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index c64b0fac39..2114338612 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/base"
context_module "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
@@ -418,3 +419,80 @@ func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions
}
return jobs[0], jobs
}
+
+type ArtifactsViewResponse struct {
+ Artifacts []*ArtifactsViewItem `json:"artifacts"`
+}
+
+type ArtifactsViewItem struct {
+ Name string `json:"name"`
+ Size int64 `json:"size"`
+ ID int64 `json:"id"`
+}
+
+func ArtifactsView(ctx *context_module.Context) {
+ runIndex := ctx.ParamsInt64("run")
+ run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, err.Error())
+ return
+ }
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+ artifacts, err := actions_model.ListUploadedArtifactsByRunID(ctx, run.ID)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+ artifactsResponse := ArtifactsViewResponse{
+ Artifacts: make([]*ArtifactsViewItem, 0, len(artifacts)),
+ }
+ for _, art := range artifacts {
+ artifactsResponse.Artifacts = append(artifactsResponse.Artifacts, &ArtifactsViewItem{
+ Name: art.ArtifactName,
+ Size: art.FileSize,
+ ID: art.ID,
+ })
+ }
+ ctx.JSON(http.StatusOK, artifactsResponse)
+}
+
+func ArtifactsDownloadView(ctx *context_module.Context) {
+ runIndex := ctx.ParamsInt64("run")
+ artifactID := ctx.ParamsInt64("id")
+
+ artifact, err := actions_model.GetArtifactByID(ctx, artifactID)
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, err.Error())
+ } else if err != nil {
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+ run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, err.Error())
+ return
+ }
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+ if artifact.RunID != run.ID {
+ ctx.Error(http.StatusNotFound, "artifact not found")
+ return
+ }
+
+ f, err := storage.ActionsArtifacts.Open(artifact.StoragePath)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+ defer f.Close()
+
+ ctx.ServeContent(f, &context_module.ServeHeaderOptions{
+ Filename: artifact.ArtifactName,
+ LastModified: artifact.CreatedUnix.AsLocalTime(),
+ })
+}
diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go
index c46ec29841..fb95e63ecf 100644
--- a/routers/web/repo/attachment.go
+++ b/routers/web/repo/attachment.go
@@ -153,7 +153,7 @@ func ServeAttachment(ctx *context.Context, uuid string) {
}
defer fr.Close()
- common.ServeContentByReadSeeker(ctx, attach.Name, attach.CreatedUnix.AsTime(), fr)
+ common.ServeContentByReadSeeker(ctx.Base, attach.Name, attach.CreatedUnix.AsTime(), fr)
}
// GetAttachment serve attachments
diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go
index 1c87f9bed7..a498180f35 100644
--- a/routers/web/repo/download.go
+++ b/routers/web/repo/download.go
@@ -47,7 +47,7 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified time.Time
log.Error("ServeBlobOrLFS: Close: %v", err)
}
closed = true
- return common.ServeBlob(ctx, blob, lastModified)
+ return common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified)
}
if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`) {
return nil
@@ -71,7 +71,7 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified time.Time
log.Error("ServeBlobOrLFS: Close: %v", err)
}
}()
- common.ServeContentByReadSeeker(ctx, ctx.Repo.TreePath, lastModified, lfsDataRc)
+ common.ServeContentByReadSeeker(ctx.Base, ctx.Repo.TreePath, lastModified, lfsDataRc)
return nil
}
if err = dataRc.Close(); err != nil {
@@ -79,7 +79,7 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified time.Time
}
closed = true
- return common.ServeBlob(ctx, blob, lastModified)
+ return common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified)
}
func getBlobForEntry(ctx *context.Context) (blob *git.Blob, lastModified time.Time) {
@@ -120,7 +120,7 @@ func SingleDownload(ctx *context.Context) {
return
}
- if err := common.ServeBlob(ctx, blob, lastModified); err != nil {
+ if err := common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified); err != nil {
ctx.ServerError("ServeBlob", err)
}
}
@@ -148,7 +148,7 @@ func DownloadByID(ctx *context.Context) {
}
return
}
- if err = common.ServeBlob(ctx, blob, time.Time{}); err != nil {
+ if err = common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, time.Time{}); err != nil {
ctx.ServerError("ServeBlob", err)
}
}
diff --git a/routers/web/repo/http.go b/routers/web/repo/http.go
index 4e45a9b6e2..b6ebd25915 100644
--- a/routers/web/repo/http.go
+++ b/routers/web/repo/http.go
@@ -109,7 +109,7 @@ func httpBase(ctx *context.Context) (h *serviceHandler) {
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
if redirectRepoID, err := repo_model.LookupRedirect(owner.ID, reponame); err == nil {
- context.RedirectToRepo(ctx, redirectRepoID)
+ context.RedirectToRepo(ctx.Base, redirectRepoID)
return
}
repoExist = false
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 66a4986139..7a0dc9940b 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -206,10 +206,10 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
if forceEmpty {
issueStats = &issues_model.IssueStats{}
} else {
- issueStats, err = issues_model.GetIssueStats(&issues_model.IssueStatsOptions{
- RepoID: repo.ID,
- Labels: selectLabels,
- MilestoneID: milestoneID,
+ issueStats, err = issues_model.GetIssueStats(&issues_model.IssuesOptions{
+ RepoIDs: []int64{repo.ID},
+ LabelIDs: labelIDs,
+ MilestoneIDs: []int64{milestoneID},
ProjectID: projectID,
AssigneeID: assigneeID,
MentionedID: mentionedID,
@@ -258,7 +258,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
Page: pager.Paginater.Current(),
PageSize: setting.UI.IssuePagingNum,
},
- RepoID: repo.ID,
+ RepoIDs: []int64{repo.ID},
AssigneeID: assigneeID,
PosterID: posterID,
MentionedID: mentionedID,
@@ -2344,7 +2344,7 @@ func UpdatePullReviewRequest(ctx *context.Context) {
// SearchIssues searches for issues across the repositories that the user has access to
func SearchIssues(ctx *context.Context) {
- before, since, err := context.GetQueryBeforeSince(ctx)
+ before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.Error(http.StatusUnprocessableEntity, err.Error())
return
@@ -2545,7 +2545,7 @@ func getUserIDForFilter(ctx *context.Context, queryName string) int64 {
// ListIssues list the issues of a repository
func ListIssues(ctx *context.Context) {
- before, since, err := context.GetQueryBeforeSince(ctx)
+ before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.Error(http.StatusUnprocessableEntity, err.Error())
return
@@ -2652,7 +2652,7 @@ func ListIssues(ctx *context.Context) {
if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
issuesOpt := &issues_model.IssuesOptions{
ListOptions: listOptions,
- RepoID: ctx.Repo.Repository.ID,
+ RepoIDs: []int64{ctx.Repo.Repository.ID},
IsClosed: isClosed,
IssueIDs: issueIDs,
LabelIDs: labelIDs,
diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go
index a335c114be..115418887d 100644
--- a/routers/web/repo/wiki.go
+++ b/routers/web/repo/wiki.go
@@ -671,7 +671,7 @@ func WikiRaw(ctx *context.Context) {
}
if entry != nil {
- if err = common.ServeBlob(ctx, entry.Blob(), time.Time{}); err != nil {
+ if err = common.ServeBlob(ctx.Base, ctx.Repo.TreePath, entry.Blob(), time.Time{}); err != nil {
ctx.ServerError("ServeBlob", err)
}
return
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index 1af56e24b0..2513fc9a98 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -521,10 +521,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// Parse ctx.FormString("repos") and remember matched repo IDs for later.
// Gets set when clicking filters on the issues overview page.
- repoIDs := getRepoIDs(ctx.FormString("repos"))
- if len(repoIDs) > 0 {
- opts.RepoCond = builder.In("issue.repo_id", repoIDs)
- }
+ opts.RepoIDs = getRepoIDs(ctx.FormString("repos"))
// ------------------------------
// Get issues as defined by opts.
@@ -580,11 +577,10 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// -------------------------------
var issueStats *issues_model.IssueStats
if !forceEmpty {
- statsOpts := issues_model.UserIssueStatsOptions{
- UserID: ctx.Doer.ID,
- FilterMode: filterMode,
- IsPull: isPullList,
- IsClosed: isShowClosed,
+ statsOpts := issues_model.IssuesOptions{
+ User: ctx.Doer,
+ IsPull: util.OptionalBoolOf(isPullList),
+ IsClosed: util.OptionalBoolOf(isShowClosed),
IssueIDs: issueIDsFromSearch,
IsArchived: util.OptionalBoolFalse,
LabelIDs: opts.LabelIDs,
@@ -593,7 +589,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
RepoCond: opts.RepoCond,
}
- issueStats, err = issues_model.GetUserIssueStats(statsOpts)
+ issueStats, err = issues_model.GetUserIssueStats(filterMode, statsOpts)
if err != nil {
ctx.ServerError("GetUserIssueStats Shown", err)
return
@@ -609,9 +605,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
} else {
shownIssues = int(issueStats.ClosedCount)
}
- if len(repoIDs) != 0 {
+ if len(opts.RepoIDs) != 0 {
shownIssues = 0
- for _, repoID := range repoIDs {
+ for _, repoID := range opts.RepoIDs {
shownIssues += int(issueCountByRepo[repoID])
}
}
@@ -622,8 +618,8 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
}
ctx.Data["TotalIssueCount"] = allIssueCount
- if len(repoIDs) == 1 {
- repo := showReposMap[repoIDs[0]]
+ if len(opts.RepoIDs) == 1 {
+ repo := showReposMap[opts.RepoIDs[0]]
if repo != nil {
ctx.Data["SingleRepoLink"] = repo.Link()
}
@@ -665,7 +661,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
ctx.Data["IssueStats"] = issueStats
ctx.Data["ViewType"] = viewType
ctx.Data["SortType"] = sortType
- ctx.Data["RepoIDs"] = repoIDs
+ ctx.Data["RepoIDs"] = opts.RepoIDs
ctx.Data["IsShowClosed"] = isShowClosed
ctx.Data["SelectLabels"] = selectedLabels
@@ -676,7 +672,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
}
// Convert []int64 to string
- reposParam, _ := json.Marshal(repoIDs)
+ reposParam, _ := json.Marshal(opts.RepoIDs)
ctx.Data["ReposParam"] = string(reposParam)
diff --git a/routers/web/web.go b/routers/web/web.go
index a7adc00ad7..c06f499304 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1193,6 +1193,8 @@ func registerRoutes(m *web.Route) {
})
m.Post("/cancel", reqRepoActionsWriter, actions.Cancel)
m.Post("/approve", reqRepoActionsWriter, actions.Approve)
+ m.Post("/artifacts", actions.ArtifactsView)
+ m.Get("/artifacts/{id}", actions.ArtifactsDownloadView)
m.Post("/rerun", reqRepoActionsWriter, actions.RerunAll)
})
}, reqRepoActionsReader, actions.MustEnableActions)
diff --git a/services/auth/middleware.go b/services/auth/middleware.go
index 3b2f883d00..d1955a4c90 100644
--- a/services/auth/middleware.go
+++ b/services/auth/middleware.go
@@ -8,6 +8,7 @@ import (
"strings"
"code.gitea.io/gitea/models/auth"
+ user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -17,11 +18,15 @@ import (
// Auth is a middleware to authenticate a web user
func Auth(authMethod Method) func(*context.Context) {
return func(ctx *context.Context) {
- if err := authShared(ctx, authMethod); err != nil {
+ ar, err := authShared(ctx.Base, ctx.Session, authMethod)
+ if err != nil {
log.Error("Failed to verify user: %v", err)
ctx.Error(http.StatusUnauthorized, "Verify")
return
}
+ ctx.Doer = ar.Doer
+ ctx.IsSigned = ar.Doer != nil
+ ctx.IsBasicAuth = ar.IsBasicAuth
if ctx.Doer == nil {
// ensure the session uid is deleted
_ = ctx.Session.Delete("uid")
@@ -32,32 +37,41 @@ func Auth(authMethod Method) func(*context.Context) {
// APIAuth is a middleware to authenticate an api user
func APIAuth(authMethod Method) func(*context.APIContext) {
return func(ctx *context.APIContext) {
- if err := authShared(ctx.Context, authMethod); err != nil {
+ ar, err := authShared(ctx.Base, nil, authMethod)
+ if err != nil {
ctx.Error(http.StatusUnauthorized, "APIAuth", err)
+ return
}
+ ctx.Doer = ar.Doer
+ ctx.IsSigned = ar.Doer != nil
+ ctx.IsBasicAuth = ar.IsBasicAuth
}
}
-func authShared(ctx *context.Context, authMethod Method) error {
- var err error
- ctx.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
+type authResult struct {
+ Doer *user_model.User
+ IsBasicAuth bool
+}
+
+func authShared(ctx *context.Base, sessionStore SessionStore, authMethod Method) (ar authResult, err error) {
+ ar.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, sessionStore)
if err != nil {
- return err
+ return ar, err
}
- if ctx.Doer != nil {
- if ctx.Locale.Language() != ctx.Doer.Language {
+ if ar.Doer != nil {
+ if ctx.Locale.Language() != ar.Doer.Language {
ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
}
- ctx.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == BasicMethodName
- ctx.IsSigned = true
- ctx.Data["IsSigned"] = ctx.IsSigned
- ctx.Data[middleware.ContextDataKeySignedUser] = ctx.Doer
- ctx.Data["SignedUserID"] = ctx.Doer.ID
- ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin
+ ar.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == BasicMethodName
+
+ ctx.Data["IsSigned"] = true
+ ctx.Data[middleware.ContextDataKeySignedUser] = ar.Doer
+ ctx.Data["SignedUserID"] = ar.Doer.ID
+ ctx.Data["IsAdmin"] = ar.Doer.IsAdmin
} else {
ctx.Data["SignedUserID"] = int64(0)
}
- return nil
+ return ar, nil
}
// VerifyOptions contains required or check options
@@ -68,7 +82,7 @@ type VerifyOptions struct {
DisableCSRF bool
}
-// Checks authentication according to options
+// VerifyAuthWithOptions checks authentication according to options
func VerifyAuthWithOptions(options *VerifyOptions) func(ctx *context.Context) {
return func(ctx *context.Context) {
// Check prohibit login users.
@@ -153,7 +167,7 @@ func VerifyAuthWithOptions(options *VerifyOptions) func(ctx *context.Context) {
}
}
-// Checks authentication according to options
+// VerifyAuthWithOptionsAPI checks authentication according to options
func VerifyAuthWithOptionsAPI(options *VerifyOptions) func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
// Check prohibit login users.
@@ -197,7 +211,9 @@ func VerifyAuthWithOptionsAPI(options *VerifyOptions) func(ctx *context.APIConte
return
} else if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm {
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
- ctx.HTML(http.StatusOK, "user/auth/activate")
+ ctx.JSON(http.StatusForbidden, map[string]string{
+ "message": "This account is not activated.",
+ })
return
}
if ctx.IsSigned && ctx.IsBasicAuth {
diff --git a/services/context/user.go b/services/context/user.go
index c713667bca..4e74aa50bd 100644
--- a/services/context/user.go
+++ b/services/context/user.go
@@ -15,7 +15,7 @@ import (
// UserAssignmentWeb returns a middleware to handle context-user assignment for web routes
func UserAssignmentWeb() func(ctx *context.Context) {
return func(ctx *context.Context) {
- userAssignment(ctx, func(status int, title string, obj interface{}) {
+ errorFn := func(status int, title string, obj interface{}) {
err, ok := obj.(error)
if !ok {
err = fmt.Errorf("%s", obj)
@@ -25,7 +25,8 @@ func UserAssignmentWeb() func(ctx *context.Context) {
} else {
ctx.ServerError(title, err)
}
- })
+ }
+ ctx.ContextUser = userAssignment(ctx.Base, ctx.Doer, errorFn)
}
}
@@ -53,18 +54,18 @@ func UserIDAssignmentAPI() func(ctx *context.APIContext) {
// UserAssignmentAPI returns a middleware to handle context-user assignment for api routes
func UserAssignmentAPI() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
- userAssignment(ctx.Context, ctx.Error)
+ ctx.ContextUser = userAssignment(ctx.Base, ctx.Doer, ctx.Error)
}
}
-func userAssignment(ctx *context.Context, errCb func(int, string, interface{})) {
+func userAssignment(ctx *context.Base, doer *user_model.User, errCb func(int, string, interface{})) (contextUser *user_model.User) {
username := ctx.Params(":username")
- if ctx.IsSigned && ctx.Doer.LowerName == strings.ToLower(username) {
- ctx.ContextUser = ctx.Doer
+ if doer != nil && doer.LowerName == strings.ToLower(username) {
+ contextUser = doer
} else {
var err error
- ctx.ContextUser, err = user_model.GetUserByName(ctx, username)
+ contextUser, err = user_model.GetUserByName(ctx, username)
if err != nil {
if user_model.IsErrUserNotExist(err) {
if redirectUserID, err := user_model.LookupUserRedirect(username); err == nil {
@@ -79,4 +80,5 @@ func userAssignment(ctx *context.Context, errCb func(int, string, interface{}))
}
}
}
+ return contextUser
}
diff --git a/services/forms/admin.go b/services/forms/admin.go
index a749f863f3..4b3cacc606 100644
--- a/services/forms/admin.go
+++ b/services/forms/admin.go
@@ -27,7 +27,7 @@ type AdminCreateUserForm struct {
// Validate validates form fields
func (f *AdminCreateUserForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -55,7 +55,7 @@ type AdminEditUserForm struct {
// Validate validates form fields
func (f *AdminEditUserForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -67,6 +67,6 @@ type AdminDashboardForm struct {
// Validate validates form fields
func (f *AdminDashboardForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
diff --git a/services/forms/auth_form.go b/services/forms/auth_form.go
index 5625aa1e2e..25acbbb99e 100644
--- a/services/forms/auth_form.go
+++ b/services/forms/auth_form.go
@@ -86,6 +86,6 @@ type AuthenticationForm struct {
// Validate validates fields
func (f *AuthenticationForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
diff --git a/services/forms/org.go b/services/forms/org.go
index d753531371..c333bead31 100644
--- a/services/forms/org.go
+++ b/services/forms/org.go
@@ -30,7 +30,7 @@ type CreateOrgForm struct {
// Validate validates the fields
func (f *CreateOrgForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -48,7 +48,7 @@ type UpdateOrgSettingForm struct {
// Validate validates the fields
func (f *UpdateOrgSettingForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -70,6 +70,6 @@ type CreateTeamForm struct {
// Validate validates the fields
func (f *CreateTeamForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
diff --git a/services/forms/package_form.go b/services/forms/package_form.go
index dfec98fff4..cf8abfb8fb 100644
--- a/services/forms/package_form.go
+++ b/services/forms/package_form.go
@@ -25,6 +25,6 @@ type PackageCleanupRuleForm struct {
}
func (f *PackageCleanupRuleForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
diff --git a/services/forms/repo_branch_form.go b/services/forms/repo_branch_form.go
index bf1183fc43..5deb0ae463 100644
--- a/services/forms/repo_branch_form.go
+++ b/services/forms/repo_branch_form.go
@@ -21,7 +21,7 @@ type NewBranchForm struct {
// Validate validates the fields
func (f *NewBranchForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -33,6 +33,6 @@ type RenameBranchForm struct {
// Validate validates the fields
func (f *RenameBranchForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index bf0a30665d..a8c3be5114 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -61,7 +61,7 @@ type CreateRepoForm struct {
// Validate validates the fields
func (f *CreateRepoForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -94,7 +94,7 @@ type MigrateRepoForm struct {
// Validate validates the fields
func (f *MigrateRepoForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -184,7 +184,7 @@ type RepoSettingForm struct {
// Validate validates the fields
func (f *RepoSettingForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -223,7 +223,7 @@ type ProtectBranchForm struct {
// Validate validates the fields
func (f *ProtectBranchForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -288,7 +288,7 @@ type NewWebhookForm struct {
// Validate validates the fields
func (f *NewWebhookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -302,7 +302,7 @@ type NewGogshookForm struct {
// Validate validates the fields
func (f *NewGogshookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -318,7 +318,7 @@ type NewSlackHookForm struct {
// Validate validates the fields
func (f *NewSlackHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
if !webhook.IsValidSlackChannel(strings.TrimSpace(f.Channel)) {
errs = append(errs, binding.Error{
FieldNames: []string{"Channel"},
@@ -339,7 +339,7 @@ type NewDiscordHookForm struct {
// Validate validates the fields
func (f *NewDiscordHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -351,7 +351,7 @@ type NewDingtalkHookForm struct {
// Validate validates the fields
func (f *NewDingtalkHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -364,7 +364,7 @@ type NewTelegramHookForm struct {
// Validate validates the fields
func (f *NewTelegramHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -378,7 +378,7 @@ type NewMatrixHookForm struct {
// Validate validates the fields
func (f *NewMatrixHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -390,7 +390,7 @@ type NewMSTeamsHookForm struct {
// Validate validates the fields
func (f *NewMSTeamsHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -402,7 +402,7 @@ type NewFeishuHookForm struct {
// Validate validates the fields
func (f *NewFeishuHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -414,7 +414,7 @@ type NewWechatWorkHookForm struct {
// Validate validates the fields
func (f *NewWechatWorkHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -428,7 +428,7 @@ type NewPackagistHookForm struct {
// Validate validates the fields
func (f *NewPackagistHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -455,7 +455,7 @@ type CreateIssueForm struct {
// Validate validates the fields
func (f *CreateIssueForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -468,7 +468,7 @@ type CreateCommentForm struct {
// Validate validates the fields
func (f *CreateCommentForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -479,7 +479,7 @@ type ReactionForm struct {
// Validate validates the fields
func (f *ReactionForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -490,7 +490,7 @@ type IssueLockForm struct {
// Validate validates the fields
func (i *IssueLockForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, i, ctx.Locale)
}
@@ -558,7 +558,7 @@ type CreateMilestoneForm struct {
// Validate validates the fields
func (f *CreateMilestoneForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -580,7 +580,7 @@ type CreateLabelForm struct {
// Validate validates the fields
func (f *CreateLabelForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -591,7 +591,7 @@ type InitializeLabelsForm struct {
// Validate validates the fields
func (f *InitializeLabelsForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -619,7 +619,7 @@ type MergePullRequestForm struct {
// Validate validates the fields
func (f *MergePullRequestForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -637,7 +637,7 @@ type CodeCommentForm struct {
// Validate validates the fields
func (f *CodeCommentForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -651,7 +651,7 @@ type SubmitReviewForm struct {
// Validate validates the fields
func (f *SubmitReviewForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -712,7 +712,7 @@ type NewReleaseForm struct {
// Validate validates the fields
func (f *NewReleaseForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -727,7 +727,7 @@ type EditReleaseForm struct {
// Validate validates the fields
func (f *EditReleaseForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -748,7 +748,7 @@ type NewWikiForm struct {
// Validate validates the fields
// FIXME: use code generation to generate this method.
func (f *NewWikiForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -773,7 +773,7 @@ type EditRepoFileForm struct {
// Validate validates the fields
func (f *EditRepoFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -784,7 +784,7 @@ type EditPreviewDiffForm struct {
// Validate validates the fields
func (f *EditPreviewDiffForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -808,7 +808,7 @@ type CherryPickForm struct {
// Validate validates the fields
func (f *CherryPickForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -833,7 +833,7 @@ type UploadRepoFileForm struct {
// Validate validates the fields
func (f *UploadRepoFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -844,7 +844,7 @@ type RemoveUploadFileForm struct {
// Validate validates the fields
func (f *RemoveUploadFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -867,7 +867,7 @@ type DeleteRepoFileForm struct {
// Validate validates the fields
func (f *DeleteRepoFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -886,7 +886,7 @@ type AddTimeManuallyForm struct {
// Validate validates the fields
func (f *AddTimeManuallyForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -902,6 +902,6 @@ type DeadlineForm struct {
// Validate validates the fields
func (f *DeadlineForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
diff --git a/services/forms/repo_tag_form.go b/services/forms/repo_tag_form.go
index 1209d2346f..4dd99f9e32 100644
--- a/services/forms/repo_tag_form.go
+++ b/services/forms/repo_tag_form.go
@@ -21,6 +21,6 @@ type ProtectTagForm struct {
// Validate validates the fields
func (f *ProtectTagForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
diff --git a/services/forms/runner.go b/services/forms/runner.go
index 9063060346..22dea49e31 100644
--- a/services/forms/runner.go
+++ b/services/forms/runner.go
@@ -20,6 +20,6 @@ type EditRunnerForm struct {
// Validate validates form fields
func (f *EditRunnerForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
diff --git a/services/forms/user_form.go b/services/forms/user_form.go
index 285bc398b2..fa8129bf85 100644
--- a/services/forms/user_form.go
+++ b/services/forms/user_form.go
@@ -78,7 +78,7 @@ type InstallForm struct {
// Validate validates the fields
func (f *InstallForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -99,7 +99,7 @@ type RegisterForm struct {
// Validate validates the fields
func (f *RegisterForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -148,7 +148,7 @@ type MustChangePasswordForm struct {
// Validate validates the fields
func (f *MustChangePasswordForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -162,7 +162,7 @@ type SignInForm struct {
// Validate validates the fields
func (f *SignInForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -182,7 +182,7 @@ type AuthorizationForm struct {
// Validate validates the fields
func (f *AuthorizationForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -197,7 +197,7 @@ type GrantApplicationForm struct {
// Validate validates the fields
func (f *GrantApplicationForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -216,7 +216,7 @@ type AccessTokenForm struct {
// Validate validates the fields
func (f *AccessTokenForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -227,7 +227,7 @@ type IntrospectTokenForm struct {
// Validate validates the fields
func (f *IntrospectTokenForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -252,7 +252,7 @@ type UpdateProfileForm struct {
// Validate validates the fields
func (f *UpdateProfileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -263,7 +263,7 @@ type UpdateLanguageForm struct {
// Validate validates the fields
func (f *UpdateLanguageForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -283,7 +283,7 @@ type AvatarForm struct {
// Validate validates the fields
func (f *AvatarForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -294,7 +294,7 @@ type AddEmailForm struct {
// Validate validates the fields
func (f *AddEmailForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -305,7 +305,7 @@ type UpdateThemeForm struct {
// Validate validates the field
func (f *UpdateThemeForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -332,7 +332,7 @@ type ChangePasswordForm struct {
// Validate validates the fields
func (f *ChangePasswordForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -343,7 +343,7 @@ type AddOpenIDForm struct {
// Validate validates the fields
func (f *AddOpenIDForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -360,7 +360,7 @@ type AddKeyForm struct {
// Validate validates the fields
func (f *AddKeyForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -372,7 +372,7 @@ type AddSecretForm struct {
// Validate validates the fields
func (f *AddSecretForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -384,7 +384,7 @@ type NewAccessTokenForm struct {
// Validate validates the fields
func (f *NewAccessTokenForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -403,7 +403,7 @@ type EditOAuth2ApplicationForm struct {
// Validate validates the fields
func (f *EditOAuth2ApplicationForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -414,7 +414,7 @@ type TwoFactorAuthForm struct {
// Validate validates the fields
func (f *TwoFactorAuthForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -425,7 +425,7 @@ type TwoFactorScratchAuthForm struct {
// Validate validates the fields
func (f *TwoFactorScratchAuthForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -436,7 +436,7 @@ type WebauthnRegistrationForm struct {
// Validate validates the fields
func (f *WebauthnRegistrationForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -447,7 +447,7 @@ type WebauthnDeleteForm struct {
// Validate validates the fields
func (f *WebauthnDeleteForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -459,6 +459,6 @@ type PackageSettingForm struct {
// Validate validates the fields
func (f *PackageSettingForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
diff --git a/services/forms/user_form_auth_openid.go b/services/forms/user_form_auth_openid.go
index f95eb98405..d8137a8d13 100644
--- a/services/forms/user_form_auth_openid.go
+++ b/services/forms/user_form_auth_openid.go
@@ -20,7 +20,7 @@ type SignInOpenIDForm struct {
// Validate validates the fields
func (f *SignInOpenIDForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -32,7 +32,7 @@ type SignUpOpenIDForm struct {
// Validate validates the fields
func (f *SignUpOpenIDForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
@@ -44,6 +44,6 @@ type ConnectOpenIDForm struct {
// Validate validates the fields
func (f *ConnectOpenIDForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetContext(req)
+ ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go
index 2897f203a9..3551f85c46 100644
--- a/services/markup/processorhelper.go
+++ b/services/markup/processorhelper.go
@@ -13,6 +13,7 @@ import (
func ProcessorHelper() *markup.ProcessorHelper {
return &markup.ProcessorHelper{
+ ElementDir: "auto", // set dir="auto" for necessary (eg: ,
-