mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-24 12:53:53 +02:00
Merge remote-tracking branch 'upstream/main' into limit-repo-size
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
#commented out tokens so you can see what they are in plaintext
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
+1
-1
@@ -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() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
+40
-31
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+1
-336
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
+83
-32
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+60
-77
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
+42
-35
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
+10
-19
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
+21
-20
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -52,6 +52,10 @@ func Test_Sanitizer(t *testing.T) {
|
||||
`<span style="bad-color: red">Hello World</span>`, `<span>Hello World</span>`,
|
||||
`<p style="bad-color: red">Hello World</p>`, `<p>Hello World</p>`,
|
||||
`<code style="bad-color: red">Hello World</code>`, `<code>Hello World</code>`,
|
||||
|
||||
// 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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+13
-2
@@ -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
|
||||
|
||||
@@ -114,6 +114,8 @@ unknown = Unknown
|
||||
|
||||
rss_feed = RSS Feed
|
||||
|
||||
artifacts = Artifacts
|
||||
|
||||
concept_system_global = Global
|
||||
concept_user_individual = Individual
|
||||
concept_code_repository = Repository
|
||||
|
||||
+706
-22
File diff suppressed because it is too large
Load Diff
@@ -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, <a href="#">başlığı <strong>%s</strong> ile başlatın</a>`
|
||||
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üğü <code>~/.chef/config.rb</code> 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, <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/en-us/packages/chef/">belgeye</a> bakabilirsiniz.
|
||||
composer.registry=Bu kütüğü <code>~/.composer/config.json</code> 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, <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/en-us/packages/composer/">belgeye</a> 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, <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/en-us/packages/conan/">belgeye</a> bakabilirsiniz.
|
||||
conda.registry=Bu kütüğü <code>.condarc</code> 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, <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/en-us/packages/conda/">belgeye</a> 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, <a
|
||||
swift.registry=Bu kütüğü komut satırını kullanarak kurun:
|
||||
swift.install=Paketi <code>Package.swift</code> dosyanıza ekleyin:
|
||||
swift.install2=ve şu komutu çalıştırın:
|
||||
swift.documentation=Swift kütüğü hakkında daha fazla bilgi için, <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/en-us/packages/swift/">belgeye</a> 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, <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/en-us/packages/vagrant/">belgeye</a> bakabilirsiniz.
|
||||
settings.link=Bu paketi bir depoya bağlayın
|
||||
|
||||
Generated
+413
-318
File diff suppressed because it is too large
Load Diff
+11
-10
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
@@ -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{},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ func Migrate(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.HasError() {
|
||||
if ctx.HasAPIError() {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", ctx.GetErrMsg())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -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)))
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
+12
-16
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
+34
-18
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+36
-36
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+24
-24
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
func ProcessorHelper() *markup.ProcessorHelper {
|
||||
return &markup.ProcessorHelper{
|
||||
ElementDir: "auto", // set dir="auto" for necessary (eg: <p>, <h?>, etc) tags
|
||||
IsUsernameMentionable: func(ctx context.Context, username string) bool {
|
||||
mentionedUser, err := user.GetUserByName(ctx, username)
|
||||
if err != nil {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user