mirror of
https://github.com/go-gitea/gitea.git
synced 2025-08-14 00:02:41 -04:00
Compare commits
11 Commits
25d4f95df2
...
1698c15cba
Author | SHA1 | Date | |
---|---|---|---|
|
1698c15cba | ||
|
bbc1456542 | ||
|
3288252dda | ||
|
c641a22f2a | ||
|
a103b79f60 | ||
|
acde12a8a2 | ||
|
f5ce2ed292 | ||
|
38cf43d060 | ||
|
c757765a9e | ||
|
7985cde84d | ||
|
3eecde3f33 |
@ -1327,6 +1327,7 @@ ROUTER = console
|
|||||||
;; Comma separated list of custom URL-Schemes that are allowed as links when rendering Markdown
|
;; 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)
|
;; 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.
|
;; 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 =
|
;CUSTOM_URL_SCHEMES =
|
||||||
;;
|
;;
|
||||||
;; List of file extensions that should be rendered/edited as Markdown
|
;; List of file extensions that should be rendered/edited as Markdown
|
||||||
|
@ -276,7 +276,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a
|
|||||||
trailing whitespace to paragraphs is not necessary to force a line break.
|
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
|
- `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
|
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.
|
- `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.
|
- `ENABLE_MATH`: **true**: Enables detection of `\(...\)`, `\[...\]`, `$...$` and `$$...$$` blocks as math blocks.
|
||||||
|
|
||||||
|
5
go.mod
5
go.mod
@ -76,7 +76,7 @@ require (
|
|||||||
github.com/mattn/go-sqlite3 v1.14.16
|
github.com/mattn/go-sqlite3 v1.14.16
|
||||||
github.com/meilisearch/meilisearch-go v0.24.0
|
github.com/meilisearch/meilisearch-go v0.24.0
|
||||||
github.com/mholt/archiver/v3 v3.5.1
|
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/minio-go/v7 v7.0.52
|
||||||
github.com/minio/sha256-simd v1.0.0
|
github.com/minio/sha256-simd v1.0.0
|
||||||
github.com/msteinert/pam v1.1.0
|
github.com/msteinert/pam v1.1.0
|
||||||
@ -109,7 +109,7 @@ require (
|
|||||||
github.com/yuin/goldmark-meta v1.1.0
|
github.com/yuin/goldmark-meta v1.1.0
|
||||||
golang.org/x/crypto v0.8.0
|
golang.org/x/crypto v0.8.0
|
||||||
golang.org/x/image v0.7.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/oauth2 v0.7.0
|
||||||
golang.org/x/sys v0.8.0
|
golang.org/x/sys v0.8.0
|
||||||
golang.org/x/text v0.9.0
|
golang.org/x/text v0.9.0
|
||||||
@ -288,7 +288,6 @@ require (
|
|||||||
go.uber.org/zap v1.24.0 // indirect
|
go.uber.org/zap v1.24.0 // indirect
|
||||||
golang.org/x/mod v0.10.0 // indirect
|
golang.org/x/mod v0.10.0 // indirect
|
||||||
golang.org/x/sync v0.2.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
|
golang.org/x/time v0.3.0 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
|
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
|
||||||
|
8
go.sum
8
go.sum
@ -876,8 +876,8 @@ github.com/mholt/acmez v1.1.0 h1:IQ9CGHKOHokorxnffsqDvmmE30mDenO1lptYZ1AYkHY=
|
|||||||
github.com/mholt/acmez v1.1.0/go.mod h1:zwo5+fbLLTowAX8o8ETfQzbDtwGEXnPhkmGdKIP+bgs=
|
github.com/mholt/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 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
|
||||||
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
|
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.24 h1:NGQoPtwGVcbGkKfvyYk1yRqknzBuoMiUrO6R7uFTPlw=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4=
|
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.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||||
github.com/miekg/dns v1.1.54 h1:5jon9mWcb0sFJGpnI99tOMhCPyJ+RPVz5b63MQG0VWI=
|
github.com/miekg/dns v1.1.54 h1:5jon9mWcb0sFJGpnI99tOMhCPyJ+RPVz5b63MQG0VWI=
|
||||||
github.com/miekg/dns v1.1.54/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
|
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.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.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.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.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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/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.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
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 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.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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
122
models/actions/artifact.go
Normal file
122
models/actions/artifact.go
Normal file
@ -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)
|
||||||
|
}
|
19
models/fixtures/action_run.yml
Normal file
19
models/fixtures/action_run.yml
Normal file
@ -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
|
14
models/fixtures/action_run_job.yml
Normal file
14
models/fixtures/action_run_job.yml
Normal file
@ -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
|
20
models/fixtures/action_task.yml
Normal file
20
models/fixtures/action_task.yml
Normal file
@ -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
|
@ -8,10 +8,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
|
||||||
project_model "code.gitea.io/gitea/models/project"
|
project_model "code.gitea.io/gitea/models/project"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
@ -212,17 +210,6 @@ func (issue *Issue) GetPullRequest() (pr *PullRequest, err error) {
|
|||||||
return pr, err
|
return pr, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadLabels loads labels
|
|
||||||
func (issue *Issue) LoadLabels(ctx context.Context) (err error) {
|
|
||||||
if issue.Labels == nil && issue.ID != 0 {
|
|
||||||
issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadPoster loads poster
|
// LoadPoster loads poster
|
||||||
func (issue *Issue) LoadPoster(ctx context.Context) (err error) {
|
func (issue *Issue) LoadPoster(ctx context.Context) (err error) {
|
||||||
if issue.Poster == nil && issue.PosterID != 0 {
|
if issue.Poster == nil && issue.PosterID != 0 {
|
||||||
@ -459,175 +446,6 @@ func (issue *Issue) IsPoster(uid int64) bool {
|
|||||||
return issue.OriginalAuthorID == 0 && issue.PosterID == uid
|
return issue.OriginalAuthorID == 0 && issue.PosterID == uid
|
||||||
}
|
}
|
||||||
|
|
||||||
func (issue *Issue) getLabels(ctx context.Context) (err error) {
|
|
||||||
if len(issue.Labels) > 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getLabelsByIssueID: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func clearIssueLabels(ctx context.Context, issue *Issue, doer *user_model.User) (err error) {
|
|
||||||
if err = issue.getLabels(ctx); err != nil {
|
|
||||||
return fmt.Errorf("getLabels: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range issue.Labels {
|
|
||||||
if err = deleteIssueLabel(ctx, issue, issue.Labels[i], doer); err != nil {
|
|
||||||
return fmt.Errorf("removeLabel: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearIssueLabels removes all issue labels as the given user.
|
|
||||||
// Triggers appropriate WebHooks, if any.
|
|
||||||
func ClearIssueLabels(issue *Issue, doer *user_model.User) (err error) {
|
|
||||||
ctx, committer, err := db.TxContext(db.DefaultContext)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer committer.Close()
|
|
||||||
|
|
||||||
if err := issue.LoadRepo(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
} else if err = issue.LoadPullRequest(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !perm.CanWriteIssuesOrPulls(issue.IsPull) {
|
|
||||||
return ErrRepoLabelNotExist{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = clearIssueLabels(ctx, issue, doer); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = committer.Commit(); err != nil {
|
|
||||||
return fmt.Errorf("Commit: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type labelSorter []*Label
|
|
||||||
|
|
||||||
func (ts labelSorter) Len() int {
|
|
||||||
return len([]*Label(ts))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts labelSorter) Less(i, j int) bool {
|
|
||||||
return []*Label(ts)[i].ID < []*Label(ts)[j].ID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts labelSorter) Swap(i, j int) {
|
|
||||||
[]*Label(ts)[i], []*Label(ts)[j] = []*Label(ts)[j], []*Label(ts)[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure only one label of a given scope exists, with labels at the end of the
|
|
||||||
// array getting preference over earlier ones.
|
|
||||||
func RemoveDuplicateExclusiveLabels(labels []*Label) []*Label {
|
|
||||||
validLabels := make([]*Label, 0, len(labels))
|
|
||||||
|
|
||||||
for i, label := range labels {
|
|
||||||
scope := label.ExclusiveScope()
|
|
||||||
if scope != "" {
|
|
||||||
foundOther := false
|
|
||||||
for _, otherLabel := range labels[i+1:] {
|
|
||||||
if otherLabel.ExclusiveScope() == scope {
|
|
||||||
foundOther = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if foundOther {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
validLabels = append(validLabels, label)
|
|
||||||
}
|
|
||||||
|
|
||||||
return validLabels
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReplaceIssueLabels removes all current labels and add new labels to the issue.
|
|
||||||
// Triggers appropriate WebHooks, if any.
|
|
||||||
func ReplaceIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (err error) {
|
|
||||||
ctx, committer, err := db.TxContext(db.DefaultContext)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer committer.Close()
|
|
||||||
|
|
||||||
if err = issue.LoadRepo(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = issue.LoadLabels(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
labels = RemoveDuplicateExclusiveLabels(labels)
|
|
||||||
|
|
||||||
sort.Sort(labelSorter(labels))
|
|
||||||
sort.Sort(labelSorter(issue.Labels))
|
|
||||||
|
|
||||||
var toAdd, toRemove []*Label
|
|
||||||
|
|
||||||
addIndex, removeIndex := 0, 0
|
|
||||||
for addIndex < len(labels) && removeIndex < len(issue.Labels) {
|
|
||||||
addLabel := labels[addIndex]
|
|
||||||
removeLabel := issue.Labels[removeIndex]
|
|
||||||
if addLabel.ID == removeLabel.ID {
|
|
||||||
// Silently drop invalid labels
|
|
||||||
if removeLabel.RepoID != issue.RepoID && removeLabel.OrgID != issue.Repo.OwnerID {
|
|
||||||
toRemove = append(toRemove, removeLabel)
|
|
||||||
}
|
|
||||||
|
|
||||||
addIndex++
|
|
||||||
removeIndex++
|
|
||||||
} else if addLabel.ID < removeLabel.ID {
|
|
||||||
// Only add if the label is valid
|
|
||||||
if addLabel.RepoID == issue.RepoID || addLabel.OrgID == issue.Repo.OwnerID {
|
|
||||||
toAdd = append(toAdd, addLabel)
|
|
||||||
}
|
|
||||||
addIndex++
|
|
||||||
} else {
|
|
||||||
toRemove = append(toRemove, removeLabel)
|
|
||||||
removeIndex++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toAdd = append(toAdd, labels[addIndex:]...)
|
|
||||||
toRemove = append(toRemove, issue.Labels[removeIndex:]...)
|
|
||||||
|
|
||||||
if len(toAdd) > 0 {
|
|
||||||
if err = newIssueLabels(ctx, issue, toAdd, doer); err != nil {
|
|
||||||
return fmt.Errorf("addLabels: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, l := range toRemove {
|
|
||||||
if err = deleteIssueLabel(ctx, issue, l, doer); err != nil {
|
|
||||||
return fmt.Errorf("removeLabel: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
issue.Labels = nil
|
|
||||||
if err = issue.LoadLabels(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return committer.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTasks returns the amount of tasks in the issues content
|
// GetTasks returns the amount of tasks in the issues content
|
||||||
func (issue *Issue) GetTasks() int {
|
func (issue *Issue) GetTasks() int {
|
||||||
return len(issueTasksPat.FindAllStringIndex(issue.Content, -1))
|
return len(issueTasksPat.FindAllStringIndex(issue.Content, -1))
|
||||||
@ -862,16 +680,6 @@ func (issue *Issue) GetExternalName() string { return issue.OriginalAuthor }
|
|||||||
// GetExternalID ExternalUserRemappable interface
|
// GetExternalID ExternalUserRemappable interface
|
||||||
func (issue *Issue) GetExternalID() int64 { return issue.OriginalAuthorID }
|
func (issue *Issue) GetExternalID() int64 { return issue.OriginalAuthorID }
|
||||||
|
|
||||||
// CountOrphanedIssues count issues without a repo
|
|
||||||
func CountOrphanedIssues(ctx context.Context) (int64, error) {
|
|
||||||
return db.GetEngine(ctx).
|
|
||||||
Table("issue").
|
|
||||||
Join("LEFT", "repository", "issue.repo_id=repository.id").
|
|
||||||
Where(builder.IsNull{"repository.id"}).
|
|
||||||
Select("COUNT(`issue`.`id`)").
|
|
||||||
Count()
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasOriginalAuthor returns if an issue was migrated and has an original author.
|
// HasOriginalAuthor returns if an issue was migrated and has an original author.
|
||||||
func (issue *Issue) HasOriginalAuthor() bool {
|
func (issue *Issue) HasOriginalAuthor() bool {
|
||||||
return issue.OriginalAuthor != "" && issue.OriginalAuthorID != 0
|
return issue.OriginalAuthor != "" && issue.OriginalAuthorID != 0
|
||||||
|
490
models/issues/issue_label.go
Normal file
490
models/issues/issue_label.go
Normal file
@ -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()
|
||||||
|
}
|
@ -22,7 +22,7 @@ import (
|
|||||||
// IssuesOptions represents options of an issue.
|
// IssuesOptions represents options of an issue.
|
||||||
type IssuesOptions struct { //nolint
|
type IssuesOptions struct { //nolint
|
||||||
db.ListOptions
|
db.ListOptions
|
||||||
RepoID int64 // overwrites RepoCond if not 0
|
RepoIDs []int64 // overwrites RepoCond if the length is not 0
|
||||||
RepoCond builder.Cond
|
RepoCond builder.Cond
|
||||||
AssigneeID int64
|
AssigneeID int64
|
||||||
PosterID int64
|
PosterID int64
|
||||||
@ -155,17 +155,24 @@ func applyMilestoneCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Sess
|
|||||||
return sess
|
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 {
|
func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
|
||||||
if len(opts.IssueIDs) > 0 {
|
if len(opts.IssueIDs) > 0 {
|
||||||
sess.In("issue.id", opts.IssueIDs)
|
sess.In("issue.id", opts.IssueIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.RepoID != 0 {
|
applyRepoConditions(sess, opts)
|
||||||
opts.RepoCond = builder.Eq{"issue.repo_id": opts.RepoID}
|
|
||||||
}
|
|
||||||
if opts.RepoCond != nil {
|
|
||||||
sess.And(opts.RepoCond)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !opts.IsClosed.IsNone() {
|
if !opts.IsClosed.IsNone() {
|
||||||
sess.And("issue.is_closed=?", opts.IsClosed.IsTrue())
|
sess.And("issue.is_closed=?", opts.IsClosed.IsTrue())
|
||||||
@ -400,31 +407,6 @@ func applySubscribedCondition(sess *xorm.Session, subscriberID int64) *xorm.Sess
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRepoIDsForIssuesOptions find all repo ids for the given options
|
// GetRepoIDsForIssuesOptions find all repo ids for the given options
|
||||||
func GetRepoIDsForIssuesOptions(opts *IssuesOptions, user *user_model.User) ([]int64, error) {
|
func GetRepoIDsForIssuesOptions(opts *IssuesOptions, user *user_model.User) ([]int64, error) {
|
||||||
repoIDs := make([]int64, 0, 5)
|
repoIDs := make([]int64, 0, 5)
|
||||||
@ -453,351 +435,18 @@ func Issues(ctx context.Context, opts *IssuesOptions) ([]*Issue, error) {
|
|||||||
applyConditions(sess, opts)
|
applyConditions(sess, opts)
|
||||||
applySorts(sess, opts.SortType, opts.PriorityRepoID)
|
applySorts(sess, opts.SortType, opts.PriorityRepoID)
|
||||||
|
|
||||||
issues := make([]*Issue, 0, opts.ListOptions.PageSize)
|
issues := make(IssueList, 0, opts.ListOptions.PageSize)
|
||||||
if err := sess.Find(&issues); err != nil {
|
if err := sess.Find(&issues); err != nil {
|
||||||
return nil, fmt.Errorf("unable to query Issues: %w", err)
|
return nil, fmt.Errorf("unable to query Issues: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := IssueList(issues).LoadAttributes(); err != nil {
|
if err := issues.LoadAttributes(); err != nil {
|
||||||
return nil, fmt.Errorf("unable to LoadAttributes for Issues: %w", err)
|
return nil, fmt.Errorf("unable to LoadAttributes for Issues: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return issues, nil
|
return issues, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CountIssues number return of issues by given conditions.
|
|
||||||
func CountIssues(ctx context.Context, opts *IssuesOptions) (int64, error) {
|
|
||||||
sess := db.GetEngine(ctx).
|
|
||||||
Select("COUNT(issue.id) AS count").
|
|
||||||
Table("issue").
|
|
||||||
Join("INNER", "repository", "`issue`.repo_id = `repository`.id")
|
|
||||||
applyConditions(sess, opts)
|
|
||||||
|
|
||||||
return sess.Count()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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).
|
|
||||||
Where("issue.repo_id = ?", opts.RepoID)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserIssueStatsOptions contains parameters accepted by GetUserIssueStats.
|
|
||||||
type UserIssueStatsOptions struct {
|
|
||||||
UserID int64
|
|
||||||
RepoIDs []int64
|
|
||||||
FilterMode int
|
|
||||||
IsPull bool
|
|
||||||
IsClosed bool
|
|
||||||
IssueIDs []int64
|
|
||||||
IsArchived util.OptionalBool
|
|
||||||
LabelIDs []int64
|
|
||||||
RepoCond builder.Cond
|
|
||||||
Org *organization.Organization
|
|
||||||
Team *organization.Team
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserIssueStats returns issue statistic information for dashboard by given conditions.
|
|
||||||
func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) {
|
|
||||||
var err error
|
|
||||||
stats := &IssueStats{}
|
|
||||||
|
|
||||||
cond := builder.NewCond()
|
|
||||||
cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull})
|
|
||||||
if len(opts.RepoIDs) > 0 {
|
|
||||||
cond = cond.And(builder.In("issue.repo_id", opts.RepoIDs))
|
|
||||||
}
|
|
||||||
if len(opts.IssueIDs) > 0 {
|
|
||||||
cond = cond.And(builder.In("issue.id", opts.IssueIDs))
|
|
||||||
}
|
|
||||||
if opts.RepoCond != nil {
|
|
||||||
cond = cond.And(opts.RepoCond)
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.UserID > 0 {
|
|
||||||
cond = cond.And(issuePullAccessibleRepoCond("issue.repo_id", opts.UserID, opts.Org, opts.Team, opts.IsPull))
|
|
||||||
}
|
|
||||||
|
|
||||||
sess := func(cond builder.Cond) *xorm.Session {
|
|
||||||
s := db.GetEngine(db.DefaultContext).Where(cond)
|
|
||||||
if len(opts.LabelIDs) > 0 {
|
|
||||||
s.Join("INNER", "issue_label", "issue_label.issue_id = issue.id").
|
|
||||||
In("issue_label.label_id", opts.LabelIDs)
|
|
||||||
}
|
|
||||||
if opts.UserID > 0 || opts.IsArchived != util.OptionalBoolNone {
|
|
||||||
s.Join("INNER", "repository", "issue.repo_id = repository.id")
|
|
||||||
if opts.IsArchived != util.OptionalBoolNone {
|
|
||||||
s.And(builder.Eq{"repository.is_archived": opts.IsArchived.IsTrue()})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
switch opts.FilterMode {
|
|
||||||
case FilterModeAll, FilterModeYourRepositories:
|
|
||||||
stats.OpenCount, err = sess(cond).
|
|
||||||
And("issue.is_closed = ?", false).
|
|
||||||
Count(new(Issue))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
stats.ClosedCount, err = sess(cond).
|
|
||||||
And("issue.is_closed = ?", true).
|
|
||||||
Count(new(Issue))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
case FilterModeAssign:
|
|
||||||
stats.OpenCount, err = applyAssigneeCondition(sess(cond), opts.UserID).
|
|
||||||
And("issue.is_closed = ?", false).
|
|
||||||
Count(new(Issue))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
stats.ClosedCount, err = applyAssigneeCondition(sess(cond), opts.UserID).
|
|
||||||
And("issue.is_closed = ?", true).
|
|
||||||
Count(new(Issue))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
case FilterModeCreate:
|
|
||||||
stats.OpenCount, err = applyPosterCondition(sess(cond), opts.UserID).
|
|
||||||
And("issue.is_closed = ?", false).
|
|
||||||
Count(new(Issue))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
stats.ClosedCount, err = applyPosterCondition(sess(cond), opts.UserID).
|
|
||||||
And("issue.is_closed = ?", true).
|
|
||||||
Count(new(Issue))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
case FilterModeMention:
|
|
||||||
stats.OpenCount, err = applyMentionedCondition(sess(cond), opts.UserID).
|
|
||||||
And("issue.is_closed = ?", false).
|
|
||||||
Count(new(Issue))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
stats.ClosedCount, err = applyMentionedCondition(sess(cond), opts.UserID).
|
|
||||||
And("issue.is_closed = ?", true).
|
|
||||||
Count(new(Issue))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
case FilterModeReviewRequested:
|
|
||||||
stats.OpenCount, err = applyReviewRequestedCondition(sess(cond), opts.UserID).
|
|
||||||
And("issue.is_closed = ?", false).
|
|
||||||
Count(new(Issue))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
stats.ClosedCount, err = applyReviewRequestedCondition(sess(cond), opts.UserID).
|
|
||||||
And("issue.is_closed = ?", true).
|
|
||||||
Count(new(Issue))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
case FilterModeReviewed:
|
|
||||||
stats.OpenCount, err = applyReviewedCondition(sess(cond), opts.UserID).
|
|
||||||
And("issue.is_closed = ?", false).
|
|
||||||
Count(new(Issue))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
stats.ClosedCount, err = applyReviewedCondition(sess(cond), opts.UserID).
|
|
||||||
And("issue.is_closed = ?", true).
|
|
||||||
Count(new(Issue))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cond = cond.And(builder.Eq{"issue.is_closed": opts.IsClosed})
|
|
||||||
stats.AssignCount, err = applyAssigneeCondition(sess(cond), opts.UserID).Count(new(Issue))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
stats.CreateCount, err = applyPosterCondition(sess(cond), opts.UserID).Count(new(Issue))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
stats.MentionCount, err = applyMentionedCondition(sess(cond), opts.UserID).Count(new(Issue))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
stats.YourRepositoriesCount, err = sess(cond).Count(new(Issue))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
stats.ReviewRequestedCount, err = applyReviewRequestedCondition(sess(cond), opts.UserID).Count(new(Issue))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
stats.ReviewedCount, err = applyReviewedCondition(sess(cond), opts.UserID).Count(new(Issue))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return stats, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRepoIssueStats returns number of open and closed repository issues by given filter mode.
|
|
||||||
func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen, numClosed int64) {
|
|
||||||
countSession := func(isClosed, isPull bool, repoID int64) *xorm.Session {
|
|
||||||
sess := db.GetEngine(db.DefaultContext).
|
|
||||||
Where("is_closed = ?", isClosed).
|
|
||||||
And("is_pull = ?", isPull).
|
|
||||||
And("repo_id = ?", repoID)
|
|
||||||
|
|
||||||
return sess
|
|
||||||
}
|
|
||||||
|
|
||||||
openCountSession := countSession(false, isPull, repoID)
|
|
||||||
closedCountSession := countSession(true, isPull, repoID)
|
|
||||||
|
|
||||||
switch filterMode {
|
|
||||||
case FilterModeAssign:
|
|
||||||
applyAssigneeCondition(openCountSession, uid)
|
|
||||||
applyAssigneeCondition(closedCountSession, uid)
|
|
||||||
case FilterModeCreate:
|
|
||||||
applyPosterCondition(openCountSession, uid)
|
|
||||||
applyPosterCondition(closedCountSession, uid)
|
|
||||||
}
|
|
||||||
|
|
||||||
openResult, _ := openCountSession.Count(new(Issue))
|
|
||||||
closedResult, _ := closedCountSession.Count(new(Issue))
|
|
||||||
|
|
||||||
return openResult, closedResult
|
|
||||||
}
|
|
||||||
|
|
||||||
// SearchIssueIDsByKeyword search issues on database
|
// SearchIssueIDsByKeyword search issues on database
|
||||||
func SearchIssueIDsByKeyword(ctx context.Context, kw string, repoIDs []int64, limit, start int) (int64, []int64, error) {
|
func SearchIssueIDsByKeyword(ctx context.Context, kw string, repoIDs []int64, limit, start int) (int64, []int64, error) {
|
||||||
repoCond := builder.In("repo_id", repoIDs)
|
repoCond := builder.In("repo_id", repoIDs)
|
||||||
|
383
models/issues/issue_stats.go
Normal file
383
models/issues/issue_stats.go
Normal file
@ -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()
|
||||||
|
}
|
@ -17,6 +17,7 @@ import (
|
|||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
@ -204,14 +205,16 @@ func TestIssues(t *testing.T) {
|
|||||||
func TestGetUserIssueStats(t *testing.T) {
|
func TestGetUserIssueStats(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
Opts issues_model.UserIssueStatsOptions
|
FilterMode int
|
||||||
|
Opts issues_model.IssuesOptions
|
||||||
ExpectedIssueStats issues_model.IssueStats
|
ExpectedIssueStats issues_model.IssueStats
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
issues_model.UserIssueStatsOptions{
|
issues_model.FilterModeAll,
|
||||||
UserID: 1,
|
issues_model.IssuesOptions{
|
||||||
RepoIDs: []int64{1},
|
User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}),
|
||||||
FilterMode: issues_model.FilterModeAll,
|
RepoIDs: []int64{1},
|
||||||
|
IsPull: util.OptionalBoolFalse,
|
||||||
},
|
},
|
||||||
issues_model.IssueStats{
|
issues_model.IssueStats{
|
||||||
YourRepositoriesCount: 1, // 6
|
YourRepositoriesCount: 1, // 6
|
||||||
@ -222,11 +225,12 @@ func TestGetUserIssueStats(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
issues_model.UserIssueStatsOptions{
|
issues_model.FilterModeAll,
|
||||||
UserID: 1,
|
issues_model.IssuesOptions{
|
||||||
RepoIDs: []int64{1},
|
User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}),
|
||||||
FilterMode: issues_model.FilterModeAll,
|
RepoIDs: []int64{1},
|
||||||
IsClosed: true,
|
IsPull: util.OptionalBoolFalse,
|
||||||
|
IsClosed: util.OptionalBoolTrue,
|
||||||
},
|
},
|
||||||
issues_model.IssueStats{
|
issues_model.IssueStats{
|
||||||
YourRepositoriesCount: 1, // 6
|
YourRepositoriesCount: 1, // 6
|
||||||
@ -237,9 +241,10 @@ func TestGetUserIssueStats(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
issues_model.UserIssueStatsOptions{
|
issues_model.FilterModeAssign,
|
||||||
UserID: 1,
|
issues_model.IssuesOptions{
|
||||||
FilterMode: issues_model.FilterModeAssign,
|
User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}),
|
||||||
|
IsPull: util.OptionalBoolFalse,
|
||||||
},
|
},
|
||||||
issues_model.IssueStats{
|
issues_model.IssueStats{
|
||||||
YourRepositoriesCount: 1, // 6
|
YourRepositoriesCount: 1, // 6
|
||||||
@ -250,9 +255,10 @@ func TestGetUserIssueStats(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
issues_model.UserIssueStatsOptions{
|
issues_model.FilterModeCreate,
|
||||||
UserID: 1,
|
issues_model.IssuesOptions{
|
||||||
FilterMode: issues_model.FilterModeCreate,
|
User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}),
|
||||||
|
IsPull: util.OptionalBoolFalse,
|
||||||
},
|
},
|
||||||
issues_model.IssueStats{
|
issues_model.IssueStats{
|
||||||
YourRepositoriesCount: 1, // 6
|
YourRepositoriesCount: 1, // 6
|
||||||
@ -263,9 +269,10 @@ func TestGetUserIssueStats(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
issues_model.UserIssueStatsOptions{
|
issues_model.FilterModeMention,
|
||||||
UserID: 1,
|
issues_model.IssuesOptions{
|
||||||
FilterMode: issues_model.FilterModeMention,
|
User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}),
|
||||||
|
IsPull: util.OptionalBoolFalse,
|
||||||
},
|
},
|
||||||
issues_model.IssueStats{
|
issues_model.IssueStats{
|
||||||
YourRepositoriesCount: 1, // 6
|
YourRepositoriesCount: 1, // 6
|
||||||
@ -277,10 +284,11 @@ func TestGetUserIssueStats(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
issues_model.UserIssueStatsOptions{
|
issues_model.FilterModeCreate,
|
||||||
UserID: 1,
|
issues_model.IssuesOptions{
|
||||||
FilterMode: issues_model.FilterModeCreate,
|
User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}),
|
||||||
IssueIDs: []int64{1},
|
IssueIDs: []int64{1},
|
||||||
|
IsPull: util.OptionalBoolFalse,
|
||||||
},
|
},
|
||||||
issues_model.IssueStats{
|
issues_model.IssueStats{
|
||||||
YourRepositoriesCount: 1, // 1
|
YourRepositoriesCount: 1, // 1
|
||||||
@ -291,11 +299,12 @@ func TestGetUserIssueStats(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
issues_model.UserIssueStatsOptions{
|
issues_model.FilterModeAll,
|
||||||
UserID: 2,
|
issues_model.IssuesOptions{
|
||||||
Org: unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}),
|
User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}),
|
||||||
Team: unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 7}),
|
Org: unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}),
|
||||||
FilterMode: issues_model.FilterModeAll,
|
Team: unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 7}),
|
||||||
|
IsPull: util.OptionalBoolFalse,
|
||||||
},
|
},
|
||||||
issues_model.IssueStats{
|
issues_model.IssueStats{
|
||||||
YourRepositoriesCount: 2,
|
YourRepositoriesCount: 2,
|
||||||
@ -306,7 +315,7 @@ func TestGetUserIssueStats(t *testing.T) {
|
|||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(fmt.Sprintf("%#v", test.Opts), func(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) {
|
if !assert.NoError(t, err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -495,7 +504,7 @@ func TestCorrectIssueStats(t *testing.T) {
|
|||||||
// Now we will call the GetIssueStats with these IDs and if working,
|
// Now we will call the GetIssueStats with these IDs and if working,
|
||||||
// get the correct stats back.
|
// get the correct stats back.
|
||||||
issueStats, err := issues_model.GetIssueStats(&issues_model.IssuesOptions{
|
issueStats, err := issues_model.GetIssueStats(&issues_model.IssuesOptions{
|
||||||
RepoID: 1,
|
RepoIDs: []int64{1},
|
||||||
IssueIDs: ids,
|
IssueIDs: ids,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update issue count of labels
|
// Update issue count of labels
|
||||||
if err := issue.getLabels(ctx); err != nil {
|
if err := issue.LoadLabels(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for idx := range issue.Labels {
|
for idx := range issue.Labels {
|
||||||
|
@ -11,7 +11,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"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/label"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"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
|
// CalOpenOrgIssues calculates the open issues of a label for a specific repo
|
||||||
func (l *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) {
|
func (l *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) {
|
||||||
counts, _ := CountIssuesByRepo(ctx, &IssuesOptions{
|
counts, _ := CountIssuesByRepo(ctx, &IssuesOptions{
|
||||||
RepoID: repoID,
|
RepoIDs: []int64{repoID},
|
||||||
LabelIDs: []int64{labelID},
|
LabelIDs: []int64{labelID},
|
||||||
IsClosed: util.OptionalBoolFalse,
|
IsClosed: util.OptionalBoolFalse,
|
||||||
})
|
})
|
||||||
@ -282,13 +281,6 @@ func GetLabelsByIDs(labelIDs []int64) ([]*Label, error) {
|
|||||||
Find(&labels)
|
Find(&labels)
|
||||||
}
|
}
|
||||||
|
|
||||||
// __________ .__ __
|
|
||||||
// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__.
|
|
||||||
// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | |
|
|
||||||
// | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ |
|
|
||||||
// |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____|
|
|
||||||
// \/ \/|__| \/ \/
|
|
||||||
|
|
||||||
// GetLabelInRepoByName returns a label by name in given repository.
|
// GetLabelInRepoByName returns a label by name in given repository.
|
||||||
func GetLabelInRepoByName(ctx context.Context, repoID int64, labelName string) (*Label, error) {
|
func GetLabelInRepoByName(ctx context.Context, repoID int64, labelName string) (*Label, error) {
|
||||||
if len(labelName) == 0 || repoID <= 0 {
|
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{})
|
return db.GetEngine(db.DefaultContext).Where("repo_id = ?", repoID).Count(&Label{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ________
|
|
||||||
// \_____ \_______ ____
|
|
||||||
// / | \_ __ \/ ___\
|
|
||||||
// / | \ | \/ /_/ >
|
|
||||||
// \_______ /__| \___ /
|
|
||||||
// \/ /_____/
|
|
||||||
|
|
||||||
// GetLabelInOrgByName returns a label by name in given organization.
|
// GetLabelInOrgByName returns a label by name in given organization.
|
||||||
func GetLabelInOrgByName(ctx context.Context, orgID int64, labelName string) (*Label, error) {
|
func GetLabelInOrgByName(ctx context.Context, orgID int64, labelName string) (*Label, error) {
|
||||||
if len(labelName) == 0 || orgID <= 0 {
|
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{})
|
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 {
|
func updateLabelCols(ctx context.Context, l *Label, cols ...string) error {
|
||||||
_, err := db.GetEngine(ctx).ID(l.ID).
|
_, err := db.GetEngine(ctx).ID(l.ID).
|
||||||
SetExpr("num_issues",
|
SetExpr("num_issues",
|
||||||
@ -529,307 +498,3 @@ func updateLabelCols(ctx context.Context, l *Label, cols ...string) error {
|
|||||||
Cols(cols...).Update(l)
|
Cols(cols...).Update(l)
|
||||||
return err
|
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()
|
|
||||||
}
|
|
||||||
|
@ -491,6 +491,8 @@ var migrations = []Migration{
|
|||||||
NewMigration("Add ArchivedUnix Column", v1_20.AddArchivedUnixToRepository),
|
NewMigration("Add ArchivedUnix Column", v1_20.AddArchivedUnixToRepository),
|
||||||
// v256 -> v257
|
// v256 -> v257
|
||||||
NewMigration("Add is_internal column to package", v1_20.AddIsInternalColumnToPackage),
|
NewMigration("Add is_internal column to package", v1_20.AddIsInternalColumnToPackage),
|
||||||
|
// v257 -> v258
|
||||||
|
NewMigration("Add Actions Artifact table", v1_20.CreateActionArtifactTable),
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentDBVersion returns the current db version
|
// GetCurrentDBVersion returns the current db version
|
||||||
|
33
models/migrations/v1_20/v257.go
Normal file
33
models/migrations/v1_20/v257.go
Normal file
@ -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)
|
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.
|
// In case is a organization.
|
||||||
org, err := user_model.GetUserByID(ctx, uid)
|
org, err := user_model.GetUserByID(ctx, uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -164,6 +170,7 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error {
|
|||||||
&actions_model.ActionRunJob{RepoID: repoID},
|
&actions_model.ActionRunJob{RepoID: repoID},
|
||||||
&actions_model.ActionRun{RepoID: repoID},
|
&actions_model.ActionRun{RepoID: repoID},
|
||||||
&actions_model.ActionRunner{RepoID: repoID},
|
&actions_model.ActionRunner{RepoID: repoID},
|
||||||
|
&actions_model.ActionArtifact{RepoID: repoID},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return fmt.Errorf("deleteBeans: %w", err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,7 +126,7 @@ func MainTest(m *testing.M, testOpts *TestOptions) {
|
|||||||
|
|
||||||
setting.Packages.Storage.Path = filepath.Join(setting.AppDataPath, "packages")
|
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")
|
setting.Git.HomePath = filepath.Join(setting.AppDataPath, "home")
|
||||||
|
|
||||||
|
@ -302,7 +302,7 @@ func populateIssueIndexer(ctx context.Context) {
|
|||||||
// UpdateRepoIndexer add/update all issues of the repositories
|
// UpdateRepoIndexer add/update all issues of the repositories
|
||||||
func UpdateRepoIndexer(ctx context.Context, repo *repo_model.Repository) {
|
func UpdateRepoIndexer(ctx context.Context, repo *repo_model.Repository) {
|
||||||
is, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
|
is, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
|
||||||
RepoID: repo.ID,
|
RepoIDs: []int64{repo.ID},
|
||||||
IsClosed: util.OptionalBoolNone,
|
IsClosed: util.OptionalBoolNone,
|
||||||
IsPull: util.OptionalBoolNone,
|
IsPull: util.OptionalBoolNone,
|
||||||
})
|
})
|
||||||
|
@ -22,7 +22,10 @@ type Sanitizer struct {
|
|||||||
init sync.Once
|
init sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
var sanitizer = &Sanitizer{}
|
var (
|
||||||
|
sanitizer = &Sanitizer{}
|
||||||
|
allowAllRegex = regexp.MustCompile(".+")
|
||||||
|
)
|
||||||
|
|
||||||
// NewSanitizer initializes sanitizer with allowed attributes based on settings.
|
// NewSanitizer initializes sanitizer with allowed attributes based on settings.
|
||||||
// Multiple calls to this function will only create one instance of Sanitizer during
|
// Multiple calls to this function will only create one instance of Sanitizer during
|
||||||
@ -74,6 +77,8 @@ func createDefaultPolicy() *bluemonday.Policy {
|
|||||||
// Custom URL-Schemes
|
// Custom URL-Schemes
|
||||||
if len(setting.Markdown.CustomURLSchemes) > 0 {
|
if len(setting.Markdown.CustomURLSchemes) > 0 {
|
||||||
policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...)
|
policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...)
|
||||||
|
} else {
|
||||||
|
policy.AllowURLSchemesMatching(allowAllRegex)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow classes for anchors
|
// 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>`,
|
`<span style="bad-color: red">Hello World</span>`, `<span>Hello World</span>`,
|
||||||
`<p style="bad-color: red">Hello World</p>`, `<p>Hello World</p>`,
|
`<p style="bad-color: red">Hello World</p>`, `<p>Hello World</p>`,
|
||||||
`<code style="bad-color: red">Hello World</code>`, `<code>Hello World</code>`,
|
`<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 {
|
for i := 0; i < len(testCases); i += 2 {
|
||||||
|
@ -10,7 +10,8 @@ import (
|
|||||||
// Actions settings
|
// Actions settings
|
||||||
var (
|
var (
|
||||||
Actions = struct {
|
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
|
Enabled bool
|
||||||
DefaultActionsURL string `ini:"DEFAULT_ACTIONS_URL"`
|
DefaultActionsURL string `ini:"DEFAULT_ACTIONS_URL"`
|
||||||
}{
|
}{
|
||||||
@ -25,5 +26,9 @@ func loadActionsFrom(rootCfg ConfigProvider) {
|
|||||||
log.Fatal("Failed to map Actions settings: %v", err)
|
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)
|
||||||
}
|
}
|
||||||
|
@ -306,11 +306,11 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
|
|||||||
log.Fatal("Failed to map Repository.PullRequest settings: %v", err)
|
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")
|
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")
|
Repository.DisabledRepoUnits = append(Repository.DisabledRepoUnits, "repo.actions")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,6 +128,8 @@ var (
|
|||||||
|
|
||||||
// Actions represents actions storage
|
// Actions represents actions storage
|
||||||
Actions ObjectStorage = uninitializedStorage
|
Actions ObjectStorage = uninitializedStorage
|
||||||
|
// Actions Artifacts represents actions artifacts storage
|
||||||
|
ActionsArtifacts ObjectStorage = uninitializedStorage
|
||||||
)
|
)
|
||||||
|
|
||||||
// Init init the stoarge
|
// Init init the stoarge
|
||||||
@ -212,9 +214,14 @@ func initPackages() (err error) {
|
|||||||
func initActions() (err error) {
|
func initActions() (err error) {
|
||||||
if !setting.Actions.Enabled {
|
if !setting.Actions.Enabled {
|
||||||
Actions = discardStorage("Actions isn't enabled")
|
Actions = discardStorage("Actions isn't enabled")
|
||||||
|
ActionsArtifacts = discardStorage("ActionsArtifacts isn't enabled")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
log.Info("Initialising Actions storage with type: %s", setting.Actions.Storage.Type)
|
log.Info("Initialising Actions storage with type: %s", setting.Actions.LogStorage.Type)
|
||||||
Actions, err = NewStorage(setting.Actions.Storage.Type, &setting.Actions.Storage)
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
@ -114,6 +114,8 @@ unknown = Unknown
|
|||||||
|
|
||||||
rss_feed = RSS Feed
|
rss_feed = RSS Feed
|
||||||
|
|
||||||
|
artifacts = Artifacts
|
||||||
|
|
||||||
concept_system_global = Global
|
concept_system_global = Global
|
||||||
concept_user_individual = Individual
|
concept_user_individual = Individual
|
||||||
concept_code_repository = Repository
|
concept_code_repository = Repository
|
||||||
|
@ -318,6 +318,7 @@ repo_no_results=Aucun dépôt correspondant n'a été trouvé.
|
|||||||
user_no_results=Aucun utilisateur correspondant n'a été trouvé.
|
user_no_results=Aucun utilisateur correspondant n'a été trouvé.
|
||||||
org_no_results=Aucune organisation correspondante n'a été trouvée.
|
org_no_results=Aucune organisation correspondante n'a été trouvée.
|
||||||
code_no_results=Aucun code source correspondant à votre terme de recherche n'a été trouvé.
|
code_no_results=Aucun code source correspondant à votre terme de recherche n'a été trouvé.
|
||||||
|
code_search_results=Résultats de la recherche pour « %s »
|
||||||
code_last_indexed_at=Dernière indexation %s
|
code_last_indexed_at=Dernière indexation %s
|
||||||
relevant_repositories_tooltip=Les dépôts qui sont des forks ou qui n'ont aucun sujet, aucune icône et aucune description sont cachés.
|
relevant_repositories_tooltip=Les dépôts qui sont des forks ou qui n'ont aucun sujet, aucune icône et aucune description sont cachés.
|
||||||
relevant_repositories=Seuls les dépôts pertinents sont affichés, <a href="%s">afficher les résultats non filtrés</a>.
|
relevant_repositories=Seuls les dépôts pertinents sont affichés, <a href="%s">afficher les résultats non filtrés</a>.
|
||||||
@ -394,6 +395,7 @@ password_pwned_err=Impossible d'envoyer la demande à HaveIBeenPwned
|
|||||||
|
|
||||||
[mail]
|
[mail]
|
||||||
view_it_on=Voir sur %s
|
view_it_on=Voir sur %s
|
||||||
|
reply=ou répondez directement à cet e-mail
|
||||||
link_not_working_do_paste=Le lien ne fonctionne pas ? Essayez de le copier-coller dans votre navigateur.
|
link_not_working_do_paste=Le lien ne fonctionne pas ? Essayez de le copier-coller dans votre navigateur.
|
||||||
hi_user_x=Bonjour <b>%s</b>,
|
hi_user_x=Bonjour <b>%s</b>,
|
||||||
|
|
||||||
@ -495,6 +497,7 @@ size_error=` doit être à la taille de %s.`
|
|||||||
min_size_error=` %s caractères minimum `
|
min_size_error=` %s caractères minimum `
|
||||||
max_size_error=` %s caractères maximum `
|
max_size_error=` %s caractères maximum `
|
||||||
email_error=` adresse e-mail invalide `
|
email_error=` adresse e-mail invalide `
|
||||||
|
url_error=`"%s" n'est pas une URL valide.`
|
||||||
include_error=` doit contenir la sous-chaîne "%s".`
|
include_error=` doit contenir la sous-chaîne "%s".`
|
||||||
glob_pattern_error=` le motif de développement est invalide : %s.`
|
glob_pattern_error=` le motif de développement est invalide : %s.`
|
||||||
regex_pattern_error=` le motif regex est invalide : %s.`
|
regex_pattern_error=` le motif regex est invalide : %s.`
|
||||||
@ -540,8 +543,10 @@ organization_leave_success=Vous avez quitté l'organisation %s avec succès.
|
|||||||
invalid_ssh_key=Impossible de vérifier votre clé SSH : %s
|
invalid_ssh_key=Impossible de vérifier votre clé SSH : %s
|
||||||
invalid_gpg_key=Impossible de vérifier votre clé GPG : %s
|
invalid_gpg_key=Impossible de vérifier votre clé GPG : %s
|
||||||
invalid_ssh_principal=Principal invalide : %s
|
invalid_ssh_principal=Principal invalide : %s
|
||||||
|
must_use_public_key=La clé que vous avez fournie est une clé privée. Veuillez ne pas divulguer votre clé privée. Utilisez votre clé publique à la place.
|
||||||
auth_failed=Échec d'authentification : %v
|
auth_failed=Échec d'authentification : %v
|
||||||
|
|
||||||
|
still_own_repo=Votre compte possède toujours un ou plusieurs dépôts, vous devez d’abord les supprimer ou les transférer.
|
||||||
still_has_org=Votre compte est un membre d’une ou plusieurs organisations, veuillez d’abord les quitter.
|
still_has_org=Votre compte est un membre d’une ou plusieurs organisations, veuillez d’abord les quitter.
|
||||||
still_own_packages=Votre compte possède toujours un ou plusieurs paquets, vous devez d’abord les supprimer ou les transférer.
|
still_own_packages=Votre compte possède toujours un ou plusieurs paquets, vous devez d’abord les supprimer ou les transférer.
|
||||||
org_still_own_repo=Cette organisation possède encore un ou plusieurs dépôts. Vous devez d’abord les supprimer ou les transférer.
|
org_still_own_repo=Cette organisation possède encore un ou plusieurs dépôts. Vous devez d’abord les supprimer ou les transférer.
|
||||||
@ -569,6 +574,7 @@ email_visibility.limited=Votre adresse e-mail est visible pour tous les utilisat
|
|||||||
email_visibility.private=Votre adresse e-mail n'est visible que pour vous et les administrateurs
|
email_visibility.private=Votre adresse e-mail n'est visible que pour vous et les administrateurs
|
||||||
|
|
||||||
form.name_reserved=Le nom d’utilisateur "%s" est réservé.
|
form.name_reserved=Le nom d’utilisateur "%s" est réservé.
|
||||||
|
form.name_pattern_not_allowed=Le motif "%s" n'est pas autorisé dans un nom de d'utilisateur.
|
||||||
form.name_chars_not_allowed=Le nom d'utilisateur "%s" contient des caractères non valides.
|
form.name_chars_not_allowed=Le nom d'utilisateur "%s" contient des caractères non valides.
|
||||||
|
|
||||||
[settings]
|
[settings]
|
||||||
@ -578,6 +584,7 @@ appearance=Apparence
|
|||||||
password=Mot de passe
|
password=Mot de passe
|
||||||
security=Sécurité
|
security=Sécurité
|
||||||
avatar=Avatar
|
avatar=Avatar
|
||||||
|
ssh_gpg_keys=Clés SSH / GPG
|
||||||
social=Réseaux Sociaux
|
social=Réseaux Sociaux
|
||||||
applications=Applications
|
applications=Applications
|
||||||
orgs=Gérer les organisations
|
orgs=Gérer les organisations
|
||||||
@ -586,10 +593,13 @@ delete=Supprimer le compte
|
|||||||
twofa=Authentification à deux facteurs
|
twofa=Authentification à deux facteurs
|
||||||
account_link=Comptes liés
|
account_link=Comptes liés
|
||||||
organization=Organisations
|
organization=Organisations
|
||||||
|
uid=Uid
|
||||||
|
webauthn=Clés de sécurité
|
||||||
|
|
||||||
public_profile=Profil public
|
public_profile=Profil public
|
||||||
biography_placeholder=Parlez-nous un peu de vous.
|
biography_placeholder=Parlez-nous un peu de vous.
|
||||||
profile_desc=Votre adresse e-mail sera utilisée pour les notifications et d'autres opérations.
|
profile_desc=Votre adresse e-mail sera utilisée pour les notifications et d'autres opérations.
|
||||||
|
password_username_disabled=Les utilisateurs externes ne sont pas autorisés à modifier leur nom d'utilisateur. Veuillez contacter l'administrateur de votre site pour plus de détails.
|
||||||
full_name=Non Complet
|
full_name=Non Complet
|
||||||
website=Site Web
|
website=Site Web
|
||||||
location=Localisation
|
location=Localisation
|
||||||
@ -604,12 +614,19 @@ cancel=Annuler
|
|||||||
language=Langue
|
language=Langue
|
||||||
ui=Thème
|
ui=Thème
|
||||||
hidden_comment_types=Types de commentaires masqués
|
hidden_comment_types=Types de commentaires masqués
|
||||||
|
hidden_comment_types.issue_ref_tooltip=Commentaires où l’utilisateur change la branche/étiquette associée au ticket
|
||||||
comment_type_group_reference=Référence
|
comment_type_group_reference=Référence
|
||||||
|
comment_type_group_label=Libellé
|
||||||
comment_type_group_milestone=Jalon
|
comment_type_group_milestone=Jalon
|
||||||
comment_type_group_assignee=Assigné à
|
comment_type_group_assignee=Assigné à
|
||||||
comment_type_group_title=Titre
|
comment_type_group_title=Titre
|
||||||
comment_type_group_branch=Branche
|
comment_type_group_branch=Branche
|
||||||
|
comment_type_group_time_tracking=Minuteur
|
||||||
|
comment_type_group_deadline=Échéance
|
||||||
comment_type_group_dependency=Dépendance
|
comment_type_group_dependency=Dépendance
|
||||||
|
comment_type_group_lock=Verrouiller le statut
|
||||||
|
comment_type_group_review_request=Demande de revue
|
||||||
|
comment_type_group_pull_request_push=Révisions ajoutées
|
||||||
comment_type_group_project=Projet
|
comment_type_group_project=Projet
|
||||||
comment_type_group_issue_ref=Référence du ticket
|
comment_type_group_issue_ref=Référence du ticket
|
||||||
saved_successfully=Vos paramètres ont été enregistrés avec succès.
|
saved_successfully=Vos paramètres ont été enregistrés avec succès.
|
||||||
@ -618,38 +635,123 @@ keep_activity_private=Masquer l'activité de la page de profil
|
|||||||
keep_activity_private_popup=Rend l'activité visible uniquement pour vous et les administrateurs
|
keep_activity_private_popup=Rend l'activité visible uniquement pour vous et les administrateurs
|
||||||
|
|
||||||
lookup_avatar_by_mail=Rechercher un avatar par adresse e-mail
|
lookup_avatar_by_mail=Rechercher un avatar par adresse e-mail
|
||||||
|
federated_avatar_lookup=Recherche d'avatars fédérés
|
||||||
enable_custom_avatar=Utiliser un avatar personnalisé
|
enable_custom_avatar=Utiliser un avatar personnalisé
|
||||||
choose_new_avatar=Sélectionner un nouvel avatar
|
choose_new_avatar=Sélectionner un nouvel avatar
|
||||||
delete_current_avatar=Supprimer l'avatar actuel
|
delete_current_avatar=Supprimer l'avatar actuel
|
||||||
uploaded_avatar_not_a_image=Le fichier téléchargé n'est pas une image.
|
uploaded_avatar_not_a_image=Le fichier téléchargé n'est pas une image.
|
||||||
|
uploaded_avatar_is_too_big=Le fichier téléchargé dépasse la taille limite.
|
||||||
update_avatar_success=Votre avatar a été mis à jour.
|
update_avatar_success=Votre avatar a été mis à jour.
|
||||||
update_user_avatar_success=L'avatar de l'utilisateur a été mis à jour.
|
update_user_avatar_success=L'avatar de l'utilisateur a été mis à jour.
|
||||||
|
|
||||||
|
old_password=Mot de passe actuel
|
||||||
|
new_password=Nouveau mot de passe
|
||||||
|
retype_new_password=Retapez le nouveau mot de passe
|
||||||
|
password_incorrect=Le mot de passe actuel est incorrect.
|
||||||
|
change_password_success=Votre mot de passe a été mis à jour. Désormais, connectez-vous avec votre nouveau mot de passe.
|
||||||
password_change_disabled=Les mots de passes des comptes utilisateurs externes ne peuvent pas être modifiées depuis l'interface web Gitea.
|
password_change_disabled=Les mots de passes des comptes utilisateurs externes ne peuvent pas être modifiées depuis l'interface web Gitea.
|
||||||
|
|
||||||
|
emails=Adresses e-mail
|
||||||
|
manage_emails=Gérer les adresses e-mail
|
||||||
|
manage_themes=Sélectionner le thème par défaut
|
||||||
|
manage_openid=Gérer les adresses OpenID
|
||||||
|
email_desc=Votre adresse e-mail principale sera utilisée pour les notifications et d'autres opérations.
|
||||||
|
theme_desc=Ce sera votre thème par défaut sur le site.
|
||||||
primary=Principale
|
primary=Principale
|
||||||
activated=Activé
|
activated=Activé
|
||||||
|
requires_activation=Nécessite une activation
|
||||||
|
primary_email=Faire de cette adresse votre adresse principale
|
||||||
delete_email=Exclure
|
delete_email=Exclure
|
||||||
openid_deletion_success=L'adresse OpenID a été supprimée.
|
openid_deletion_success=L'adresse OpenID a été supprimée.
|
||||||
add_new_openid=Ajouter une nouvelle URI OpenID
|
add_new_openid=Ajouter une nouvelle URI OpenID
|
||||||
|
add_openid=Ajouter une URI OpenID
|
||||||
|
email_preference_set_success=L'e-mail de préférence a été défini avec succès.
|
||||||
|
add_openid_success=La nouvelle adresse OpenID a été ajoutée.
|
||||||
|
keep_email_private=Cacher l'adresse e-mail
|
||||||
|
keep_email_private_popup=Votre adresse e-mail ne sera visible que de vous et des administrateurs
|
||||||
|
openid_desc=OpenID vous permet de confier l'authentification à une tierce partie.
|
||||||
|
|
||||||
|
manage_ssh_keys=Gérer les clés SSH
|
||||||
|
manage_ssh_principals=Gérer les certificats principaux SSH
|
||||||
|
manage_gpg_keys=Gérer les clés GPG
|
||||||
|
add_key=Ajouter une clé
|
||||||
|
ssh_desc=Ces clefs SSH publiques sont associées à votre compte. Les clefs privées correspondantes permettent l'accès complet à vos repos.
|
||||||
|
principal_desc=Ces Principaux de certificats SSH sont associés à votre compte et permettent un accès complet à vos dépôts.
|
||||||
|
gpg_desc=Ces clefs GPG sont associées avec votre compte. Conservez-les en lieu sûr, car elles permettent la vérification de vos commits.
|
||||||
|
ssh_helper=<strong>Besoin d'aide ?</strong> Consultez le guide de GitHub pour <a href="%s">créer vos propres clés SSH</a> ou <a href="%s">résoudre les problèmes courants</a> que vous pourriez rencontrer en utilisant SSH.
|
||||||
|
gpg_helper=<strong>Besoin d'aide ?</strong> Consultez le guide de GitHub <a href="%s">sur GPG</a>.
|
||||||
|
add_new_key=Ajouter une clé SSH
|
||||||
|
add_new_gpg_key=Ajouter une clé GPG
|
||||||
|
key_content_ssh_placeholder=Commence par 'ssh-ed25519', 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'sk-ecdsa-sha2-nistp256@openssh.com' ou par 'sk-ssh-ed25519@openssh.com'
|
||||||
|
key_content_gpg_placeholder=Commence par '-----BEGIN PGP PUBLIC KEY BLOCK-----'
|
||||||
|
add_new_principal=Ajouter le principal
|
||||||
|
ssh_key_been_used=Cette clé SSH a déjà été ajoutée au serveur.
|
||||||
|
ssh_key_name_used=Une clé SSH avec le même nom existe déjà sur votre compte.
|
||||||
|
ssh_principal_been_used=Ce principal a déjà été ajouté au serveur.
|
||||||
|
gpg_key_id_used=Une clé publique GPG avec le même ID existe déjà.
|
||||||
|
gpg_no_key_email_found=Cette clé GPG ne correspond à aucune adresse e-mail activée associée à votre compte. Elle peut toujours être ajoutée si vous signez le jeton fourni.
|
||||||
|
gpg_key_matched_identities=Identités correspondantes :
|
||||||
|
gpg_key_matched_identities_long=Les identités intégrées dans cette clé correspondent aux adresses e-mail activées suivantes pour cet utilisateur. Les commits correspondant à ces adresses e-mail peuvent être vérifiés avec cette clé.
|
||||||
|
gpg_key_verified=Clé vérifiée
|
||||||
|
gpg_key_verified_long=La clé a été vérifiée avec un jeton et peut être utilisée pour vérifier les révisions correspondant à toutes les adresses e-mails pour cet utilisateur en plus de toutes les identités pour cette clé.
|
||||||
gpg_key_verify=Vérifier
|
gpg_key_verify=Vérifier
|
||||||
|
gpg_invalid_token_signature=La clé GPG, la signature et le jeton fournis ne correspondent pas ou le jeton n'est pas à jour.
|
||||||
|
gpg_token_required=Vous devez fournir une signature pour le jeton ci-dessous
|
||||||
gpg_token=Jeton
|
gpg_token=Jeton
|
||||||
|
gpg_token_help=Vous pouvez générer une signature en utilisant :
|
||||||
|
gpg_token_signature=Signature GPG renforcée
|
||||||
|
key_signature_gpg_placeholder=Commence par '-----BEGIN PGP SIGNATURE-----'
|
||||||
|
verify_gpg_key_success=La clé GPG "%s" a été vérifiée.
|
||||||
|
ssh_key_verified=Clé vérifiée
|
||||||
|
ssh_key_verified_long=La clé a été vérifiée avec un jeton et peut être utilisée pour vérifier les commits correspondant à toutes les adresses mail activées pour cet utilisateur.
|
||||||
ssh_key_verify=Vérifier
|
ssh_key_verify=Vérifier
|
||||||
|
ssh_invalid_token_signature=La clé SSH, la signature ou le jeton fournis ne correspondent pas ou le jeton est périmé.
|
||||||
|
ssh_token_required=Vous devez fournir une signature pour le jeton ci-dessous
|
||||||
ssh_token=Jeton
|
ssh_token=Jeton
|
||||||
|
ssh_token_help=Vous pouvez générer une signature en utilisant :
|
||||||
|
ssh_token_signature=Signature SSH renforcée
|
||||||
|
key_signature_ssh_placeholder=Commence par '-----BEGIN SSH SIGNATURE-----'
|
||||||
|
verify_ssh_key_success=La clé SSH "%s" a été vérifiée.
|
||||||
|
subkeys=Sous-clés
|
||||||
|
key_id=ID de la clé
|
||||||
|
key_name=Nom de la Clé
|
||||||
key_content=Contenu
|
key_content=Contenu
|
||||||
principal_content=Contenu
|
principal_content=Contenu
|
||||||
delete_key=Exclure
|
delete_key=Exclure
|
||||||
|
gpg_key_deletion_desc=Supprimer une clé GPG renie les révisions signées par celle-ci. Continuer ?
|
||||||
can_read_info=Lue(s)
|
can_read_info=Lue(s)
|
||||||
|
can_write_info=Écriture
|
||||||
|
|
||||||
delete_token=Supprimer
|
delete_token=Supprimer
|
||||||
access_token_deletion_cancel_action=Annuler
|
access_token_deletion_cancel_action=Annuler
|
||||||
access_token_deletion_confirm_action=Supprimer
|
access_token_deletion_confirm_action=Supprimer
|
||||||
|
delete_token_success=Ce jeton a été supprimé. Les applications l'utilisant n'ont plus accès à votre compte.
|
||||||
|
select_scopes=Sélectionner les périmètres
|
||||||
|
scopes_list=Périmètres :
|
||||||
|
|
||||||
|
manage_oauth2_applications=Gérer les applications OAuth2
|
||||||
|
edit_oauth2_application=Modifier l'application OAuth2
|
||||||
|
oauth2_applications_desc=Les applications OAuth2 permettent à votre application tierce d'authentifier en toute sécurité les utilisateurs de cette instance Gitea.
|
||||||
|
remove_oauth2_application=Supprimer l'application OAuth2
|
||||||
|
remove_oauth2_application_desc=La suppression d'une application OAuth2 révoquera l'accès à tous les jetons d'accès signés. Continuer ?
|
||||||
|
remove_oauth2_application_success=L'application a été supprimée.
|
||||||
|
create_oauth2_application=Créer une nouvelle application OAuth2
|
||||||
|
create_oauth2_application_button=Créer une application
|
||||||
|
create_oauth2_application_success=Vous avez créé une nouvelle application OAuth2 avec succès.
|
||||||
|
update_oauth2_application_success=Vous avez mis à jour l'application OAuth2 avec succès.
|
||||||
|
oauth2_application_name=Nom de l'Application
|
||||||
|
oauth2_redirect_uri=URI de redirection
|
||||||
save_application=Enregistrer
|
save_application=Enregistrer
|
||||||
|
oauth2_client_id=ID du client
|
||||||
oauth2_client_secret=Secret du client
|
oauth2_client_secret=Secret du client
|
||||||
|
oauth2_regenerate_secret=Regénérer le secret
|
||||||
|
oauth2_regenerate_secret_hint=Avez-vous perdu votre secret ?
|
||||||
|
oauth2_client_secret_hint=Le secret ne sera plus visible si vous revenez sur cette page. Veuillez sauvegarder votre secret.
|
||||||
oauth2_application_edit=Éditer
|
oauth2_application_edit=Éditer
|
||||||
|
oauth2_application_create_description=Les applications OAuth2 permettent à votre application tierce d'accéder aux comptes d'utilisateurs de cette instance.
|
||||||
|
oauth2_application_remove_description=La suppression d'une application OAuth2 l'empêchera d'accéder aux comptes d'utilisateurs autorisés sur cette instance. Poursuivre ?
|
||||||
|
|
||||||
|
authorized_oauth2_applications=Applications OAuth2 autorisées
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -661,6 +763,7 @@ confirm_delete_account=Confirmez la suppression
|
|||||||
delete_account_title=Supprimer cet utilisateur
|
delete_account_title=Supprimer cet utilisateur
|
||||||
|
|
||||||
email_notifications.enable=Activer les notifications par e-mail
|
email_notifications.enable=Activer les notifications par e-mail
|
||||||
|
email_notifications.disable=Désactiver les notifications par e-mail
|
||||||
|
|
||||||
visibility.public=Public
|
visibility.public=Public
|
||||||
visibility.limited=Limité
|
visibility.limited=Limité
|
||||||
@ -864,6 +967,7 @@ issues=Tickets
|
|||||||
pulls=Demandes d'ajout
|
pulls=Demandes d'ajout
|
||||||
project_board=Projets
|
project_board=Projets
|
||||||
packages=Paquets
|
packages=Paquets
|
||||||
|
actions=Actions
|
||||||
labels=Étiquettes
|
labels=Étiquettes
|
||||||
org_labels_desc=Les étiquettes de niveau d'une organisation peuvent être utilisés avec <strong>tous les dépôts</strong> de cette organisation
|
org_labels_desc=Les étiquettes de niveau d'une organisation peuvent être utilisés avec <strong>tous les dépôts</strong> de cette organisation
|
||||||
org_labels_desc_manage=gérer
|
org_labels_desc_manage=gérer
|
||||||
@ -937,6 +1041,7 @@ editor.commit_directly_to_this_branch=Soumettre directement dans la branche <str
|
|||||||
editor.create_new_branch=Créer une <strong>nouvelle branche</strong> pour cette révision et envoyer une nouvelle demande d'ajout.
|
editor.create_new_branch=Créer une <strong>nouvelle branche</strong> pour cette révision et envoyer une nouvelle demande d'ajout.
|
||||||
editor.create_new_branch_np=Créer une <strong>nouvelle branche</strong> pour cette révision.
|
editor.create_new_branch_np=Créer une <strong>nouvelle branche</strong> pour cette révision.
|
||||||
editor.propose_file_change=Proposer une modification du fichier
|
editor.propose_file_change=Proposer une modification du fichier
|
||||||
|
editor.new_branch_name=Nommer la nouvelle branche pour cette révision
|
||||||
editor.new_branch_name_desc=Nouveau nom de la branche…
|
editor.new_branch_name_desc=Nouveau nom de la branche…
|
||||||
editor.cancel=Annuler
|
editor.cancel=Annuler
|
||||||
editor.filename_cannot_be_empty=Le nom de fichier ne peut être vide.
|
editor.filename_cannot_be_empty=Le nom de fichier ne peut être vide.
|
||||||
@ -959,6 +1064,7 @@ commits.desc=Naviguer dans l'historique des modifications.
|
|||||||
commits.commits=Révisions
|
commits.commits=Révisions
|
||||||
commits.nothing_to_compare=Ces branches sont égales.
|
commits.nothing_to_compare=Ces branches sont égales.
|
||||||
commits.search=Rechercher des révisions…
|
commits.search=Rechercher des révisions…
|
||||||
|
commits.search.tooltip=Vous pouvez préfixer les mots-clés avec "author:", "committer:", "after:", ou "before:", par exemple "revert author:Alice before:2019-01-13".
|
||||||
commits.find=Chercher
|
commits.find=Chercher
|
||||||
commits.search_all=Toutes les branches
|
commits.search_all=Toutes les branches
|
||||||
commits.author=Auteur
|
commits.author=Auteur
|
||||||
@ -1053,6 +1159,7 @@ issues.label_templates.title=Charger un ensemble prédéfini d'étiquettes
|
|||||||
issues.label_templates.info=Il n'existe pas encore d'étiquettes. Créez une étiquette avec 'Nouvelle étiquette' ou utilisez un jeu d'étiquettes prédéfini :
|
issues.label_templates.info=Il n'existe pas encore d'étiquettes. Créez une étiquette avec 'Nouvelle étiquette' ou utilisez un jeu d'étiquettes prédéfini :
|
||||||
issues.label_templates.helper=Sélectionnez un ensemble d'étiquettes
|
issues.label_templates.helper=Sélectionnez un ensemble d'étiquettes
|
||||||
issues.label_templates.use=Utiliser le jeu de labels
|
issues.label_templates.use=Utiliser le jeu de labels
|
||||||
|
issues.label_templates.fail_to_load_file=Impossible de charger le fichier de modèle de libellé "%s" : %v
|
||||||
issues.add_label=a ajouté l'étiquette %s %s
|
issues.add_label=a ajouté l'étiquette %s %s
|
||||||
issues.add_labels=a ajouté les étiquettes %s %s
|
issues.add_labels=a ajouté les étiquettes %s %s
|
||||||
issues.remove_label=a supprimé l'étiquette %s %s
|
issues.remove_label=a supprimé l'étiquette %s %s
|
||||||
@ -1157,6 +1264,8 @@ issues.save=Enregistrer
|
|||||||
issues.label_title=Nom de l'étiquette
|
issues.label_title=Nom de l'étiquette
|
||||||
issues.label_description=Description de l’étiquette
|
issues.label_description=Description de l’étiquette
|
||||||
issues.label_color=Couleur de l'étiquette
|
issues.label_color=Couleur de l'étiquette
|
||||||
|
issues.label_exclusive_desc=Nommez le libellé <code>périmètre/élément</code> pour qu'il soit mutuellement exclusif avec d'autres libellés du <code>périmètre</code>.
|
||||||
|
issues.label_exclusive_warning=Tout libellé conflictuel sera supprimé lors de l'édition des libellés d'un ticket ou d'une demande de tirage.
|
||||||
issues.label_count=%d étiquettes
|
issues.label_count=%d étiquettes
|
||||||
issues.label_open_issues=%d tickets ouverts
|
issues.label_open_issues=%d tickets ouverts
|
||||||
issues.label_edit=Éditer
|
issues.label_edit=Éditer
|
||||||
@ -1326,6 +1435,7 @@ pulls.remove_prefix=Enlever le préfixe <strong>%s</strong>
|
|||||||
pulls.data_broken=Cette demande de fusion est impossible par manque d'informations de bifurcation.
|
pulls.data_broken=Cette demande de fusion est impossible par manque d'informations de bifurcation.
|
||||||
pulls.files_conflicted=Cette demande d'ajout contient des modifications en conflit avec la branche ciblée.
|
pulls.files_conflicted=Cette demande d'ajout contient des modifications en conflit avec la branche ciblée.
|
||||||
pulls.is_checking=Vérification des conflits de fusion en cours. Réessayez dans quelques instants.
|
pulls.is_checking=Vérification des conflits de fusion en cours. Réessayez dans quelques instants.
|
||||||
|
pulls.is_empty=Les changements sur cette branche sont déjà sur la branche cible. Cette révision sera vide.
|
||||||
pulls.required_status_check_failed=Certains contrôles requis n'ont pas réussi.
|
pulls.required_status_check_failed=Certains contrôles requis n'ont pas réussi.
|
||||||
pulls.required_status_check_missing=Certains contrôles requis sont manquants.
|
pulls.required_status_check_missing=Certains contrôles requis sont manquants.
|
||||||
pulls.required_status_check_administrator=En tant qu'administrateur, vous pouvez toujours fusionner cette requête de pull.
|
pulls.required_status_check_administrator=En tant qu'administrateur, vous pouvez toujours fusionner cette requête de pull.
|
||||||
@ -1552,6 +1662,7 @@ settings.hooks=Webhooks
|
|||||||
settings.githooks=Déclencheurs Git
|
settings.githooks=Déclencheurs Git
|
||||||
settings.basic_settings=Paramètres de base
|
settings.basic_settings=Paramètres de base
|
||||||
settings.mirror_settings=Réglages Miroir
|
settings.mirror_settings=Réglages Miroir
|
||||||
|
settings.mirror_settings.docs=Configurez votre projet pour automatiquement pousser ou tirer les modifications d'un autre dépôt. Les branches, étiquettes et révisions seront automatiquement synchronisées. <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/fr-fr/repo-mirror/">Comment mettre en miroir les dépôts ?</a>
|
||||||
settings.mirror_settings.mirrored_repository=Dépôt en miroir
|
settings.mirror_settings.mirrored_repository=Dépôt en miroir
|
||||||
settings.mirror_settings.direction=Direction
|
settings.mirror_settings.direction=Direction
|
||||||
settings.mirror_settings.direction.push=Pousser
|
settings.mirror_settings.direction.push=Pousser
|
||||||
@ -1737,6 +1848,7 @@ settings.event_pull_request_review_desc=Demande d'ajout approvée, rejetée ou c
|
|||||||
settings.event_pull_request_sync=Demande d'ajout synchronisée
|
settings.event_pull_request_sync=Demande d'ajout synchronisée
|
||||||
settings.event_pull_request_sync_desc=Demande d'ajout synchronisée.
|
settings.event_pull_request_sync_desc=Demande d'ajout synchronisée.
|
||||||
settings.branch_filter=Filtre de branche
|
settings.branch_filter=Filtre de branche
|
||||||
|
settings.branch_filter_desc=Liste blanche pour les poussées, la création et la suppression de branches, spécifiées par motif de glob. Si vide ou <code>*</code>, les événements pour toutes les branches sont signalés. Voir la documentation <a href="https://pkg.go.dev/github.com/gobwas/glob#Compile">github.com/gobwas/glob</a> pour la syntaxe. Exemples: <code>master</code>, <code>{master,release*}</code>.
|
||||||
settings.active=Actif
|
settings.active=Actif
|
||||||
settings.active_helper=Les informations sur les événements déclenchés seront envoyées à cette url de Webhook.
|
settings.active_helper=Les informations sur les événements déclenchés seront envoyées à cette url de Webhook.
|
||||||
settings.add_hook_success=Nouveau Webhook ajouté.
|
settings.add_hook_success=Nouveau Webhook ajouté.
|
||||||
@ -1813,7 +1925,12 @@ settings.choose_branch=Choisissez une branche…
|
|||||||
settings.no_protected_branch=Il n'y a pas de branche protégée.
|
settings.no_protected_branch=Il n'y a pas de branche protégée.
|
||||||
settings.edit_protected_branch=Éditer
|
settings.edit_protected_branch=Éditer
|
||||||
settings.protected_branch_required_approvals_min=Le nombre de revues nécessaires ne peut être négatif.
|
settings.protected_branch_required_approvals_min=Le nombre de revues nécessaires ne peut être négatif.
|
||||||
settings.tags=Tags
|
settings.tags=Étiquettes
|
||||||
|
settings.tags.protection=Protection d'étiquette
|
||||||
|
settings.tags.protection.pattern=Motif d'étiquette
|
||||||
|
settings.tags.protection.create=Protéger l'étiquette
|
||||||
|
settings.tags.protection.none=Il n'y a pas d'étiquettes protégées.
|
||||||
|
settings.tags.protection.pattern.description=Vous pouvez utiliser soit un nom unique, soit un motif de glob ou une expression régulière qui correspondront à plusieurs étiquettes. Pour plus d'informations, veuillez vous reporter au <a target="_blank" rel="noopener" href="https://docs.gitea.io/fr-fr/protected-tags/">guide sur les étiquettes protégées</a>.
|
||||||
settings.bot_token=Jeton de Bot
|
settings.bot_token=Jeton de Bot
|
||||||
settings.chat_id=ID de conversation
|
settings.chat_id=ID de conversation
|
||||||
settings.matrix.homeserver_url=URL du serveur d'accueil
|
settings.matrix.homeserver_url=URL du serveur d'accueil
|
||||||
@ -1826,6 +1943,7 @@ settings.archive.success=Ce dépôt a été archivé avec succès.
|
|||||||
settings.archive.error=Une erreur s'est produite lors de l'archivage du dépôt. Voir le journal pour plus de détails.
|
settings.archive.error=Une erreur s'est produite lors de l'archivage du dépôt. Voir le journal pour plus de détails.
|
||||||
settings.archive.error_ismirror=Vous ne pouvez pas archiver un dépôt en miroir.
|
settings.archive.error_ismirror=Vous ne pouvez pas archiver un dépôt en miroir.
|
||||||
settings.archive.branchsettings_unavailable=Le paramétrage des branches n'est pas disponible quand le dépôt est archivé.
|
settings.archive.branchsettings_unavailable=Le paramétrage des branches n'est pas disponible quand le dépôt est archivé.
|
||||||
|
settings.archive.tagsettings_unavailable=Le paramétrage des étiquettes n'est pas disponible si le dépôt est archivé.
|
||||||
settings.unarchive.button=Désarchiver ce dépôt
|
settings.unarchive.button=Désarchiver ce dépôt
|
||||||
settings.unarchive.header=Désarchiver ce dépôt
|
settings.unarchive.header=Désarchiver ce dépôt
|
||||||
settings.unarchive.text=Désarchiver le dépôt lui permettra de recevoir des révisions, ainsi que des nouveaux tickets ou demandes d'ajout.
|
settings.unarchive.text=Désarchiver le dépôt lui permettra de recevoir des révisions, ainsi que des nouveaux tickets ou demandes d'ajout.
|
||||||
@ -1886,6 +2004,7 @@ diff.file_image_height=Hauteur
|
|||||||
diff.file_byte_size=Taille
|
diff.file_byte_size=Taille
|
||||||
diff.file_suppressed=Fichier diff supprimé car celui-ci est trop grand
|
diff.file_suppressed=Fichier diff supprimé car celui-ci est trop grand
|
||||||
diff.file_suppressed_line_too_long=Diff de fichier supprimé car une ou plusieurs lignes sont trop longues
|
diff.file_suppressed_line_too_long=Diff de fichier supprimé car une ou plusieurs lignes sont trop longues
|
||||||
|
diff.generated=générée
|
||||||
diff.comment.placeholder=Laisser un commentaire
|
diff.comment.placeholder=Laisser un commentaire
|
||||||
diff.comment.markdown_info=Mise en page avec markdown est prise en charge.
|
diff.comment.markdown_info=Mise en page avec markdown est prise en charge.
|
||||||
diff.comment.add_single_comment=Ajouter un commentaire
|
diff.comment.add_single_comment=Ajouter un commentaire
|
||||||
@ -1914,13 +2033,17 @@ release.prerelease=Pré-publication
|
|||||||
release.stable=Stable
|
release.stable=Stable
|
||||||
release.compare=Comparer
|
release.compare=Comparer
|
||||||
release.edit=Éditer
|
release.edit=Éditer
|
||||||
|
release.ahead.commits=<strong>%d</strong> révisions
|
||||||
release.ahead.target=à %s depuis cette livraison
|
release.ahead.target=à %s depuis cette livraison
|
||||||
|
tag.ahead.target=à %s depuis cette étiquette
|
||||||
release.source_code=Code source
|
release.source_code=Code source
|
||||||
release.new_subheader=Les versions organisent les versions publiées du projet.
|
release.new_subheader=Les versions organisent les versions publiées du projet.
|
||||||
release.edit_subheader=Les versions organisent les versions publiées du projet.
|
release.edit_subheader=Les versions organisent les versions publiées du projet.
|
||||||
release.tag_name=Nom du tag
|
release.tag_name=Nom du tag
|
||||||
release.target=Cible
|
release.target=Cible
|
||||||
release.tag_helper=Choisissez une étiquette existante ou créez une nouvelle étiquette.
|
release.tag_helper=Choisissez une étiquette existante ou créez une nouvelle étiquette.
|
||||||
|
release.tag_helper_new=Nouvelle étiquette. Cette étiquette sera créée à partir de la cible.
|
||||||
|
release.tag_helper_existing=Étiquette existante.
|
||||||
release.prerelease_desc=Marquer comme pré-version
|
release.prerelease_desc=Marquer comme pré-version
|
||||||
release.prerelease_helper=Marquer cette version comme impropre à la production.
|
release.prerelease_helper=Marquer cette version comme impropre à la production.
|
||||||
release.cancel=Annuler
|
release.cancel=Annuler
|
||||||
@ -1930,13 +2053,19 @@ release.edit_release=Modifier la version
|
|||||||
release.delete_release=Supprimer cette version
|
release.delete_release=Supprimer cette version
|
||||||
release.delete_tag=Supprimer l'étiquette
|
release.delete_tag=Supprimer l'étiquette
|
||||||
release.deletion=Supprimer cette version
|
release.deletion=Supprimer cette version
|
||||||
|
release.deletion_desc=La suppression d'une version la supprime seulement de Gitea. Les étiquettes Git, le contenu du dépôt et l'historique restent inchangés. Voulez vous continuer ?
|
||||||
release.deletion_success=Cette livraison a été supprimée.
|
release.deletion_success=Cette livraison a été supprimée.
|
||||||
|
release.deletion_tag_desc=Ceci supprimera cette étiquette du dépôt. Le contenu du dépôt et l'historique resteront inchangés. Continuer ?
|
||||||
release.deletion_tag_success=L'étiquette a été supprimée.
|
release.deletion_tag_success=L'étiquette a été supprimée.
|
||||||
release.tag_name_already_exist=Une version avec ce nom d'étiquette existe déjà.
|
release.tag_name_already_exist=Une version avec ce nom d'étiquette existe déjà.
|
||||||
release.tag_name_invalid=Le nom de l'étiquette est invalide.
|
release.tag_name_invalid=Le nom de l'étiquette est invalide.
|
||||||
|
release.tag_name_protected=Ce nom d'étiquette est protégé.
|
||||||
release.tag_already_exist=Ce nom d'étiquette existe déjà.
|
release.tag_already_exist=Ce nom d'étiquette existe déjà.
|
||||||
release.downloads=Téléchargements
|
release.downloads=Téléchargements
|
||||||
release.download_count=Télécharger: %s
|
release.download_count=Télécharger: %s
|
||||||
|
release.add_tag_msg=Utiliser le titre et le contenu de la version comme message d'étiquette.
|
||||||
|
release.add_tag=Créer uniquement une Balise
|
||||||
|
release.tags_for=Étiquettes pour %s
|
||||||
|
|
||||||
branch.name=Nom de la branche
|
branch.name=Nom de la branche
|
||||||
branch.search=Rechercher des branches
|
branch.search=Rechercher des branches
|
||||||
@ -1944,22 +2073,38 @@ branch.delete_head=Supprimer
|
|||||||
branch.delete_html=Supprimer la branche
|
branch.delete_html=Supprimer la branche
|
||||||
branch.delete_desc=Supprimer une branche est permanent. Cela <strong>NE PEUVENT</strong> être annulées. Continuer ?
|
branch.delete_desc=Supprimer une branche est permanent. Cela <strong>NE PEUVENT</strong> être annulées. Continuer ?
|
||||||
branch.create_branch=Créer la branche <strong>%s</strong>
|
branch.create_branch=Créer la branche <strong>%s</strong>
|
||||||
|
branch.create_from=`de "%s"`
|
||||||
|
branch.tag_collision=La branche "%s" ne peut être créée car une étiquette avec un nom identique existe déjà dans le dépôt.
|
||||||
branch.deleted_by=Supprimée par %s
|
branch.deleted_by=Supprimée par %s
|
||||||
|
branch.restore=`Restaurer la branche "%s"`
|
||||||
|
branch.download=`Télécharger la branche "%s"`
|
||||||
branch.included_desc=Cette branche fait partie de la branche par défaut
|
branch.included_desc=Cette branche fait partie de la branche par défaut
|
||||||
branch.included=Incluses
|
branch.included=Incluses
|
||||||
branch.create_new_branch=Créer une branche à partir de la branche :
|
branch.create_new_branch=Créer une branche à partir de la branche :
|
||||||
branch.confirm_create_branch=Créer une branche
|
branch.confirm_create_branch=Créer une branche
|
||||||
branch.create_branch_operation=Créer une branche
|
branch.create_branch_operation=Créer une branche
|
||||||
branch.new_branch=Créer une nouvelle branche
|
branch.new_branch=Créer une nouvelle branche
|
||||||
|
branch.new_branch_from=`Créer une nouvelle branche à partir de "%s"`
|
||||||
|
branch.renamed=La branche %s à été renommée en %s.
|
||||||
|
|
||||||
|
tag.create_tag=Créer l'étiquette <strong>%s</strong>
|
||||||
|
tag.create_tag_operation=Créer une étiquette
|
||||||
|
tag.confirm_create_tag=Créer une étiquette
|
||||||
|
tag.create_tag_from=`Créer une nouvelle étiquette à partir de "%s"`
|
||||||
|
|
||||||
|
tag.create_success=L'étiquette "%s" a été créée.
|
||||||
|
|
||||||
topic.manage_topics=Gérer les sujets
|
topic.manage_topics=Gérer les sujets
|
||||||
topic.done=Terminé
|
topic.done=Terminé
|
||||||
topic.count_prompt=Vous ne pouvez pas sélectionner plus de 25 sujets
|
topic.count_prompt=Vous ne pouvez pas sélectionner plus de 25 sujets
|
||||||
topic.format_prompt=Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
|
topic.format_prompt=Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
|
||||||
|
|
||||||
|
find_file.go_to_file=Aller au fichier
|
||||||
|
find_file.no_matching=Aucun fichier correspondant trouvé
|
||||||
|
|
||||||
|
error.csv.too_large=Impossible de visualiser le fichier car il est trop volumineux.
|
||||||
|
error.csv.unexpected=Impossible de visualiser ce fichier car il contient un caractère inattendu ligne %d, colonne %d.
|
||||||
|
error.csv.invalid_field_count=Impossible de visualiser ce fichier car il contient un nombre de champs incorrect à la ligne %d.
|
||||||
|
|
||||||
[org]
|
[org]
|
||||||
org_name_holder=Nom de l'organisation
|
org_name_holder=Nom de l'organisation
|
||||||
@ -1969,6 +2114,7 @@ create_org=Créer une organisation
|
|||||||
repo_updated=Mis à jour
|
repo_updated=Mis à jour
|
||||||
members=Membres
|
members=Membres
|
||||||
teams=Équipes
|
teams=Équipes
|
||||||
|
code=Code
|
||||||
lower_members=Membres
|
lower_members=Membres
|
||||||
lower_repositories=dépôts
|
lower_repositories=dépôts
|
||||||
create_new_team=Nouvelle équipe
|
create_new_team=Nouvelle équipe
|
||||||
@ -1983,6 +2129,7 @@ team_permission_desc=Autorisation
|
|||||||
team_unit_desc=Permettre l’accès aux Sections du dépôt
|
team_unit_desc=Permettre l’accès aux Sections du dépôt
|
||||||
team_unit_disabled=(Désactivé)
|
team_unit_disabled=(Désactivé)
|
||||||
|
|
||||||
|
form.name_pattern_not_allowed=Le motif "%s" n'est pas autorisé dans un nom d'organisation.
|
||||||
form.create_org_not_allowed=Vous n'êtes pas autorisé à créer une organisation.
|
form.create_org_not_allowed=Vous n'êtes pas autorisé à créer une organisation.
|
||||||
|
|
||||||
settings=Paramètres
|
settings=Paramètres
|
||||||
@ -1994,6 +2141,7 @@ settings.permission=Autorisations
|
|||||||
settings.repoadminchangeteam=L'administrateur de dépôt peut ajouter et supprimer l'accès aux équipes
|
settings.repoadminchangeteam=L'administrateur de dépôt peut ajouter et supprimer l'accès aux équipes
|
||||||
settings.visibility=Visibilité
|
settings.visibility=Visibilité
|
||||||
settings.visibility.public=Public
|
settings.visibility.public=Public
|
||||||
|
settings.visibility.limited=Limité (Visible uniquement aux utilisateurs authentifiés)
|
||||||
settings.visibility.limited_shortname=Limité
|
settings.visibility.limited_shortname=Limité
|
||||||
settings.visibility.private=Privé (Visible uniquement aux membres de l’organisation)
|
settings.visibility.private=Privé (Visible uniquement aux membres de l’organisation)
|
||||||
settings.visibility.private_shortname=Privé
|
settings.visibility.private_shortname=Privé
|
||||||
@ -2033,8 +2181,13 @@ teams.leave=Quitter
|
|||||||
teams.leave.detail=Quitter %s?
|
teams.leave.detail=Quitter %s?
|
||||||
teams.can_create_org_repo=Créer des dépôts
|
teams.can_create_org_repo=Créer des dépôts
|
||||||
teams.can_create_org_repo_helper=Les membres peuvent créer de nouveaux dépôts dans l'organisation. Le créateur obtiendra l'accès administrateur au nouveau dépôt.
|
teams.can_create_org_repo_helper=Les membres peuvent créer de nouveaux dépôts dans l'organisation. Le créateur obtiendra l'accès administrateur au nouveau dépôt.
|
||||||
teams.read_access=Lue(s)
|
teams.none_access=Aucun accès
|
||||||
|
teams.none_access_helper=Les membres ne peuvent voir ou faire quoi que ce soit sur cette partie. Sans effet pour les dépôts publics.
|
||||||
|
teams.general_access=Accès général
|
||||||
|
teams.general_access_helper=Les permissions des membres seront déterminées par la table des permissions ci-dessous.
|
||||||
|
teams.read_access=Lecture
|
||||||
teams.read_access_helper=Les membres peuvent voir et cloner les dépôts de l'équipe.
|
teams.read_access_helper=Les membres peuvent voir et cloner les dépôts de l'équipe.
|
||||||
|
teams.write_access=Écriture
|
||||||
teams.write_access_helper=Les membres peuvent voir et pousser dans les dépôts de l'équipe.
|
teams.write_access_helper=Les membres peuvent voir et pousser dans les dépôts de l'équipe.
|
||||||
teams.admin_access=Accès Administrateur
|
teams.admin_access=Accès Administrateur
|
||||||
teams.admin_access_helper=Les membres peuvent tirer et pousser des modifications vers les dépôts de l'équipe, et y ajouter des collaborateurs.
|
teams.admin_access_helper=Les membres peuvent tirer et pousser des modifications vers les dépôts de l'équipe, et y ajouter des collaborateurs.
|
||||||
@ -2045,6 +2198,8 @@ teams.members=Membres de L'Équipe
|
|||||||
teams.update_settings=Valider
|
teams.update_settings=Valider
|
||||||
teams.delete_team=Supprimer l'équipe
|
teams.delete_team=Supprimer l'équipe
|
||||||
teams.add_team_member=Ajouter un Membre
|
teams.add_team_member=Ajouter un Membre
|
||||||
|
teams.invite_team_member=Inviter à %s
|
||||||
|
teams.invite_team_member.list=Invitations en attente
|
||||||
teams.delete_team_title=Supprimer l'équipe
|
teams.delete_team_title=Supprimer l'équipe
|
||||||
teams.delete_team_desc=Supprimer une équipe supprime l'accès aux dépôts à ses membres. Continuer ?
|
teams.delete_team_desc=Supprimer une équipe supprime l'accès aux dépôts à ses membres. Continuer ?
|
||||||
teams.delete_team_success=L’équipe a été supprimée.
|
teams.delete_team_success=L’équipe a été supprimée.
|
||||||
@ -2058,6 +2213,7 @@ teams.remove_all_repos_title=Supprimer tous les dépôts de l'équipe
|
|||||||
teams.remove_all_repos_desc=Ceci supprimera tous les dépôts de l'équipe.
|
teams.remove_all_repos_desc=Ceci supprimera tous les dépôts de l'équipe.
|
||||||
teams.add_all_repos_title=Ajouter tous les dépôts
|
teams.add_all_repos_title=Ajouter tous les dépôts
|
||||||
teams.add_all_repos_desc=Ceci ajoutera tous les dépôts de l'organisation à l'équipe.
|
teams.add_all_repos_desc=Ceci ajoutera tous les dépôts de l'organisation à l'équipe.
|
||||||
|
teams.add_nonexistent_repo=Le dépôt que vous essayez d'ajouter n'existe pas, veuillez le créer d'abord.
|
||||||
teams.add_duplicate_users=L’utilisateur est déjà un membre de l’équipe.
|
teams.add_duplicate_users=L’utilisateur est déjà un membre de l’équipe.
|
||||||
teams.repos.none=Aucun dépôt n'est accessible par cette équipe.
|
teams.repos.none=Aucun dépôt n'est accessible par cette équipe.
|
||||||
teams.members.none=Aucun membre dans cette équipe.
|
teams.members.none=Aucun membre dans cette équipe.
|
||||||
@ -2068,12 +2224,16 @@ teams.all_repositories_helper=L'équipe a accès à tous les dépôts. Sélectio
|
|||||||
teams.all_repositories_read_permission_desc=Cette équipe accorde l'accès <strong>en lecture</strong> à <strong>tous les dépôts</strong> : les membres peuvent voir et cloner les dépôts.
|
teams.all_repositories_read_permission_desc=Cette équipe accorde l'accès <strong>en lecture</strong> à <strong>tous les dépôts</strong> : les membres peuvent voir et cloner les dépôts.
|
||||||
teams.all_repositories_write_permission_desc=Cette équipe accorde l'accès <strong>en écriture</strong> à <strong>tous les dépôts</strong> : les membres peuvent lire et écrire dans les dépôts.
|
teams.all_repositories_write_permission_desc=Cette équipe accorde l'accès <strong>en écriture</strong> à <strong>tous les dépôts</strong> : les membres peuvent lire et écrire dans les dépôts.
|
||||||
teams.all_repositories_admin_permission_desc=Cette équipe accorde l'accès <strong>administrateur</strong> à <strong>tous les dépôts</strong> : les membres peuvent lire, écrire dans et ajouter des collaborateurs aux dépôts.
|
teams.all_repositories_admin_permission_desc=Cette équipe accorde l'accès <strong>administrateur</strong> à <strong>tous les dépôts</strong> : les membres peuvent lire, écrire dans et ajouter des collaborateurs aux dépôts.
|
||||||
|
teams.invite.title=Vous avez été invité à rejoindre l’équipe <strong>%s</strong> dans l’organisation <strong>%s</strong>.
|
||||||
|
teams.invite.by=Invité par %s
|
||||||
|
teams.invite.description=Veuillez cliquer sur le bouton ci-dessous pour rejoindre l’équipe.
|
||||||
|
|
||||||
[admin]
|
[admin]
|
||||||
dashboard=Tableau de bord
|
dashboard=Tableau de bord
|
||||||
users=Comptes utilisateurs
|
users=Comptes utilisateurs
|
||||||
organizations=Organisations
|
organizations=Organisations
|
||||||
repositories=Dépôts
|
repositories=Dépôts
|
||||||
|
hooks=Déclencheurs web
|
||||||
authentication=Sources d'authentification
|
authentication=Sources d'authentification
|
||||||
emails=Courriels de l'utilisateur
|
emails=Courriels de l'utilisateur
|
||||||
config=Configuration
|
config=Configuration
|
||||||
@ -2083,6 +2243,7 @@ first_page=Première
|
|||||||
last_page=Dernière
|
last_page=Dernière
|
||||||
total=Total : %d
|
total=Total : %d
|
||||||
|
|
||||||
|
dashboard.new_version_hint=Gitea %s est maintenant disponible, vous utilisez %s. Consultez <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">le blog</a> pour plus de détails.
|
||||||
dashboard.statistic=Résumé
|
dashboard.statistic=Résumé
|
||||||
dashboard.operations=Opérations de maintenance
|
dashboard.operations=Opérations de maintenance
|
||||||
dashboard.system_status=État du système
|
dashboard.system_status=État du système
|
||||||
@ -2097,6 +2258,7 @@ dashboard.task.cancelled=Tâche: %[1]s a annulé: %[3]s
|
|||||||
dashboard.task.error=Erreur dans la tâche: %[1]s: %[3]s
|
dashboard.task.error=Erreur dans la tâche: %[1]s: %[3]s
|
||||||
dashboard.task.finished=Tâche: %[1]s démarrée par %[2]s est terminée
|
dashboard.task.finished=Tâche: %[1]s démarrée par %[2]s est terminée
|
||||||
dashboard.task.unknown=Tâche inconnue: %[1]s
|
dashboard.task.unknown=Tâche inconnue: %[1]s
|
||||||
|
dashboard.cron.started=Tâche planifiée démarrée : %[1]s
|
||||||
dashboard.cron.process=Tâche planifiée: %[1]s
|
dashboard.cron.process=Tâche planifiée: %[1]s
|
||||||
dashboard.cron.cancelled=Tâche planifiée : %s annulée : %[3]s
|
dashboard.cron.cancelled=Tâche planifiée : %s annulée : %[3]s
|
||||||
dashboard.cron.error=Erreur dans la tâche planifiée : %s: %[3]s
|
dashboard.cron.error=Erreur dans la tâche planifiée : %s: %[3]s
|
||||||
@ -2120,6 +2282,7 @@ dashboard.resync_all_hooks=Re-synchroniser les déclencheurs Git pre-receive, up
|
|||||||
dashboard.reinit_missing_repos=Réinitialiser tous les dépôts Git manquants pour lesquels un enregistrement existe
|
dashboard.reinit_missing_repos=Réinitialiser tous les dépôts Git manquants pour lesquels un enregistrement existe
|
||||||
dashboard.sync_external_users=Synchroniser les données de l’utilisateur externe
|
dashboard.sync_external_users=Synchroniser les données de l’utilisateur externe
|
||||||
dashboard.cleanup_hook_task_table=Nettoyer la table hook_task
|
dashboard.cleanup_hook_task_table=Nettoyer la table hook_task
|
||||||
|
dashboard.cleanup_packages=Nettoyer des paquets expirés
|
||||||
dashboard.server_uptime=Uptime du serveur
|
dashboard.server_uptime=Uptime du serveur
|
||||||
dashboard.current_goroutine=Goroutines actuelles
|
dashboard.current_goroutine=Goroutines actuelles
|
||||||
dashboard.current_memory_usage=Utilisation Mémoire actuelle
|
dashboard.current_memory_usage=Utilisation Mémoire actuelle
|
||||||
@ -2150,6 +2313,12 @@ dashboard.total_gc_pause=Pause GC
|
|||||||
dashboard.last_gc_pause=Dernière Pause GC
|
dashboard.last_gc_pause=Dernière Pause GC
|
||||||
dashboard.gc_times=Nombres de GC
|
dashboard.gc_times=Nombres de GC
|
||||||
dashboard.delete_old_actions=Supprimer toutes les anciennes actions de la base de données
|
dashboard.delete_old_actions=Supprimer toutes les anciennes actions de la base de données
|
||||||
|
dashboard.delete_old_actions.started=Suppression de toutes les anciennes actions de la base de données démarrée.
|
||||||
|
dashboard.update_checker=Vérificateur de mise à jour
|
||||||
|
dashboard.delete_old_system_notices=Supprimer toutes les anciennes observations de la base de données
|
||||||
|
dashboard.stop_zombie_tasks=Arrêter les tâches zombies
|
||||||
|
dashboard.stop_endless_tasks=Arrêter les tâches sans fin
|
||||||
|
dashboard.cancel_abandoned_jobs=Annuler les jobs abandonnés
|
||||||
|
|
||||||
users.user_manage_panel=Gestion du compte utilisateur
|
users.user_manage_panel=Gestion du compte utilisateur
|
||||||
users.new_account=Créer un compte
|
users.new_account=Créer un compte
|
||||||
@ -2164,6 +2333,7 @@ users.created=Créés
|
|||||||
users.last_login=Dernière connexion
|
users.last_login=Dernière connexion
|
||||||
users.never_login=Jamais connecté
|
users.never_login=Jamais connecté
|
||||||
users.send_register_notify=Envoyer une notification d'inscription
|
users.send_register_notify=Envoyer une notification d'inscription
|
||||||
|
users.new_success=Le compte "%s" a bien été créé.
|
||||||
users.edit=Éditer
|
users.edit=Éditer
|
||||||
users.auth_source=Sources d'authentification
|
users.auth_source=Sources d'authentification
|
||||||
users.local=Locales
|
users.local=Locales
|
||||||
@ -2178,14 +2348,21 @@ users.prohibit_login=Désactiver la connexion
|
|||||||
users.is_admin=Est Administrateur
|
users.is_admin=Est Administrateur
|
||||||
users.is_restricted=Est restreint
|
users.is_restricted=Est restreint
|
||||||
users.allow_git_hook=Autoriser la création de Git Hooks
|
users.allow_git_hook=Autoriser la création de Git Hooks
|
||||||
|
users.allow_git_hook_tooltip=Les Déclencheurs Git sont exécutés par le même utilisateur que Gitea, qui a des privilèges systèmes élevés. Les utilisateurs ayant ce droit peuvent altérer touts les dépôts, compromettre la base de données applicative, et se promouvoir administrateurs de Gitea.
|
||||||
users.allow_import_local=Autoriser l'importation de dépôts locaux
|
users.allow_import_local=Autoriser l'importation de dépôts locaux
|
||||||
users.allow_create_organization=Autoriser la création d'organisations
|
users.allow_create_organization=Autoriser la création d'organisations
|
||||||
users.update_profile=Modifier un compte
|
users.update_profile=Modifier un compte
|
||||||
users.delete_account=Supprimer cet utilisateur
|
users.delete_account=Supprimer cet utilisateur
|
||||||
|
users.cannot_delete_self=Vous ne pouvez pas vous supprimer vous-même
|
||||||
users.still_own_repo=Cet utilisateur possède un ou plusieurs dépôts. Veuillez les supprimer ou les transférer à un autre utilisateur.
|
users.still_own_repo=Cet utilisateur possède un ou plusieurs dépôts. Veuillez les supprimer ou les transférer à un autre utilisateur.
|
||||||
users.still_has_org=Cet utilisateur est membre d'une organisation. Veuillez le retirer de toutes les organisations dont il est membre au préalable.
|
users.still_has_org=Cet utilisateur est membre d'une organisation. Veuillez le retirer de toutes les organisations dont il est membre au préalable.
|
||||||
|
users.purge=Purger l'utilisateur
|
||||||
|
users.purge_help=Éradique l'utilisateur et tous ses dépôts, organisations, commentaires et paquets.
|
||||||
|
users.still_own_packages=Cet utilisateur possède encore un ou plusieurs paquets. Supprimez d’abord ces paquets.
|
||||||
users.deletion_success=Le compte a été supprimé.
|
users.deletion_success=Le compte a été supprimé.
|
||||||
users.reset_2fa=Réinitialiser l'authentification à deux facteurs
|
users.reset_2fa=Réinitialiser l'authentification à deux facteurs
|
||||||
|
users.list_status_filter.menu_text=Filtrer
|
||||||
|
users.list_status_filter.reset=Réinitialiser
|
||||||
users.list_status_filter.is_active=Actif
|
users.list_status_filter.is_active=Actif
|
||||||
users.list_status_filter.is_admin=Administrateur
|
users.list_status_filter.is_admin=Administrateur
|
||||||
users.list_status_filter.is_restricted=Restreint
|
users.list_status_filter.is_restricted=Restreint
|
||||||
@ -2313,6 +2490,7 @@ auths.tip.yandex=`Créez une nouvelle application sur https://oauth.yandex.com/c
|
|||||||
auths.tip.mastodon=Entrez une URL d'instance personnalisée pour l'instance mastodon avec laquelle vous voulez vous authentifier (ou utiliser celle par défaut)
|
auths.tip.mastodon=Entrez une URL d'instance personnalisée pour l'instance mastodon avec laquelle vous voulez vous authentifier (ou utiliser celle par défaut)
|
||||||
auths.edit=Mettre à jour la source d'authentification
|
auths.edit=Mettre à jour la source d'authentification
|
||||||
auths.activated=Cette source d'authentification est activée
|
auths.activated=Cette source d'authentification est activée
|
||||||
|
auths.new_success=L'authentification "%s" a été ajoutée.
|
||||||
auths.update_success=La source d'authentification a été mise à jour.
|
auths.update_success=La source d'authentification a été mise à jour.
|
||||||
auths.update=Mettre à jour la source d'authentification
|
auths.update=Mettre à jour la source d'authentification
|
||||||
auths.delete=Supprimer la source d'authentification
|
auths.delete=Supprimer la source d'authentification
|
||||||
@ -2320,7 +2498,10 @@ auths.delete_auth_title=Suppression de la source d'authentification
|
|||||||
auths.delete_auth_desc=La suppression d'une source d'authentification empêche les utilisateurs de l'utiliser pour se connecter. Continuer ?
|
auths.delete_auth_desc=La suppression d'une source d'authentification empêche les utilisateurs de l'utiliser pour se connecter. Continuer ?
|
||||||
auths.still_in_used=Cette source d'authentification est utilisée par un ou plusieurs utilisateurs, veuillez convertir ou supprimer ces comptes utilisateurs avant toute action.
|
auths.still_in_used=Cette source d'authentification est utilisée par un ou plusieurs utilisateurs, veuillez convertir ou supprimer ces comptes utilisateurs avant toute action.
|
||||||
auths.deletion_success=La source d'authentification a été supprimée.
|
auths.deletion_success=La source d'authentification a été supprimée.
|
||||||
|
auths.login_source_exist=La source d'authentification "%s" existe déjà.
|
||||||
auths.login_source_of_type_exist=Une source d'authentification de ce type existe déjà.
|
auths.login_source_of_type_exist=Une source d'authentification de ce type existe déjà.
|
||||||
|
auths.unable_to_initialize_openid=Impossible d'initialiser le fournisseur OpenID Connect : %s
|
||||||
|
auths.invalid_openIdConnectAutoDiscoveryURL=URL de découverte automatique invalide (une URL valide commence par http:// ou https://)
|
||||||
|
|
||||||
config.server_config=Configuration du serveur
|
config.server_config=Configuration du serveur
|
||||||
config.app_name=Titre du site
|
config.app_name=Titre du site
|
||||||
@ -2328,6 +2509,7 @@ config.app_ver=Version de Gitea
|
|||||||
config.app_url=URL de base de Gitea
|
config.app_url=URL de base de Gitea
|
||||||
config.custom_conf=Chemin du fichier de configuration
|
config.custom_conf=Chemin du fichier de configuration
|
||||||
config.custom_file_root_path=Emplacement personnalisé du fichier racine
|
config.custom_file_root_path=Emplacement personnalisé du fichier racine
|
||||||
|
config.domain=Domaine du serveur
|
||||||
config.offline_mode=Mode hors-ligne
|
config.offline_mode=Mode hors-ligne
|
||||||
config.disable_router_log=Désactiver la Journalisation du Routeur
|
config.disable_router_log=Désactiver la Journalisation du Routeur
|
||||||
config.run_user=Exécuter avec l'utilisateur
|
config.run_user=Exécuter avec l'utilisateur
|
||||||
@ -2343,6 +2525,7 @@ config.reverse_auth_user=Annuler l'Authentification de l'Utilisateur
|
|||||||
config.ssh_config=Configuration SSH
|
config.ssh_config=Configuration SSH
|
||||||
config.ssh_enabled=Activé
|
config.ssh_enabled=Activé
|
||||||
config.ssh_start_builtin_server=Utiliser le serveur incorporé
|
config.ssh_start_builtin_server=Utiliser le serveur incorporé
|
||||||
|
config.ssh_domain=Domaine du serveur SSH
|
||||||
config.ssh_port=Port
|
config.ssh_port=Port
|
||||||
config.ssh_listen_port=Port d'écoute
|
config.ssh_listen_port=Port d'écoute
|
||||||
config.ssh_root_path=Emplacement racine
|
config.ssh_root_path=Emplacement racine
|
||||||
@ -2393,16 +2576,23 @@ config.queue_length=Longueur de la file d'attente
|
|||||||
config.deliver_timeout=Expiration d'Envoi
|
config.deliver_timeout=Expiration d'Envoi
|
||||||
config.skip_tls_verify=Passer la vérification TLS
|
config.skip_tls_verify=Passer la vérification TLS
|
||||||
|
|
||||||
|
config.mailer_config=Configuration du service SMTP
|
||||||
config.mailer_enabled=Activé
|
config.mailer_enabled=Activé
|
||||||
|
config.mailer_enable_helo=Activer HELO
|
||||||
config.mailer_name=Nom
|
config.mailer_name=Nom
|
||||||
|
config.mailer_protocol=Protocole
|
||||||
|
config.mailer_smtp_addr=Adresse SMTP
|
||||||
config.mailer_smtp_port=Port SMTP
|
config.mailer_smtp_port=Port SMTP
|
||||||
config.mailer_user=Utilisateur
|
config.mailer_user=Utilisateur
|
||||||
config.mailer_use_sendmail=Utiliser Sendmail
|
config.mailer_use_sendmail=Utiliser Sendmail
|
||||||
config.mailer_sendmail_path=Chemin d’accès à Sendmail
|
config.mailer_sendmail_path=Chemin d’accès à Sendmail
|
||||||
config.mailer_sendmail_args=Arguments supplémentaires pour Sendmail
|
config.mailer_sendmail_args=Arguments supplémentaires pour Sendmail
|
||||||
config.mailer_sendmail_timeout=Délai d’attente de Sendmail
|
config.mailer_sendmail_timeout=Délai d’attente de Sendmail
|
||||||
|
config.mailer_use_dummy=Factice
|
||||||
config.test_email_placeholder=E-mail (ex: test@example.com)
|
config.test_email_placeholder=E-mail (ex: test@example.com)
|
||||||
config.send_test_mail=Envoyer un e-mail de test
|
config.send_test_mail=Envoyer un e-mail de test
|
||||||
|
config.test_mail_failed=Impossible d'envoyer un email de test à "%s" : %v
|
||||||
|
config.test_mail_sent=Un e-mail de test a été envoyé à "%s".
|
||||||
|
|
||||||
config.oauth_config=Configuration OAuth
|
config.oauth_config=Configuration OAuth
|
||||||
config.oauth_enabled=Activé
|
config.oauth_enabled=Activé
|
||||||
@ -2432,6 +2622,8 @@ config.git_disable_diff_highlight=Désactiver la surbrillance syntaxique de Diff
|
|||||||
config.git_max_diff_lines=Lignes de Diff Max (pour un seul fichier)
|
config.git_max_diff_lines=Lignes de Diff Max (pour un seul fichier)
|
||||||
config.git_max_diff_line_characters=Nombre max de caractères de Diff (pour une seule ligne)
|
config.git_max_diff_line_characters=Nombre max de caractères de Diff (pour une seule ligne)
|
||||||
config.git_max_diff_files=Nombre max de fichiers de Diff (à afficher)
|
config.git_max_diff_files=Nombre max de fichiers de Diff (à afficher)
|
||||||
|
config.git_enable_reflogs=Activer les reflogs
|
||||||
|
config.git_reflog_expiry_time=Délai d'expiration
|
||||||
config.git_gc_args=Arguments de GC
|
config.git_gc_args=Arguments de GC
|
||||||
config.git_migrate_timeout=Délai imparti pour une migration
|
config.git_migrate_timeout=Délai imparti pour une migration
|
||||||
config.git_mirror_timeout=Délai imparti pour mettre à jour le miroir
|
config.git_mirror_timeout=Délai imparti pour mettre à jour le miroir
|
||||||
@ -2451,6 +2643,8 @@ config.access_log_template=Modèle
|
|||||||
config.xorm_log_mode=Mode de journalisation de XORM
|
config.xorm_log_mode=Mode de journalisation de XORM
|
||||||
config.xorm_log_sql=Activer la journalisation SQL
|
config.xorm_log_sql=Activer la journalisation SQL
|
||||||
|
|
||||||
|
config.get_setting_failed=Impossible d'obtenir le paramètre %s
|
||||||
|
config.set_setting_failed=Impossible de définir le paramètre %s
|
||||||
|
|
||||||
monitor.cron=Tâches récurrentes
|
monitor.cron=Tâches récurrentes
|
||||||
monitor.name=Nom
|
monitor.name=Nom
|
||||||
@ -2459,12 +2653,16 @@ monitor.next=Suivant
|
|||||||
monitor.previous=Précédent
|
monitor.previous=Précédent
|
||||||
monitor.execute_times=Exécutions
|
monitor.execute_times=Exécutions
|
||||||
monitor.process=Processus en cours d'exécution
|
monitor.process=Processus en cours d'exécution
|
||||||
|
monitor.stacktrace=Piles d'execution
|
||||||
|
monitor.goroutines=%d Goroutines
|
||||||
monitor.desc=Description
|
monitor.desc=Description
|
||||||
monitor.start=Heure de démarrage
|
monitor.start=Heure de démarrage
|
||||||
monitor.execute_time=Heure d'Éxécution
|
monitor.execute_time=Heure d'Éxécution
|
||||||
|
monitor.last_execution_result=Résultat
|
||||||
monitor.process.cancel=Annuler le processus
|
monitor.process.cancel=Annuler le processus
|
||||||
monitor.process.cancel_desc=L'annulation d'un processus peut entraîner une perte de données
|
monitor.process.cancel_desc=L'annulation d'un processus peut entraîner une perte de données
|
||||||
monitor.process.cancel_notices=Annuler : <strong>%s</strong>?
|
monitor.process.cancel_notices=Annuler : <strong>%s</strong>?
|
||||||
|
monitor.process.children=Enfant
|
||||||
monitor.queues=Files d'attente
|
monitor.queues=Files d'attente
|
||||||
monitor.queue=File d'attente : %s
|
monitor.queue=File d'attente : %s
|
||||||
monitor.queue.name=Nom
|
monitor.queue.name=Nom
|
||||||
@ -2472,6 +2670,7 @@ monitor.queue.type=Type
|
|||||||
monitor.queue.exemplar=Type d'exemple
|
monitor.queue.exemplar=Type d'exemple
|
||||||
monitor.queue.numberworkers=Nombre de processus
|
monitor.queue.numberworkers=Nombre de processus
|
||||||
monitor.queue.maxnumberworkers=Nombre maximale de processus
|
monitor.queue.maxnumberworkers=Nombre maximale de processus
|
||||||
|
monitor.queue.numberinqueue=Position dans la queue
|
||||||
monitor.queue.review=Revoir la configuration
|
monitor.queue.review=Revoir la configuration
|
||||||
monitor.queue.review_add=Réviser/Ajouter des processus
|
monitor.queue.review_add=Réviser/Ajouter des processus
|
||||||
monitor.queue.configuration=Configuration initiale
|
monitor.queue.configuration=Configuration initiale
|
||||||
@ -2479,6 +2678,7 @@ monitor.queue.nopool.title=Pas de réservoir de processus
|
|||||||
monitor.queue.nopool.desc=Cette file d'attente contient d'autres files d'attente et ne possède pas de réservoir de processus.
|
monitor.queue.nopool.desc=Cette file d'attente contient d'autres files d'attente et ne possède pas de réservoir de processus.
|
||||||
monitor.queue.wrapped.desc=Une file d'attente enveloppée, enveloppe une file d'attente de démarrage lente, mettant en mémoire tampon les requêtes dans un canal. Elle n'a pas de pool de processus elle-même.
|
monitor.queue.wrapped.desc=Une file d'attente enveloppée, enveloppe une file d'attente de démarrage lente, mettant en mémoire tampon les requêtes dans un canal. Elle n'a pas de pool de processus elle-même.
|
||||||
monitor.queue.persistable-channel.desc=Un canal persistant enveloppe deux files d'attente, une file d'attente de canal qui a son propre pool de processus et une file d'attente de niveau pour les requêtes persistantes des arrêts précédents. Il ne dispose pas d'un pool de travailleurs lui-même.
|
monitor.queue.persistable-channel.desc=Un canal persistant enveloppe deux files d'attente, une file d'attente de canal qui a son propre pool de processus et une file d'attente de niveau pour les requêtes persistantes des arrêts précédents. Il ne dispose pas d'un pool de travailleurs lui-même.
|
||||||
|
monitor.queue.flush=Évacuer la queue
|
||||||
monitor.queue.pool.timeout=Expiration du délai
|
monitor.queue.pool.timeout=Expiration du délai
|
||||||
monitor.queue.pool.addworkers.title=Ajouter un processus
|
monitor.queue.pool.addworkers.title=Ajouter un processus
|
||||||
monitor.queue.pool.addworkers.submit=Ajouter un processus
|
monitor.queue.pool.addworkers.submit=Ajouter un processus
|
||||||
@ -2491,6 +2691,12 @@ monitor.queue.pool.flush.title=Vider la file d'attente
|
|||||||
monitor.queue.pool.flush.desc=Vider va ajouter un processus qui se terminera une fois que la file d'attente est vide, ou qu'elle est en délai d'attente dépassé.
|
monitor.queue.pool.flush.desc=Vider va ajouter un processus qui se terminera une fois que la file d'attente est vide, ou qu'elle est en délai d'attente dépassé.
|
||||||
monitor.queue.pool.flush.submit=Ajouter un processus de vidage
|
monitor.queue.pool.flush.submit=Ajouter un processus de vidage
|
||||||
monitor.queue.pool.flush.added=Processus de vidage ajouté pour %[1]s
|
monitor.queue.pool.flush.added=Processus de vidage ajouté pour %[1]s
|
||||||
|
monitor.queue.pool.pause.title=Suspendre la queue
|
||||||
|
monitor.queue.pool.pause.desc=Suspendre une queue l'empêchera de traiter les données
|
||||||
|
monitor.queue.pool.pause.submit=Suspendre la queue
|
||||||
|
monitor.queue.pool.resume.title=Reprendre la queue
|
||||||
|
monitor.queue.pool.resume.desc=Définit cette queue pour reprendre le travail
|
||||||
|
monitor.queue.pool.resume.submit=Reprendre la queue
|
||||||
|
|
||||||
monitor.queue.settings.title=Paramètres du réservoir
|
monitor.queue.settings.title=Paramètres du réservoir
|
||||||
monitor.queue.settings.desc=Les pools se développent dynamiquement en réponse au blocage de la file d'attente des processus. Ces changements n'affecteront pas les groupes de processus actuels.
|
monitor.queue.settings.desc=Les pools se développent dynamiquement en réponse au blocage de la file d'attente des processus. Ces changements n'affecteront pas les groupes de processus actuels.
|
||||||
@ -2520,6 +2726,7 @@ monitor.queue.pool.cancel_desc=Quitter une file d'attente sans aucun groupe de p
|
|||||||
|
|
||||||
notices.system_notice_list=Informations
|
notices.system_notice_list=Informations
|
||||||
notices.view_detail_header=Voir les détails de l'information système
|
notices.view_detail_header=Voir les détails de l'information système
|
||||||
|
notices.operations=Opérations
|
||||||
notices.select_all=Tout Sélectionner
|
notices.select_all=Tout Sélectionner
|
||||||
notices.deselect_all=Tout désélectionner
|
notices.deselect_all=Tout désélectionner
|
||||||
notices.inverse_selection=Inverser la sélection
|
notices.inverse_selection=Inverser la sélection
|
||||||
@ -2535,12 +2742,15 @@ notices.delete_success=Les informations systèmes ont été supprimées.
|
|||||||
[action]
|
[action]
|
||||||
create_repo=a créé le dépôt <a href="%s">%s</a>
|
create_repo=a créé le dépôt <a href="%s">%s</a>
|
||||||
rename_repo=a rebaptisé le dépôt de <code>%[1]s</code> vers <a href="%[2]s">%[3]s</a>
|
rename_repo=a rebaptisé le dépôt de <code>%[1]s</code> vers <a href="%[2]s">%[3]s</a>
|
||||||
|
create_issue=`ticket ouvert <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||||
|
close_issue=`ticket fermé <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||||
create_pull_request=`a créé la demande d'ajout <a href="%[1]s">%[3]s#%[2]s</a>`
|
create_pull_request=`a créé la demande d'ajout <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||||
close_pull_request=`a fermé la demande d'ajout <a href="%[1]s">%[3]s#%[2]s</a>`
|
close_pull_request=`a fermé la demande d'ajout <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||||
reopen_pull_request=`a réouvert la demande d'ajout <a href="%[1]s">%[3]s#%[2]s</a>`
|
reopen_pull_request=`a réouvert la demande d'ajout <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||||
comment_pull=`a commenté la demande d'ajout <a href="%[1]s">%[3]s#%[2]s</a>`
|
comment_pull=`a commenté la demande d'ajout <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||||
merge_pull_request=`a fusionné la demande d'ajout <a href="%[1]s">%[3]s#%[2]s</a>`
|
merge_pull_request=`a fusionné la demande d'ajout <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||||
transfer_repo=a transféré le dépôt <code>%s</code> à <a href="%s">%s</a>
|
transfer_repo=a transféré le dépôt <code>%s</code> à <a href="%s">%s</a>
|
||||||
|
push_tag=a poussé l'étiquette <a href="%[2]s">%[3]s</a> vers <a href="%[1]s">%[4]s</a>
|
||||||
delete_tag=étiquette supprimée %[2]s de <a href="%[1]s">%[3]s</a>
|
delete_tag=étiquette supprimée %[2]s de <a href="%[1]s">%[3]s</a>
|
||||||
delete_branch=branche %[2]s supprimée de <a href="%[1]s">%[3]s</a>
|
delete_branch=branche %[2]s supprimée de <a href="%[1]s">%[3]s</a>
|
||||||
compare_branch=Comparer
|
compare_branch=Comparer
|
||||||
@ -2588,6 +2798,9 @@ pin=Epingler la notification
|
|||||||
mark_as_read=Marquer comme lu
|
mark_as_read=Marquer comme lu
|
||||||
mark_as_unread=Marquer comme non lue
|
mark_as_unread=Marquer comme non lue
|
||||||
mark_all_as_read=Tout marquer comme lu
|
mark_all_as_read=Tout marquer comme lu
|
||||||
|
subscriptions=Abonnements
|
||||||
|
watching=Suivi
|
||||||
|
no_subscriptions=Pas d'abonnements
|
||||||
|
|
||||||
[gpg]
|
[gpg]
|
||||||
default_key=Signé avec la clé par défaut
|
default_key=Signé avec la clé par défaut
|
||||||
@ -2601,35 +2814,216 @@ error.probable_bad_signature=AVERTISSEMENT ! Bien qu'il y ait une clé avec cet
|
|||||||
error.probable_bad_default_signature=AVERTISSEMENT ! Bien que la clé par défaut ait cet ID, elle ne vérifie pas cette livraison ! Cette livraison est SUSPECTE.
|
error.probable_bad_default_signature=AVERTISSEMENT ! Bien que la clé par défaut ait cet ID, elle ne vérifie pas cette livraison ! Cette livraison est SUSPECTE.
|
||||||
|
|
||||||
[units]
|
[units]
|
||||||
|
unit=Unité
|
||||||
error.no_unit_allowed_repo=Vous n'êtes pas autorisé à accéder à n'importe quelle section de ce dépôt.
|
error.no_unit_allowed_repo=Vous n'êtes pas autorisé à accéder à n'importe quelle section de ce dépôt.
|
||||||
error.unit_not_allowed=Vous n'êtes pas autorisé à accéder à cette section du dépôt.
|
error.unit_not_allowed=Vous n'êtes pas autorisé à accéder à cette section du dépôt.
|
||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
|
title=Paquets
|
||||||
|
desc=Gérer les paquets du dépôt.
|
||||||
|
empty=Il n'y pas de paquet pour le moment.
|
||||||
|
empty.documentation=Pour plus d'informations sur le registre de paquets, voir <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/en-us/usage/packages/overview/">la documentation</a>.
|
||||||
empty.repo=Avez-vous téléchargé un paquet, mais il n'est pas affiché ici? Allez dans les <a href="%[1]s">paramètres du paquet</a> et liez le à ce dépôt.
|
empty.repo=Avez-vous téléchargé un paquet, mais il n'est pas affiché ici? Allez dans les <a href="%[1]s">paramètres du paquet</a> et liez le à ce dépôt.
|
||||||
filter.type=Type
|
filter.type=Type
|
||||||
|
filter.type.all=Tous
|
||||||
|
filter.no_result=Votre filtre n'affiche aucun résultat.
|
||||||
|
filter.container.tagged=Balisé
|
||||||
|
filter.container.untagged=Débalisé
|
||||||
|
published_by=%[1]s publié par <a href="%[2]s">%[3]s</a>
|
||||||
published_by_in=%[1]s publié par <a href="%[2]s">%[3]s</a> en <a href="%[4]s"><strong>%[5]s</strong></a>
|
published_by_in=%[1]s publié par <a href="%[2]s">%[3]s</a> en <a href="%[4]s"><strong>%[5]s</strong></a>
|
||||||
|
installation=Installation
|
||||||
|
about=À propos de ce paquet
|
||||||
|
requirements=Exigences
|
||||||
|
dependencies=Dépendances
|
||||||
|
keywords=Mots-clés
|
||||||
|
details=Détails
|
||||||
|
details.author=Auteur
|
||||||
|
details.project_site=Site du projet
|
||||||
|
details.repository_site=Site du dépôt
|
||||||
|
details.documentation_site=Site de documentation
|
||||||
|
details.license=Licence
|
||||||
|
assets=Ressources
|
||||||
|
versions=Versions
|
||||||
|
versions.view_all=Voir tout
|
||||||
|
dependency.id=ID
|
||||||
|
dependency.version=Version
|
||||||
|
cargo.install=Pour installer le paquet en utilisant Cargo, exécutez la commande suivante :
|
||||||
|
cargo.documentation=Pour plus d'informations sur le registre Cargo, voir <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/fr-fr/packages/cargo/">la documentation</a>.
|
||||||
|
cargo.details.repository_site=Site du dépôt
|
||||||
|
cargo.details.documentation_site=Site de documentation
|
||||||
|
chef.registry=Configurer ce registre dans votre fichier <code>~/.chef/config.rb</code>:
|
||||||
|
chef.install=Pour installer le paquet, exécutez la commande suivante :
|
||||||
|
chef.documentation=Pour plus d'informations sur le registre Chef, voir <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/fr-fr/packages/chef/">la documentation</a>.
|
||||||
|
composer.registry=Configurez ce registre dans votre fichier <code>~/.composer/config.json</code> :
|
||||||
|
composer.install=Pour installer le paquet en utilisant Composer, exécutez la commande suivante :
|
||||||
|
composer.documentation=Pour plus d'informations sur le registre Composer voir <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/fr-fr/packages/composer/">la documentation</a>.
|
||||||
|
composer.dependencies=Dépendances
|
||||||
|
composer.dependencies.development=Dépendances de développement
|
||||||
conan.details.repository=Dépôt
|
conan.details.repository=Dépôt
|
||||||
|
conan.registry=Configurez ce registre à partir d'un terminal :
|
||||||
|
conan.install=Pour installer le paquet en utilisant Conan, exécutez la commande suivante :
|
||||||
|
conan.documentation=Pour plus d'informations sur le registre Conan, voir <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/fr-fr/packages/conan/">la documentation</a>.
|
||||||
|
conda.documentation=Pour plus d'informations sur le registre Conda, voir <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/fr-fr/packages/conda/">la documentation</a>.
|
||||||
|
conda.details.repository_site=Site du dépôt
|
||||||
|
conda.details.documentation_site=Site de documentation
|
||||||
|
container.details.type=Type d'image
|
||||||
|
container.details.platform=Plateforme
|
||||||
|
container.pull=Tirez l'image depuis un terminal :
|
||||||
|
container.digest=Empreinte :
|
||||||
|
container.documentation=Pour plus d'informations sur le registre Container, voir <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/fr-fr/packages/container/">la documentation</a>.
|
||||||
|
container.multi_arch=SE / Arch
|
||||||
|
container.layers=Calques d'image
|
||||||
|
container.labels=Étiquettes
|
||||||
|
container.labels.key=Clé
|
||||||
|
container.labels.value=Valeur
|
||||||
|
generic.download=Télécharger le paquet depuis un terminal :
|
||||||
|
generic.documentation=Pour plus d'informations sur le registre générique, voir <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/fr-fr/packages/generic/">la documentation</a>.
|
||||||
|
helm.registry=Configurer ce registre à partir d'un terminal :
|
||||||
|
helm.install=Pour installer le paquet, exécutez la commande suivante :
|
||||||
|
helm.documentation=Pour plus d'informations sur le registre Helm, voir <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/fr-fr/packages/helm/">la documentation</a>.
|
||||||
|
maven.install2=Exécuter dans un terminal :
|
||||||
|
maven.documentation=Pour plus d'informations sur le registre Maven, voir <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/fr-fr/packages/maven/">la documentation</a>.
|
||||||
|
nuget.registry=Configurer ce registre à partir d'un terminal :
|
||||||
|
nuget.documentation=Pour plus d'informations sur le registre NuGet, voir <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/fr-fr/packages/nuget/">la documentation</a>.
|
||||||
|
nuget.dependency.framework=Cadriciel cible
|
||||||
|
npm.install2=ou ajoutez-le au fichier package.json :
|
||||||
|
npm.documentation=Pour plus d'informations sur le registre npm, voir <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/fr-fr/packages/npm/">la documentation</a>.
|
||||||
|
npm.dependencies=Dépendances
|
||||||
|
npm.dependencies.development=Dépendances de développement
|
||||||
|
npm.dependencies.peer=Dépendances de pairs
|
||||||
|
npm.dependencies.optional=Dépendances optionnelles
|
||||||
|
npm.details.tag=Balise
|
||||||
|
pub.documentation=Pour davantage d'informations sur le registre Pub, voir <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/fr-fr/packages/pub/">la documentation</a>.
|
||||||
|
pypi.requires=Nécessite Python
|
||||||
|
pypi.documentation=Pour plus d'informations sur le registre PyPI, voir <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/fr-fr/packages/pypi/">la documentation</a>.
|
||||||
|
rubygems.install2=ou ajoutez-le au Gemfile :
|
||||||
|
rubygems.dependencies.runtime=Dépendances d'exécution
|
||||||
|
rubygems.dependencies.development=Dépendances de développement
|
||||||
rubygems.required.ruby=Nécessite la version de Ruby
|
rubygems.required.ruby=Nécessite la version de Ruby
|
||||||
rubygems.required.rubygems=Nécessite la version de RubyGem
|
rubygems.required.rubygems=Nécessite la version de RubyGem
|
||||||
|
rubygems.documentation=Pour plus d'informations sur le registre RubyGems, consulter <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/fr-fr/packages/rubygems/">la documentation</a>.
|
||||||
|
swift.registry=Configurez ce registre à partir d'un terminal :
|
||||||
|
swift.install=Ajoutez le paquet dans votre fichier <code>Package.swift</code>:
|
||||||
|
swift.install2=et exécutez la commande suivante :
|
||||||
|
swift.documentation=Pour plus d'informations sur le registre Swift, voir <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/fr-fr/packages/swift/">la documentation</a>.
|
||||||
|
vagrant.documentation=Pour plus d'informations sur le registre Vagrant, voir <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/fr-fr/packages/vagrant/">la documentation</a>.
|
||||||
|
settings.link=Lier ce paquet à un dépôt
|
||||||
|
settings.link.select=Sélectionner un dépôt
|
||||||
|
settings.link.button=Actualiser le lien du dépôt
|
||||||
|
settings.link.success=Le lien du dépôt a été mis à jour avec succès.
|
||||||
|
settings.link.error=Impossible de mettre à jour le lien du dépôt.
|
||||||
|
settings.delete=Supprimer le paquet
|
||||||
|
settings.delete.success=Le paquet a été supprimé.
|
||||||
|
settings.delete.error=Impossible de supprimer le paquet.
|
||||||
|
owner.settings.cargo.title=Index du Registre Cargo
|
||||||
|
owner.settings.cargo.initialize=Initialiser l'index
|
||||||
|
owner.settings.cargo.initialize.description=Pour utiliser le registre Cargo, un dépôt git d'index spécial est nécessaire. Ici, vous pouvez le (re)créer avec la configuration requise.
|
||||||
|
owner.settings.cargo.initialize.error=Impossible d'initialiser l'index de Cargo : %v
|
||||||
|
owner.settings.cargo.initialize.success=L'index Cargo a été créé avec succès.
|
||||||
|
owner.settings.cargo.rebuild=Reconstruire l'index
|
||||||
|
owner.settings.cargo.rebuild.description=Si l’index est désynchronisé avec les paquets cargo stockés, vous pouvez le reconstruire ici.
|
||||||
|
owner.settings.cargo.rebuild.error=Impossible de reconstruire l'index Cargo : %v
|
||||||
|
owner.settings.cargo.rebuild.success=L'index Cargo a été reconstruit avec succès.
|
||||||
|
owner.settings.cleanuprules.title=Gérer les règles de nettoyage
|
||||||
|
owner.settings.cleanuprules.add=Ajouter une règle de nettoyage
|
||||||
|
owner.settings.cleanuprules.edit=Modifier la règle de nettoyage
|
||||||
|
owner.settings.cleanuprules.none=Aucune règle de nettoyage disponible. Consultez la documentation pour en savoir plus.
|
||||||
|
owner.settings.cleanuprules.preview=Aperçu des règles de nettoyage
|
||||||
|
owner.settings.cleanuprules.preview.overview=%d paquets sont programmés pour être supprimés.
|
||||||
|
owner.settings.cleanuprules.preview.none=La règle de nettoyage ne correspond à aucun paquet.
|
||||||
owner.settings.cleanuprules.enabled=Activé
|
owner.settings.cleanuprules.enabled=Activé
|
||||||
|
owner.settings.cleanuprules.pattern_full_match=Appliquer le modèle au nom complet du paquet
|
||||||
|
owner.settings.cleanuprules.keep.title=Les versions qui correspondent à ces règles sont conservées, même si elles correspondent à une règle de suppression ci-dessous.
|
||||||
|
owner.settings.cleanuprules.keep.count=Garder le plus récent
|
||||||
|
owner.settings.cleanuprules.keep.count.1=1 version par paquet
|
||||||
|
owner.settings.cleanuprules.keep.count.n=%d versions par paquet
|
||||||
|
owner.settings.cleanuprules.keep.pattern=Garder les versions correspondantes
|
||||||
|
owner.settings.cleanuprules.keep.pattern.container=La version <code>la plus récente</code> est toujours conservée pour les paquets Container.
|
||||||
|
owner.settings.cleanuprules.remove.title=Les versions qui correspondent à ces règles sont supprimées, sauf si une règle ci-dessus dit de les garder.
|
||||||
|
owner.settings.cleanuprules.remove.days=Supprimer les versions antérieures à
|
||||||
|
owner.settings.cleanuprules.remove.pattern=Supprimer les versions correspondantes
|
||||||
|
owner.settings.cleanuprules.success.update=La règle de nettoyage a été mise à jour.
|
||||||
|
owner.settings.cleanuprules.success.delete=La règle de nettoyage a été supprimée.
|
||||||
|
owner.settings.chef.title=Dépôt Chef
|
||||||
|
owner.settings.chef.keypair=Générer une paire de clés
|
||||||
|
owner.settings.chef.keypair.description=Génère une paire de clés utilisée pour s’authentifier auprès du registre Chef. La précédente clé ne pourra plus être utilisée.
|
||||||
|
|
||||||
[secrets]
|
[secrets]
|
||||||
|
secrets=Secrets
|
||||||
|
description=Les secrets seront transmis à certaines actions et ne pourront pas être lus autrement.
|
||||||
|
none=Il n'y a pas encore de secrets.
|
||||||
|
value=Valeur
|
||||||
name=Nom
|
name=Nom
|
||||||
|
creation=Ajouter un secret
|
||||||
|
creation.name_placeholder=Caractères alphanumériques ou tirets bas uniquement, insensibles à la casse, ne peut commencer par GITEA_ ou GITHUB_
|
||||||
|
creation.success=Le secret "%s" a été ajouté.
|
||||||
|
creation.failed=Impossible d'ajouter le secret.
|
||||||
|
deletion=Supprimer le secret
|
||||||
|
deletion.description=La suppression d'un secret est permanente et ne peut pas être annulée. Continuer ?
|
||||||
|
deletion.success=Le secret a été supprimé.
|
||||||
|
deletion.failed=Impossible de supprimer le secret.
|
||||||
|
|
||||||
[actions]
|
[actions]
|
||||||
|
actions=Actions
|
||||||
|
|
||||||
|
unit.desc=Gérer les actions
|
||||||
|
|
||||||
|
status.unknown=Inconnu
|
||||||
|
status.waiting=En attente
|
||||||
|
status.running=En cours d'exécution
|
||||||
|
status.success=Succès
|
||||||
|
status.failure=Échec
|
||||||
|
status.cancelled=Annulé
|
||||||
|
status.skipped=Ignoré
|
||||||
|
status.blocked=Bloqué
|
||||||
|
|
||||||
|
runners=Exécuteurs
|
||||||
|
runners.runner_manage_panel=Gestion des exécuteurs
|
||||||
|
runners.new=Créer un nouvel exécuteur
|
||||||
|
runners.new_notice=Comment démarrer un exécuteur
|
||||||
|
runners.status=Statut
|
||||||
|
runners.id=ID
|
||||||
runners.name=Nom
|
runners.name=Nom
|
||||||
runners.owner_type=Type
|
runners.owner_type=Type
|
||||||
runners.description=Description
|
runners.description=Description
|
||||||
|
runners.labels=Étiquettes
|
||||||
|
runners.last_online=Dernière fois en ligne
|
||||||
|
runners.agent_labels=Étiquettes de l'agent
|
||||||
|
runners.custom_labels=Étiquettes personnalisées
|
||||||
|
runners.custom_labels_helper=Les libellés personnalisées sont des libellés ajoutées manuellement par un administrateur. Ils sont séparés par des virgules et les blancs les cernant seront taillés.
|
||||||
|
runners.runner_title=Exécuteur
|
||||||
|
runners.task_list=Tâches récentes sur cet exécuteur
|
||||||
runners.task_list.run=Exécuter
|
runners.task_list.run=Exécuter
|
||||||
|
runners.task_list.status=Statut
|
||||||
runners.task_list.repository=Dépôt
|
runners.task_list.repository=Dépôt
|
||||||
runners.task_list.commit=Commit
|
runners.task_list.commit=Commit
|
||||||
|
runners.task_list.done_at=Fait à
|
||||||
|
runners.edit_runner=Éditer l'Exécuteur
|
||||||
|
runners.update_runner=Mettre à jour les modifications
|
||||||
|
runners.update_runner_success=Exécuteur mis à jour avec succès
|
||||||
|
runners.update_runner_failed=Impossible de mettre à jour l'Exécuteur
|
||||||
|
runners.delete_runner=Supprimer cet exécuteur
|
||||||
|
runners.delete_runner_success=Exécuteur supprimé avec succès
|
||||||
|
runners.delete_runner_failed=Impossible de supprimer l'Exécuteur
|
||||||
|
runners.delete_runner_header=Êtes-vous sûr de vouloir supprimer cet exécuteur ?
|
||||||
|
runners.none=Aucun exécuteur disponible
|
||||||
|
runners.status.unspecified=Inconnu
|
||||||
|
runners.status.idle=Inactif
|
||||||
runners.status.active=Actif
|
runners.status.active=Actif
|
||||||
|
runners.status.offline=Hors-ligne
|
||||||
|
runners.version=Version
|
||||||
|
runners.reset_registration_token_success=Le jeton d’inscription de l’exécuteur a été réinitialisé avec succès
|
||||||
|
|
||||||
|
runs.open_tab=%d Ouvert
|
||||||
|
runs.closed_tab=%d Fermé
|
||||||
runs.commit=Commit
|
runs.commit=Commit
|
||||||
|
runs.pushed_by=Poussée par
|
||||||
|
runs.no_matching_runner_helper=Aucun exécuteur correspondant : %s
|
||||||
|
|
||||||
|
need_approval_desc=Besoin d'approbation pour exécuter des workflows pour une demande de fusion de bifurcation.
|
||||||
|
|
||||||
[projects]
|
[projects]
|
||||||
|
type-1.display_name=Projet personnel
|
||||||
|
type-2.display_name=Projet de dépôt
|
||||||
|
type-3.display_name=Projet d’organisation
|
||||||
|
|
||||||
|
@ -462,6 +462,7 @@ team_invite.text_3=Not: Bu davet %[1] içindi. Bu daveti beklemiyorsanız, e-pos
|
|||||||
[modal]
|
[modal]
|
||||||
yes=Evet
|
yes=Evet
|
||||||
no=Hayır
|
no=Hayır
|
||||||
|
confirm=Onayla
|
||||||
cancel=İptal
|
cancel=İptal
|
||||||
modify=Güncelle
|
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.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.cant_reopen_deleted_branch=Dal silindiğinden bu değişiklik isteği yeniden açılamaz.
|
||||||
pulls.merged=Birleştirildi
|
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.manually_merged=Elle birleştirildi
|
||||||
|
pulls.merged_info_text=%s dalı şimdi silinebilir.
|
||||||
pulls.is_closed=Değişiklik isteği kapatıldı.
|
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.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.
|
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
|
cargo.details.documentation_site=Belge Sitesi
|
||||||
chef.registry=Bu kütüğü <code>~/.chef/config.rb</code> dosyasında ayarlayın:
|
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.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.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.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.
|
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.
|
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.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.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.repository_site=Depo Sitesi
|
||||||
conda.details.documentation_site=Belge Sitesi
|
conda.details.documentation_site=Belge Sitesi
|
||||||
container.details.type=Görüntü Türü
|
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.registry=Bu kütüğü komut satırını kullanarak kurun:
|
||||||
swift.install=Paketi <code>Package.swift</code> dosyanıza ekleyin:
|
swift.install=Paketi <code>Package.swift</code> dosyanıza ekleyin:
|
||||||
swift.install2=ve şu komutu çalıştırın:
|
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.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.
|
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
|
settings.link=Bu paketi bir depoya bağlayın
|
||||||
|
618
package-lock.json
generated
618
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@ -17,13 +17,12 @@
|
|||||||
"@github/text-expander-element": "2.3.0",
|
"@github/text-expander-element": "2.3.0",
|
||||||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
||||||
"@primer/octicons": "19.1.0",
|
"@primer/octicons": "19.1.0",
|
||||||
"@vue/compiler-sfc": "3.3.2",
|
|
||||||
"@webcomponents/custom-elements": "1.6.0",
|
"@webcomponents/custom-elements": "1.6.0",
|
||||||
"add-asset-webpack-plugin": "2.0.1",
|
"add-asset-webpack-plugin": "2.0.1",
|
||||||
"ansi-to-html": "0.7.2",
|
"ansi-to-html": "0.7.2",
|
||||||
"asciinema-player": "3.3.0",
|
"asciinema-player": "3.4.0",
|
||||||
"clippie": "3.1.4",
|
"clippie": "4.0.1",
|
||||||
"css-loader": "6.7.3",
|
"css-loader": "6.7.4",
|
||||||
"dropzone": "6.0.0-beta.2",
|
"dropzone": "6.0.0-beta.2",
|
||||||
"easymde": "2.18.0",
|
"easymde": "2.18.0",
|
||||||
"esbuild-loader": "3.0.1",
|
"esbuild-loader": "3.0.1",
|
||||||
@ -34,7 +33,7 @@
|
|||||||
"katex": "0.16.7",
|
"katex": "0.16.7",
|
||||||
"license-checker-webpack-plugin": "0.2.1",
|
"license-checker-webpack-plugin": "0.2.1",
|
||||||
"mermaid": "10.1.0",
|
"mermaid": "10.1.0",
|
||||||
"mini-css-extract-plugin": "2.7.5",
|
"mini-css-extract-plugin": "2.7.6",
|
||||||
"minimatch": "9.0.0",
|
"minimatch": "9.0.0",
|
||||||
"monaco-editor": "0.38.0",
|
"monaco-editor": "0.38.0",
|
||||||
"monaco-editor-webpack-plugin": "7.0.1",
|
"monaco-editor-webpack-plugin": "7.0.1",
|
||||||
@ -45,11 +44,11 @@
|
|||||||
"tippy.js": "6.3.7",
|
"tippy.js": "6.3.7",
|
||||||
"tributejs": "5.1.3",
|
"tributejs": "5.1.3",
|
||||||
"uint8-to-base64": "0.2.0",
|
"uint8-to-base64": "0.2.0",
|
||||||
"vue": "3.3.2",
|
"vue": "3.3.4",
|
||||||
"vue-bar-graph": "2.0.0",
|
"vue-bar-graph": "2.0.0",
|
||||||
"vue-loader": "17.1.1",
|
"vue-loader": "17.1.1",
|
||||||
"vue3-calendar-heatmap": "2.0.5",
|
"vue3-calendar-heatmap": "2.0.5",
|
||||||
"webpack": "5.82.1",
|
"webpack": "5.83.1",
|
||||||
"webpack-cli": "5.1.1",
|
"webpack-cli": "5.1.1",
|
||||||
"workbox-routing": "6.5.4",
|
"workbox-routing": "6.5.4",
|
||||||
"workbox-strategies": "6.5.4",
|
"workbox-strategies": "6.5.4",
|
||||||
@ -71,15 +70,15 @@
|
|||||||
"eslint-plugin-regexp": "1.15.0",
|
"eslint-plugin-regexp": "1.15.0",
|
||||||
"eslint-plugin-sonarjs": "0.19.0",
|
"eslint-plugin-sonarjs": "0.19.0",
|
||||||
"eslint-plugin-unicorn": "47.0.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",
|
"eslint-plugin-wc": "1.5.0",
|
||||||
"jsdom": "22.0.0",
|
"jsdom": "22.0.0",
|
||||||
"markdownlint-cli": "0.34.0",
|
"markdownlint-cli": "0.34.0",
|
||||||
"stylelint": "15.6.1",
|
"stylelint": "15.6.2",
|
||||||
"stylelint-declaration-strict-value": "1.9.2",
|
"stylelint-declaration-strict-value": "1.9.2",
|
||||||
"svgo": "3.0.2",
|
"svgo": "3.0.2",
|
||||||
"updates": "14.1.0",
|
"updates": "14.1.0",
|
||||||
"vitest": "0.31.0"
|
"vitest": "0.31.1"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"defaults",
|
"defaults",
|
||||||
|
587
routers/api/actions/artifacts.go
Normal file
587
routers/api/actions/artifacts.go
Normal file
@ -0,0 +1,587 @@
|
|||||||
|
// 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"
|
||||||
|
gocontext "context"
|
||||||
|
"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"
|
||||||
|
|
||||||
|
func ArtifactsRoutes(goctx gocontext.Context, prefix string) *web.Route {
|
||||||
|
m := web.NewRoute()
|
||||||
|
m.Use(withContexter(goctx))
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// withContexter initializes a package context for a request.
|
||||||
|
func withContexter(goctx gocontext.Context) 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.Context{
|
||||||
|
Resp: context.NewResponse(resp),
|
||||||
|
Data: map[string]interface{}{},
|
||||||
|
}
|
||||||
|
defer ctx.Close()
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
ctx.Data["task"] = task
|
||||||
|
|
||||||
|
if err := task.LoadJob(goctx); err != nil {
|
||||||
|
log.Error("Error runner api getting job: %v", err)
|
||||||
|
ctx.Error(http.StatusInternalServerError, "Error runner api getting job")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Req = context.WithContext(req, &ctx)
|
||||||
|
|
||||||
|
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 *context.Context) (*actions.ActionTask, int64, bool) {
|
||||||
|
task, ok := ctx.Data["task"].(*actions.ActionTask)
|
||||||
|
if !ok {
|
||||||
|
log.Error("Error getting task in context")
|
||||||
|
ctx.Error(http.StatusInternalServerError, "Error getting task in context")
|
||||||
|
return nil, 0, false
|
||||||
|
}
|
||||||
|
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 *context.Context) {
|
||||||
|
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 *context.Context) (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 *context.Context,
|
||||||
|
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 *context.Context) {
|
||||||
|
_, 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 *context.Context) {
|
||||||
|
_, 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 *context.Context, 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))
|
||||||
|
readerClosers := make([]io.Closer, 0, len(allChunks))
|
||||||
|
for _, c := range allChunks {
|
||||||
|
reader, err := ar.fs.Open(c.Path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open chunk error: %v, %s", err, c.Path)
|
||||||
|
}
|
||||||
|
readers = append(readers, reader)
|
||||||
|
readerClosers = append(readerClosers, reader)
|
||||||
|
}
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// close readers
|
||||||
|
for _, r := range readerClosers {
|
||||||
|
r.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 *context.Context) {
|
||||||
|
_, runID, ok := ar.validateRunID(ctx)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
artficats, 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(artficats))
|
||||||
|
for _, a := range artficats {
|
||||||
|
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 *context.Context) {
|
||||||
|
_, 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 *context.Context) {
|
||||||
|
_, 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(),
|
||||||
|
})
|
||||||
|
}
|
@ -470,7 +470,7 @@ func ListIssues(ctx *context.APIContext) {
|
|||||||
if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
|
if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
|
||||||
issuesOpt := &issues_model.IssuesOptions{
|
issuesOpt := &issues_model.IssuesOptions{
|
||||||
ListOptions: listOptions,
|
ListOptions: listOptions,
|
||||||
RepoID: ctx.Repo.Repository.ID,
|
RepoIDs: []int64{ctx.Repo.Repository.ID},
|
||||||
IsClosed: isClosed,
|
IsClosed: isClosed,
|
||||||
IssueIDs: issueIDs,
|
IssueIDs: issueIDs,
|
||||||
LabelIDs: labelIDs,
|
LabelIDs: labelIDs,
|
||||||
|
@ -193,6 +193,12 @@ func NormalRoutes(ctx context.Context) *web.Route {
|
|||||||
if setting.Actions.Enabled {
|
if setting.Actions.Enabled {
|
||||||
prefix := "/api/actions"
|
prefix := "/api/actions"
|
||||||
r.Mount(prefix, actions_router.Routes(ctx, prefix))
|
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(ctx, prefix))
|
||||||
}
|
}
|
||||||
|
|
||||||
return r
|
return r
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/actions"
|
"code.gitea.io/gitea/modules/actions"
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
context_module "code.gitea.io/gitea/modules/context"
|
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/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
@ -418,3 +419,80 @@ func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions
|
|||||||
}
|
}
|
||||||
return jobs[0], jobs
|
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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -207,7 +207,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
|
|||||||
issueStats = &issues_model.IssueStats{}
|
issueStats = &issues_model.IssueStats{}
|
||||||
} else {
|
} else {
|
||||||
issueStats, err = issues_model.GetIssueStats(&issues_model.IssuesOptions{
|
issueStats, err = issues_model.GetIssueStats(&issues_model.IssuesOptions{
|
||||||
RepoID: repo.ID,
|
RepoIDs: []int64{repo.ID},
|
||||||
LabelIDs: labelIDs,
|
LabelIDs: labelIDs,
|
||||||
MilestoneIDs: []int64{milestoneID},
|
MilestoneIDs: []int64{milestoneID},
|
||||||
ProjectID: projectID,
|
ProjectID: projectID,
|
||||||
@ -258,7 +258,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
|
|||||||
Page: pager.Paginater.Current(),
|
Page: pager.Paginater.Current(),
|
||||||
PageSize: setting.UI.IssuePagingNum,
|
PageSize: setting.UI.IssuePagingNum,
|
||||||
},
|
},
|
||||||
RepoID: repo.ID,
|
RepoIDs: []int64{repo.ID},
|
||||||
AssigneeID: assigneeID,
|
AssigneeID: assigneeID,
|
||||||
PosterID: posterID,
|
PosterID: posterID,
|
||||||
MentionedID: mentionedID,
|
MentionedID: mentionedID,
|
||||||
@ -2652,7 +2652,7 @@ func ListIssues(ctx *context.Context) {
|
|||||||
if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
|
if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
|
||||||
issuesOpt := &issues_model.IssuesOptions{
|
issuesOpt := &issues_model.IssuesOptions{
|
||||||
ListOptions: listOptions,
|
ListOptions: listOptions,
|
||||||
RepoID: ctx.Repo.Repository.ID,
|
RepoIDs: []int64{ctx.Repo.Repository.ID},
|
||||||
IsClosed: isClosed,
|
IsClosed: isClosed,
|
||||||
IssueIDs: issueIDs,
|
IssueIDs: issueIDs,
|
||||||
LabelIDs: labelIDs,
|
LabelIDs: labelIDs,
|
||||||
|
@ -521,10 +521,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
|||||||
|
|
||||||
// Parse ctx.FormString("repos") and remember matched repo IDs for later.
|
// Parse ctx.FormString("repos") and remember matched repo IDs for later.
|
||||||
// Gets set when clicking filters on the issues overview page.
|
// Gets set when clicking filters on the issues overview page.
|
||||||
repoIDs := getRepoIDs(ctx.FormString("repos"))
|
opts.RepoIDs = getRepoIDs(ctx.FormString("repos"))
|
||||||
if len(repoIDs) > 0 {
|
|
||||||
opts.RepoCond = builder.In("issue.repo_id", repoIDs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------
|
// ------------------------------
|
||||||
// Get issues as defined by opts.
|
// Get issues as defined by opts.
|
||||||
@ -580,11 +577,10 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
|||||||
// -------------------------------
|
// -------------------------------
|
||||||
var issueStats *issues_model.IssueStats
|
var issueStats *issues_model.IssueStats
|
||||||
if !forceEmpty {
|
if !forceEmpty {
|
||||||
statsOpts := issues_model.UserIssueStatsOptions{
|
statsOpts := issues_model.IssuesOptions{
|
||||||
UserID: ctx.Doer.ID,
|
User: ctx.Doer,
|
||||||
FilterMode: filterMode,
|
IsPull: util.OptionalBoolOf(isPullList),
|
||||||
IsPull: isPullList,
|
IsClosed: util.OptionalBoolOf(isShowClosed),
|
||||||
IsClosed: isShowClosed,
|
|
||||||
IssueIDs: issueIDsFromSearch,
|
IssueIDs: issueIDsFromSearch,
|
||||||
IsArchived: util.OptionalBoolFalse,
|
IsArchived: util.OptionalBoolFalse,
|
||||||
LabelIDs: opts.LabelIDs,
|
LabelIDs: opts.LabelIDs,
|
||||||
@ -593,7 +589,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
|||||||
RepoCond: opts.RepoCond,
|
RepoCond: opts.RepoCond,
|
||||||
}
|
}
|
||||||
|
|
||||||
issueStats, err = issues_model.GetUserIssueStats(statsOpts)
|
issueStats, err = issues_model.GetUserIssueStats(filterMode, statsOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetUserIssueStats Shown", err)
|
ctx.ServerError("GetUserIssueStats Shown", err)
|
||||||
return
|
return
|
||||||
@ -609,9 +605,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
|||||||
} else {
|
} else {
|
||||||
shownIssues = int(issueStats.ClosedCount)
|
shownIssues = int(issueStats.ClosedCount)
|
||||||
}
|
}
|
||||||
if len(repoIDs) != 0 {
|
if len(opts.RepoIDs) != 0 {
|
||||||
shownIssues = 0
|
shownIssues = 0
|
||||||
for _, repoID := range repoIDs {
|
for _, repoID := range opts.RepoIDs {
|
||||||
shownIssues += int(issueCountByRepo[repoID])
|
shownIssues += int(issueCountByRepo[repoID])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -622,8 +618,8 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
|||||||
}
|
}
|
||||||
ctx.Data["TotalIssueCount"] = allIssueCount
|
ctx.Data["TotalIssueCount"] = allIssueCount
|
||||||
|
|
||||||
if len(repoIDs) == 1 {
|
if len(opts.RepoIDs) == 1 {
|
||||||
repo := showReposMap[repoIDs[0]]
|
repo := showReposMap[opts.RepoIDs[0]]
|
||||||
if repo != nil {
|
if repo != nil {
|
||||||
ctx.Data["SingleRepoLink"] = repo.Link()
|
ctx.Data["SingleRepoLink"] = repo.Link()
|
||||||
}
|
}
|
||||||
@ -665,7 +661,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
|||||||
ctx.Data["IssueStats"] = issueStats
|
ctx.Data["IssueStats"] = issueStats
|
||||||
ctx.Data["ViewType"] = viewType
|
ctx.Data["ViewType"] = viewType
|
||||||
ctx.Data["SortType"] = sortType
|
ctx.Data["SortType"] = sortType
|
||||||
ctx.Data["RepoIDs"] = repoIDs
|
ctx.Data["RepoIDs"] = opts.RepoIDs
|
||||||
ctx.Data["IsShowClosed"] = isShowClosed
|
ctx.Data["IsShowClosed"] = isShowClosed
|
||||||
ctx.Data["SelectLabels"] = selectedLabels
|
ctx.Data["SelectLabels"] = selectedLabels
|
||||||
|
|
||||||
@ -676,7 +672,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert []int64 to string
|
// Convert []int64 to string
|
||||||
reposParam, _ := json.Marshal(repoIDs)
|
reposParam, _ := json.Marshal(opts.RepoIDs)
|
||||||
|
|
||||||
ctx.Data["ReposParam"] = string(reposParam)
|
ctx.Data["ReposParam"] = string(reposParam)
|
||||||
|
|
||||||
|
@ -1192,6 +1192,8 @@ func registerRoutes(m *web.Route) {
|
|||||||
})
|
})
|
||||||
m.Post("/cancel", reqRepoActionsWriter, actions.Cancel)
|
m.Post("/cancel", reqRepoActionsWriter, actions.Cancel)
|
||||||
m.Post("/approve", reqRepoActionsWriter, actions.Approve)
|
m.Post("/approve", reqRepoActionsWriter, actions.Approve)
|
||||||
|
m.Post("/artifacts", actions.ArtifactsView)
|
||||||
|
m.Get("/artifacts/{id}", actions.ArtifactsDownloadView)
|
||||||
m.Post("/rerun", reqRepoActionsWriter, actions.RerunAll)
|
m.Post("/rerun", reqRepoActionsWriter, actions.RerunAll)
|
||||||
})
|
})
|
||||||
}, reqRepoActionsReader, actions.MustEnableActions)
|
}, reqRepoActionsReader, actions.MustEnableActions)
|
||||||
|
@ -104,7 +104,7 @@ func TestGiteaUploadRepo(t *testing.T) {
|
|||||||
assert.Len(t, releases, 1)
|
assert.Len(t, releases, 1)
|
||||||
|
|
||||||
issues, err := issues_model.Issues(db.DefaultContext, &issues_model.IssuesOptions{
|
issues, err := issues_model.Issues(db.DefaultContext, &issues_model.IssuesOptions{
|
||||||
RepoID: repo.ID,
|
RepoIDs: []int64{repo.ID},
|
||||||
IsPull: util.OptionalBoolFalse,
|
IsPull: util.OptionalBoolFalse,
|
||||||
SortType: "oldest",
|
SortType: "oldest",
|
||||||
})
|
})
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
data-locale-status-cancelled="{{.locale.Tr "actions.status.cancelled"}}"
|
data-locale-status-cancelled="{{.locale.Tr "actions.status.cancelled"}}"
|
||||||
data-locale-status-skipped="{{.locale.Tr "actions.status.skipped"}}"
|
data-locale-status-skipped="{{.locale.Tr "actions.status.skipped"}}"
|
||||||
data-locale-status-blocked="{{.locale.Tr "actions.status.blocked"}}"
|
data-locale-status-blocked="{{.locale.Tr "actions.status.blocked"}}"
|
||||||
|
data-locale-artifacts-title="{{$.locale.Tr "artifacts"}}"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -59,7 +59,7 @@
|
|||||||
{{.Fingerprint}}
|
{{.Fingerprint}}
|
||||||
</div>
|
</div>
|
||||||
<div class="activity meta">
|
<div class="activity meta">
|
||||||
<i>{{$.locale.Tr "settings.add_on" (DateTime "short" .CreatedUnix) | Safe}} — {{svg "octicon-info"}} {{if .HasUsed}}{{$.locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{$.locale.Tr "settings.no_activity"}}{{end}}</i>
|
<i>{{$.locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}} — {{svg "octicon-info"}} {{if .HasUsed}}{{$.locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{$.locale.Tr "settings.no_activity"}}{{end}}</i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
143
tests/integration/api_actions_artifact_test.go
Normal file
143
tests/integration/api_actions_artifact_test.go
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestActionsArtifactUpload(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
type uploadArtifactResponse struct {
|
||||||
|
FileContainerResourceURL string `json:"fileContainerResourceUrl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type getUploadArtifactRequest struct {
|
||||||
|
Type string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// acquire artifact upload url
|
||||||
|
req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts", getUploadArtifactRequest{
|
||||||
|
Type: "actions_storage",
|
||||||
|
Name: "artifact",
|
||||||
|
})
|
||||||
|
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
var uploadResp uploadArtifactResponse
|
||||||
|
DecodeJSON(t, resp, &uploadResp)
|
||||||
|
assert.Contains(t, uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
||||||
|
|
||||||
|
// get upload url
|
||||||
|
idx := strings.Index(uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
|
||||||
|
url := uploadResp.FileContainerResourceURL[idx:] + "?itemPath=artifact/abc.txt"
|
||||||
|
|
||||||
|
// upload artifact chunk
|
||||||
|
body := strings.Repeat("A", 1024)
|
||||||
|
req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body))
|
||||||
|
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||||
|
req.Header.Add("Content-Range", "bytes 0-1023/1024")
|
||||||
|
req.Header.Add("x-tfs-filelength", "1024")
|
||||||
|
req.Header.Add("x-actions-results-md5", "1HsSe8LeLWh93ILaw1TEFQ==") // base64(md5(body))
|
||||||
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
t.Logf("Create artifact confirm")
|
||||||
|
|
||||||
|
// confirm artifact upload
|
||||||
|
req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
||||||
|
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||||
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionsArtifactUploadNotExist(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
// artifact id 54321 not exist
|
||||||
|
url := "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts/54321/upload?itemPath=artifact/abc.txt"
|
||||||
|
body := strings.Repeat("A", 1024)
|
||||||
|
req := NewRequestWithBody(t, "PUT", url, strings.NewReader(body))
|
||||||
|
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||||
|
req.Header.Add("Content-Range", "bytes 0-1023/1024")
|
||||||
|
req.Header.Add("x-tfs-filelength", "1024")
|
||||||
|
req.Header.Add("x-actions-results-md5", "1HsSe8LeLWh93ILaw1TEFQ==") // base64(md5(body))
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionsArtifactConfirmUpload(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
||||||
|
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
assert.Contains(t, resp.Body.String(), "success")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionsArtifactUploadWithoutToken(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/1/artifacts", nil)
|
||||||
|
MakeRequest(t, req, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionsArtifactDownload(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
type (
|
||||||
|
listArtifactsResponseItem struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
FileContainerResourceURL string `json:"fileContainerResourceUrl"`
|
||||||
|
}
|
||||||
|
listArtifactsResponse struct {
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
Value []listArtifactsResponseItem `json:"value"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
||||||
|
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
var listResp listArtifactsResponse
|
||||||
|
DecodeJSON(t, resp, &listResp)
|
||||||
|
assert.Equal(t, int64(1), listResp.Count)
|
||||||
|
assert.Equal(t, "artifact", listResp.Value[0].Name)
|
||||||
|
assert.Contains(t, listResp.Value[0].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
||||||
|
|
||||||
|
type (
|
||||||
|
downloadArtifactResponseItem struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
ItemType string `json:"itemType"`
|
||||||
|
ContentLocation string `json:"contentLocation"`
|
||||||
|
}
|
||||||
|
downloadArtifactResponse struct {
|
||||||
|
Value []downloadArtifactResponseItem `json:"value"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
idx := strings.Index(listResp.Value[0].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
|
||||||
|
url := listResp.Value[0].FileContainerResourceURL[idx+1:]
|
||||||
|
req = NewRequest(t, "GET", url)
|
||||||
|
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
var downloadResp downloadArtifactResponse
|
||||||
|
DecodeJSON(t, resp, &downloadResp)
|
||||||
|
assert.Len(t, downloadResp.Value, 1)
|
||||||
|
assert.Equal(t, "artifact/abc.txt", downloadResp.Value[0].Path)
|
||||||
|
assert.Equal(t, "file", downloadResp.Value[0].ItemType)
|
||||||
|
assert.Contains(t, downloadResp.Value[0].ContentLocation, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
||||||
|
|
||||||
|
idx = strings.Index(downloadResp.Value[0].ContentLocation, "/api/actions_pipeline/_apis/pipelines/")
|
||||||
|
url = downloadResp.Value[0].ContentLocation[idx:]
|
||||||
|
req = NewRequest(t, "GET", url)
|
||||||
|
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
body := strings.Repeat("A", 1024)
|
||||||
|
assert.Equal(t, resp.Body.String(), body)
|
||||||
|
}
|
@ -108,3 +108,6 @@ PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data/lfs
|
|||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
ENABLED = true
|
ENABLED = true
|
||||||
|
|
||||||
|
[actions]
|
||||||
|
ENABLED = true
|
||||||
|
@ -117,3 +117,6 @@ PASSWORD = debug
|
|||||||
USE_TLS = true
|
USE_TLS = true
|
||||||
SKIP_TLS_VERIFY = true
|
SKIP_TLS_VERIFY = true
|
||||||
REPLY_TO_ADDRESS = incoming+%{token}@localhost
|
REPLY_TO_ADDRESS = incoming+%{token}@localhost
|
||||||
|
|
||||||
|
[actions]
|
||||||
|
ENABLED = true
|
||||||
|
@ -105,3 +105,6 @@ PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql8/data/lfs
|
|||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
ENABLED = true
|
ENABLED = true
|
||||||
|
|
||||||
|
[actions]
|
||||||
|
ENABLED = true
|
||||||
|
@ -129,3 +129,6 @@ MINIO_CHECKSUM_ALGORITHM = md5
|
|||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
ENABLED = true
|
ENABLED = true
|
||||||
|
|
||||||
|
[actions]
|
||||||
|
ENABLED = true
|
||||||
|
@ -114,3 +114,6 @@ FILE_EXTENSIONS = .html
|
|||||||
RENDER_COMMAND = `go run build/test-echo.go`
|
RENDER_COMMAND = `go run build/test-echo.go`
|
||||||
IS_INPUT_FILE = false
|
IS_INPUT_FILE = false
|
||||||
RENDER_CONTENT_MODE=sanitized
|
RENDER_CONTENT_MODE=sanitized
|
||||||
|
|
||||||
|
[actions]
|
||||||
|
ENABLED = true
|
||||||
|
@ -180,10 +180,13 @@
|
|||||||
--color-caret: var(--color-text-dark);
|
--color-caret: var(--color-text-dark);
|
||||||
--color-reaction-bg: #0000000a;
|
--color-reaction-bg: #0000000a;
|
||||||
--color-reaction-active-bg: var(--color-primary-alpha-20);
|
--color-reaction-active-bg: var(--color-primary-alpha-20);
|
||||||
--color-tooltip-bg: #000000f0;
|
|
||||||
--color-tooltip-text: #ffffff;
|
--color-tooltip-text: #ffffff;
|
||||||
|
--color-tooltip-bg: #000000f0;
|
||||||
--color-header-bar: #ffffff;
|
--color-header-bar: #ffffff;
|
||||||
--color-label-active-bg: #d0d0d0;
|
--color-label-text: #232323;
|
||||||
|
--color-label-bg: #cacaca5b;
|
||||||
|
--color-label-hover-bg: #cacacaa0;
|
||||||
|
--color-label-active-bg: #cacacaff;
|
||||||
--color-accent: var(--color-primary-light-1);
|
--color-accent: var(--color-primary-light-1);
|
||||||
--color-small-accent: var(--color-primary-light-6);
|
--color-small-accent: var(--color-primary-light-6);
|
||||||
--color-active-line: #fffbdd;
|
--color-active-line: #fffbdd;
|
||||||
@ -820,16 +823,6 @@ a.label,
|
|||||||
margin-right: 0.35em;
|
margin-right: 0.35em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.menu .item > .label {
|
|
||||||
background: var(--color-grey);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ui.active.label {
|
|
||||||
background: var(--color-label-active-bg);
|
|
||||||
border-color: var(--color-label-active-bg);
|
|
||||||
color: var(--color-text-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ui.menu .dropdown.item:hover,
|
.ui.menu .dropdown.item:hover,
|
||||||
.ui.menu a.item:hover {
|
.ui.menu a.item:hover {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
@ -1976,22 +1969,32 @@ i.icon.centerlock {
|
|||||||
|
|
||||||
.ui.label {
|
.ui.label {
|
||||||
padding: 0.3em 0.5em;
|
padding: 0.3em 0.5em;
|
||||||
background: var(--color-light);
|
}
|
||||||
color: var(--color-text-light);
|
|
||||||
|
.ui.label,
|
||||||
|
.ui.menu .item > .label {
|
||||||
|
background: var(--color-label-bg);
|
||||||
|
color: var(--color-label-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.active.label {
|
||||||
|
background: var(--color-label-active-bg);
|
||||||
|
border-color: var(--color-label-active-bg);
|
||||||
|
color: var(--color-label-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.labels a.label:hover,
|
.ui.labels a.label:hover,
|
||||||
a.ui.label:hover {
|
a.ui.label:hover {
|
||||||
background: var(--color-hover);
|
background: var(--color-label-hover-bg);
|
||||||
border-color: var(--color-hover);
|
border-color: var(--color-label-hover-bg);
|
||||||
color: var(--color-text);
|
color: var(--color-label-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.labels a.active.label:hover,
|
.ui.labels a.active.label:hover,
|
||||||
a.ui.active.label:hover {
|
a.ui.active.label:hover {
|
||||||
background: var(--color-active);
|
background: var(--color-label-active-bg);
|
||||||
border-color: var(--color-active);
|
border-color: var(--color-label-active-bg);
|
||||||
color: var(--color-text);
|
color: var(--color-label-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.label > .detail .icons {
|
.ui.label > .detail .icons {
|
||||||
|
@ -61,6 +61,9 @@
|
|||||||
.comment-code-cloud {
|
.comment-code-cloud {
|
||||||
padding: 0.5rem 1rem !important;
|
padding: 0.5rem 1rem !important;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-holder .comment-code-cloud {
|
||||||
max-width: 820px;
|
max-width: 820px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,8 +165,13 @@
|
|||||||
--color-caret: var(--color-text); /* should ideally be --color-text-dark, see #15651 */
|
--color-caret: var(--color-text); /* should ideally be --color-text-dark, see #15651 */
|
||||||
--color-reaction-bg: #ffffff12;
|
--color-reaction-bg: #ffffff12;
|
||||||
--color-reaction-active-bg: var(--color-primary-alpha-40);
|
--color-reaction-active-bg: var(--color-primary-alpha-40);
|
||||||
|
--color-tooltip-text: #ffffff;
|
||||||
|
--color-tooltip-bg: #000000f0;
|
||||||
--color-header-bar: #2e323e;
|
--color-header-bar: #2e323e;
|
||||||
--color-label-active-bg: #4c525e;
|
--color-label-text: #dfe3ec;
|
||||||
|
--color-label-bg: #7c84974b;
|
||||||
|
--color-label-hover-bg: #7c8497a0;
|
||||||
|
--color-label-active-bg: #7c8497ff;
|
||||||
--color-accent: var(--color-primary-light-1);
|
--color-accent: var(--color-primary-light-1);
|
||||||
--color-small-accent: var(--color-primary-light-5);
|
--color-small-accent: var(--color-primary-light-5);
|
||||||
--color-active-line: #534d1b;
|
--color-active-line: #534d1b;
|
||||||
|
@ -71,7 +71,7 @@
|
|||||||
<div v-if="repos.length" class="ui attached table segment gt-rounded-bottom">
|
<div v-if="repos.length" class="ui attached table segment gt-rounded-bottom">
|
||||||
<ul class="repo-owner-name-list">
|
<ul class="repo-owner-name-list">
|
||||||
<li v-for="repo in repos" :key="repo.id">
|
<li v-for="repo in repos" :key="repo.id">
|
||||||
<a class="repo-list-link gt-df gt-ac gt-sb" :href="repo.link">
|
<a class="repo-list-link muted gt-df gt-ac gt-sb" :href="repo.link">
|
||||||
<div class="item-name gt-df gt-ac gt-f1">
|
<div class="item-name gt-df gt-ac gt-f1">
|
||||||
<svg-icon :name="repoIcon(repo)" :size="16" class-name="gt-mr-2"/>
|
<svg-icon :name="repoIcon(repo)" :size="16" class-name="gt-mr-2"/>
|
||||||
<div class="text gt-bold truncate gt-ml-1">{{ repo.full_name }}</div>
|
<div class="text gt-bold truncate gt-ml-1">{{ repo.full_name }}</div>
|
||||||
|
@ -42,6 +42,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="job-artifacts" v-if="artifacts.length > 0">
|
||||||
|
<div class="job-artifacts-title">
|
||||||
|
{{ locale.artifactsTitle }}
|
||||||
|
</div>
|
||||||
|
<ul class="job-artifacts-list">
|
||||||
|
<li class="job-artifacts-item" v-for="artifact in artifacts" :key="artifact.id">
|
||||||
|
<a class="job-artifacts-link" target="_blank" :href="run.link+'/artifacts/'+artifact.id">
|
||||||
|
<SvgIcon name="octicon-file" class="ui text black job-artifacts-icon" />{{ artifact.name }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="action-view-right">
|
<div class="action-view-right">
|
||||||
@ -102,6 +114,7 @@ const sfc = {
|
|||||||
loading: false,
|
loading: false,
|
||||||
intervalID: null,
|
intervalID: null,
|
||||||
currentJobStepsStates: [],
|
currentJobStepsStates: [],
|
||||||
|
artifacts: [],
|
||||||
|
|
||||||
// provided by backend
|
// provided by backend
|
||||||
run: {
|
run: {
|
||||||
@ -156,6 +169,15 @@ const sfc = {
|
|||||||
this.intervalID = setInterval(this.loadJob, 1000);
|
this.intervalID = setInterval(this.loadJob, 1000);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
unmounted() {
|
||||||
|
// clear the interval timer when the component is unmounted
|
||||||
|
// even our page is rendered once, not spa style
|
||||||
|
if (this.intervalID) {
|
||||||
|
clearInterval(this.intervalID);
|
||||||
|
this.intervalID = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
// get the active container element, either the `job-step-logs` or the `job-log-list` in the `job-log-group`
|
// get the active container element, either the `job-step-logs` or the `job-log-list` in the `job-log-group`
|
||||||
getLogsContainer(idx) {
|
getLogsContainer(idx) {
|
||||||
@ -259,6 +281,11 @@ const sfc = {
|
|||||||
try {
|
try {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
|
// refresh artifacts if upload-artifact step done
|
||||||
|
const resp = await this.fetchPost(`${this.actionsURL}/runs/${this.runIndex}/artifacts`);
|
||||||
|
const artifacts = await resp.json();
|
||||||
|
this.artifacts = artifacts['artifacts'] || [];
|
||||||
|
|
||||||
const response = await this.fetchJob();
|
const response = await this.fetchJob();
|
||||||
|
|
||||||
// save the state to Vue data, then the UI will be updated
|
// save the state to Vue data, then the UI will be updated
|
||||||
@ -287,6 +314,7 @@ const sfc = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
fetchPost(url, body) {
|
fetchPost(url, body) {
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -319,6 +347,7 @@ export function initRepositoryActionView() {
|
|||||||
approve: el.getAttribute('data-locale-approve'),
|
approve: el.getAttribute('data-locale-approve'),
|
||||||
cancel: el.getAttribute('data-locale-cancel'),
|
cancel: el.getAttribute('data-locale-cancel'),
|
||||||
rerun: el.getAttribute('data-locale-rerun'),
|
rerun: el.getAttribute('data-locale-rerun'),
|
||||||
|
artifactsTitle: el.getAttribute('data-locale-artifacts-title'),
|
||||||
status: {
|
status: {
|
||||||
unknown: el.getAttribute('data-locale-status-unknown'),
|
unknown: el.getAttribute('data-locale-status-unknown'),
|
||||||
waiting: el.getAttribute('data-locale-status-waiting'),
|
waiting: el.getAttribute('data-locale-status-waiting'),
|
||||||
@ -423,6 +452,27 @@ export function ansiLogToHTML(line) {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.job-artifacts-title {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 16px 10px 0px 20px;
|
||||||
|
border-top: 1px solid var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-artifacts-item {
|
||||||
|
margin: 5px 0;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-artifacts-list {
|
||||||
|
padding-left: 12px;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-artifacts-icon {
|
||||||
|
padding-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.job-group-section .job-brief-list .job-brief-item {
|
.job-group-section .job-brief-list .job-brief-item {
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
@ -20,7 +20,7 @@ export function createTippy(target, opts = {}) {
|
|||||||
onShow: (instance) => {
|
onShow: (instance) => {
|
||||||
// hide other tooltip instances so only one tooltip shows at a time
|
// hide other tooltip instances so only one tooltip shows at a time
|
||||||
for (const visibleInstance of visibleInstances) {
|
for (const visibleInstance of visibleInstances) {
|
||||||
if (visibleInstance.role === 'tooltip') {
|
if (visibleInstance.props.role === 'tooltip') {
|
||||||
visibleInstance.hide();
|
visibleInstance.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user