mirror of https://github.com/go-gitea/gitea.git
Merge branch 'main' into label_delete_event_fix
commit
6aa8ba38cd
|
@ -80,6 +80,11 @@ wiki, issues, labels, releases, release_assets, milestones, pull_requests, comme
|
|||
}
|
||||
|
||||
func runDumpRepository(ctx *cli.Context) error {
|
||||
setupConsoleLogger(log.INFO, log.CanColorStderr, os.Stderr)
|
||||
|
||||
setting.DisableLoggerInit()
|
||||
setting.LoadSettings() // cannot access skip_tls_verify settings otherwise
|
||||
|
||||
stdCtx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
|
|
|
@ -20,11 +20,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1739214665,
|
||||
"narHash": "sha256-26L8VAu3/1YRxS8MHgBOyOM8xALdo6N0I04PgorE7UM=",
|
||||
"lastModified": 1747179050,
|
||||
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "64e75cd44acf21c7933d61d7721e812eac1b5a0a",
|
||||
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
2
go.mod
2
go.mod
|
@ -317,7 +317,7 @@ replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1
|
|||
|
||||
replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0
|
||||
|
||||
replace github.com/nektos/act => gitea.com/gitea/act v0.261.4
|
||||
replace github.com/nektos/act => gitea.com/gitea/act v0.261.6
|
||||
|
||||
// TODO: the only difference is in `PutObject`: the fork doesn't use `NewVerifyingReader(r, sha256.New(), oid, expectedSize)`, need to figure out why
|
||||
replace github.com/charmbracelet/git-lfs-transfer => gitea.com/gitea/git-lfs-transfer v0.2.0
|
||||
|
|
4
go.sum
4
go.sum
|
@ -14,8 +14,8 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
|||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
gitea.com/gitea/act v0.261.4 h1:Tf9eLlvsYFtKcpuxlMvf9yT3g4Hshb2Beqw6C1STuH8=
|
||||
gitea.com/gitea/act v0.261.4/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
|
||||
gitea.com/gitea/act v0.261.6 h1:CjZwKOyejonNFDmsXOw3wGm5Vet573hHM6VMLsxtvPY=
|
||||
gitea.com/gitea/act v0.261.6/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
|
||||
gitea.com/gitea/git-lfs-transfer v0.2.0 h1:baHaNoBSRaeq/xKayEXwiDQtlIjps4Ac/Ll4KqLMB40=
|
||||
gitea.com/gitea/git-lfs-transfer v0.2.0/go.mod h1:UrXUCm3xLQkq15fu7qlXHUMlrhdlXHoi13KH2Dfiits=
|
||||
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4=
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
@ -171,6 +172,7 @@ func (run *ActionRun) IsSchedule() bool {
|
|||
|
||||
func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error {
|
||||
_, err := db.GetEngine(ctx).ID(repo.ID).
|
||||
NoAutoTime().
|
||||
SetExpr("num_action_runs",
|
||||
builder.Select("count(*)").From("action_run").
|
||||
Where(builder.Eq{"repo_id": repo.ID}),
|
||||
|
@ -342,13 +344,13 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork
|
|||
return committer.Commit()
|
||||
}
|
||||
|
||||
func GetRunByID(ctx context.Context, id int64) (*ActionRun, error) {
|
||||
func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, error) {
|
||||
var run ActionRun
|
||||
has, err := db.GetEngine(ctx).Where("id=?", id).Get(&run)
|
||||
has, err := db.GetEngine(ctx).Where("id=? AND repo_id=?", runID, repoID).Get(&run)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, fmt.Errorf("run with id %d: %w", id, util.ErrNotExist)
|
||||
return nil, fmt.Errorf("run with id %d: %w", runID, util.ErrNotExist)
|
||||
}
|
||||
|
||||
return &run, nil
|
||||
|
@ -419,17 +421,10 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error {
|
|||
|
||||
if run.Status != 0 || slices.Contains(cols, "status") {
|
||||
if run.RepoID == 0 {
|
||||
run, err = GetRunByID(ctx, run.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setting.PanicInDevOrTesting("RepoID should not be 0")
|
||||
}
|
||||
if run.Repo == nil {
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
run.Repo = repo
|
||||
if err = run.LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil {
|
||||
return err
|
||||
|
|
|
@ -51,7 +51,7 @@ func (job *ActionRunJob) Duration() time.Duration {
|
|||
|
||||
func (job *ActionRunJob) LoadRun(ctx context.Context) error {
|
||||
if job.Run == nil {
|
||||
run, err := GetRunByID(ctx, job.RunID)
|
||||
run, err := GetRunByRepoAndID(ctx, job.RepoID, job.RunID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -142,7 +142,7 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
|
|||
{
|
||||
// Other goroutines may aggregate the status of the run and update it too.
|
||||
// So we need load the run and its jobs before updating the run.
|
||||
run, err := GetRunByID(ctx, job.RunID)
|
||||
run, err := GetRunByRepoAndID(ctx, job.RepoID, job.RunID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ package actions
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -298,6 +299,23 @@ func DeleteRunner(ctx context.Context, id int64) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// DeleteEphemeralRunner deletes a ephemeral runner by given ID.
|
||||
func DeleteEphemeralRunner(ctx context.Context, id int64) error {
|
||||
runner, err := GetRunnerByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !runner.Ephemeral {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = db.DeleteByID[ActionRunner](ctx, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateRunner creates new runner.
|
||||
func CreateRunner(ctx context.Context, t *ActionRunner) error {
|
||||
if t.OwnerID != 0 && t.RepoID != 0 {
|
||||
|
|
|
@ -336,6 +336,11 @@ func UpdateTask(ctx context.Context, task *ActionTask, cols ...string) error {
|
|||
sess.Cols(cols...)
|
||||
}
|
||||
_, err := sess.Update(task)
|
||||
|
||||
// Automatically delete the ephemeral runner if the task is done
|
||||
if err == nil && task.Status.IsDone() && util.SliceContainsString(cols, "status") {
|
||||
return DeleteEphemeralRunner(ctx, task.RunnerID)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -48,6 +48,7 @@ func (tasks TaskList) LoadAttributes(ctx context.Context) error {
|
|||
type FindTaskOptions struct {
|
||||
db.ListOptions
|
||||
RepoID int64
|
||||
JobID int64
|
||||
OwnerID int64
|
||||
CommitSHA string
|
||||
Status Status
|
||||
|
@ -61,6 +62,9 @@ func (opts FindTaskOptions) ToConds() builder.Cond {
|
|||
if opts.RepoID > 0 {
|
||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||
}
|
||||
if opts.JobID > 0 {
|
||||
cond = cond.And(builder.Eq{"job_id": opts.JobID})
|
||||
}
|
||||
if opts.OwnerID > 0 {
|
||||
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
||||
}
|
||||
|
|
|
@ -82,3 +82,22 @@ func calculateDuration(started, stopped timeutil.TimeStamp, status Status) time.
|
|||
}
|
||||
return timeSince(s).Truncate(time.Second)
|
||||
}
|
||||
|
||||
// best effort function to convert an action schedule to action run, to be used in GenerateGiteaContext
|
||||
func (s *ActionSchedule) ToActionRun() *ActionRun {
|
||||
return &ActionRun{
|
||||
Title: s.Title,
|
||||
RepoID: s.RepoID,
|
||||
Repo: s.Repo,
|
||||
OwnerID: s.OwnerID,
|
||||
WorkflowID: s.WorkflowID,
|
||||
TriggerUserID: s.TriggerUserID,
|
||||
TriggerUser: s.TriggerUser,
|
||||
Ref: s.Ref,
|
||||
CommitSHA: s.CommitSHA,
|
||||
Event: s.Event,
|
||||
EventPayload: s.EventPayload,
|
||||
Created: s.Created,
|
||||
Updated: s.Updated,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -530,7 +530,7 @@ func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.
|
|||
|
||||
if opts.RequestedTeam != nil {
|
||||
env := repo_model.AccessibleTeamReposEnv(organization.OrgFromUser(opts.RequestedUser), opts.RequestedTeam)
|
||||
teamRepoIDs, err := env.RepoIDs(ctx, 1, opts.RequestedUser.NumRepos)
|
||||
teamRepoIDs, err := env.RepoIDs(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetTeamRepositories: %w", err)
|
||||
}
|
||||
|
|
|
@ -105,3 +105,39 @@
|
|||
created_unix: 1730330775
|
||||
updated_unix: 1730330775
|
||||
expired_unix: 1738106775
|
||||
|
||||
-
|
||||
id: 24
|
||||
run_id: 795
|
||||
runner_id: 1
|
||||
repo_id: 2
|
||||
owner_id: 2
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
storage_path: "27/5/1730330775594233150.chunk"
|
||||
file_size: 1024
|
||||
file_compressed_size: 1024
|
||||
content_encoding: "application/zip"
|
||||
artifact_path: "artifact-795-1.zip"
|
||||
artifact_name: "artifact-795-1"
|
||||
status: 2
|
||||
created_unix: 1730330775
|
||||
updated_unix: 1730330775
|
||||
expired_unix: 1738106775
|
||||
|
||||
-
|
||||
id: 25
|
||||
run_id: 795
|
||||
runner_id: 1
|
||||
repo_id: 2
|
||||
owner_id: 2
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
storage_path: "27/5/1730330775594233150.chunk"
|
||||
file_size: 1024
|
||||
file_compressed_size: 1024
|
||||
content_encoding: "application/zip"
|
||||
artifact_path: "artifact-795-2.zip"
|
||||
artifact_name: "artifact-795-2"
|
||||
status: 2
|
||||
created_unix: 1730330775
|
||||
updated_unix: 1730330775
|
||||
expired_unix: 1738106775
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
|
||||
event: "push"
|
||||
is_fork_pull_request: 0
|
||||
status: 1
|
||||
status: 6 # running
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
created: 1683636108
|
||||
|
@ -74,3 +74,23 @@
|
|||
updated: 1683636626
|
||||
need_approval: 0
|
||||
approved_by: 0
|
||||
|
||||
-
|
||||
id: 795
|
||||
title: "to be deleted (test)"
|
||||
repo_id: 2
|
||||
owner_id: 2
|
||||
workflow_id: "test.yaml"
|
||||
index: 191
|
||||
trigger_user_id: 1
|
||||
ref: "refs/heads/test"
|
||||
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
|
||||
event: "push"
|
||||
is_fork_pull_request: 0
|
||||
status: 2
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
created: 1683636108
|
||||
updated: 1683636626
|
||||
need_approval: 0
|
||||
approved_by: 0
|
||||
|
|
|
@ -69,3 +69,33 @@
|
|||
status: 5
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
|
||||
-
|
||||
id: 198
|
||||
run_id: 795
|
||||
repo_id: 2
|
||||
owner_id: 2
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
is_fork_pull_request: 0
|
||||
name: job_1
|
||||
attempt: 1
|
||||
job_id: job_1
|
||||
task_id: 53
|
||||
status: 1
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
|
||||
-
|
||||
id: 199
|
||||
run_id: 795
|
||||
repo_id: 2
|
||||
owner_id: 2
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
is_fork_pull_request: 0
|
||||
name: job_2
|
||||
attempt: 1
|
||||
job_id: job_2
|
||||
task_id: 54
|
||||
status: 2
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
|
|
|
@ -38,3 +38,14 @@
|
|||
repo_id: 0
|
||||
description: "This runner is going to be deleted"
|
||||
agent_labels: '["runner_to_be_deleted","linux"]'
|
||||
-
|
||||
id: 34350
|
||||
name: runner_to_be_deleted-org-ephemeral
|
||||
uuid: 3FF231BD-FBB7-4E4B-9602-E6F28363EF20
|
||||
token_hash: 3FF231BD-FBB7-4E4B-9602-E6F28363EF20
|
||||
ephemeral: true
|
||||
version: "1.0.0"
|
||||
owner_id: 3
|
||||
repo_id: 0
|
||||
description: "This runner is going to be deleted"
|
||||
agent_labels: '["runner_to_be_deleted","linux"]'
|
||||
|
|
|
@ -117,3 +117,63 @@
|
|||
log_length: 707
|
||||
log_size: 90179
|
||||
log_expired: 0
|
||||
-
|
||||
id: 52
|
||||
job_id: 196
|
||||
attempt: 1
|
||||
runner_id: 34350
|
||||
status: 6 # running
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
repo_id: 4
|
||||
owner_id: 1
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
is_fork_pull_request: 0
|
||||
token_hash: f8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784222
|
||||
token_salt: ffffffffff
|
||||
token_last_eight: ffffffff
|
||||
log_filename: artifact-test2/2f/47.log
|
||||
log_in_storage: 1
|
||||
log_length: 707
|
||||
log_size: 90179
|
||||
log_expired: 0
|
||||
-
|
||||
id: 53
|
||||
job_id: 198
|
||||
attempt: 1
|
||||
runner_id: 1
|
||||
status: 1
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
repo_id: 2
|
||||
owner_id: 2
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
is_fork_pull_request: 0
|
||||
token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784223
|
||||
token_salt: ffffffffff
|
||||
token_last_eight: ffffffff
|
||||
log_filename: artifact-test2/2f/47.log
|
||||
log_in_storage: 1
|
||||
log_length: 0
|
||||
log_size: 0
|
||||
log_expired: 0
|
||||
-
|
||||
id: 54
|
||||
job_id: 199
|
||||
attempt: 1
|
||||
runner_id: 1
|
||||
status: 2
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
repo_id: 2
|
||||
owner_id: 2
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
is_fork_pull_request: 0
|
||||
token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784224
|
||||
token_salt: ffffffffff
|
||||
token_last_eight: ffffffff
|
||||
log_filename: artifact-test2/2f/47.log
|
||||
log_in_storage: 1
|
||||
log_length: 0
|
||||
log_size: 0
|
||||
log_expired: 0
|
||||
|
|
|
@ -14,5 +14,8 @@ func AddIndexToActionTaskStoppedLogExpired(x *xorm.Engine) error {
|
|||
Stopped timeutil.TimeStamp `xorm:"index(stopped_log_expired)"`
|
||||
LogExpired bool `xorm:"index(stopped_log_expired)"`
|
||||
}
|
||||
return x.Sync(new(ActionTask))
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreDropIndices: true,
|
||||
}, new(ActionTask))
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_23 //nolint
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/migrations/base"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_AddIndexToActionTaskStoppedLogExpired(t *testing.T) {
|
||||
type ActionTask struct {
|
||||
ID int64
|
||||
JobID int64
|
||||
Attempt int64
|
||||
RunnerID int64 `xorm:"index"`
|
||||
Status int `xorm:"index"`
|
||||
Started timeutil.TimeStamp `xorm:"index"`
|
||||
Stopped timeutil.TimeStamp `xorm:"index(stopped_log_expired)"`
|
||||
|
||||
RepoID int64 `xorm:"index"`
|
||||
OwnerID int64 `xorm:"index"`
|
||||
CommitSHA string `xorm:"index"`
|
||||
IsForkPullRequest bool
|
||||
|
||||
Token string `xorm:"-"`
|
||||
TokenHash string `xorm:"UNIQUE"` // sha256 of token
|
||||
TokenSalt string
|
||||
TokenLastEight string `xorm:"index token_last_eight"`
|
||||
|
||||
LogFilename string // file name of log
|
||||
LogInStorage bool // read log from database or from storage
|
||||
LogLength int64 // lines count
|
||||
LogSize int64 // blob size
|
||||
LogIndexes []int64 `xorm:"LONGBLOB"` // line number to offset
|
||||
LogExpired bool `xorm:"index(stopped_log_expired)"` // files that are too old will be deleted
|
||||
|
||||
Created timeutil.TimeStamp `xorm:"created"`
|
||||
Updated timeutil.TimeStamp `xorm:"updated index"`
|
||||
}
|
||||
|
||||
// Prepare and load the testing database
|
||||
x, deferable := base.PrepareTestEnv(t, 0, new(ActionTask))
|
||||
defer deferable()
|
||||
|
||||
assert.NoError(t, AddIndexToActionTaskStoppedLogExpired(x))
|
||||
}
|
|
@ -9,5 +9,8 @@ func AddIndexForReleaseSha1(x *xorm.Engine) error {
|
|||
type Release struct {
|
||||
Sha1 string `xorm:"INDEX VARCHAR(64)"`
|
||||
}
|
||||
return x.Sync(new(Release))
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreDropIndices: true,
|
||||
}, new(Release))
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_23 //nolint
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/migrations/base"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_AddIndexForReleaseSha1(t *testing.T) {
|
||||
type Release struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX UNIQUE(n)"`
|
||||
PublisherID int64 `xorm:"INDEX"`
|
||||
TagName string `xorm:"INDEX UNIQUE(n)"`
|
||||
OriginalAuthor string
|
||||
OriginalAuthorID int64 `xorm:"index"`
|
||||
LowerTagName string
|
||||
Target string
|
||||
Title string
|
||||
Sha1 string `xorm:"VARCHAR(64)"`
|
||||
NumCommits int64
|
||||
Note string `xorm:"TEXT"`
|
||||
IsDraft bool `xorm:"NOT NULL DEFAULT false"`
|
||||
IsPrerelease bool `xorm:"NOT NULL DEFAULT false"`
|
||||
IsTag bool `xorm:"NOT NULL DEFAULT false"` // will be true only if the record is a tag and has no related releases
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||
}
|
||||
|
||||
// Prepare and load the testing database
|
||||
x, deferable := base.PrepareTestEnv(t, 0, new(Release))
|
||||
defer deferable()
|
||||
|
||||
assert.NoError(t, AddIndexForReleaseSha1(x))
|
||||
}
|
|
@ -334,7 +334,7 @@ func TestAccessibleReposEnv_RepoIDs(t *testing.T) {
|
|||
testSuccess := func(userID int64, expectedRepoIDs []int64) {
|
||||
env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID)
|
||||
assert.NoError(t, err)
|
||||
repoIDs, err := env.RepoIDs(db.DefaultContext, 1, 100)
|
||||
repoIDs, err := env.RepoIDs(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedRepoIDs, repoIDs)
|
||||
}
|
||||
|
@ -342,25 +342,6 @@ func TestAccessibleReposEnv_RepoIDs(t *testing.T) {
|
|||
testSuccess(4, []int64{3, 32})
|
||||
}
|
||||
|
||||
func TestAccessibleReposEnv_Repos(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
testSuccess := func(userID int64, expectedRepoIDs []int64) {
|
||||
env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID)
|
||||
assert.NoError(t, err)
|
||||
repos, err := env.Repos(db.DefaultContext, 1, 100)
|
||||
assert.NoError(t, err)
|
||||
expectedRepos := make(repo_model.RepositoryList, len(expectedRepoIDs))
|
||||
for i, repoID := range expectedRepoIDs {
|
||||
expectedRepos[i] = unittest.AssertExistsAndLoadBean(t,
|
||||
&repo_model.Repository{ID: repoID})
|
||||
}
|
||||
assert.Equal(t, expectedRepos, repos)
|
||||
}
|
||||
testSuccess(2, []int64{3, 5, 32})
|
||||
testSuccess(4, []int64{3, 32})
|
||||
}
|
||||
|
||||
func TestAccessibleReposEnv_MirrorRepos(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
|
|
|
@ -48,8 +48,7 @@ func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (Repo
|
|||
// accessible to a particular user
|
||||
type AccessibleReposEnvironment interface {
|
||||
CountRepos(ctx context.Context) (int64, error)
|
||||
RepoIDs(ctx context.Context, page, pageSize int) ([]int64, error)
|
||||
Repos(ctx context.Context, page, pageSize int) (RepositoryList, error)
|
||||
RepoIDs(ctx context.Context) ([]int64, error)
|
||||
MirrorRepos(ctx context.Context) (RepositoryList, error)
|
||||
AddKeyword(keyword string)
|
||||
SetSort(db.SearchOrderBy)
|
||||
|
@ -132,40 +131,18 @@ func (env *accessibleReposEnv) CountRepos(ctx context.Context) (int64, error) {
|
|||
return repoCount, nil
|
||||
}
|
||||
|
||||
func (env *accessibleReposEnv) RepoIDs(ctx context.Context, page, pageSize int) ([]int64, error) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
repoIDs := make([]int64, 0, pageSize)
|
||||
func (env *accessibleReposEnv) RepoIDs(ctx context.Context) ([]int64, error) {
|
||||
var repoIDs []int64
|
||||
return repoIDs, db.GetEngine(ctx).
|
||||
Table("repository").
|
||||
Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id").
|
||||
Where(env.cond()).
|
||||
GroupBy("`repository`.id,`repository`."+strings.Fields(string(env.orderBy))[0]).
|
||||
GroupBy("`repository`.id,`repository`." + strings.Fields(string(env.orderBy))[0]).
|
||||
OrderBy(string(env.orderBy)).
|
||||
Limit(pageSize, (page-1)*pageSize).
|
||||
Cols("`repository`.id").
|
||||
Find(&repoIDs)
|
||||
}
|
||||
|
||||
func (env *accessibleReposEnv) Repos(ctx context.Context, page, pageSize int) (RepositoryList, error) {
|
||||
repoIDs, err := env.RepoIDs(ctx, page, pageSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetUserRepositoryIDs: %w", err)
|
||||
}
|
||||
|
||||
repos := make([]*Repository, 0, len(repoIDs))
|
||||
if len(repoIDs) == 0 {
|
||||
return repos, nil
|
||||
}
|
||||
|
||||
return repos, db.GetEngine(ctx).
|
||||
In("`repository`.id", repoIDs).
|
||||
OrderBy(string(env.orderBy)).
|
||||
Find(&repos)
|
||||
}
|
||||
|
||||
func (env *accessibleReposEnv) MirrorRepoIDs(ctx context.Context) ([]int64, error) {
|
||||
repoIDs := make([]int64, 0, 10)
|
||||
return repoIDs, db.GetEngine(ctx).
|
||||
|
|
|
@ -249,7 +249,7 @@ func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_m
|
|||
}
|
||||
|
||||
repo.Status = RepositoryPendingTransfer
|
||||
if err := UpdateRepositoryCols(ctx, repo, "status"); err != nil {
|
||||
if err := UpdateRepositoryColsNoAutoTime(ctx, repo, "status"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ func UpdateRepositoryOwnerNames(ctx context.Context, ownerID int64, ownerName st
|
|||
}
|
||||
defer committer.Close()
|
||||
|
||||
if _, err := db.GetEngine(ctx).Where("owner_id = ?", ownerID).Cols("owner_name").Update(&Repository{
|
||||
if _, err := db.GetEngine(ctx).Where("owner_id = ?", ownerID).Cols("owner_name").NoAutoTime().Update(&Repository{
|
||||
OwnerName: ownerName,
|
||||
}); err != nil {
|
||||
return err
|
||||
|
@ -40,8 +40,8 @@ func UpdateRepositoryUpdatedTime(ctx context.Context, repoID int64, updateTime t
|
|||
return err
|
||||
}
|
||||
|
||||
// UpdateRepositoryCols updates repository's columns
|
||||
func UpdateRepositoryCols(ctx context.Context, repo *Repository, cols ...string) error {
|
||||
// UpdateRepositoryColsWithAutoTime updates repository's columns
|
||||
func UpdateRepositoryColsWithAutoTime(ctx context.Context, repo *Repository, cols ...string) error {
|
||||
_, err := db.GetEngine(ctx).ID(repo.ID).Cols(cols...).Update(repo)
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -166,6 +166,8 @@ type CommitsCountOptions struct {
|
|||
Not string
|
||||
Revision []string
|
||||
RelPath []string
|
||||
Since string
|
||||
Until string
|
||||
}
|
||||
|
||||
// CommitsCount returns number of total commits of until given revision.
|
||||
|
@ -199,8 +201,8 @@ func (c *Commit) CommitsCount() (int64, error) {
|
|||
}
|
||||
|
||||
// CommitsByRange returns the specific page commits before current revision, every page's number default by CommitsRangeSize
|
||||
func (c *Commit) CommitsByRange(page, pageSize int, not string) ([]*Commit, error) {
|
||||
return c.repo.commitsByRange(c.ID, page, pageSize, not)
|
||||
func (c *Commit) CommitsByRange(page, pageSize int, not, since, until string) ([]*Commit, error) {
|
||||
return c.repo.commitsByRangeWithTime(c.ID, page, pageSize, not, since, until)
|
||||
}
|
||||
|
||||
// CommitsBefore returns all the commits before current revision
|
||||
|
|
|
@ -89,7 +89,8 @@ func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) {
|
|||
return commits[0], nil
|
||||
}
|
||||
|
||||
func (repo *Repository) commitsByRange(id ObjectID, page, pageSize int, not string) ([]*Commit, error) {
|
||||
// commitsByRangeWithTime returns the specific page commits before current revision, with not, since, until support
|
||||
func (repo *Repository) commitsByRangeWithTime(id ObjectID, page, pageSize int, not, since, until string) ([]*Commit, error) {
|
||||
cmd := NewCommand("log").
|
||||
AddOptionFormat("--skip=%d", (page-1)*pageSize).
|
||||
AddOptionFormat("--max-count=%d", pageSize).
|
||||
|
@ -99,6 +100,12 @@ func (repo *Repository) commitsByRange(id ObjectID, page, pageSize int, not stri
|
|||
if not != "" {
|
||||
cmd.AddOptionValues("--not", not)
|
||||
}
|
||||
if since != "" {
|
||||
cmd.AddOptionFormat("--since=%s", since)
|
||||
}
|
||||
if until != "" {
|
||||
cmd.AddOptionFormat("--until=%s", until)
|
||||
}
|
||||
|
||||
stdout, _, err := cmd.RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path})
|
||||
if err != nil {
|
||||
|
@ -212,6 +219,8 @@ type CommitsByFileAndRangeOptions struct {
|
|||
File string
|
||||
Not string
|
||||
Page int
|
||||
Since string
|
||||
Until string
|
||||
}
|
||||
|
||||
// CommitsByFileAndRange return the commits according revision file and the page
|
||||
|
@ -231,6 +240,12 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions)
|
|||
if opts.Not != "" {
|
||||
gitCmd.AddOptionValues("--not", opts.Not)
|
||||
}
|
||||
if opts.Since != "" {
|
||||
gitCmd.AddOptionFormat("--since=%s", opts.Since)
|
||||
}
|
||||
if opts.Until != "" {
|
||||
gitCmd.AddOptionFormat("--until=%s", opts.Until)
|
||||
}
|
||||
|
||||
gitCmd.AddDashesAndList(opts.File)
|
||||
err := gitCmd.Run(repo.Ctx, &RunOpts{
|
||||
|
|
|
@ -40,7 +40,9 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string)
|
|||
|
||||
since := fromTime.Format(time.RFC3339)
|
||||
|
||||
stdout, _, runErr := NewCommand("rev-list", "--count", "--no-merges", "--branches=*", "--date=iso").AddOptionFormat("--since='%s'", since).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path})
|
||||
stdout, _, runErr := NewCommand("rev-list", "--count", "--no-merges", "--branches=*", "--date=iso").
|
||||
AddOptionFormat("--since=%s", since).
|
||||
RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path})
|
||||
if runErr != nil {
|
||||
return nil, runErr
|
||||
}
|
||||
|
@ -60,7 +62,8 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string)
|
|||
_ = stdoutWriter.Close()
|
||||
}()
|
||||
|
||||
gitCmd := NewCommand("log", "--numstat", "--no-merges", "--pretty=format:---%n%h%n%aN%n%aE%n", "--date=iso").AddOptionFormat("--since='%s'", since)
|
||||
gitCmd := NewCommand("log", "--numstat", "--no-merges", "--pretty=format:---%n%h%n%aN%n%aE%n", "--date=iso").
|
||||
AddOptionFormat("--since=%s", since)
|
||||
if len(branch) == 0 {
|
||||
gitCmd.AddArguments("--branches=*")
|
||||
} else {
|
||||
|
|
|
@ -200,5 +200,3 @@ func TestListToPushCommits(t *testing.T) {
|
|||
assert.Equal(t, now, pushCommits.Commits[1].Timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO TestPushUpdate
|
||||
|
|
|
@ -101,6 +101,8 @@ type Repository struct {
|
|||
AllowSquash bool `json:"allow_squash_merge"`
|
||||
AllowFastForwardOnly bool `json:"allow_fast_forward_only_merge"`
|
||||
AllowRebaseUpdate bool `json:"allow_rebase_update"`
|
||||
AllowManualMerge bool `json:"allow_manual_merge"`
|
||||
AutodetectManualMerge bool `json:"autodetect_manual_merge"`
|
||||
DefaultDeleteBranchAfterMerge bool `json:"default_delete_branch_after_merge"`
|
||||
DefaultMergeStyle string `json:"default_merge_style"`
|
||||
DefaultAllowMaintainerEdit bool `json:"default_allow_maintainer_edit"`
|
||||
|
|
|
@ -66,6 +66,7 @@ Piotr Orzechowski <piotr AT orzechowski DOT tech>
|
|||
Richard Bukovansky <richard DOT bukovansky AT gmail DOT com>
|
||||
Robert Nuske <robert DOT nuske AT web DOT de>
|
||||
Robin Hübner <profan AT prfn DOT se>
|
||||
Ryo Hanafusa <ryo7gumi AT gmail DOT com>
|
||||
SeongJae Park <sj38 DOT park AT gmail DOT com>
|
||||
Thiago Avelino <thiago AT avelino DOT xxx>
|
||||
Thomas Fanninger <gogs DOT thomas AT fanninger DOT at>
|
||||
|
|
|
@ -1228,6 +1228,7 @@ migrate.migrating_issues = Migrating Issues
|
|||
migrate.migrating_pulls = Migrating Pull Requests
|
||||
migrate.cancel_migrating_title = Cancel Migration
|
||||
migrate.cancel_migrating_confirm = Do you want to cancel this migration?
|
||||
migrating_status = Migrating status
|
||||
|
||||
mirror_from = mirror of
|
||||
forked_from = forked from
|
||||
|
@ -3811,6 +3812,9 @@ runs.no_workflows.documentation = For more information on Gitea Actions, see <a
|
|||
runs.no_runs = The workflow has no runs yet.
|
||||
runs.empty_commit_message = (empty commit message)
|
||||
runs.expire_log_message = Logs have been purged because they were too old.
|
||||
runs.delete = Delete workflow run
|
||||
runs.delete.description = Are you sure you want to permanently delete this workflow run? This action cannot be undone.
|
||||
runs.not_done = This workflow run is not done.
|
||||
|
||||
workflow.disable = Disable Workflow
|
||||
workflow.disable_success = Workflow '%s' disabled successfully.
|
||||
|
|
|
@ -3728,7 +3728,11 @@ creation.name_placeholder=Caractères alphanumériques ou tirets bas uniquement,
|
|||
creation.value_placeholder=Entrez n’importe quoi. Les blancs cernant seront taillés.
|
||||
creation.description_placeholder=Décrire brièvement votre dépôt (optionnel).
|
||||
|
||||
save_success=Le secret « %s » a été enregistré.
|
||||
save_failed=Impossible d’enregistrer le secret.
|
||||
|
||||
add_secret=Ajouter un secret
|
||||
edit_secret=Modifier le secret
|
||||
deletion=Supprimer le secret
|
||||
deletion.description=La suppression d'un secret est permanente et irréversible. Continuer ?
|
||||
deletion.success=Le secret a été supprimé.
|
||||
|
@ -3806,6 +3810,9 @@ runs.no_workflows.documentation=Pour plus d’informations sur les actions Gitea
|
|||
runs.no_runs=Le flux de travail n'a pas encore d'exécution.
|
||||
runs.empty_commit_message=(message de révision vide)
|
||||
runs.expire_log_message=Les journaux ont été supprimés car ils étaient trop anciens.
|
||||
runs.delete=Supprimer cette exécution
|
||||
runs.delete.description=Êtes-vous sûr de vouloir supprimer définitivement cette exécution ? Cette action ne peut pas être annulée.
|
||||
runs.not_done=Cette exécution du flux de travail n’est pas terminée.
|
||||
|
||||
workflow.disable=Désactiver le flux de travail
|
||||
workflow.disable_success=Le flux de travail « %s » a bien été désactivé.
|
||||
|
|
|
@ -3728,7 +3728,11 @@ creation.name_placeholder=carachtair alfanumair nó íoslaghda amháin nach féi
|
|||
creation.value_placeholder=Ionchur ábhar ar bith. Fágfar spás bán ag tús agus ag deireadh ar lár.
|
||||
creation.description_placeholder=Cuir isteach cur síos gairid (roghnach).
|
||||
|
||||
save_success=Tá an rún "%s" sábháilte.
|
||||
save_failed=Theip ar an rún a shábháil.
|
||||
|
||||
add_secret=Cuir rún leis
|
||||
edit_secret=Cuir rún in eagar
|
||||
deletion=Bain rún
|
||||
deletion.description=Is buan rún a bhaint agus ní féidir é a chealú. Lean ort?
|
||||
deletion.success=Tá an rún bainte.
|
||||
|
@ -3806,6 +3810,9 @@ runs.no_workflows.documentation=Le haghaidh tuilleadh eolais ar Gitea Actions, f
|
|||
runs.no_runs=Níl aon rith ag an sreabhadh oibre fós.
|
||||
runs.empty_commit_message=(teachtaireacht tiomantas folamh)
|
||||
runs.expire_log_message=Glanadh logaí toisc go raibh siad ró-sean.
|
||||
runs.delete=Scrios rith sreabha oibre
|
||||
runs.delete.description=An bhfuil tú cinnte gur mian leat an rith sreabha oibre seo a scriosadh go buan? Ní féidir an gníomh seo a chealú.
|
||||
runs.not_done=Níl an rith sreabha oibre seo críochnaithe.
|
||||
|
||||
workflow.disable=Díchumasaigh sreabhadh oibre
|
||||
workflow.disable_success=D'éirigh le sreabhadh oibre '%s' a dhíchumasú.
|
||||
|
|
|
@ -450,6 +450,7 @@ use_scratch_code=スクラッチコードを使う
|
|||
twofa_scratch_used=あなたはスクラッチコードを使用しました。 2要素認証の設定ページにリダイレクトしましたので、デバイスの登録を解除するか、新しいスクラッチコードを生成しましょう。
|
||||
twofa_passcode_incorrect=パスコードが正しくありません。デバイスを紛失した場合は、スクラッチコードを使ってサインインしてください。
|
||||
twofa_scratch_token_incorrect=スクラッチコードが正しくありません。
|
||||
twofa_required=リポジトリにアクセスするには2段階認証を設定するか、再度ログインしてください。
|
||||
login_userpass=サインイン
|
||||
login_openid=OpenID
|
||||
oauth_signup_tab=新規アカウント登録
|
||||
|
@ -1878,6 +1879,7 @@ pulls.add_prefix=先頭に <strong>%s</strong> を追加
|
|||
pulls.remove_prefix=先頭の <strong>%s</strong> を除去
|
||||
pulls.data_broken=このプルリクエストは、フォークの情報が見つからないため壊れています。
|
||||
pulls.files_conflicted=このプルリクエストは、ターゲットブランチと競合する変更を含んでいます。
|
||||
pulls.is_checking=マージの競合を確認しています...
|
||||
pulls.is_ancestor=このブランチは既にターゲットブランチに含まれています。マージするものはありません。
|
||||
pulls.is_empty=このブランチの変更は既にターゲットブランチにあります。これは空のコミットになります。
|
||||
pulls.required_status_check_failed=いくつかの必要なステータスチェックが成功していません。
|
||||
|
@ -3725,7 +3727,11 @@ creation.name_placeholder=大文字小文字の区別なし、英数字とアン
|
|||
creation.value_placeholder=内容を入力してください。前後の空白は除去されます。
|
||||
creation.description_placeholder=簡単な説明を入力してください。 (オプション)
|
||||
|
||||
save_success=シークレット "%s" を保存しました。
|
||||
save_failed=シークレットの保存に失敗しました。
|
||||
|
||||
add_secret=シークレットを追加
|
||||
edit_secret=シークレットを編集
|
||||
deletion=シークレットの削除
|
||||
deletion.description=シークレットの削除は恒久的で元に戻すことはできません。 続行しますか?
|
||||
deletion.success=シークレットを削除しました。
|
||||
|
@ -3842,6 +3848,8 @@ deleted.display_name=削除されたプロジェクト
|
|||
type-1.display_name=個人プロジェクト
|
||||
type-2.display_name=リポジトリ プロジェクト
|
||||
type-3.display_name=組織プロジェクト
|
||||
enter_fullscreen=フルスクリーン
|
||||
exit_fullscreen=フルスクリーンを終了
|
||||
|
||||
[git.filemode]
|
||||
changed_filemode=%[1]s → %[2]s
|
||||
|
|
|
@ -3810,6 +3810,9 @@ runs.no_workflows.documentation=Para mais informação sobre o Gitea Actions vej
|
|||
runs.no_runs=A sequência de trabalho ainda não foi executada.
|
||||
runs.empty_commit_message=(mensagem de cometimento vazia)
|
||||
runs.expire_log_message=Os registros foram removidos porque eram muito antigos.
|
||||
runs.delete=Eliminar execução da sequência de trabalho
|
||||
runs.delete.description=Tem a certeza que pretende eliminar permanentemente a execução desta sequência de trabalho? Esta operação não poderá ser desfeita.
|
||||
runs.not_done=A execução desta sequência de trabalho ainda não terminou.
|
||||
|
||||
workflow.disable=Desabilitar sequência de trabalho
|
||||
workflow.disable_success=A sequência de trabalho '%s' foi desabilitada com sucesso.
|
||||
|
|
|
@ -113,9 +113,11 @@ copy_type_unsupported=Bu dosya türü kopyalanamaz
|
|||
write=Yaz
|
||||
preview=Önizleme
|
||||
loading=Yükleniyor…
|
||||
files=Dosyalar
|
||||
|
||||
error=Hata
|
||||
error404=Ulaşmaya çalıştığınız sayfa <strong>mevcut değil</strong> veya <strong>görüntüleme yetkiniz yok</strong>.
|
||||
error503=Sunucu isteğinizi gerçekleştiremedi. Lütfen daha sonra tekrar deneyin.
|
||||
go_back=Geri Git
|
||||
invalid_data=Geçersiz veri: %v
|
||||
|
||||
|
@ -128,6 +130,7 @@ pin=Sabitle
|
|||
unpin=Sabitlemeyi kaldır
|
||||
|
||||
artifacts=Yapılar
|
||||
expired=Süresi doldu
|
||||
confirm_delete_artifact=%s yapısını silmek istediğinizden emin misiniz?
|
||||
|
||||
archived=Arşivlenmiş
|
||||
|
@ -169,6 +172,10 @@ search=Ara...
|
|||
type_tooltip=Arama türü
|
||||
fuzzy=Bulanık
|
||||
fuzzy_tooltip=Arama terimine benzeyen sonuçları da içer
|
||||
words=Kelimeler
|
||||
words_tooltip=Sadece arama terimi kelimeleriyle eşleşen sonuçları içer
|
||||
regexp=Regexp
|
||||
regexp_tooltip=Sadece regexp arama terimiyle tamamen eşleşen sonuçları içer
|
||||
exact=Tam
|
||||
exact_tooltip=Sadece arama terimiyle tamamen eşleşen sonuçları içer
|
||||
repo_kind=Depoları ara...
|
||||
|
@ -235,13 +242,17 @@ network_error=Ağ hatası
|
|||
[startpage]
|
||||
app_desc=Zahmetsiz, kendi sunucunuzda barındırabileceğiniz Git servisi
|
||||
install=Kurulumu kolay
|
||||
install_desc=Platformunuz için <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-binary">ikili dosyayı çalıştırın</a>, <a target="_blank" rel="noopener noreferrer" href="https://github.com/go-gitea/gitea/tree/master/docker">Docker</a> ile yükleyin veya <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-package">paket</a> olarak edinin.
|
||||
platform=Farklı platformlarda çalışablir
|
||||
platform_desc=Gitea <a target="_blank" rel="noopener noreferrer" href="%s">Go</a> ile derleme yapılabilecek her yerde çalışmaktadır: Windows, macOS, Linux, ARM, vb. Hangisini seviyorsanız onu seçin!
|
||||
lightweight=Hafif
|
||||
lightweight_desc=Gitea'nın minimal gereksinimleri çok düşüktür ve ucuz bir Raspberry Pi üzerinde çalışabilmektedir. Makine enerjinizden tasarruf edin!
|
||||
license=Açık Kaynak
|
||||
license_desc=Gidin ve <a target="_blank" rel="noopener noreferrer" href="https://code.gitea.io/gitea">code.gitea.io/gitea</a>'yı edinin! Bu projeyi daha da iyi yapmak için <a target="_blank" rel="noopener noreferrer" href="https://github.com/go-gitea/gitea">katkıda bulunarak</a> bize katılın. Katkıda bulunmaktan çekinmeyin!
|
||||
|
||||
[install]
|
||||
install=Kurulum
|
||||
installing_desc=Şimdi kuruluyor, lütfen bekleyin...
|
||||
title=Başlangıç Yapılandırması
|
||||
docker_helper=Eğer Gitea'yı Docker içerisinde çalıştırıyorsanız, lütfen herhangi bir değişiklik yapmadan önce <a target="_blank" rel="noopener noreferrer" href="%s">belgeleri</a> okuyun.
|
||||
require_db_desc=Gitea MySQL, PostgreSQL, MSSQL, SQLite3 veya TiDB (MySQL protokolü) gerektirir.
|
||||
|
@ -352,6 +363,7 @@ enable_update_checker=Güncelleme Denetleyicisini Etkinleştir
|
|||
enable_update_checker_helper=Düzenli olarak gitea.io'ya bağlanarak yeni yayınlanan sürümleri denetler.
|
||||
env_config_keys=Ortam Yapılandırma
|
||||
env_config_keys_prompt=Aşağıdaki ortam değişkenleri de yapılandırma dosyanıza eklenecektir:
|
||||
config_write_file_prompt=Bu yapılandırma seçenekleri şuraya yazılacak: %s
|
||||
|
||||
[home]
|
||||
nav_menu=Gezinti Menüsü
|
||||
|
@ -380,6 +392,12 @@ show_only_public=Yalnızca açık olanlar gösteriliyor
|
|||
|
||||
issues.in_your_repos=Depolarınızda
|
||||
|
||||
guide_title=Etkinlik yok
|
||||
guide_desc=Herhangi bir depo veya kullanıcı takip etmiyorsunuz, bu yüzden görüntülenecek bir içerik yok. Aşağıdaki bağlantıları kullanarak ilgi çekici depo ve kullanıcıları keşfedebilirsiniz.
|
||||
explore_repos=Depoları keşfet
|
||||
explore_users=Kullanıcıları keşfet
|
||||
empty_org=Henüz bir organizasyon yok.
|
||||
empty_repo=Henüz bir depo yok.
|
||||
|
||||
[explore]
|
||||
repos=Depolar
|
||||
|
@ -433,6 +451,7 @@ use_scratch_code=Bir çizgi kodu kullanınız
|
|||
twofa_scratch_used=Geçici kodunuzu kullandınız. İki aşamalı ayarlar sayfasına yönlendirildiniz, burada aygıt kaydınızı kaldırabilir veya yeni bir geçici kod oluşturabilirsiniz.
|
||||
twofa_passcode_incorrect=Şifreniz yanlış. Aygıtınızı yanlış yerleştirdiyseniz, oturum açmak için çizgi kodunuzu kullanın.
|
||||
twofa_scratch_token_incorrect=Çizgi kodunuz doğru değildir.
|
||||
twofa_required=Depolara erişmek için iki aşama doğrulama kullanmanız veya tekrar oturum açmayı denemeniz gereklidir.
|
||||
login_userpass=Oturum Aç
|
||||
login_openid=Açık Kimlik
|
||||
oauth_signup_tab=Yeni Hesap Oluştur
|
||||
|
@ -441,6 +460,7 @@ oauth_signup_submit=Hesabı Tamamla
|
|||
oauth_signin_tab=Mevcut Hesaba Bağla
|
||||
oauth_signin_title=Bağlantılı Hesabı Yetkilendirmek için Giriş Yapın
|
||||
oauth_signin_submit=Hesabı Bağla
|
||||
oauth.signin.error.general=Yetkilendirme isteğini işlerken bir hata oluştu: %s. Eğer hata devam ederse lütfen site yöneticisiyle bağlantıya geçin.
|
||||
oauth.signin.error.access_denied=Yetkilendirme isteği reddedildi.
|
||||
oauth.signin.error.temporarily_unavailable=Yetkilendirme sunucusu geçici olarak erişilemez olduğu için yetkilendirme başarısız oldu. Lütfen daha sonra tekrar deneyin.
|
||||
oauth_callback_unable_auto_reg=Otomatik kayıt etkin ancak OAuth2 Sağlayıcı %[1] eksik sahalar döndürdü: %[2]s, otomatik olarak hesap oluşturulamıyor, lütfen bir hesap oluşturun veya bağlantı verin, veya site yöneticisiyle iletişim kurun.
|
||||
|
@ -457,10 +477,12 @@ authorize_application=Uygulamayı Yetkilendir
|
|||
authorize_redirect_notice=Bu uygulamayı yetkilendirirseniz %s adresine yönlendirileceksiniz.
|
||||
authorize_application_created_by=Bu uygulama %s tarafından oluşturuldu.
|
||||
authorize_application_description=Erişime izin verirseniz, özel depolar ve organizasyonlar da dahil olmak üzere tüm hesap bilgilerinize erişebilir ve yazabilir.
|
||||
authorize_application_with_scopes=Kapsamlar: %s
|
||||
authorize_title=Hesabınıza erişmesi için "%s" yetkilendirilsin mi?
|
||||
authorization_failed=Yetkilendirme başarısız oldu
|
||||
authorization_failed_desc=Geçersiz bir istek tespit ettiğimiz için yetkilendirme başarısız oldu. Lütfen izin vermeye çalıştığınız uygulamanın sağlayıcısı ile iletişim kurun.
|
||||
sspi_auth_failed=SSPI kimlik doğrulaması başarısız oldu
|
||||
password_pwned=Seçtiğiniz parola, daha önce herkese açık veri ihlallerinde açığa çıkan bir <a target="_blank" rel="noopener noreferrer" href="%s">çalınan parola listesindedir</a>. Lütfen farklı bir parola ile tekrar deneyin ve başka yerlerde de bu parolayı değiştirmeyi düşünün.
|
||||
password_pwned_err=HaveIBeenPwned'e yapılan istek tamamlanamadı
|
||||
last_admin=Son yöneticiyi silemezsiniz. En azından bir yönetici olmalıdır.
|
||||
signin_passkey=Bir parola anahtarı ile oturum aç
|
||||
|
@ -583,6 +605,8 @@ lang_select_error=Listeden bir dil seçin.
|
|||
|
||||
username_been_taken=Bu kullanıcı adı daha önce alınmış.
|
||||
username_change_not_local_user=Yerel olmayan kullanıcılar kendi kullanıcı adlarını değiştiremezler.
|
||||
change_username_disabled=Kullanıcı adı değişikliği devre dışıdır.
|
||||
change_full_name_disabled=Tam ad değişikliği devre dışıdır.
|
||||
username_has_not_been_changed=Kullanıcı adı değişmedi
|
||||
repo_name_been_taken=Depo adı zaten kullanılıyor.
|
||||
repository_force_private=Gizliyi Zorla devrede: gizli depolar herkese açık yapılamaz.
|
||||
|
@ -632,6 +656,7 @@ org_still_own_repo=Bu organizasyon hala bir veya daha fazla depoya sahip, önce
|
|||
org_still_own_packages=Bu organizasyon hala bir veya daha fazla pakete sahip, önce onları silin.
|
||||
|
||||
target_branch_not_exist=Hedef dal mevcut değil.
|
||||
target_ref_not_exist=Hedef referans mevcut değil %s
|
||||
|
||||
admin_cannot_delete_self=Yöneticiyken kendinizi silemezsiniz. Lütfen önce yönetici haklarınızı kaldırın.
|
||||
|
||||
|
@ -698,14 +723,18 @@ applications=Uygulamalar
|
|||
orgs=Organizasyonları Yönet
|
||||
repos=Depolar
|
||||
delete=Hesabı Sil
|
||||
twofa=İki Aşamalı Kimlik Doğrulama (TOTP)
|
||||
account_link=Bağlı Hesaplar
|
||||
organization=Organizasyonlar
|
||||
uid=UID
|
||||
webauthn=İki-Aşamalı Kimlik Doğrulama (Güvenlik Anahtarları)
|
||||
|
||||
public_profile=Herkese Açık Profil
|
||||
biography_placeholder=Bize kendiniz hakkında birşeyler söyleyin! (Markdown kullanabilirsiniz)
|
||||
location_placeholder=Yaklaşık konumunuzu başkalarıyla paylaşın
|
||||
profile_desc=Profilinizin başkalarına nasıl gösterildiğini yönetin. Ana e-posta adresiniz bildirimler, parola kurtarma ve web tabanlı Git işlemleri için kullanılacaktır.
|
||||
password_username_disabled=Yerel olmayan kullanıcılara kullanıcı adlarını değiştirme izni verilmemiştir. Daha fazla bilgi edinmek için lütfen site yöneticisi ile iletişime geçiniz.
|
||||
password_full_name_disabled=Tam adınızı değiştirme izniniz yoktur. Daha fazla bilgi edinmek için lütfen site yöneticisi ile iletişime geçiniz.
|
||||
full_name=Ad Soyad
|
||||
website=Web Sitesi
|
||||
location=Konum
|
||||
|
@ -755,6 +784,7 @@ uploaded_avatar_not_a_image=Yüklenen dosya bir resim dosyası değil.
|
|||
uploaded_avatar_is_too_big=Yüklenen dosyanın boyutu (%d KiB), azami boyutu (%d KiB) aşıyor.
|
||||
update_avatar_success=Profil resminiz değiştirildi.
|
||||
update_user_avatar_success=Kullanıcının avatarı güncellendi.
|
||||
cropper_prompt=Kaydetmeden önce resmi düzenleyebilirsiniz. Düzenlenen resim PNG biçiminde kaydedilecektir.
|
||||
|
||||
change_password=Parolayı Güncelle
|
||||
old_password=Mevcut Parola
|
||||
|
@ -797,6 +827,7 @@ add_email_success=Yeni e-posta adresi eklendi.
|
|||
email_preference_set_success=E-posta tercihi başarıyla ayarlandı.
|
||||
add_openid_success=Yeni OpenID adresi eklendi.
|
||||
keep_email_private=E-posta Adresini Gizle
|
||||
keep_email_private_popup=Bu, e-posta adresinizi profilde, değişiklik isteği yaptığınızda veya web arayüzünde dosya düzenlediğinizde gizleyecektir. İtilen işlemeler değişmeyecektir.
|
||||
openid_desc=OpenID, kimlik doğrulama işlemini harici bir sağlayıcıya devretmenize olanak sağlar.
|
||||
|
||||
manage_ssh_keys=SSH Anahtarlarını Yönet
|
||||
|
@ -898,6 +929,9 @@ permission_not_set=Ayarlanmadı
|
|||
permission_no_access=Erişim Yok
|
||||
permission_read=Okunmuş
|
||||
permission_write=Okuma ve Yazma
|
||||
permission_anonymous_read=Anonim Okuma
|
||||
permission_everyone_read=Herkes Okuyabilir
|
||||
permission_everyone_write=Herkes Yazabilir
|
||||
access_token_desc=Seçili token izinleri, yetkilendirmeyi ilgili <a %s>API</a> yollarıyla sınırlandıracaktır. Daha fazla bilgi için <a %s>belgeleri</a> okuyun.
|
||||
at_least_one_permission=Bir token oluşturmak için en azından bir izin seçmelisiniz
|
||||
permissions_list=İzinler:
|
||||
|
@ -925,6 +959,7 @@ oauth2_client_secret_hint=Bu sayfadan ayrıldıktan veya yeniledikten sonra gizl
|
|||
oauth2_application_edit=Düzenle
|
||||
oauth2_application_create_description=OAuth2 uygulamaları, üçüncü taraf uygulamanıza bu durumda kullanıcı hesaplarına erişim sağlar.
|
||||
oauth2_application_remove_description=Bir OAuth2 uygulamasının kaldırılması, bu sunucudaki yetkili kullanıcı hesaplarına erişmesini önler. Devam edilsin mi?
|
||||
oauth2_application_locked=Gitea kimi OAuth2 uygulamalarının başlangıçta ön kaydını, yapılandırmada etkinleştirilmişse yapabilir. Beklenmeyen davranışı önlemek için bunlar ne düzenlenmeli ne de kaldırılmalı. Daha fazla bilgi için OAuth2 belgesine bakın.
|
||||
|
||||
authorized_oauth2_applications=Yetkili OAuth2 Uygulamaları
|
||||
authorized_oauth2_applications_description=Kişisel Gitea hesabınıza bu üçüncü parti uygulamalara erişim izni verdiniz. Lütfen artık ihtiyaç duyulmayan uygulamalara erişimi iptal edin.
|
||||
|
@ -933,13 +968,17 @@ revoke_oauth2_grant=Erişimi İptal Et
|
|||
revoke_oauth2_grant_description=Bu üçüncü taraf uygulamasına erişimin iptal edilmesi bu uygulamanın verilerinize erişmesini önleyecektir. Emin misiniz?
|
||||
revoke_oauth2_grant_success=Erişim başarıyla kaldırıldı.
|
||||
|
||||
twofa_desc=İki aşamalı kimlik doğrulama, hesabınızın güvenliğini artırır.
|
||||
twofa_recovery_tip=Aygıtınızı kaybetmeniz durumunda, hesabınıza tekrar erişmek için tek kullanımlık kurtarma anahtarını kullanabileceksiniz.
|
||||
twofa_is_enrolled=Hesabınız şu anda iki faktörlü kimlik doğrulaması içinde <strong>kaydedilmiş</strong>.
|
||||
twofa_not_enrolled=Hesabınız şu anda iki faktörlü kimlik doğrulaması içinde kaydedilmemiş.
|
||||
twofa_disable=İki Aşamalı Doğrulamayı Devre Dışı Bırak
|
||||
twofa_scratch_token_regenerate=Geçici Kodu Yeniden Üret
|
||||
twofa_scratch_token_regenerated=Geçici kodunuz şimdi %s. Güvenli bir yerde saklayın, tekrar gösterilmeyecektir.
|
||||
twofa_enroll=İki Faktörlü Kimlik Doğrulamaya Kaydolun
|
||||
twofa_disable_note=Gerekirse iki faktörlü kimlik doğrulamayı devre dışı bırakabilirsiniz.
|
||||
twofa_disable_desc=İki faktörlü kimlik doğrulamayı devre dışı bırakmak hesabınızı daha az güvenli hale getirir. Devam edilsin mi?
|
||||
regenerate_scratch_token_desc=Geçici kodunuzu kaybettiyseniz veya oturum açmak için kullandıysanız, buradan sıfırlayabilirsiniz.
|
||||
twofa_disabled=İki faktörlü kimlik doğrulama devre dışı bırakıldı.
|
||||
scan_this_image=Kim doğrulama uygulamanızla bu görüntüyü tarayın:
|
||||
or_enter_secret=Veya gizli şeyi girin: %s
|
||||
|
@ -993,6 +1032,8 @@ new_repo_helper=Bir depo, sürüm geçmişi dahil tüm proje dosyalarını içer
|
|||
owner=Sahibi
|
||||
owner_helper=Bazı organizasyonlar, en çok depo sayısı sınırı nedeniyle açılır menüde görünmeyebilir.
|
||||
repo_name=Depo İsmi
|
||||
repo_name_profile_public_hint=.profile herkese açık organizasyonunuzun profiline herkesin görüntüleyebileceği bir README.md dosyası eklemek için kullanabileceğiniz özel bir depodur. Başlamak için herkese açık olduğundan ve profile dizininde README ile başladığınızdan emin olun.
|
||||
repo_name_profile_private_hint=.profile-private organizasyonunuzun üye profiline sadece organizasyon üyelerinin görüntüleyebileceği bir README.md eklemek için kullanabileceğiniz özel bir depodur. Başlamak için özel olduğundan ve profil dizininde README ile başladığınızdan emin olun.
|
||||
repo_size=Depo Boyutu
|
||||
template=Şablon
|
||||
template_select=Bir şablon seçin.
|
||||
|
@ -1011,6 +1052,8 @@ fork_to_different_account=Başka bir hesaba çatalla
|
|||
fork_visibility_helper=Çatallanmış bir deponun görünürlüğü değiştirilemez.
|
||||
fork_branch=Çatala klonlanacak dal
|
||||
all_branches=Tüm dallar
|
||||
view_all_branches=Tüm dalları görüntüle
|
||||
view_all_tags=Tüm etiketleri görüntüle
|
||||
fork_no_valid_owners=Geçerli bir sahibi olmadığı için bu depo çatallanamaz.
|
||||
fork.blocked_user=Depo çatallanamıyor, depo sahibi tarafından engellenmişsiniz.
|
||||
use_template=Bu şablonu kullan
|
||||
|
@ -1022,6 +1065,8 @@ generate_repo=Depo Oluştur
|
|||
generate_from=Şuradan Oluştur
|
||||
repo_desc=Açıklama
|
||||
repo_desc_helper=Kısa açıklama girin (isteğe bağlı)
|
||||
repo_no_desc=Hiçbir açıklama sağlanmadı
|
||||
repo_lang=Dil
|
||||
repo_gitignore_helper=.gitignore şablonlarını seç.
|
||||
repo_gitignore_helper_desc=Sık kullanılan diller için bir şablon listesinden hangi dosyaların izlenmeyeceğini seçin. Her dilin oluşturma araçları tarafından oluşturulan tipik yapılar, varsayılan olarak .gitignore dosyasına dahil edilmiştir.
|
||||
issue_labels=Konu Etiketleri
|
||||
|
@ -1029,6 +1074,7 @@ issue_labels_helper=Bir konu etiket seti seçin.
|
|||
license=Lisans
|
||||
license_helper=Bir lisans dosyası seçin.
|
||||
license_helper_desc=Bir lisans, başkalarının kodunuzla neler yapıp yapamayacağını yönetir. Projeniz için hangisinin doğru olduğundan emin değil misiniz? <a target="_blank" rel="noopener noreferrer" href="%s">Lisans seçme</a> konusuna bakın
|
||||
multiple_licenses=Çoklu Lisans
|
||||
object_format=Nesne Biçimi
|
||||
object_format_helper=Deponun nesne biçimi. Daha sonra değiştirilemez. SHA1 en uyumlu olandır.
|
||||
readme=README
|
||||
|
@ -1082,15 +1128,20 @@ delete_preexisting_success=%s içindeki kabul edilmeyen dosyalar silindi
|
|||
blame_prior=Bu değişiklikten önceki suçu görüntüle
|
||||
blame.ignore_revs=<a href="%s">.git-blame-ignore-revs</a> dosyasındaki sürümler yok sayılıyor. Bunun yerine normal sorumlu görüntüsü için <a href="%s">buraya tıklayın</a>.
|
||||
blame.ignore_revs.failed=<a href="%s">.git-blame-ignore-revs</a> dosyasındaki sürümler yok sayılamadı.
|
||||
user_search_tooltip=En fazla 30 kullanıcı görüntüler
|
||||
|
||||
tree_path_not_found=%[1] yolu, %[2]s deposunda mevcut değil
|
||||
|
||||
transfer.accept=Aktarımı Kabul Et
|
||||
transfer.accept_desc=`"%s" tarafına aktar`
|
||||
transfer.reject=Aktarımı Reddet
|
||||
transfer.reject_desc=`"%s" tarafına aktarımı iptal et`
|
||||
transfer.no_permission_to_accept=Bu aktarımı kabul etme izniniz yok.
|
||||
transfer.no_permission_to_reject=Bu aktarımı reddetme izniniz yok.
|
||||
|
||||
desc.private=Özel
|
||||
desc.public=Genel
|
||||
desc.public_access=Herkese Açık Erişim
|
||||
desc.template=Şablon
|
||||
desc.internal=Dahili
|
||||
desc.archived=Arşivlenmiş
|
||||
|
@ -1160,6 +1211,10 @@ migrate.gogs.description=Notabug.org veya diğer Gogs sunucularından veri aktar
|
|||
migrate.onedev.description=Code.onedev.io ve diğer OneDev sunucularından veri aktar.
|
||||
migrate.codebase.description=Codebasehq.com sitesinden veri aktar.
|
||||
migrate.gitbucket.description=GitBucket sunucularından veri aktar.
|
||||
migrate.codecommit.aws_access_key_id=AWS Erişim Anahtarı Kimliği
|
||||
migrate.codecommit.aws_secret_access_key=AWS Gizli Erişim Anahtarı
|
||||
migrate.codecommit.https_git_credentials_username=HTTPS Git Kimliği Kullanıcı Adı
|
||||
migrate.codecommit.https_git_credentials_password=HTTPS Git Kimliği Parolası
|
||||
migrate.migrating_git=Git Verilerini Taşıma
|
||||
migrate.migrating_topics=Konuları Taşıma
|
||||
migrate.migrating_milestones=Kilometre Taşlarını Taşıma
|
||||
|
@ -1193,6 +1248,7 @@ create_new_repo_command=Komut satırında yeni bir depo oluşturuluyor
|
|||
push_exist_repo=Komut satırından mevcut bir depo itiliyor
|
||||
empty_message=Bu depoda herhangi bir içerik yok.
|
||||
broken_message=Bu deponun altındaki Git verisi okunamıyor. Bu sunucunun yöneticisiyle bağlantıya geçin veya bu depoyu silin.
|
||||
no_branch=Bu deponun hiç bir dalı yok.
|
||||
|
||||
code=Kod
|
||||
code.desc=Kaynak koda, dosyalara, işlemelere ve dallara eriş.
|
||||
|
@ -1302,6 +1358,8 @@ editor.new_branch_name_desc=Yeni dal ismi…
|
|||
editor.cancel=İptal
|
||||
editor.filename_cannot_be_empty=Dosya adı boş olamaz.
|
||||
editor.filename_is_invalid=Dosya adı geçersiz: "%s".
|
||||
editor.commit_email=İşleme e-postası
|
||||
editor.invalid_commit_email=İşleme e-postası hatalı.
|
||||
editor.branch_does_not_exist=Bu depoda "%s" dalı yok.
|
||||
editor.branch_already_exists=Bu depoda "%s" dalı zaten var.
|
||||
editor.directory_is_a_file=Dizin adı "%s" zaten bu depoda bir dosya adı olarak kullanılmaktadır.
|
||||
|
@ -1350,6 +1408,7 @@ commits.signed_by_untrusted_user_unmatched=İşleyici ile eşleşmeyen güvenilm
|
|||
commits.gpg_key_id=GPG Anahtar Kimliği
|
||||
commits.ssh_key_fingerprint=SSH Anahtar Parmak İzi
|
||||
commits.view_path=Geçmişte bu noktayı görüntüle
|
||||
commits.view_file_diff=Bu dosyanın bu işlemedeki değişikliklerini görüntüle
|
||||
|
||||
commit.operations=İşlemler
|
||||
commit.revert=Geri Al
|
||||
|
@ -1410,6 +1469,8 @@ issues.filter_milestones=Kilometre Taşı Süzgeci
|
|||
issues.filter_projects=Projeyi Süz
|
||||
issues.filter_labels=Etiket Süzgeci
|
||||
issues.filter_reviewers=Gözden Geçiren Süzgeci
|
||||
issues.filter_no_results=Sonuç yok
|
||||
issues.filter_no_results_placeholder=Arama filtrelerinizi ayarlamayı deneyin.
|
||||
issues.new=Yeni Konu
|
||||
issues.new.title_empty=Başlık boş olamaz
|
||||
issues.new.labels=Etiketler
|
||||
|
@ -1427,6 +1488,7 @@ issues.new.clear_milestone=Kilometre Taşlarını Temizle
|
|||
issues.new.assignees=Atananlar
|
||||
issues.new.clear_assignees=Atamaları Temizle
|
||||
issues.new.no_assignees=Atanan Kişi Yok
|
||||
issues.new.no_reviewers=Gözden geçiren yok
|
||||
issues.new.blocked_user=Konu oluşturulamıyor, depo sahibi tarafından engellenmişsiniz.
|
||||
issues.edit.already_changed=Konuya yapılan değişiklikler kaydedilemiyor. İçerik başka kullanıcı tarafından değiştirilmiş gözüküyor. Diğerlerinin değişikliklerinin üzerine yazmamak için lütfen sayfayı yenileyin ve tekrar düzenlemeye çalışın
|
||||
issues.edit.blocked_user=İçerik düzenlenemiyor, gönderen veya depo sahibi tarafından engellenmişsiniz.
|
||||
|
@ -1483,6 +1545,7 @@ issues.filter_project=Proje
|
|||
issues.filter_project_all=Tüm projeler
|
||||
issues.filter_project_none=Proje yok
|
||||
issues.filter_assignee=Atanan
|
||||
issues.filter_assignee_no_assignee=Hiç kimseye atanmamış
|
||||
issues.filter_poster=Yazar
|
||||
issues.filter_type=Tür
|
||||
issues.filter_type.all_issues=Tüm konular
|
||||
|
@ -2029,6 +2092,7 @@ contributors.contribution_type.deletions=Silmeler
|
|||
settings=Ayarlar
|
||||
settings.desc=Ayarlar, deponun ayarlarını yönetebileceğiniz yerdir
|
||||
settings.options=Depo
|
||||
settings.public_access=Herkese Açık Erişim
|
||||
settings.collaboration=Katkıcılar
|
||||
settings.collaboration.admin=Yönetici
|
||||
settings.collaboration.write=Yazma
|
||||
|
|
|
@ -117,6 +117,7 @@ files=文件
|
|||
|
||||
error=错误
|
||||
error404=您正尝试访问的页面 <strong>不存在</strong> 或 <strong>您尚未被授权</strong> 查看该页面。
|
||||
error503=服务器无法完成您的请求,请稍后重试。
|
||||
go_back=返回
|
||||
invalid_data=无效数据: %v
|
||||
|
||||
|
@ -129,6 +130,7 @@ pin=固定
|
|||
unpin=取消置顶
|
||||
|
||||
artifacts=制品
|
||||
expired=已过期
|
||||
confirm_delete_artifact=您确定要删除制品'%s'吗?
|
||||
|
||||
archived=已归档
|
||||
|
@ -449,6 +451,7 @@ use_scratch_code=使用验证口令
|
|||
twofa_scratch_used=你已经使用了你的验证口令。你将会转到两步验证设置页面以便移除你的注册设备或者重新生成新的验证口令。
|
||||
twofa_passcode_incorrect=你的验证码不正确。如果你丢失了你的设备,请使用你的验证口令。
|
||||
twofa_scratch_token_incorrect=你的验证口令不正确。
|
||||
twofa_required=您必须设置两步验证来访问仓库,或者尝试重新登录。
|
||||
login_userpass=登录
|
||||
login_openid=OpenID
|
||||
oauth_signup_tab=注册帐号
|
||||
|
@ -1524,7 +1527,7 @@ issues.move_to_column_of_project=`将此对象移至 %s 的 %s 中在 %s 上`
|
|||
issues.change_milestone_at=`%[3]s 修改了里程碑从 <b>%[1]s</b> 到 <b>%[2]s</b>`
|
||||
issues.change_project_at=于 %[3]s 将此从项目 <b>%[1]s</b> 移到 <b>%[2]s</b>
|
||||
issues.remove_milestone_at=`%[2]s 删除了里程碑 <b>%[1]s</b>`
|
||||
issues.remove_project_at=`从 <b>%s</b> 项目 %s 中删除`
|
||||
issues.remove_project_at=`于 %[2]s 将此工单从项目 <b>%[1]s</b> 中删除`
|
||||
issues.deleted_milestone=(已删除)
|
||||
issues.deleted_project=`(已删除)`
|
||||
issues.self_assign_at=`于 %s 指派给自己`
|
||||
|
@ -1877,6 +1880,7 @@ pulls.add_prefix=添加 <strong>%s</strong> 前缀
|
|||
pulls.remove_prefix=删除 <strong>%s</strong> 前缀
|
||||
pulls.data_broken=此合并请求因为派生仓库信息缺失而中断。
|
||||
pulls.files_conflicted=此合并请求有变更与目标分支冲突。
|
||||
pulls.is_checking=正在进行合并冲突检测 ...
|
||||
pulls.is_ancestor=此分支已经包含在目标分支中,没有什么可以合并。
|
||||
pulls.is_empty=此分支上的更改已经在目标分支上。这将是一个空提交。
|
||||
pulls.required_status_check_failed=一些必要的检查没有成功
|
||||
|
@ -3724,7 +3728,11 @@ creation.name_placeholder=不区分大小写,字母数字或下划线不能以
|
|||
creation.value_placeholder=输入任何内容,开头和结尾的空白都会被省略
|
||||
creation.description_placeholder=输入简短描述(可选)。
|
||||
|
||||
save_success=密钥 '%s' 保存成功。
|
||||
save_failed=密钥保存失败。
|
||||
|
||||
add_secret=添加密钥
|
||||
edit_secret=编辑密钥
|
||||
deletion=删除密钥
|
||||
deletion.description=删除密钥是永久性的,无法撤消。继续吗?
|
||||
deletion.success=此Secret已被删除。
|
||||
|
@ -3841,6 +3849,8 @@ deleted.display_name=已删除项目
|
|||
type-1.display_name=个人项目
|
||||
type-2.display_name=仓库项目
|
||||
type-3.display_name=组织项目
|
||||
enter_fullscreen=全屏
|
||||
exit_fullscreen=退出全屏
|
||||
|
||||
[git.filemode]
|
||||
changed_filemode=%[1]s -> %[2]s
|
||||
|
|
|
@ -1279,7 +1279,10 @@ func Routes() *web.Router {
|
|||
}, reqToken(), reqAdmin())
|
||||
m.Group("/actions", func() {
|
||||
m.Get("/tasks", repo.ListActionTasks)
|
||||
m.Get("/runs/{run}/artifacts", repo.GetArtifactsOfRun)
|
||||
m.Group("/runs/{run}", func() {
|
||||
m.Get("/artifacts", repo.GetArtifactsOfRun)
|
||||
m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun)
|
||||
})
|
||||
m.Get("/artifacts", repo.GetArtifacts)
|
||||
m.Group("/artifacts/{artifact_id}", func() {
|
||||
m.Get("", repo.GetArtifact)
|
||||
|
|
|
@ -1061,6 +1061,58 @@ func GetArtifactsOfRun(ctx *context.APIContext) {
|
|||
ctx.JSON(http.StatusOK, &res)
|
||||
}
|
||||
|
||||
// DeleteActionRun Delete a workflow run
|
||||
func DeleteActionRun(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/actions/runs/{run} repository deleteActionRun
|
||||
// ---
|
||||
// summary: Delete a workflow run
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: name of the owner
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repository
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: run
|
||||
// in: path
|
||||
// description: runid of the workflow run
|
||||
// type: integer
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// description: "No Content"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
runID := ctx.PathParamInt64("run")
|
||||
run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID)
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.APIError(http.StatusNotFound, err)
|
||||
return
|
||||
} else if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if !run.Status.IsDone() {
|
||||
ctx.APIError(http.StatusBadRequest, "this workflow run is not done")
|
||||
return
|
||||
}
|
||||
|
||||
if err := actions_service.DeleteRun(ctx, run); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// GetArtifacts Lists all artifacts for a repository.
|
||||
func GetArtifacts(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/actions/artifacts repository getArtifacts
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
@ -116,6 +117,16 @@ func GetAllCommits(ctx *context.APIContext) {
|
|||
// in: query
|
||||
// description: filepath of a file/dir
|
||||
// type: string
|
||||
// - name: since
|
||||
// in: query
|
||||
// description: Only commits after this date will be returned (ISO 8601 format)
|
||||
// type: string
|
||||
// format: date-time
|
||||
// - name: until
|
||||
// in: query
|
||||
// description: Only commits before this date will be returned (ISO 8601 format)
|
||||
// type: string
|
||||
// format: date-time
|
||||
// - name: stat
|
||||
// in: query
|
||||
// description: include diff stats for every commit (disable for speedup, default 'true')
|
||||
|
@ -148,6 +159,23 @@ func GetAllCommits(ctx *context.APIContext) {
|
|||
// "409":
|
||||
// "$ref": "#/responses/EmptyRepository"
|
||||
|
||||
since := ctx.FormString("since")
|
||||
until := ctx.FormString("until")
|
||||
|
||||
// Validate since/until as ISO 8601 (RFC3339)
|
||||
if since != "" {
|
||||
if _, err := time.Parse(time.RFC3339, since); err != nil {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "invalid 'since' format, expected ISO 8601 (RFC3339)")
|
||||
return
|
||||
}
|
||||
}
|
||||
if until != "" {
|
||||
if _, err := time.Parse(time.RFC3339, until); err != nil {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "invalid 'until' format, expected ISO 8601 (RFC3339)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.Repo.Repository.IsEmpty {
|
||||
ctx.JSON(http.StatusConflict, api.APIError{
|
||||
Message: "Git Repository is empty.",
|
||||
|
@ -198,6 +226,8 @@ func GetAllCommits(ctx *context.APIContext) {
|
|||
RepoPath: ctx.Repo.GitRepo.Path,
|
||||
Not: not,
|
||||
Revision: []string{baseCommit.ID.String()},
|
||||
Since: since,
|
||||
Until: until,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
|
@ -205,7 +235,7 @@ func GetAllCommits(ctx *context.APIContext) {
|
|||
}
|
||||
|
||||
// Query commits
|
||||
commits, err = baseCommit.CommitsByRange(listOptions.Page, listOptions.PageSize, not)
|
||||
commits, err = baseCommit.CommitsByRange(listOptions.Page, listOptions.PageSize, not, since, until)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
|
@ -221,6 +251,8 @@ func GetAllCommits(ctx *context.APIContext) {
|
|||
Not: not,
|
||||
Revision: []string{sha},
|
||||
RelPath: []string{path},
|
||||
Since: since,
|
||||
Until: until,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
@ -237,6 +269,8 @@ func GetAllCommits(ctx *context.APIContext) {
|
|||
File: path,
|
||||
Not: not,
|
||||
Page: listOptions.Page,
|
||||
Since: since,
|
||||
Until: until,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
|
|
|
@ -895,6 +895,15 @@ func EditIssue(ctx *context.APIContext) {
|
|||
issue.MilestoneID != *form.Milestone {
|
||||
oldMilestoneID := issue.MilestoneID
|
||||
issue.MilestoneID = *form.Milestone
|
||||
if issue.MilestoneID > 0 {
|
||||
issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, *form.Milestone)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
issue.Milestone = nil
|
||||
}
|
||||
if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
|
|
|
@ -609,15 +609,17 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
|
|||
return
|
||||
}
|
||||
|
||||
oldContent := comment.Content
|
||||
comment.Content = form.Body
|
||||
if err := issue_service.UpdateComment(ctx, comment, comment.ContentVersion, ctx.Doer, oldContent); err != nil {
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.APIError(http.StatusForbidden, err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
if form.Body != comment.Content {
|
||||
oldContent := comment.Content
|
||||
comment.Content = form.Body
|
||||
if err := issue_service.UpdateComment(ctx, comment, comment.ContentVersion, ctx.Doer, oldContent); err != nil {
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.APIError(http.StatusForbidden, err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIComment(ctx, ctx.Repo.Repository, comment))
|
||||
|
|
|
@ -706,6 +706,11 @@ func EditPullRequest(ctx *context.APIContext) {
|
|||
issue.MilestoneID != form.Milestone {
|
||||
oldMilestoneID := issue.MilestoneID
|
||||
issue.MilestoneID = form.Milestone
|
||||
issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, form.Milestone)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
|
|
|
@ -67,6 +67,28 @@ func ListRunners(ctx *context.APIContext, ownerID, repoID int64) {
|
|||
ctx.JSON(http.StatusOK, &res)
|
||||
}
|
||||
|
||||
func getRunnerByID(ctx *context.APIContext, ownerID, repoID, runnerID int64) (*actions_model.ActionRunner, bool) {
|
||||
if ownerID != 0 && repoID != 0 {
|
||||
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
|
||||
}
|
||||
|
||||
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.APIErrorNotFound("Runner not found")
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if !runner.EditableInContext(ownerID, repoID) {
|
||||
ctx.APIErrorNotFound("No permission to access this runner")
|
||||
return nil, false
|
||||
}
|
||||
return runner, true
|
||||
}
|
||||
|
||||
// GetRunner get the runner for api route validated ownerID and repoID
|
||||
// ownerID == 0 and repoID == 0 means any runner including global runners
|
||||
// ownerID == 0 and repoID != 0 means any runner for the given repo
|
||||
|
@ -77,13 +99,8 @@ func GetRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) {
|
|||
if ownerID != 0 && repoID != 0 {
|
||||
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
|
||||
}
|
||||
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
|
||||
if err != nil {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
}
|
||||
if !runner.EditableInContext(ownerID, repoID) {
|
||||
ctx.APIErrorNotFound("No permission to get this runner")
|
||||
runner, ok := getRunnerByID(ctx, ownerID, repoID, runnerID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToActionRunner(ctx, runner))
|
||||
|
@ -96,20 +113,12 @@ func GetRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) {
|
|||
// ownerID != 0 and repoID != 0 undefined behavior
|
||||
// Access rights are checked at the API route level
|
||||
func DeleteRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) {
|
||||
if ownerID != 0 && repoID != 0 {
|
||||
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
|
||||
}
|
||||
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if !runner.EditableInContext(ownerID, repoID) {
|
||||
ctx.APIErrorNotFound("No permission to delete this runner")
|
||||
runner, ok := getRunnerByID(ctx, ownerID, repoID, runnerID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
err = actions_model.DeleteRunner(ctx, runner.ID)
|
||||
err := actions_model.DeleteRunner(ctx, runner.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
webhook_service "code.gitea.io/gitea/services/webhook"
|
||||
|
@ -92,6 +93,10 @@ func checkCreateHookOption(ctx *context.APIContext, form *api.CreateHookOption)
|
|||
ctx.APIError(http.StatusUnprocessableEntity, "Invalid content type")
|
||||
return false
|
||||
}
|
||||
if !validation.IsValidURL(form.Config["url"]) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "Invalid url")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -154,6 +159,41 @@ func pullHook(events []string, event string) bool {
|
|||
return util.SliceContainsString(events, event, true) || util.SliceContainsString(events, string(webhook_module.HookEventPullRequest), true)
|
||||
}
|
||||
|
||||
func updateHookEvents(events []string) webhook_module.HookEvents {
|
||||
if len(events) == 0 {
|
||||
events = []string{"push"}
|
||||
}
|
||||
hookEvents := make(webhook_module.HookEvents)
|
||||
hookEvents[webhook_module.HookEventCreate] = util.SliceContainsString(events, string(webhook_module.HookEventCreate), true)
|
||||
hookEvents[webhook_module.HookEventPush] = util.SliceContainsString(events, string(webhook_module.HookEventPush), true)
|
||||
hookEvents[webhook_module.HookEventDelete] = util.SliceContainsString(events, string(webhook_module.HookEventDelete), true)
|
||||
hookEvents[webhook_module.HookEventFork] = util.SliceContainsString(events, string(webhook_module.HookEventFork), true)
|
||||
hookEvents[webhook_module.HookEventRepository] = util.SliceContainsString(events, string(webhook_module.HookEventRepository), true)
|
||||
hookEvents[webhook_module.HookEventWiki] = util.SliceContainsString(events, string(webhook_module.HookEventWiki), true)
|
||||
hookEvents[webhook_module.HookEventRelease] = util.SliceContainsString(events, string(webhook_module.HookEventRelease), true)
|
||||
hookEvents[webhook_module.HookEventPackage] = util.SliceContainsString(events, string(webhook_module.HookEventPackage), true)
|
||||
hookEvents[webhook_module.HookEventStatus] = util.SliceContainsString(events, string(webhook_module.HookEventStatus), true)
|
||||
hookEvents[webhook_module.HookEventWorkflowJob] = util.SliceContainsString(events, string(webhook_module.HookEventWorkflowJob), true)
|
||||
|
||||
// Issues
|
||||
hookEvents[webhook_module.HookEventIssues] = issuesHook(events, "issues_only")
|
||||
hookEvents[webhook_module.HookEventIssueAssign] = issuesHook(events, string(webhook_module.HookEventIssueAssign))
|
||||
hookEvents[webhook_module.HookEventIssueLabel] = issuesHook(events, string(webhook_module.HookEventIssueLabel))
|
||||
hookEvents[webhook_module.HookEventIssueMilestone] = issuesHook(events, string(webhook_module.HookEventIssueMilestone))
|
||||
hookEvents[webhook_module.HookEventIssueComment] = issuesHook(events, string(webhook_module.HookEventIssueComment))
|
||||
|
||||
// Pull requests
|
||||
hookEvents[webhook_module.HookEventPullRequest] = pullHook(events, "pull_request_only")
|
||||
hookEvents[webhook_module.HookEventPullRequestAssign] = pullHook(events, string(webhook_module.HookEventPullRequestAssign))
|
||||
hookEvents[webhook_module.HookEventPullRequestLabel] = pullHook(events, string(webhook_module.HookEventPullRequestLabel))
|
||||
hookEvents[webhook_module.HookEventPullRequestMilestone] = pullHook(events, string(webhook_module.HookEventPullRequestMilestone))
|
||||
hookEvents[webhook_module.HookEventPullRequestComment] = pullHook(events, string(webhook_module.HookEventPullRequestComment))
|
||||
hookEvents[webhook_module.HookEventPullRequestReview] = pullHook(events, "pull_request_review")
|
||||
hookEvents[webhook_module.HookEventPullRequestReviewRequest] = pullHook(events, string(webhook_module.HookEventPullRequestReviewRequest))
|
||||
hookEvents[webhook_module.HookEventPullRequestSync] = pullHook(events, string(webhook_module.HookEventPullRequestSync))
|
||||
return hookEvents
|
||||
}
|
||||
|
||||
// addHook add the hook specified by `form`, `ownerID` and `repoID`. If there is
|
||||
// an error, write to `ctx` accordingly. Return (webhook, ok)
|
||||
func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoID int64) (*webhook.Webhook, bool) {
|
||||
|
@ -162,9 +202,6 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
|
|||
return nil, false
|
||||
}
|
||||
|
||||
if len(form.Events) == 0 {
|
||||
form.Events = []string{"push"}
|
||||
}
|
||||
if form.Config["is_system_webhook"] != "" {
|
||||
sw, err := strconv.ParseBool(form.Config["is_system_webhook"])
|
||||
if err != nil {
|
||||
|
@ -183,31 +220,7 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
|
|||
IsSystemWebhook: isSystemWebhook,
|
||||
HookEvent: &webhook_module.HookEvent{
|
||||
ChooseEvents: true,
|
||||
HookEvents: webhook_module.HookEvents{
|
||||
webhook_module.HookEventCreate: util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true),
|
||||
webhook_module.HookEventDelete: util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true),
|
||||
webhook_module.HookEventFork: util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true),
|
||||
webhook_module.HookEventIssues: issuesHook(form.Events, "issues_only"),
|
||||
webhook_module.HookEventIssueAssign: issuesHook(form.Events, string(webhook_module.HookEventIssueAssign)),
|
||||
webhook_module.HookEventIssueLabel: issuesHook(form.Events, string(webhook_module.HookEventIssueLabel)),
|
||||
webhook_module.HookEventIssueMilestone: issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone)),
|
||||
webhook_module.HookEventIssueComment: issuesHook(form.Events, string(webhook_module.HookEventIssueComment)),
|
||||
webhook_module.HookEventPush: util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true),
|
||||
webhook_module.HookEventPullRequest: pullHook(form.Events, "pull_request_only"),
|
||||
webhook_module.HookEventPullRequestAssign: pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign)),
|
||||
webhook_module.HookEventPullRequestLabel: pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel)),
|
||||
webhook_module.HookEventPullRequestMilestone: pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone)),
|
||||
webhook_module.HookEventPullRequestComment: pullHook(form.Events, string(webhook_module.HookEventPullRequestComment)),
|
||||
webhook_module.HookEventPullRequestReview: pullHook(form.Events, "pull_request_review"),
|
||||
webhook_module.HookEventPullRequestReviewRequest: pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest)),
|
||||
webhook_module.HookEventPullRequestSync: pullHook(form.Events, string(webhook_module.HookEventPullRequestSync)),
|
||||
webhook_module.HookEventWiki: util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true),
|
||||
webhook_module.HookEventRepository: util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true),
|
||||
webhook_module.HookEventRelease: util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true),
|
||||
webhook_module.HookEventPackage: util.SliceContainsString(form.Events, string(webhook_module.HookEventPackage), true),
|
||||
webhook_module.HookEventStatus: util.SliceContainsString(form.Events, string(webhook_module.HookEventStatus), true),
|
||||
webhook_module.HookEventWorkflowJob: util.SliceContainsString(form.Events, string(webhook_module.HookEventWorkflowJob), true),
|
||||
},
|
||||
HookEvents: updateHookEvents(form.Events),
|
||||
BranchFilter: form.BranchFilter,
|
||||
},
|
||||
IsActive: form.Active,
|
||||
|
@ -324,6 +337,10 @@ func EditRepoHook(ctx *context.APIContext, form *api.EditHookOption, hookID int6
|
|||
func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webhook) bool {
|
||||
if form.Config != nil {
|
||||
if url, ok := form.Config["url"]; ok {
|
||||
if !validation.IsValidURL(url) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "Invalid url")
|
||||
return false
|
||||
}
|
||||
w.URL = url
|
||||
}
|
||||
if ct, ok := form.Config["content_type"]; ok {
|
||||
|
@ -352,19 +369,10 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
|
|||
}
|
||||
|
||||
// Update events
|
||||
if len(form.Events) == 0 {
|
||||
form.Events = []string{"push"}
|
||||
}
|
||||
w.HookEvents = updateHookEvents(form.Events)
|
||||
w.PushOnly = false
|
||||
w.SendEverything = false
|
||||
w.ChooseEvents = true
|
||||
w.HookEvents[webhook_module.HookEventCreate] = util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true)
|
||||
w.HookEvents[webhook_module.HookEventPush] = util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true)
|
||||
w.HookEvents[webhook_module.HookEventDelete] = util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true)
|
||||
w.HookEvents[webhook_module.HookEventFork] = util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true)
|
||||
w.HookEvents[webhook_module.HookEventRepository] = util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true)
|
||||
w.HookEvents[webhook_module.HookEventWiki] = util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true)
|
||||
w.HookEvents[webhook_module.HookEventRelease] = util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true)
|
||||
w.BranchFilter = form.BranchFilter
|
||||
|
||||
err := w.SetHeaderAuthorization(form.AuthorizationHeader)
|
||||
|
@ -373,23 +381,6 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
|
|||
return false
|
||||
}
|
||||
|
||||
// Issues
|
||||
w.HookEvents[webhook_module.HookEventIssues] = issuesHook(form.Events, "issues_only")
|
||||
w.HookEvents[webhook_module.HookEventIssueAssign] = issuesHook(form.Events, string(webhook_module.HookEventIssueAssign))
|
||||
w.HookEvents[webhook_module.HookEventIssueLabel] = issuesHook(form.Events, string(webhook_module.HookEventIssueLabel))
|
||||
w.HookEvents[webhook_module.HookEventIssueMilestone] = issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone))
|
||||
w.HookEvents[webhook_module.HookEventIssueComment] = issuesHook(form.Events, string(webhook_module.HookEventIssueComment))
|
||||
|
||||
// Pull requests
|
||||
w.HookEvents[webhook_module.HookEventPullRequest] = pullHook(form.Events, "pull_request_only")
|
||||
w.HookEvents[webhook_module.HookEventPullRequestAssign] = pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign))
|
||||
w.HookEvents[webhook_module.HookEventPullRequestLabel] = pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel))
|
||||
w.HookEvents[webhook_module.HookEventPullRequestMilestone] = pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone))
|
||||
w.HookEvents[webhook_module.HookEventPullRequestComment] = pullHook(form.Events, string(webhook_module.HookEventPullRequestComment))
|
||||
w.HookEvents[webhook_module.HookEventPullRequestReview] = pullHook(form.Events, "pull_request_review")
|
||||
w.HookEvents[webhook_module.HookEventPullRequestReviewRequest] = pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest))
|
||||
w.HookEvents[webhook_module.HookEventPullRequestSync] = pullHook(form.Events, string(webhook_module.HookEventPullRequestSync))
|
||||
|
||||
if err := w.UpdateEvent(); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return false
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/services/contexttest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTestHookValidation(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
|
||||
t.Run("Test Validation", func(t *testing.T) {
|
||||
ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/hooks")
|
||||
contexttest.LoadRepo(t, ctx, 1)
|
||||
contexttest.LoadRepoCommit(t, ctx)
|
||||
contexttest.LoadUser(t, ctx, 2)
|
||||
|
||||
checkCreateHookOption(ctx, &structs.CreateHookOption{
|
||||
Type: "gitea",
|
||||
Config: map[string]string{
|
||||
"content_type": "json",
|
||||
"url": "https://example.com/webhook",
|
||||
},
|
||||
})
|
||||
assert.Equal(t, 0, ctx.Resp.WrittenStatus()) // not written yet
|
||||
})
|
||||
|
||||
t.Run("Test Validation with invalid URL", func(t *testing.T) {
|
||||
ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/hooks")
|
||||
contexttest.LoadRepo(t, ctx, 1)
|
||||
contexttest.LoadRepoCommit(t, ctx)
|
||||
contexttest.LoadUser(t, ctx, 2)
|
||||
|
||||
checkCreateHookOption(ctx, &structs.CreateHookOption{
|
||||
Type: "gitea",
|
||||
Config: map[string]string{
|
||||
"content_type": "json",
|
||||
"url": "example.com/webhook",
|
||||
},
|
||||
})
|
||||
assert.Equal(t, http.StatusUnprocessableEntity, ctx.Resp.WrittenStatus())
|
||||
})
|
||||
|
||||
t.Run("Test Validation with invalid webhook type", func(t *testing.T) {
|
||||
ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/hooks")
|
||||
contexttest.LoadRepo(t, ctx, 1)
|
||||
contexttest.LoadRepoCommit(t, ctx)
|
||||
contexttest.LoadUser(t, ctx, 2)
|
||||
|
||||
checkCreateHookOption(ctx, &structs.CreateHookOption{
|
||||
Type: "unknown",
|
||||
Config: map[string]string{
|
||||
"content_type": "json",
|
||||
"url": "example.com/webhook",
|
||||
},
|
||||
})
|
||||
assert.Equal(t, http.StatusUnprocessableEntity, ctx.Resp.WrittenStatus())
|
||||
})
|
||||
|
||||
t.Run("Test Validation with empty content type", func(t *testing.T) {
|
||||
ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/hooks")
|
||||
contexttest.LoadRepo(t, ctx, 1)
|
||||
contexttest.LoadRepoCommit(t, ctx)
|
||||
contexttest.LoadUser(t, ctx, 2)
|
||||
|
||||
checkCreateHookOption(ctx, &structs.CreateHookOption{
|
||||
Type: "unknown",
|
||||
Config: map[string]string{
|
||||
"url": "https://example.com/webhook",
|
||||
},
|
||||
})
|
||||
assert.Equal(t, http.StatusUnprocessableEntity, ctx.Resp.WrittenStatus())
|
||||
})
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
webhook_service "code.gitea.io/gitea/services/webhook"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m, &unittest.TestOptions{
|
||||
SetUp: func() error {
|
||||
setting.LoadQueueSettings()
|
||||
return webhook_service.Init()
|
||||
},
|
||||
})
|
||||
}
|
|
@ -220,7 +220,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
|
|||
}
|
||||
|
||||
if len(cols) > 0 {
|
||||
if err := repo_model.UpdateRepositoryCols(ctx, repo, cols...); err != nil {
|
||||
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, cols...); err != nil {
|
||||
log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
|
||||
Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
|
||||
|
|
|
@ -15,7 +15,7 @@ import (
|
|||
|
||||
// ShowBranchFeed shows tags and/or releases on the repo as RSS / Atom feed
|
||||
func ShowBranchFeed(ctx *context.Context, repo *repo.Repository, formatType string) {
|
||||
commits, err := ctx.Repo.Commit.CommitsByRange(0, 10, "")
|
||||
commits, err := ctx.Repo.Commit.CommitsByRange(0, 10, "", "", "")
|
||||
if err != nil {
|
||||
ctx.ServerError("ShowBranchFeed", err)
|
||||
return
|
||||
|
|
|
@ -317,6 +317,8 @@ func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
|
|||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0
|
||||
|
||||
ctx.Data["AllowDeleteWorkflowRuns"] = ctx.Repo.CanWrite(unit.TypeActions)
|
||||
}
|
||||
|
||||
// loadIsRefDeleted loads the IsRefDeleted field for each run in the list.
|
||||
|
|
|
@ -577,6 +577,33 @@ func Approve(ctx *context_module.Context) {
|
|||
ctx.JSON(http.StatusOK, struct{}{})
|
||||
}
|
||||
|
||||
func Delete(ctx *context_module.Context) {
|
||||
runIndex := getRunIndex(ctx)
|
||||
repoID := ctx.Repo.Repository.ID
|
||||
|
||||
run, err := actions_model.GetRunByIndex(ctx, repoID, runIndex)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.JSONErrorNotFound()
|
||||
return
|
||||
}
|
||||
ctx.ServerError("GetRunByIndex", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !run.Status.IsDone() {
|
||||
ctx.JSONError(ctx.Tr("actions.runs.not_done"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := actions_service.DeleteRun(ctx, run); err != nil {
|
||||
ctx.ServerError("DeleteRun", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
// getRunJobs gets the jobs of runIndex, and returns jobs[jobIndex], jobs.
|
||||
// Any error will be written to the ctx.
|
||||
// It never returns a nil job of an empty jobs, if the jobIndex is out of range, it will be treated as 0.
|
||||
|
@ -584,20 +611,20 @@ func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions
|
|||
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.HTTPError(http.StatusNotFound, err.Error())
|
||||
ctx.NotFound(nil)
|
||||
return nil, nil
|
||||
}
|
||||
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
||||
ctx.ServerError("GetRunByIndex", err)
|
||||
return nil, nil
|
||||
}
|
||||
run.Repo = ctx.Repo.Repository
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
||||
ctx.ServerError("GetRunJobsByRunID", err)
|
||||
return nil, nil
|
||||
}
|
||||
if len(jobs) == 0 {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
ctx.NotFound(nil)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -264,7 +264,7 @@ func MergeUpstream(ctx *context.Context) {
|
|||
_, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.JSONError(ctx.Tr("error.not_found"))
|
||||
ctx.JSONErrorNotFound()
|
||||
return
|
||||
} else if pull_service.IsErrMergeConflicts(err) {
|
||||
ctx.JSONError(ctx.Tr("repo.pulls.merge_conflict"))
|
||||
|
|
|
@ -78,7 +78,7 @@ func Commits(ctx *context.Context) {
|
|||
}
|
||||
|
||||
// Both `git log branchName` and `git log commitId` work.
|
||||
commits, err := ctx.Repo.Commit.CommitsByRange(page, pageSize, "")
|
||||
commits, err := ctx.Repo.Commit.CommitsByRange(page, pageSize, "", "", "")
|
||||
if err != nil {
|
||||
ctx.ServerError("CommitsByRange", err)
|
||||
return
|
||||
|
|
|
@ -376,12 +376,6 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
|
|||
}
|
||||
}
|
||||
|
||||
if ctx.Repo.Repository.IsEmpty {
|
||||
if isEmpty, err := ctx.Repo.GitRepo.IsEmpty(); err == nil && !isEmpty {
|
||||
_ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty")
|
||||
}
|
||||
}
|
||||
|
||||
redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath)
|
||||
}
|
||||
|
||||
|
@ -790,7 +784,7 @@ func UploadFilePost(ctx *context.Context) {
|
|||
|
||||
if ctx.Repo.Repository.IsEmpty {
|
||||
if isEmpty, err := ctx.Repo.GitRepo.IsEmpty(); err == nil && !isEmpty {
|
||||
_ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty")
|
||||
_ = repo_model.UpdateRepositoryColsWithAutoTime(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -418,6 +418,16 @@ func UpdateIssueMilestone(ctx *context.Context) {
|
|||
continue
|
||||
}
|
||||
issue.MilestoneID = milestoneID
|
||||
if milestoneID > 0 {
|
||||
var err error
|
||||
issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetMilestoneByRepoID", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
issue.Milestone = nil
|
||||
}
|
||||
if err := issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
|
||||
ctx.ServerError("ChangeMilestoneAssign", err)
|
||||
return
|
||||
|
|
|
@ -239,23 +239,30 @@ func UpdateCommentContent(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
oldContent := comment.Content
|
||||
newContent := ctx.FormString("content")
|
||||
contentVersion := ctx.FormInt("content_version")
|
||||
|
||||
// allow to save empty content
|
||||
comment.Content = newContent
|
||||
if err = issue_service.UpdateComment(ctx, comment, contentVersion, ctx.Doer, oldContent); err != nil {
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
|
||||
} else if errors.Is(err, issues_model.ErrCommentAlreadyChanged) {
|
||||
ctx.JSONError(ctx.Tr("repo.comments.edit.already_changed"))
|
||||
} else {
|
||||
ctx.ServerError("UpdateComment", err)
|
||||
}
|
||||
if contentVersion != comment.ContentVersion {
|
||||
ctx.JSONError(ctx.Tr("repo.comments.edit.already_changed"))
|
||||
return
|
||||
}
|
||||
|
||||
if newContent != comment.Content {
|
||||
// allow to save empty content
|
||||
oldContent := comment.Content
|
||||
comment.Content = newContent
|
||||
|
||||
if err = issue_service.UpdateComment(ctx, comment, contentVersion, ctx.Doer, oldContent); err != nil {
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
|
||||
} else if errors.Is(err, issues_model.ErrCommentAlreadyChanged) {
|
||||
ctx.JSONError(ctx.Tr("repo.comments.edit.already_changed"))
|
||||
} else {
|
||||
ctx.ServerError("UpdateComment", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := comment.LoadAttachments(ctx); err != nil {
|
||||
ctx.ServerError("LoadAttachments", err)
|
||||
return
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
giturl "code.gitea.io/gitea/modules/git/url"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
|
@ -261,6 +262,10 @@ func updateContextRepoEmptyAndStatus(ctx *context.Context, empty bool, status re
|
|||
|
||||
func handleRepoEmptyOrBroken(ctx *context.Context) {
|
||||
showEmpty := true
|
||||
if ctx.Repo.GitRepo == nil {
|
||||
// in case the repo really exists and works, but the status was incorrectly marked as "broken", we need to open and check it again
|
||||
ctx.Repo.GitRepo, _ = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository)
|
||||
}
|
||||
if ctx.Repo.GitRepo != nil {
|
||||
reallyEmpty, err := ctx.Repo.GitRepo.IsEmpty()
|
||||
if err != nil {
|
||||
|
@ -396,10 +401,8 @@ func Home(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
prepareHomeTreeSideBarSwitch(ctx)
|
||||
|
||||
title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name
|
||||
if len(ctx.Repo.Repository.Description) > 0 {
|
||||
if ctx.Repo.Repository.Description != "" {
|
||||
title += ": " + ctx.Repo.Repository.Description
|
||||
}
|
||||
ctx.Data["Title"] = title
|
||||
|
@ -412,6 +415,8 @@ func Home(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
prepareHomeTreeSideBarSwitch(ctx)
|
||||
|
||||
// get the current git entry which doer user is currently looking at.
|
||||
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
|
||||
if err != nil {
|
||||
|
|
|
@ -109,7 +109,7 @@ func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, err
|
|||
return wikiGitRepo, nil, errBranch
|
||||
}
|
||||
// update the default branch in the database
|
||||
errDb := repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, DefaultWikiBranch: gitRepoDefaultBranch}, "default_wiki_branch")
|
||||
errDb := repo_model.UpdateRepositoryColsNoAutoTime(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, DefaultWikiBranch: gitRepoDefaultBranch}, "default_wiki_branch")
|
||||
if errDb != nil {
|
||||
return wikiGitRepo, nil, errDb
|
||||
}
|
||||
|
|
|
@ -245,7 +245,7 @@ func TestDefaultWikiBranch(t *testing.T) {
|
|||
assert.NoError(t, wiki_service.ChangeDefaultWikiBranch(db.DefaultContext, repoWithNoWiki, "main"))
|
||||
|
||||
// repo with wiki
|
||||
assert.NoError(t, repo_model.UpdateRepositoryCols(db.DefaultContext, &repo_model.Repository{ID: 1, DefaultWikiBranch: "wrong-branch"}))
|
||||
assert.NoError(t, repo_model.UpdateRepositoryColsNoAutoTime(db.DefaultContext, &repo_model.Repository{ID: 1, DefaultWikiBranch: "wrong-branch"}))
|
||||
|
||||
ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki")
|
||||
ctx.SetPathParam("*", "Home")
|
||||
|
|
|
@ -1447,6 +1447,7 @@ func registerWebRoutes(m *web.Router) {
|
|||
})
|
||||
m.Post("/cancel", reqRepoActionsWriter, actions.Cancel)
|
||||
m.Post("/approve", reqRepoActionsWriter, actions.Approve)
|
||||
m.Post("/delete", reqRepoActionsWriter, actions.Delete)
|
||||
m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView)
|
||||
m.Delete("/artifacts/{artifact_name}", reqRepoActionsWriter, actions.ArtifactsDeleteView)
|
||||
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
|
||||
|
|
|
@ -5,12 +5,14 @@ package actions
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
actions_module "code.gitea.io/gitea/modules/actions"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
|
@ -27,7 +29,7 @@ func Cleanup(ctx context.Context) error {
|
|||
}
|
||||
|
||||
// clean up old logs
|
||||
if err := CleanupLogs(ctx); err != nil {
|
||||
if err := CleanupExpiredLogs(ctx); err != nil {
|
||||
return fmt.Errorf("cleanup logs: %w", err)
|
||||
}
|
||||
|
||||
|
@ -98,8 +100,15 @@ func cleanNeedDeleteArtifacts(taskCtx context.Context) error {
|
|||
|
||||
const deleteLogBatchSize = 100
|
||||
|
||||
// CleanupLogs removes logs which are older than the configured retention time
|
||||
func CleanupLogs(ctx context.Context) error {
|
||||
func removeTaskLog(ctx context.Context, task *actions_model.ActionTask) {
|
||||
if err := actions_module.RemoveLogs(ctx, task.LogInStorage, task.LogFilename); err != nil {
|
||||
log.Error("Failed to remove log %s (in storage %v) of task %v: %v", task.LogFilename, task.LogInStorage, task.ID, err)
|
||||
// do not return error here, go on
|
||||
}
|
||||
}
|
||||
|
||||
// CleanupExpiredLogs removes logs which are older than the configured retention time
|
||||
func CleanupExpiredLogs(ctx context.Context) error {
|
||||
olderThan := timeutil.TimeStampNow().AddDuration(-time.Duration(setting.Actions.LogRetentionDays) * 24 * time.Hour)
|
||||
|
||||
count := 0
|
||||
|
@ -109,10 +118,7 @@ func CleanupLogs(ctx context.Context) error {
|
|||
return fmt.Errorf("find old tasks: %w", err)
|
||||
}
|
||||
for _, task := range tasks {
|
||||
if err := actions_module.RemoveLogs(ctx, task.LogInStorage, task.LogFilename); err != nil {
|
||||
log.Error("Failed to remove log %s (in storage %v) of task %v: %v", task.LogFilename, task.LogInStorage, task.ID, err)
|
||||
// do not return error here, go on
|
||||
}
|
||||
removeTaskLog(ctx, task)
|
||||
task.LogIndexes = nil // clear log indexes since it's a heavy field
|
||||
task.LogExpired = true
|
||||
if err := actions_model.UpdateTask(ctx, task, "log_indexes", "log_expired"); err != nil {
|
||||
|
@ -148,3 +154,107 @@ func CleanupEphemeralRunners(ctx context.Context) error {
|
|||
log.Info("Removed %d runners", affected)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupEphemeralRunnersByPickedTaskOfRepo removes all ephemeral runners that have active/finished tasks on the given repository
|
||||
func CleanupEphemeralRunnersByPickedTaskOfRepo(ctx context.Context, repoID int64) error {
|
||||
subQuery := builder.Select("`action_runner`.id").
|
||||
From(builder.Select("*").From("`action_runner`"), "`action_runner`"). // mysql needs this redundant subquery
|
||||
Join("INNER", "`action_task`", "`action_task`.`runner_id` = `action_runner`.`id`").
|
||||
Where(builder.And(builder.Eq{"`action_runner`.`ephemeral`": true}, builder.Eq{"`action_task`.`repo_id`": repoID}))
|
||||
b := builder.Delete(builder.In("id", subQuery)).From("`action_runner`")
|
||||
res, err := db.GetEngine(ctx).Exec(b)
|
||||
if err != nil {
|
||||
return fmt.Errorf("find runners: %w", err)
|
||||
}
|
||||
affected, _ := res.RowsAffected()
|
||||
log.Info("Removed %d runners", affected)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteRun deletes workflow run, including all logs and artifacts.
|
||||
func DeleteRun(ctx context.Context, run *actions_model.ActionRun) error {
|
||||
if !run.Status.IsDone() {
|
||||
return errors.New("run is not done")
|
||||
}
|
||||
|
||||
repoID := run.RepoID
|
||||
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
jobIDs := container.FilterSlice(jobs, func(j *actions_model.ActionRunJob) (int64, bool) {
|
||||
return j.ID, true
|
||||
})
|
||||
tasks := make(actions_model.TaskList, 0)
|
||||
if len(jobIDs) > 0 {
|
||||
if err := db.GetEngine(ctx).Where("repo_id = ?", repoID).In("job_id", jobIDs).Find(&tasks); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
artifacts, err := db.Find[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{
|
||||
RepoID: repoID,
|
||||
RunID: run.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var recordsToDelete []any
|
||||
|
||||
recordsToDelete = append(recordsToDelete, &actions_model.ActionRun{
|
||||
RepoID: repoID,
|
||||
ID: run.ID,
|
||||
})
|
||||
recordsToDelete = append(recordsToDelete, &actions_model.ActionRunJob{
|
||||
RepoID: repoID,
|
||||
RunID: run.ID,
|
||||
})
|
||||
for _, tas := range tasks {
|
||||
recordsToDelete = append(recordsToDelete, &actions_model.ActionTask{
|
||||
RepoID: repoID,
|
||||
ID: tas.ID,
|
||||
})
|
||||
recordsToDelete = append(recordsToDelete, &actions_model.ActionTaskStep{
|
||||
RepoID: repoID,
|
||||
TaskID: tas.ID,
|
||||
})
|
||||
recordsToDelete = append(recordsToDelete, &actions_model.ActionTaskOutput{
|
||||
TaskID: tas.ID,
|
||||
})
|
||||
}
|
||||
recordsToDelete = append(recordsToDelete, &actions_model.ActionArtifact{
|
||||
RepoID: repoID,
|
||||
RunID: run.ID,
|
||||
})
|
||||
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
// TODO: Deleting task records could break current ephemeral runner implementation. This is a temporary workaround suggested by ChristopherHX.
|
||||
// Since you delete potentially the only task an ephemeral act_runner has ever run, please delete the affected runners first.
|
||||
// one of
|
||||
// call cleanup ephemeral runners first
|
||||
// delete affected ephemeral act_runners
|
||||
// I would make ephemeral runners fully delete directly before formally finishing the task
|
||||
//
|
||||
// See also: https://github.com/go-gitea/gitea/pull/34337#issuecomment-2862222788
|
||||
if err := CleanupEphemeralRunners(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return db.DeleteBeans(ctx, recordsToDelete...)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete files on storage
|
||||
for _, tas := range tasks {
|
||||
removeTaskLog(ctx, tas)
|
||||
}
|
||||
for _, art := range artifacts {
|
||||
if err := storage.ActionsArtifacts.Delete(art.StoragePath); err != nil {
|
||||
log.Error("remove artifact file %q: %v", art.StoragePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -15,11 +15,15 @@ import (
|
|||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/nektos/act/pkg/model"
|
||||
)
|
||||
|
||||
type GiteaContext map[string]any
|
||||
|
||||
// GenerateGiteaContext generate the gitea context without token and gitea_runtime_token
|
||||
// job can be nil when generating a context for parsing workflow-level expressions
|
||||
func GenerateGiteaContext(run *actions_model.ActionRun, job *actions_model.ActionRunJob) map[string]any {
|
||||
func GenerateGiteaContext(run *actions_model.ActionRun, job *actions_model.ActionRunJob) GiteaContext {
|
||||
event := map[string]any{}
|
||||
_ = json.Unmarshal([]byte(run.EventPayload), &event)
|
||||
|
||||
|
@ -42,7 +46,7 @@ func GenerateGiteaContext(run *actions_model.ActionRun, job *actions_model.Actio
|
|||
|
||||
refName := git.RefName(ref)
|
||||
|
||||
gitContext := map[string]any{
|
||||
gitContext := GiteaContext{
|
||||
// standard contexts, see https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
|
||||
"action": "", // string, The name of the action currently running, or the id of a step. GitHub removes special characters, and uses the name __run when the current step runs a script without an id. If you use the same action more than once in the same job, the name will include a suffix with the sequence number with underscore before it. For example, the first script you run will have the name __run, and the second script will be named __run_2. Similarly, the second invocation of actions/checkout will be actionscheckout2.
|
||||
"action_path": "", // string, The path where an action is located. This property is only supported in composite actions. You can use this path to access files located in the same repository as the action.
|
||||
|
@ -160,3 +164,37 @@ func mergeTwoOutputs(o1, o2 map[string]string) map[string]string {
|
|||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (g *GiteaContext) ToGitHubContext() *model.GithubContext {
|
||||
return &model.GithubContext{
|
||||
Event: (*g)["event"].(map[string]any),
|
||||
EventPath: (*g)["event_path"].(string),
|
||||
Workflow: (*g)["workflow"].(string),
|
||||
RunID: (*g)["run_id"].(string),
|
||||
RunNumber: (*g)["run_number"].(string),
|
||||
Actor: (*g)["actor"].(string),
|
||||
Repository: (*g)["repository"].(string),
|
||||
EventName: (*g)["event_name"].(string),
|
||||
Sha: (*g)["sha"].(string),
|
||||
Ref: (*g)["ref"].(string),
|
||||
RefName: (*g)["ref_name"].(string),
|
||||
RefType: (*g)["ref_type"].(string),
|
||||
HeadRef: (*g)["head_ref"].(string),
|
||||
BaseRef: (*g)["base_ref"].(string),
|
||||
Token: "", // deliberately omitted for security
|
||||
Workspace: (*g)["workspace"].(string),
|
||||
Action: (*g)["action"].(string),
|
||||
ActionPath: (*g)["action_path"].(string),
|
||||
ActionRef: (*g)["action_ref"].(string),
|
||||
ActionRepository: (*g)["action_repository"].(string),
|
||||
Job: (*g)["job"].(string),
|
||||
JobName: "", // not present in GiteaContext
|
||||
RepositoryOwner: (*g)["repository_owner"].(string),
|
||||
RetentionDays: (*g)["retention_days"].(string),
|
||||
RunnerPerflog: "", // not present in GiteaContext
|
||||
RunnerTrackingID: "", // not present in GiteaContext
|
||||
ServerURL: (*g)["server_url"].(string),
|
||||
APIURL: (*g)["api_url"].(string),
|
||||
GraphQLURL: (*g)["graphql_url"].(string),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -302,9 +302,11 @@ func handleWorkflows(
|
|||
run := &actions_model.ActionRun{
|
||||
Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0],
|
||||
RepoID: input.Repo.ID,
|
||||
Repo: input.Repo,
|
||||
OwnerID: input.Repo.OwnerID,
|
||||
WorkflowID: dwf.EntryName,
|
||||
TriggerUserID: input.Doer.ID,
|
||||
TriggerUser: input.Doer,
|
||||
Ref: ref,
|
||||
CommitSHA: commit.ID.String(),
|
||||
IsForkPullRequest: isForkPullRequest,
|
||||
|
@ -333,12 +335,18 @@ func handleWorkflows(
|
|||
continue
|
||||
}
|
||||
|
||||
jobs, err := jobparser.Parse(dwf.Content, jobparser.WithVars(vars))
|
||||
giteaCtx := GenerateGiteaContext(run, nil)
|
||||
|
||||
jobs, err := jobparser.Parse(dwf.Content, jobparser.WithVars(vars), jobparser.WithGitContext(giteaCtx.ToGitHubContext()))
|
||||
if err != nil {
|
||||
log.Error("jobparser.Parse: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(jobs) > 0 && jobs[0].RunName != "" {
|
||||
run.Title = jobs[0].RunName
|
||||
}
|
||||
|
||||
// cancel running jobs if the event is push or pull_request_sync
|
||||
if run.Event == webhook_module.HookEventPush ||
|
||||
run.Event == webhook_module.HookEventPullRequestSync {
|
||||
|
@ -508,9 +516,11 @@ func handleSchedules(
|
|||
run := &actions_model.ActionSchedule{
|
||||
Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0],
|
||||
RepoID: input.Repo.ID,
|
||||
Repo: input.Repo,
|
||||
OwnerID: input.Repo.OwnerID,
|
||||
WorkflowID: dwf.EntryName,
|
||||
TriggerUserID: user_model.ActionsUserID,
|
||||
TriggerUser: user_model.NewActionsUser(),
|
||||
Ref: ref,
|
||||
CommitSHA: commit.ID.String(),
|
||||
Event: input.Event,
|
||||
|
@ -518,6 +528,25 @@ func handleSchedules(
|
|||
Specs: schedules,
|
||||
Content: dwf.Content,
|
||||
}
|
||||
|
||||
vars, err := actions_model.GetVariablesOfRun(ctx, run.ToActionRun())
|
||||
if err != nil {
|
||||
log.Error("GetVariablesOfRun: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
giteaCtx := GenerateGiteaContext(run.ToActionRun(), nil)
|
||||
|
||||
jobs, err := jobparser.Parse(dwf.Content, jobparser.WithVars(vars), jobparser.WithGitContext(giteaCtx.ToGitHubContext()))
|
||||
if err != nil {
|
||||
log.Error("jobparser.Parse: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(jobs) > 0 && jobs[0].RunName != "" {
|
||||
run.Title = jobs[0].RunName
|
||||
}
|
||||
|
||||
crons = append(crons, run)
|
||||
}
|
||||
|
||||
|
|
|
@ -192,22 +192,55 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
|
|||
|
||||
// find workflow from commit
|
||||
var workflows []*jobparser.SingleWorkflow
|
||||
for _, entry := range entries {
|
||||
if entry.Name() != workflowID {
|
||||
var entry *git.TreeEntry
|
||||
|
||||
run := &actions_model.ActionRun{
|
||||
Title: strings.SplitN(runTargetCommit.CommitMessage, "\n", 2)[0],
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
OwnerID: repo.OwnerID,
|
||||
WorkflowID: workflowID,
|
||||
TriggerUserID: doer.ID,
|
||||
TriggerUser: doer,
|
||||
Ref: string(refName),
|
||||
CommitSHA: runTargetCommit.ID.String(),
|
||||
IsForkPullRequest: false,
|
||||
Event: "workflow_dispatch",
|
||||
TriggerEvent: "workflow_dispatch",
|
||||
Status: actions_model.StatusWaiting,
|
||||
}
|
||||
|
||||
for _, e := range entries {
|
||||
if e.Name() != workflowID {
|
||||
continue
|
||||
}
|
||||
|
||||
content, err := actions.GetContentFromEntry(entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
workflows, err = jobparser.Parse(content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entry = e
|
||||
break
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
return util.ErrorWrapLocale(
|
||||
util.NewNotExistErrorf("workflow %q doesn't exist", workflowID),
|
||||
"actions.workflow.not_found", workflowID,
|
||||
)
|
||||
}
|
||||
|
||||
content, err := actions.GetContentFromEntry(entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
giteaCtx := GenerateGiteaContext(run, nil)
|
||||
|
||||
workflows, err = jobparser.Parse(content, jobparser.WithGitContext(giteaCtx.ToGitHubContext()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(workflows) > 0 && workflows[0].RunName != "" {
|
||||
run.Title = workflows[0].RunName
|
||||
}
|
||||
|
||||
if len(workflows) == 0 {
|
||||
return util.ErrorWrapLocale(
|
||||
util.NewNotExistErrorf("workflow %q doesn't exist", workflowID),
|
||||
|
@ -236,25 +269,12 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
|
|||
Inputs: inputsWithDefaults,
|
||||
Sender: convert.ToUserWithAccessMode(ctx, doer, perm.AccessModeNone),
|
||||
}
|
||||
|
||||
var eventPayload []byte
|
||||
if eventPayload, err = workflowDispatchPayload.JSONPayload(); err != nil {
|
||||
return fmt.Errorf("JSONPayload: %w", err)
|
||||
}
|
||||
|
||||
run := &actions_model.ActionRun{
|
||||
Title: strings.SplitN(runTargetCommit.CommitMessage, "\n", 2)[0],
|
||||
RepoID: repo.ID,
|
||||
OwnerID: repo.OwnerID,
|
||||
WorkflowID: workflowID,
|
||||
TriggerUserID: doer.ID,
|
||||
Ref: string(refName),
|
||||
CommitSHA: runTargetCommit.ID.String(),
|
||||
IsForkPullRequest: false,
|
||||
Event: "workflow_dispatch",
|
||||
TriggerEvent: "workflow_dispatch",
|
||||
EventPayload: string(eventPayload),
|
||||
Status: actions_model.StatusWaiting,
|
||||
}
|
||||
run.EventPayload = string(eventPayload)
|
||||
|
||||
// cancel running jobs of the same workflow
|
||||
if err := CancelPreviousJobs(
|
||||
|
@ -280,6 +300,5 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
|
|||
for _, job := range allJobs {
|
||||
notify_service.WorkflowJobStatusUpdate(ctx, repo, doer, job, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -245,7 +245,7 @@ func APIContexter() func(http.Handler) http.Handler {
|
|||
// APIErrorNotFound handles 404s for APIContext
|
||||
// String will replace message, errors will be added to a slice
|
||||
func (ctx *APIContext) APIErrorNotFound(objs ...any) {
|
||||
message := ctx.Locale.TrString("error.not_found")
|
||||
var message string
|
||||
var errs []string
|
||||
for _, obj := range objs {
|
||||
// Ignore nil
|
||||
|
@ -259,9 +259,8 @@ func (ctx *APIContext) APIErrorNotFound(objs ...any) {
|
|||
message = obj.(string)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusNotFound, map[string]any{
|
||||
"message": message,
|
||||
"message": util.IfZero(message, "not found"), // do not use locale in API
|
||||
"url": setting.API.SwaggerURL,
|
||||
"errors": errs,
|
||||
})
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
web_types "code.gitea.io/gitea/modules/web/types"
|
||||
|
@ -261,3 +262,11 @@ func (ctx *Context) JSONError(msg any) {
|
|||
panic(fmt.Sprintf("unsupported type: %T", msg))
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *Context) JSONErrorNotFound(optMsg ...string) {
|
||||
msg := util.OptionalArg(optMsg)
|
||||
if msg == "" {
|
||||
msg = ctx.Locale.TrString("error.not_found")
|
||||
}
|
||||
ctx.JSON(http.StatusNotFound, map[string]any{"errorMessage": msg, "renderFormat": "text"})
|
||||
}
|
||||
|
|
|
@ -795,8 +795,8 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) {
|
|||
return func(ctx *Context) {
|
||||
var err error
|
||||
refType := detectRefType
|
||||
if ctx.Repo.Repository.IsBeingCreated() {
|
||||
return // no git repo, so do nothing, users will see a "migrating" UI provided by "migrate/migrating.tmpl"
|
||||
if ctx.Repo.Repository.IsBeingCreated() || ctx.Repo.Repository.IsBroken() {
|
||||
return // no git repo, so do nothing, users will see a "migrating" UI provided by "migrate/migrating.tmpl", or empty repo guide
|
||||
}
|
||||
// Empty repository does not have reference information.
|
||||
if ctx.Repo.Repository.IsEmpty {
|
||||
|
|
|
@ -98,6 +98,8 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
|||
allowSquash := false
|
||||
allowFastForwardOnly := false
|
||||
allowRebaseUpdate := false
|
||||
allowManualMerge := true
|
||||
autodetectManualMerge := false
|
||||
defaultDeleteBranchAfterMerge := false
|
||||
defaultMergeStyle := repo_model.MergeStyleMerge
|
||||
defaultAllowMaintainerEdit := false
|
||||
|
@ -111,6 +113,8 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
|||
allowSquash = config.AllowSquash
|
||||
allowFastForwardOnly = config.AllowFastForwardOnly
|
||||
allowRebaseUpdate = config.AllowRebaseUpdate
|
||||
allowManualMerge = config.AllowManualMerge
|
||||
autodetectManualMerge = config.AutodetectManualMerge
|
||||
defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge
|
||||
defaultMergeStyle = config.GetDefaultMergeStyle()
|
||||
defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit
|
||||
|
@ -235,6 +239,8 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
|||
AllowSquash: allowSquash,
|
||||
AllowFastForwardOnly: allowFastForwardOnly,
|
||||
AllowRebaseUpdate: allowRebaseUpdate,
|
||||
AllowManualMerge: allowManualMerge,
|
||||
AutodetectManualMerge: autodetectManualMerge,
|
||||
DefaultDeleteBranchAfterMerge: defaultDeleteBranchAfterMerge,
|
||||
DefaultMergeStyle: string(defaultMergeStyle),
|
||||
DefaultAllowMaintainerEdit: defaultAllowMaintainerEdit,
|
||||
|
|
|
@ -148,7 +148,7 @@ func (g *GiteaLocalUploader) CreateRepo(ctx context.Context, repo *base.Reposito
|
|||
return err
|
||||
}
|
||||
g.repo.ObjectFormatName = objectFormat.Name()
|
||||
return repo_model.UpdateRepositoryCols(ctx, g.repo, "object_format_name")
|
||||
return repo_model.UpdateRepositoryColsNoAutoTime(ctx, g.repo, "object_format_name")
|
||||
}
|
||||
|
||||
// Close closes this uploader
|
||||
|
@ -975,7 +975,7 @@ func (g *GiteaLocalUploader) Finish(ctx context.Context) error {
|
|||
}
|
||||
|
||||
g.repo.Status = repo_model.RepositoryReady
|
||||
return repo_model.UpdateRepositoryCols(ctx, g.repo, "status")
|
||||
return repo_model.UpdateRepositoryColsWithAutoTime(ctx, g.repo, "status")
|
||||
}
|
||||
|
||||
func (g *GiteaLocalUploader) remapUser(ctx context.Context, source user_model.ExternalUserMigrated, target user_model.ExternalUserRemappable) error {
|
||||
|
|
|
@ -71,7 +71,7 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error
|
|||
// erase authentication before storing in database
|
||||
u.User = nil
|
||||
m.Repo.OriginalURL = u.String()
|
||||
return repo_model.UpdateRepositoryCols(ctx, m.Repo, "original_url")
|
||||
return repo_model.UpdateRepositoryColsNoAutoTime(ctx, m.Repo, "original_url")
|
||||
}
|
||||
|
||||
// mirrorSyncResult contains information of a updated reference.
|
||||
|
@ -653,7 +653,7 @@ func checkAndUpdateEmptyRepository(ctx context.Context, m *repo_model.Mirror, re
|
|||
}
|
||||
m.Repo.IsEmpty = false
|
||||
// Update the is empty and default_branch columns
|
||||
if err := repo_model.UpdateRepositoryCols(ctx, m.Repo, "default_branch", "is_empty"); err != nil {
|
||||
if err := repo_model.UpdateRepositoryColsWithAutoTime(ctx, m.Repo, "default_branch", "is_empty"); err != nil {
|
||||
log.Error("Failed to update default branch of repository %-v. Error: %v", m.Repo, err)
|
||||
desc := fmt.Sprintf("Failed to update default branch of repository '%s': %v", m.Repo.RepoPath(), err)
|
||||
if err = system_model.CreateRepositoryNotice(desc); err != nil {
|
||||
|
|
|
@ -64,10 +64,11 @@ func RemoveOrgUser(ctx context.Context, org *organization.Organization, user *us
|
|||
if err != nil {
|
||||
return fmt.Errorf("AccessibleReposEnv: %w", err)
|
||||
}
|
||||
repoIDs, err := env.RepoIDs(ctx, 1, org.NumRepos)
|
||||
repoIDs, err := env.RepoIDs(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetUserRepositories [%d]: %w", user.ID, err)
|
||||
}
|
||||
|
||||
for _, repoID := range repoIDs {
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
|
||||
if err != nil {
|
||||
|
|
|
@ -100,7 +100,7 @@ func AdoptRepository(ctx context.Context, doer, owner *user_model.User, opts Cre
|
|||
|
||||
// 4 - update repository status
|
||||
repo.Status = repo_model.RepositoryReady
|
||||
if err = repo_model.UpdateRepositoryCols(ctx, repo, "status"); err != nil {
|
||||
if err = repo_model.UpdateRepositoryColsWithAutoTime(ctx, repo, "status"); err != nil {
|
||||
return nil, fmt.Errorf("UpdateRepositoryCols: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ func UploadAvatar(ctx context.Context, repo *repo_model.Repository, data []byte)
|
|||
// Users can upload the same image to other repo - prefix it with ID
|
||||
// Then repo will be removed - only it avatar file will be removed
|
||||
repo.Avatar = newAvatar
|
||||
if err := repo_model.UpdateRepositoryCols(ctx, repo, "avatar"); err != nil {
|
||||
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "avatar"); err != nil {
|
||||
return fmt.Errorf("UploadAvatar: Update repository avatar: %w", err)
|
||||
}
|
||||
|
||||
|
@ -77,7 +77,7 @@ func DeleteAvatar(ctx context.Context, repo *repo_model.Repository) error {
|
|||
defer committer.Close()
|
||||
|
||||
repo.Avatar = ""
|
||||
if err := repo_model.UpdateRepositoryCols(ctx, repo, "avatar"); err != nil {
|
||||
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "avatar"); err != nil {
|
||||
return fmt.Errorf("DeleteAvatar: Update repository avatar: %w", err)
|
||||
}
|
||||
|
||||
|
@ -112,5 +112,5 @@ func generateAvatar(ctx context.Context, templateRepo, generateRepo *repo_model.
|
|||
return err
|
||||
}
|
||||
|
||||
return repo_model.UpdateRepositoryCols(ctx, generateRepo, "avatar")
|
||||
return repo_model.UpdateRepositoryColsNoAutoTime(ctx, generateRepo, "avatar")
|
||||
}
|
||||
|
|
|
@ -321,7 +321,7 @@ func CreateRepositoryDirectly(ctx context.Context, doer, owner *user_model.User,
|
|||
// 7 - update repository status to be ready
|
||||
if needsUpdateToReady {
|
||||
repo.Status = repo_model.RepositoryReady
|
||||
if err = repo_model.UpdateRepositoryCols(ctx, repo, "status"); err != nil {
|
||||
if err = repo_model.UpdateRepositoryColsWithAutoTime(ctx, repo, "status"); err != nil {
|
||||
return nil, fmt.Errorf("UpdateRepositoryCols: %w", err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/lfs"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
actions_service "code.gitea.io/gitea/services/actions"
|
||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||
|
||||
"xorm.io/builder"
|
||||
|
@ -133,6 +134,14 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
|
|||
return err
|
||||
}
|
||||
|
||||
// CleanupEphemeralRunnersByPickedTaskOfRepo deletes ephemeral global/org/user that have started any task of this repo
|
||||
// The cannot pick a second task hardening for ephemeral runners expect that task objects remain available until runner deletion
|
||||
// This method will delete affected ephemeral global/org/user runners
|
||||
// &actions_model.ActionRunner{RepoID: repoID} does only handle ephemeral repository runners
|
||||
if err := actions_service.CleanupEphemeralRunnersByPickedTaskOfRepo(ctx, repoID); err != nil {
|
||||
return fmt.Errorf("cleanupEphemeralRunners: %w", err)
|
||||
}
|
||||
|
||||
if err := db.DeleteBeans(ctx,
|
||||
&access_model.Access{RepoID: repo.ID},
|
||||
&activities_model.Action{RepoID: repo.ID},
|
||||
|
|
|
@ -306,7 +306,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
|
|||
|
||||
if repo.IsEmpty {
|
||||
if isEmpty, err := gitRepo.IsEmpty(); err == nil && !isEmpty {
|
||||
_ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: repo.ID, IsEmpty: false, DefaultBranch: opts.NewBranch}, "is_empty", "default_branch")
|
||||
_ = repo_model.UpdateRepositoryColsWithAutoTime(ctx, &repo_model.Repository{ID: repo.ID, IsEmpty: false, DefaultBranch: opts.NewBranch}, "is_empty", "default_branch")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -198,7 +198,7 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
|
|||
|
||||
// 8 - update repository status to be ready
|
||||
repo.Status = repo_model.RepositoryReady
|
||||
if err = repo_model.UpdateRepositoryCols(ctx, repo, "status"); err != nil {
|
||||
if err = repo_model.UpdateRepositoryColsWithAutoTime(ctx, repo, "status"); err != nil {
|
||||
return nil, fmt.Errorf("UpdateRepositoryCols: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -283,7 +283,7 @@ func pushNewBranch(ctx context.Context, repo *repo_model.Repository, pusher *use
|
|||
}
|
||||
}
|
||||
// Update the is empty and default_branch columns
|
||||
if err := repo_model.UpdateRepositoryCols(ctx, repo, "default_branch", "is_empty"); err != nil {
|
||||
if err := repo_model.UpdateRepositoryColsWithAutoTime(ctx, repo, "default_branch", "is_empty"); err != nil {
|
||||
return nil, fmt.Errorf("UpdateRepositoryCols: %w", err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -205,7 +205,7 @@ func updateRepository(ctx context.Context, repo *repo_model.Repository, visibili
|
|||
|
||||
e := db.GetEngine(ctx)
|
||||
|
||||
if _, err = e.ID(repo.ID).AllCols().Update(repo); err != nil {
|
||||
if _, err = e.ID(repo.ID).NoAutoTime().AllCols().Update(repo); err != nil {
|
||||
return fmt.Errorf("update: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -184,7 +184,7 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ
|
|||
|
||||
// 6 - update repository status to be ready
|
||||
generateRepo.Status = repo_model.RepositoryReady
|
||||
if err = repo_model.UpdateRepositoryCols(ctx, generateRepo, "status"); err != nil {
|
||||
if err = repo_model.UpdateRepositoryColsWithAutoTime(ctx, generateRepo, "status"); err != nil {
|
||||
return nil, fmt.Errorf("UpdateRepositoryCols: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -160,7 +160,7 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
|
|||
repo.OwnerName = newOwner.Name
|
||||
|
||||
// Update repository.
|
||||
if err := repo_model.UpdateRepositoryCols(ctx, repo, "owner_id", "owner_name"); err != nil {
|
||||
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "owner_id", "owner_name"); err != nil {
|
||||
return fmt.Errorf("update owner: %w", err)
|
||||
}
|
||||
|
||||
|
@ -304,7 +304,7 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
|
|||
return fmt.Errorf("deleteRepositoryTransfer: %w", err)
|
||||
}
|
||||
repo.Status = repo_model.RepositoryReady
|
||||
if err := repo_model.UpdateRepositoryCols(ctx, repo, "status"); err != nil {
|
||||
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "status"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -495,7 +495,7 @@ func RejectRepositoryTransfer(ctx context.Context, repo *repo_model.Repository,
|
|||
}
|
||||
|
||||
repo.Status = repo_model.RepositoryReady
|
||||
if err := repo_model.UpdateRepositoryCols(ctx, repo, "status"); err != nil {
|
||||
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "status"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -543,7 +543,7 @@ func CancelRepositoryTransfer(ctx context.Context, repoTransfer *repo_model.Repo
|
|||
}
|
||||
|
||||
repoTransfer.Repo.Status = repo_model.RepositoryReady
|
||||
if err := repo_model.UpdateRepositoryCols(ctx, repoTransfer.Repo, "status"); err != nil {
|
||||
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repoTransfer.Repo, "status"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -305,7 +305,7 @@ func parseHookPullRequestEventType(event webhook_module.HookEventType) (string,
|
|||
case webhook_module.HookEventPullRequestReviewApproved:
|
||||
return "approved", nil
|
||||
case webhook_module.HookEventPullRequestReviewRejected:
|
||||
return "rejected", nil
|
||||
return "requested changes", nil
|
||||
case webhook_module.HookEventPullRequestReviewComment:
|
||||
return "comment", nil
|
||||
default:
|
||||
|
|
|
@ -365,7 +365,7 @@ func ChangeDefaultWikiBranch(ctx context.Context, repo *repo_model.Repository, n
|
|||
}
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
repo.DefaultWikiBranch = newBranch
|
||||
if err := repo_model.UpdateRepositoryCols(ctx, repo, "default_wiki_branch"); err != nil {
|
||||
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "default_wiki_branch"); err != nil {
|
||||
return fmt.Errorf("unable to update database: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,37 +5,46 @@
|
|||
<h2>{{if $.IsFiltered}}{{ctx.Locale.Tr "actions.runs.no_results"}}{{else}}{{ctx.Locale.Tr "actions.runs.no_runs"}}{{end}}</h2>
|
||||
</div>
|
||||
{{end}}
|
||||
{{range .Runs}}
|
||||
{{range $run := .Runs}}
|
||||
<div class="flex-item tw-items-center">
|
||||
<div class="flex-item-leading">
|
||||
{{template "repo/actions/status" (dict "status" .Status.String)}}
|
||||
{{template "repo/actions/status" (dict "status" $run.Status.String)}}
|
||||
</div>
|
||||
<div class="flex-item-main">
|
||||
<a class="flex-item-title" title="{{.Title}}" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}">
|
||||
{{if .Title}}{{.Title}}{{else}}{{ctx.Locale.Tr "actions.runs.empty_commit_message"}}{{end}}
|
||||
<a class="flex-item-title" title="{{$run.Title}}" href="{{$run.Link}}">
|
||||
{{or $run.Title (ctx.Locale.Tr "actions.runs.empty_commit_message")}}
|
||||
</a>
|
||||
<div class="flex-item-body">
|
||||
<span><b>{{if not $.CurWorkflow}}{{.WorkflowID}} {{end}}#{{.Index}}</b>:</span>
|
||||
{{- if .ScheduleID -}}
|
||||
<span><b>{{if not $.CurWorkflow}}{{$run.WorkflowID}} {{end}}#{{$run.Index}}</b>:</span>
|
||||
{{- if $run.ScheduleID -}}
|
||||
{{ctx.Locale.Tr "actions.runs.scheduled"}}
|
||||
{{- else -}}
|
||||
{{ctx.Locale.Tr "actions.runs.commit"}}
|
||||
<a href="{{$.RepoLink}}/commit/{{.CommitSHA}}">{{ShortSha .CommitSHA}}</a>
|
||||
<a href="{{$.RepoLink}}/commit/{{$run.CommitSHA}}">{{ShortSha $run.CommitSHA}}</a>
|
||||
{{ctx.Locale.Tr "actions.runs.pushed_by"}}
|
||||
<a href="{{.TriggerUser.HomeLink}}">{{.TriggerUser.GetDisplayName}}</a>
|
||||
<a href="{{$run.TriggerUser.HomeLink}}">{{$run.TriggerUser.GetDisplayName}}</a>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-item-trailing">
|
||||
{{if .IsRefDeleted}}
|
||||
<span class="ui label run-list-ref gt-ellipsis tw-line-through" data-tooltip-content="{{.PrettyRef}}">{{.PrettyRef}}</span>
|
||||
{{if $run.IsRefDeleted}}
|
||||
<span class="ui label run-list-ref gt-ellipsis tw-line-through" data-tooltip-content="{{$run.PrettyRef}}">{{$run.PrettyRef}}</span>
|
||||
{{else}}
|
||||
<a class="ui label run-list-ref gt-ellipsis" href="{{.RefLink}}" data-tooltip-content="{{.PrettyRef}}">{{.PrettyRef}}</a>
|
||||
<a class="ui label run-list-ref gt-ellipsis" href="{{$run.RefLink}}" data-tooltip-content="{{$run.PrettyRef}}">{{$run.PrettyRef}}</a>
|
||||
{{end}}
|
||||
<div class="run-list-item-right">
|
||||
<div class="run-list-meta">{{svg "octicon-calendar" 16}}{{DateUtils.TimeSince .Updated}}</div>
|
||||
<div class="run-list-meta">{{svg "octicon-stopwatch" 16}}{{.Duration}}</div>
|
||||
<div class="run-list-meta">{{svg "octicon-calendar" 16}}{{DateUtils.TimeSince $run.Updated}}</div>
|
||||
<div class="run-list-meta">{{svg "octicon-stopwatch" 16}}{{$run.Duration}}</div>
|
||||
</div>
|
||||
{{if and ($.AllowDeleteWorkflowRuns) ($run.Status.IsDone)}}
|
||||
<button class="btn interact-bg link-action tw-p-2"
|
||||
data-url="{{$run.Link}}/delete"
|
||||
data-modal-confirm="{{ctx.Locale.Tr "actions.runs.delete.description"}}"
|
||||
data-tooltip-content="{{ctx.Locale.Tr "actions.runs.delete"}}"
|
||||
>
|
||||
{{svg "octicon-trash"}}
|
||||
</button>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
|
@ -226,11 +226,19 @@
|
|||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else if .Permission.IsAdmin}}
|
||||
{{else}}
|
||||
<div class="overflow-menu-items">
|
||||
{{if(and .Repository.IsBeingCreated (.Permission.CanRead ctx.Consts.RepoUnitTypeCode))}}
|
||||
<a class="{{if not .PageIsRepoSettings}}active {{end}}item" href="{{.RepoLink}}">
|
||||
{{svg "octicon-clock"}} {{ctx.Locale.Tr "repo.migrating_status"}}
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{if .Permission.IsAdmin}}
|
||||
<a class="{{if .PageIsRepoSettings}}active {{end}} item" href="{{.RepoLink}}/settings">
|
||||
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</overflow-menu>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="flex-item">
|
||||
<div class="flex-item-main">
|
||||
<div class="flex-item-title">
|
||||
<a class="item muted" href="{{.Link}}/releases">
|
||||
<a class="item muted" href="{{.RepoLink}}/releases">
|
||||
{{ctx.Locale.Tr "repo.releases"}}
|
||||
<span class="ui small label">{{.NumReleases}}</span>
|
||||
</a>
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
<span class="color-text-light-2">
|
||||
{{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .CreatedUnix)}}
|
||||
</span>
|
||||
<button class="ui btn interact-bg show-modal tw-p-2"
|
||||
<button class="btn interact-bg show-modal tw-p-2"
|
||||
data-modal="#add-secret-modal"
|
||||
data-modal-form.action="{{$.Link}}"
|
||||
data-modal-header="{{ctx.Locale.Tr "secrets.edit_secret"}}"
|
||||
|
@ -49,7 +49,7 @@
|
|||
>
|
||||
{{svg "octicon-pencil"}}
|
||||
</button>
|
||||
<button class="ui btn interact-bg link-action tw-p-2"
|
||||
<button class="btn interact-bg link-action tw-p-2"
|
||||
data-url="{{$.Link}}/delete?id={{.ID}}"
|
||||
data-modal-confirm="{{ctx.Locale.Tr "secrets.deletion.description"}}"
|
||||
data-tooltip-content="{{ctx.Locale.Tr "secrets.deletion"}}"
|
||||
|
|
|
@ -4758,6 +4758,52 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/actions/runs/{run}": {
|
||||
"delete": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"repository"
|
||||
],
|
||||
"summary": "Delete a workflow run",
|
||||
"operationId": "deleteActionRun",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the owner",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repository",
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "runid of the workflow run",
|
||||
"name": "run",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/responses/error"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/actions/runs/{run}/artifacts": {
|
||||
"get": {
|
||||
"produces": [
|
||||
|
@ -6558,6 +6604,20 @@
|
|||
"name": "path",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Only commits after this date will be returned (ISO 8601 format)",
|
||||
"name": "since",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Only commits before this date will be returned (ISO 8601 format)",
|
||||
"name": "until",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "include diff stats for every commit (disable for speedup, default 'true')",
|
||||
|
@ -26138,6 +26198,10 @@
|
|||
"type": "boolean",
|
||||
"x-go-name": "AllowFastForwardOnly"
|
||||
},
|
||||
"allow_manual_merge": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "AllowManualMerge"
|
||||
},
|
||||
"allow_merge_commits": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "AllowMerge"
|
||||
|
@ -26167,6 +26231,10 @@
|
|||
"format": "date-time",
|
||||
"x-go-name": "ArchivedAt"
|
||||
},
|
||||
"autodetect_manual_merge": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "AutodetectManualMerge"
|
||||
},
|
||||
"avatar_url": {
|
||||
"type": "string",
|
||||
"x-go-name": "AvatarURL"
|
||||
|
|
|
@ -0,0 +1,181 @@
|
|||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/routers/web/repo/actions"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
func TestActionsDeleteRun(t *testing.T) {
|
||||
now := time.Now()
|
||||
testCase := struct {
|
||||
treePath string
|
||||
fileContent string
|
||||
outcomes map[string]*mockTaskOutcome
|
||||
expectedStatuses map[string]string
|
||||
}{
|
||||
treePath: ".gitea/workflows/test1.yml",
|
||||
fileContent: `name: test1
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- .gitea/workflows/test1.yml
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job1
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job2
|
||||
job3:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job3
|
||||
`,
|
||||
outcomes: map[string]*mockTaskOutcome{
|
||||
"job1": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
logRows: []*runnerv1.LogRow{
|
||||
{
|
||||
Time: timestamppb.New(now.Add(4 * time.Second)),
|
||||
Content: " \U0001F433 docker create image",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(5 * time.Second)),
|
||||
Content: "job1",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(6 * time.Second)),
|
||||
Content: "\U0001F3C1 Job succeeded",
|
||||
},
|
||||
},
|
||||
},
|
||||
"job2": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
logRows: []*runnerv1.LogRow{
|
||||
{
|
||||
Time: timestamppb.New(now.Add(4 * time.Second)),
|
||||
Content: " \U0001F433 docker create image",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(5 * time.Second)),
|
||||
Content: "job2",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(6 * time.Second)),
|
||||
Content: "\U0001F3C1 Job succeeded",
|
||||
},
|
||||
},
|
||||
},
|
||||
"job3": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
logRows: []*runnerv1.LogRow{
|
||||
{
|
||||
Time: timestamppb.New(now.Add(4 * time.Second)),
|
||||
Content: " \U0001F433 docker create image",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(5 * time.Second)),
|
||||
Content: "job3",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(6 * time.Second)),
|
||||
Content: "\U0001F3C1 Job succeeded",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedStatuses: map[string]string{
|
||||
"job1": actions_model.StatusSuccess.String(),
|
||||
"job2": actions_model.StatusSuccess.String(),
|
||||
"job3": actions_model.StatusSuccess.String(),
|
||||
},
|
||||
}
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
session := loginUser(t, user2.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
apiRepo := createActionsTestRepo(t, token, "actions-delete-run-test", false)
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
|
||||
|
||||
opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, "create "+testCase.treePath, testCase.fileContent)
|
||||
createWorkflowFile(t, token, user2.Name, apiRepo.Name, testCase.treePath, opts)
|
||||
|
||||
runIndex := ""
|
||||
for i := 0; i < len(testCase.outcomes); i++ {
|
||||
task := runner.fetchTask(t)
|
||||
jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id)
|
||||
outcome := testCase.outcomes[jobName]
|
||||
assert.NotNil(t, outcome)
|
||||
runner.execTask(t, task, outcome)
|
||||
runIndex = task.Context.GetFields()["run_number"].GetStringValue()
|
||||
assert.Equal(t, "1", runIndex)
|
||||
}
|
||||
|
||||
for i := 0; i < len(testCase.outcomes); i++ {
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d", user2.Name, apiRepo.Name, runIndex, i), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
var listResp actions.ViewResponse
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &listResp)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, listResp.State.Run.Jobs, 3)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d/logs", user2.Name, apiRepo.Name, runIndex, i)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
req := NewRequestWithValues(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s", user2.Name, apiRepo.Name, runIndex), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/delete", user2.Name, apiRepo.Name, runIndex), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/delete", user2.Name, apiRepo.Name, runIndex), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
req = NewRequestWithValues(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s", user2.Name, apiRepo.Name, runIndex), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
for i := 0; i < len(testCase.outcomes); i++ {
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d", user2.Name, apiRepo.Name, runIndex, i), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d/logs", user2.Name, apiRepo.Name, runIndex, i)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1156,6 +1156,7 @@ jobs:
|
|||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
|
||||
Title: "add workflow",
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
Event: "workflow_dispatch",
|
||||
Ref: "refs/heads/dispatch",
|
||||
WorkflowID: "dispatch.yml",
|
||||
|
@ -1448,3 +1449,157 @@ jobs:
|
|||
assert.Equal(t, pullRequest.MergedCommitID, actionRun.CommitSHA)
|
||||
})
|
||||
}
|
||||
|
||||
func TestActionRunNameWithContextVariables(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
// create the repo
|
||||
repo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{
|
||||
Name: "action-run-name-with-variables",
|
||||
Description: "test action run name",
|
||||
AutoInit: true,
|
||||
Gitignores: "Go",
|
||||
License: "MIT",
|
||||
Readme: "Default",
|
||||
DefaultBranch: "main",
|
||||
IsPrivate: false,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, repo)
|
||||
|
||||
// add workflow file to the repo
|
||||
addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "create",
|
||||
TreePath: ".gitea/workflows/runname.yml",
|
||||
ContentReader: strings.NewReader(`name: test
|
||||
on:
|
||||
[create,delete]
|
||||
run-name: ${{ gitea.actor }} is running this workflow
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo helloworld
|
||||
`),
|
||||
},
|
||||
},
|
||||
Message: "add workflow with run-name",
|
||||
OldBranch: "main",
|
||||
NewBranch: "main",
|
||||
Author: &files_service.IdentityOptions{
|
||||
GitUserName: user2.Name,
|
||||
GitUserEmail: user2.Email,
|
||||
},
|
||||
Committer: &files_service.IdentityOptions{
|
||||
GitUserName: user2.Name,
|
||||
GitUserEmail: user2.Email,
|
||||
},
|
||||
Dates: &files_service.CommitDateOptions{
|
||||
Author: time.Now(),
|
||||
Committer: time.Now(),
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, addWorkflowToBaseResp)
|
||||
|
||||
// Get the commit ID of the default branch
|
||||
gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo)
|
||||
assert.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
branch, err := git_model.GetBranch(db.DefaultContext, repo.ID, repo.DefaultBranch)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// create a branch
|
||||
err = repo_service.CreateNewBranchFromCommit(db.DefaultContext, user2, repo, gitRepo, branch.CommitID, "test-action-run-name-with-variables")
|
||||
assert.NoError(t, err)
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
|
||||
Title: user2.LoginName + " is running this workflow",
|
||||
RepoID: repo.ID,
|
||||
Event: "create",
|
||||
Ref: "refs/heads/test-action-run-name-with-variables",
|
||||
WorkflowID: "runname.yml",
|
||||
CommitSHA: branch.CommitID,
|
||||
})
|
||||
assert.NotNil(t, run)
|
||||
})
|
||||
}
|
||||
|
||||
func TestActionRunName(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
// create the repo
|
||||
repo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{
|
||||
Name: "action-run-name",
|
||||
Description: "test action run-name",
|
||||
AutoInit: true,
|
||||
Gitignores: "Go",
|
||||
License: "MIT",
|
||||
Readme: "Default",
|
||||
DefaultBranch: "main",
|
||||
IsPrivate: false,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, repo)
|
||||
|
||||
// add workflow file to the repo
|
||||
addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "create",
|
||||
TreePath: ".gitea/workflows/runname.yml",
|
||||
ContentReader: strings.NewReader(`name: test
|
||||
on:
|
||||
[create,delete]
|
||||
run-name: run name without variables
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo helloworld
|
||||
`),
|
||||
},
|
||||
},
|
||||
Message: "add workflow with run name",
|
||||
OldBranch: "main",
|
||||
NewBranch: "main",
|
||||
Author: &files_service.IdentityOptions{
|
||||
GitUserName: user2.Name,
|
||||
GitUserEmail: user2.Email,
|
||||
},
|
||||
Committer: &files_service.IdentityOptions{
|
||||
GitUserName: user2.Name,
|
||||
GitUserEmail: user2.Email,
|
||||
},
|
||||
Dates: &files_service.CommitDateOptions{
|
||||
Author: time.Now(),
|
||||
Committer: time.Now(),
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, addWorkflowToBaseResp)
|
||||
|
||||
// Get the commit ID of the default branch
|
||||
gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo)
|
||||
assert.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
branch, err := git_model.GetBranch(db.DefaultContext, repo.ID, repo.DefaultBranch)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// create a branch
|
||||
err = repo_service.CreateNewBranchFromCommit(db.DefaultContext, user2, repo, gitRepo, branch.CommitID, "test-action-run-name")
|
||||
assert.NoError(t, err)
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
|
||||
Title: "run name without variables",
|
||||
RepoID: repo.ID,
|
||||
Event: "create",
|
||||
Ref: "refs/heads/test-action-run-name",
|
||||
WorkflowID: "runname.yml",
|
||||
CommitSHA: branch.CommitID,
|
||||
})
|
||||
assert.NotNil(t, run)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
@ -72,12 +73,37 @@ func TestAdminDeleteUser(t *testing.T) {
|
|||
|
||||
session := loginUser(t, "user1")
|
||||
|
||||
csrf := GetUserCSRFToken(t, session)
|
||||
req := NewRequestWithValues(t, "POST", "/-/admin/users/8/delete", map[string]string{
|
||||
"_csrf": csrf,
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
usersToDelete := []struct {
|
||||
userID int64
|
||||
purge bool
|
||||
}{
|
||||
{
|
||||
userID: 2,
|
||||
purge: true,
|
||||
},
|
||||
{
|
||||
userID: 8,
|
||||
},
|
||||
}
|
||||
|
||||
assertUserDeleted(t, 8)
|
||||
unittest.CheckConsistencyFor(t, &user_model.User{})
|
||||
for _, entry := range usersToDelete {
|
||||
t.Run(fmt.Sprintf("DeleteUser%d", entry.userID), func(t *testing.T) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: entry.userID})
|
||||
assert.NotNil(t, user)
|
||||
|
||||
var query string
|
||||
if entry.purge {
|
||||
query = "?purge=true"
|
||||
}
|
||||
|
||||
csrf := GetUserCSRFToken(t, session)
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/-/admin/users/%d/delete%s", entry.userID, query), map[string]string{
|
||||
"_csrf": csrf,
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
assertUserDeleted(t, entry.userID)
|
||||
unittest.CheckConsistencyFor(t, &user_model.User{})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIActionsDeleteRunCheckPermission(t *testing.T) {
|
||||
defer prepareTestEnvActionsArtifacts(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
session := loginUser(t, user.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
testAPIActionsDeleteRun(t, repo, token, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAPIActionsDeleteRun(t *testing.T) {
|
||||
defer prepareTestEnvActionsArtifacts(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
session := loginUser(t, user.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
testAPIActionsDeleteRunListArtifacts(t, repo, token, 2)
|
||||
testAPIActionsDeleteRunListTasks(t, repo, token, true)
|
||||
testAPIActionsDeleteRun(t, repo, token, http.StatusNoContent)
|
||||
|
||||
testAPIActionsDeleteRunListArtifacts(t, repo, token, 0)
|
||||
testAPIActionsDeleteRunListTasks(t, repo, token, false)
|
||||
testAPIActionsDeleteRun(t, repo, token, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAPIActionsDeleteRunRunning(t *testing.T) {
|
||||
defer prepareTestEnvActionsArtifacts(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
session := loginUser(t, user.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/actions/runs/793", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func testAPIActionsDeleteRun(t *testing.T, repo *repo_model.Repository, token string, expected int) {
|
||||
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, expected)
|
||||
}
|
||||
|
||||
func testAPIActionsDeleteRunListArtifacts(t *testing.T, repo *repo_model.Repository, token string, artifacts int) {
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/artifacts", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var listResp api.ActionArtifactsResponse
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &listResp)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, listResp.Entries, artifacts)
|
||||
}
|
||||
|
||||
func testAPIActionsDeleteRunListTasks(t *testing.T, repo *repo_model.Repository, token string, expected bool) {
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/tasks", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var listResp api.ActionTaskResponse
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &listResp)
|
||||
assert.NoError(t, err)
|
||||
findTask1 := false
|
||||
findTask2 := false
|
||||
for _, entry := range listResp.Entries {
|
||||
if entry.ID == 53 {
|
||||
findTask1 = true
|
||||
continue
|
||||
}
|
||||
if entry.ID == 54 {
|
||||
findTask2 = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
assert.Equal(t, expected, findTask1)
|
||||
assert.Equal(t, expected, findTask2)
|
||||
}
|
|
@ -41,8 +41,6 @@ func testActionsRunnerAdmin(t *testing.T) {
|
|||
runnerList := api.ActionRunnersResponse{}
|
||||
DecodeJSON(t, runnerListResp, &runnerList)
|
||||
|
||||
assert.Len(t, runnerList.Entries, 4)
|
||||
|
||||
idx := slices.IndexFunc(runnerList.Entries, func(e *api.ActionRunner) bool { return e.ID == 34349 })
|
||||
require.NotEqual(t, -1, idx)
|
||||
expectedRunner := runnerList.Entries[idx]
|
||||
|
@ -160,16 +158,20 @@ func testActionsRunnerOwner(t *testing.T) {
|
|||
runnerList := api.ActionRunnersResponse{}
|
||||
DecodeJSON(t, runnerListResp, &runnerList)
|
||||
|
||||
assert.Len(t, runnerList.Entries, 1)
|
||||
assert.Equal(t, "runner_to_be_deleted-org", runnerList.Entries[0].Name)
|
||||
assert.Equal(t, int64(34347), runnerList.Entries[0].ID)
|
||||
assert.False(t, runnerList.Entries[0].Ephemeral)
|
||||
assert.Len(t, runnerList.Entries[0].Labels, 2)
|
||||
assert.Equal(t, "runner_to_be_deleted", runnerList.Entries[0].Labels[0].Name)
|
||||
assert.Equal(t, "linux", runnerList.Entries[0].Labels[1].Name)
|
||||
idx := slices.IndexFunc(runnerList.Entries, func(e *api.ActionRunner) bool { return e.ID == 34347 })
|
||||
require.NotEqual(t, -1, idx)
|
||||
expectedRunner := runnerList.Entries[idx]
|
||||
|
||||
require.NotNil(t, expectedRunner)
|
||||
assert.Equal(t, "runner_to_be_deleted-org", expectedRunner.Name)
|
||||
assert.Equal(t, int64(34347), expectedRunner.ID)
|
||||
assert.False(t, expectedRunner.Ephemeral)
|
||||
assert.Len(t, expectedRunner.Labels, 2)
|
||||
assert.Equal(t, "runner_to_be_deleted", expectedRunner.Labels[0].Name)
|
||||
assert.Equal(t, "linux", expectedRunner.Labels[1].Name)
|
||||
|
||||
// Verify get the runner by id
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID)).AddTokenAuth(token)
|
||||
runnerResp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
runner := api.ActionRunner{}
|
||||
|
@ -183,11 +185,11 @@ func testActionsRunnerOwner(t *testing.T) {
|
|||
assert.Equal(t, "linux", runner.Labels[1].Name)
|
||||
|
||||
// Verify delete the runner by id
|
||||
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
|
||||
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID)).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
// Verify runner deletion
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID)).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
|
@ -329,4 +331,12 @@ func testActionsRunnerRepo(t *testing.T) {
|
|||
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34349)).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("DeleteAdminRunnerNotFoundUnknownID", func(t *testing.T) {
|
||||
userUsername := "user2"
|
||||
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteRepository)
|
||||
// Verify delete a runner by unknown id is not found
|
||||
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 4384797347934)).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -100,22 +101,29 @@ func TestEmptyRepoAddFile(t *testing.T) {
|
|||
assert.Contains(t, resp.Body.String(), "test-file.md")
|
||||
|
||||
// if the repo is in incorrect state, it should be able to self-heal (recover to correct state)
|
||||
user30EmptyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 30, Name: "empty"})
|
||||
user30EmptyRepo.IsEmpty = true
|
||||
user30EmptyRepo.DefaultBranch = "no-such"
|
||||
_, err := db.GetEngine(db.DefaultContext).ID(user30EmptyRepo.ID).Cols("is_empty", "default_branch").Update(user30EmptyRepo)
|
||||
require.NoError(t, err)
|
||||
user30EmptyRepo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 30, Name: "empty"})
|
||||
assert.True(t, user30EmptyRepo.IsEmpty)
|
||||
testEmptyOrBrokenRecover := func(t *testing.T, isEmpty, isBroken bool) {
|
||||
user30EmptyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 30, Name: "empty"})
|
||||
user30EmptyRepo.IsEmpty = isEmpty
|
||||
user30EmptyRepo.Status = util.Iif(isBroken, repo_model.RepositoryBroken, repo_model.RepositoryReady)
|
||||
user30EmptyRepo.DefaultBranch = "no-such"
|
||||
_, err := db.GetEngine(db.DefaultContext).ID(user30EmptyRepo.ID).Cols("is_empty", "status", "default_branch").Update(user30EmptyRepo)
|
||||
require.NoError(t, err)
|
||||
user30EmptyRepo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 30, Name: "empty"})
|
||||
assert.Equal(t, isEmpty, user30EmptyRepo.IsEmpty)
|
||||
assert.Equal(t, isBroken, user30EmptyRepo.Status == repo_model.RepositoryBroken)
|
||||
|
||||
req = NewRequest(t, "GET", "/user30/empty")
|
||||
resp = session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
redirect = test.RedirectURL(resp)
|
||||
assert.Equal(t, "/user30/empty", redirect)
|
||||
req = NewRequest(t, "GET", "/user30/empty")
|
||||
resp = session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
redirect = test.RedirectURL(resp)
|
||||
assert.Equal(t, "/user30/empty", redirect)
|
||||
|
||||
req = NewRequest(t, "GET", "/user30/empty")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), "test-file.md")
|
||||
req = NewRequest(t, "GET", "/user30/empty")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), "test-file.md")
|
||||
}
|
||||
testEmptyOrBrokenRecover(t, true, false)
|
||||
testEmptyOrBrokenRecover(t, false, true)
|
||||
testEmptyOrBrokenRecover(t, true, true)
|
||||
}
|
||||
|
||||
func TestEmptyRepoUploadFile(t *testing.T) {
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEphemeralActionsRunnerDeletion(t *testing.T) {
|
||||
t.Run("ByTaskCompletion", testEphemeralActionsRunnerDeletionByTaskCompletion)
|
||||
t.Run("ByRepository", testEphemeralActionsRunnerDeletionByRepository)
|
||||
t.Run("ByUser", testEphemeralActionsRunnerDeletionByUser)
|
||||
}
|
||||
|
||||
// Test that the ephemeral runner is deleted when the task is finished
|
||||
func testEphemeralActionsRunnerDeletionByTaskCompletion(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
_, err := actions_model.GetRunnerByID(t.Context(), 34350)
|
||||
assert.NoError(t, err)
|
||||
|
||||
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 52})
|
||||
assert.Equal(t, actions_model.StatusRunning, task.Status)
|
||||
|
||||
task.Status = actions_model.StatusSuccess
|
||||
err = actions_model.UpdateTask(t.Context(), task, "status")
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = actions_model.GetRunnerByID(t.Context(), 34350)
|
||||
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||
}
|
||||
|
||||
func testEphemeralActionsRunnerDeletionByRepository(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
_, err := actions_model.GetRunnerByID(t.Context(), 34350)
|
||||
assert.NoError(t, err)
|
||||
|
||||
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 52})
|
||||
assert.Equal(t, actions_model.StatusRunning, task.Status)
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
err = repo_service.DeleteRepositoryDirectly(t.Context(), user, task.RepoID, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = actions_model.GetRunnerByID(t.Context(), 34350)
|
||||
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||
}
|
||||
|
||||
// Test that the ephemeral runner is deleted when a user is deleted
|
||||
func testEphemeralActionsRunnerDeletionByUser(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
_, err := actions_model.GetRunnerByID(t.Context(), 34350)
|
||||
assert.NoError(t, err)
|
||||
|
||||
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 52})
|
||||
assert.Equal(t, actions_model.StatusRunning, task.Status)
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
err = user_service.DeleteUser(t.Context(), user, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = actions_model.GetRunnerByID(t.Context(), 34350)
|
||||
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||
}
|
|
@ -184,6 +184,15 @@ func testIssueAddComment(t *testing.T, session *TestSession, issueURL, content,
|
|||
return int64(id)
|
||||
}
|
||||
|
||||
func testIssueChangeMilestone(t *testing.T, session *TestSession, repoLink string, issueID, milestoneID int64) {
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf(repoLink+"/issues/milestone?issue_ids=%d", issueID), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
"id": strconv.FormatInt(milestoneID, 10),
|
||||
})
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, `{"ok":true}`, strings.TrimSpace(resp.Body.String()))
|
||||
}
|
||||
|
||||
func TestNewIssue(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
session := loginUser(t, "user2")
|
||||
|
|
|
@ -56,16 +56,21 @@ func TestNewWebHookLink(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func testAPICreateWebhookForRepo(t *testing.T, session *TestSession, userName, repoName, url, event string) {
|
||||
func testAPICreateWebhookForRepo(t *testing.T, session *TestSession, userName, repoName, url, event string, branchFilter ...string) {
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
|
||||
var branchFilterString string
|
||||
if len(branchFilter) > 0 {
|
||||
branchFilterString = branchFilter[0]
|
||||
}
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/"+userName+"/"+repoName+"/hooks", api.CreateHookOption{
|
||||
Type: "gitea",
|
||||
Config: api.CreateHookOptionConfig{
|
||||
"content_type": "json",
|
||||
"url": url,
|
||||
},
|
||||
Events: []string{event},
|
||||
Active: true,
|
||||
Events: []string{event},
|
||||
Active: true,
|
||||
BranchFilter: branchFilterString,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
}
|
||||
|
@ -241,19 +246,68 @@ func Test_WebhookIssueComment(t *testing.T) {
|
|||
|
||||
testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "issue_comment")
|
||||
|
||||
// 2. trigger the webhook
|
||||
issueURL := testNewIssue(t, session, "user2", "repo1", "Title2", "Description2")
|
||||
testIssueAddComment(t, session, issueURL, "issue title2 comment1", "")
|
||||
t.Run("create comment", func(t *testing.T) {
|
||||
// 2. trigger the webhook
|
||||
issueURL := testNewIssue(t, session, "user2", "repo1", "Title2", "Description2")
|
||||
testIssueAddComment(t, session, issueURL, "issue title2 comment1", "")
|
||||
|
||||
// 3. validate the webhook is triggered
|
||||
assert.Equal(t, "issue_comment", triggeredEvent)
|
||||
assert.Len(t, payloads, 1)
|
||||
assert.EqualValues(t, "created", payloads[0].Action)
|
||||
assert.Equal(t, "repo1", payloads[0].Issue.Repo.Name)
|
||||
assert.Equal(t, "user2/repo1", payloads[0].Issue.Repo.FullName)
|
||||
assert.Equal(t, "Title2", payloads[0].Issue.Title)
|
||||
assert.Equal(t, "Description2", payloads[0].Issue.Body)
|
||||
assert.Equal(t, "issue title2 comment1", payloads[0].Comment.Body)
|
||||
// 3. validate the webhook is triggered
|
||||
assert.Equal(t, "issue_comment", triggeredEvent)
|
||||
assert.Len(t, payloads, 1)
|
||||
assert.EqualValues(t, "created", payloads[0].Action)
|
||||
assert.Equal(t, "repo1", payloads[0].Issue.Repo.Name)
|
||||
assert.Equal(t, "user2/repo1", payloads[0].Issue.Repo.FullName)
|
||||
assert.Equal(t, "Title2", payloads[0].Issue.Title)
|
||||
assert.Equal(t, "Description2", payloads[0].Issue.Body)
|
||||
assert.Equal(t, "issue title2 comment1", payloads[0].Comment.Body)
|
||||
})
|
||||
|
||||
t.Run("update comment", func(t *testing.T) {
|
||||
payloads = make([]api.IssueCommentPayload, 0, 2)
|
||||
triggeredEvent = ""
|
||||
|
||||
// 2. trigger the webhook
|
||||
issueURL := testNewIssue(t, session, "user2", "repo1", "Title3", "Description3")
|
||||
commentID := testIssueAddComment(t, session, issueURL, "issue title3 comment1", "")
|
||||
modifiedContent := "issue title2 comment1 - modified"
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
"content": modifiedContent,
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// 3. validate the webhook is triggered
|
||||
assert.Equal(t, "issue_comment", triggeredEvent)
|
||||
assert.Len(t, payloads, 2)
|
||||
assert.EqualValues(t, "edited", payloads[1].Action)
|
||||
assert.Equal(t, "repo1", payloads[1].Issue.Repo.Name)
|
||||
assert.Equal(t, "user2/repo1", payloads[1].Issue.Repo.FullName)
|
||||
assert.Equal(t, "Title3", payloads[1].Issue.Title)
|
||||
assert.Equal(t, "Description3", payloads[1].Issue.Body)
|
||||
assert.Equal(t, modifiedContent, payloads[1].Comment.Body)
|
||||
})
|
||||
|
||||
t.Run("Update comment with no content change", func(t *testing.T) {
|
||||
payloads = make([]api.IssueCommentPayload, 0, 2)
|
||||
triggeredEvent = ""
|
||||
commentContent := "issue title3 comment1"
|
||||
|
||||
// 2. trigger the webhook
|
||||
issueURL := testNewIssue(t, session, "user2", "repo1", "Title3", "Description3")
|
||||
commentID := testIssueAddComment(t, session, issueURL, commentContent, "")
|
||||
|
||||
payloads = make([]api.IssueCommentPayload, 0, 2)
|
||||
triggeredEvent = ""
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
"content": commentContent,
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// 3. validate the webhook is not triggered because no content change
|
||||
assert.Empty(t, triggeredEvent)
|
||||
assert.Empty(t, payloads)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -322,6 +376,45 @@ func Test_WebhookPush(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func Test_WebhookPushDevBranch(t *testing.T) {
|
||||
var payloads []api.PushPayload
|
||||
var triggeredEvent string
|
||||
provider := newMockWebhookProvider(func(r *http.Request) {
|
||||
content, _ := io.ReadAll(r.Body)
|
||||
var payload api.PushPayload
|
||||
err := json.Unmarshal(content, &payload)
|
||||
assert.NoError(t, err)
|
||||
payloads = append(payloads, payload)
|
||||
triggeredEvent = "push"
|
||||
}, http.StatusOK)
|
||||
defer provider.Close()
|
||||
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
// 1. create a new webhook with special webhook for repo1
|
||||
session := loginUser(t, "user2")
|
||||
|
||||
// only for dev branch
|
||||
testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "push", "develop")
|
||||
|
||||
// 2. this should not trigger the webhook
|
||||
testCreateFile(t, session, "user2", "repo1", "master", "test_webhook_push.md", "# a test file for webhook push")
|
||||
assert.Empty(t, triggeredEvent)
|
||||
assert.Empty(t, payloads)
|
||||
|
||||
// 3. trigger the webhook
|
||||
testCreateFile(t, session, "user2", "repo1", "develop", "test_webhook_push.md", "# a test file for webhook push")
|
||||
|
||||
// 4. validate the webhook is triggered
|
||||
assert.Equal(t, "push", triggeredEvent)
|
||||
assert.Len(t, payloads, 1)
|
||||
assert.Equal(t, "repo1", payloads[0].Repo.Name)
|
||||
assert.Equal(t, "develop", payloads[0].Branch())
|
||||
assert.Equal(t, "user2/repo1", payloads[0].Repo.FullName)
|
||||
assert.Len(t, payloads[0].Commits, 1)
|
||||
assert.Equal(t, []string{"test_webhook_push.md"}, payloads[0].Commits[0].Added)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_WebhookIssue(t *testing.T) {
|
||||
var payloads []api.IssuePayload
|
||||
var triggeredEvent string
|
||||
|
@ -355,6 +448,78 @@ func Test_WebhookIssue(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func Test_WebhookIssueMilestone(t *testing.T) {
|
||||
var payloads []api.IssuePayload
|
||||
var triggeredEvent string
|
||||
provider := newMockWebhookProvider(func(r *http.Request) {
|
||||
content, _ := io.ReadAll(r.Body)
|
||||
var payload api.IssuePayload
|
||||
err := json.Unmarshal(content, &payload)
|
||||
assert.NoError(t, err)
|
||||
payloads = append(payloads, payload)
|
||||
triggeredEvent = "issues"
|
||||
}, http.StatusOK)
|
||||
defer provider.Close()
|
||||
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
// create a new webhook with special webhook for repo1
|
||||
session := loginUser(t, "user2")
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1})
|
||||
testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "issue_milestone")
|
||||
|
||||
t.Run("assign a milestone", func(t *testing.T) {
|
||||
// trigger the webhook
|
||||
testIssueChangeMilestone(t, session, repo1.Link(), 1, 1)
|
||||
|
||||
// validate the webhook is triggered
|
||||
assert.Equal(t, "issues", triggeredEvent)
|
||||
assert.Len(t, payloads, 1)
|
||||
assert.Equal(t, "milestoned", string(payloads[0].Action))
|
||||
assert.Equal(t, "repo1", payloads[0].Issue.Repo.Name)
|
||||
assert.Equal(t, "user2/repo1", payloads[0].Issue.Repo.FullName)
|
||||
assert.Equal(t, "issue1", payloads[0].Issue.Title)
|
||||
assert.Equal(t, "content for the first issue", payloads[0].Issue.Body)
|
||||
assert.EqualValues(t, 1, payloads[0].Issue.Milestone.ID)
|
||||
})
|
||||
|
||||
t.Run("change a milestong", func(t *testing.T) {
|
||||
// trigger the webhook again
|
||||
triggeredEvent = ""
|
||||
payloads = make([]api.IssuePayload, 0, 1)
|
||||
// change milestone to 2
|
||||
testIssueChangeMilestone(t, session, repo1.Link(), 1, 2)
|
||||
|
||||
// validate the webhook is triggered
|
||||
assert.Equal(t, "issues", triggeredEvent)
|
||||
assert.Len(t, payloads, 1)
|
||||
assert.Equal(t, "milestoned", string(payloads[0].Action))
|
||||
assert.Equal(t, "repo1", payloads[0].Issue.Repo.Name)
|
||||
assert.Equal(t, "user2/repo1", payloads[0].Issue.Repo.FullName)
|
||||
assert.Equal(t, "issue1", payloads[0].Issue.Title)
|
||||
assert.Equal(t, "content for the first issue", payloads[0].Issue.Body)
|
||||
assert.EqualValues(t, 2, payloads[0].Issue.Milestone.ID)
|
||||
})
|
||||
|
||||
t.Run("remove a milestone", func(t *testing.T) {
|
||||
// trigger the webhook again
|
||||
triggeredEvent = ""
|
||||
payloads = make([]api.IssuePayload, 0, 1)
|
||||
// change milestone to 0
|
||||
testIssueChangeMilestone(t, session, repo1.Link(), 1, 0)
|
||||
|
||||
// validate the webhook is triggered
|
||||
assert.Equal(t, "issues", triggeredEvent)
|
||||
assert.Len(t, payloads, 1)
|
||||
assert.Equal(t, "demilestoned", string(payloads[0].Action))
|
||||
assert.Equal(t, "repo1", payloads[0].Issue.Repo.Name)
|
||||
assert.Equal(t, "user2/repo1", payloads[0].Issue.Repo.FullName)
|
||||
assert.Equal(t, "issue1", payloads[0].Issue.Title)
|
||||
assert.Equal(t, "content for the first issue", payloads[0].Issue.Body)
|
||||
assert.Nil(t, payloads[0].Issue.Milestone)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func Test_WebhookPullRequest(t *testing.T) {
|
||||
var payloads []api.PullRequestPayload
|
||||
var triggeredEvent string
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
.project-header {
|
||||
padding: 0.5em 0;
|
||||
overflow-x: auto; /* in fullscreen mode, the position is fixed, so we can't use "flex wrap" which would change the height */
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.project-header h2 {
|
||||
|
@ -101,17 +101,11 @@
|
|||
opacity: 0;
|
||||
}
|
||||
|
||||
.fullscreen.projects-view .project-header {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 0.5em;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
background-color: var(--color-body);
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
.fullscreen.projects-view {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Hide project-description in full-screen due to its variable height. No need to show it for better space use. */
|
||||
|
@ -120,9 +114,7 @@
|
|||
}
|
||||
|
||||
.fullscreen.projects-view #project-board {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-height: calc(100vh - 70px);
|
||||
flex: 1;
|
||||
max-height: unset;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
|
|
@ -161,6 +161,7 @@ export function autosize(textarea: HTMLTextAreaElement, {viewportMarginBottom =
|
|||
function resizeToFit() {
|
||||
if (isUserResized) return;
|
||||
if (textarea.offsetWidth <= 0 && textarea.offsetHeight <= 0) return;
|
||||
const previousMargin = textarea.style.marginBottom;
|
||||
|
||||
try {
|
||||
const {top, bottom} = overflowOffset();
|
||||
|
@ -176,6 +177,9 @@ export function autosize(textarea: HTMLTextAreaElement, {viewportMarginBottom =
|
|||
const curHeight = parseFloat(computedStyle.height);
|
||||
const maxHeight = curHeight + bottom - adjustedViewportMarginBottom;
|
||||
|
||||
// In Firefox, setting auto height momentarily may cause the page to scroll up
|
||||
// unexpectedly, prevent this by setting a temporary margin.
|
||||
textarea.style.marginBottom = `${textarea.clientHeight}px`;
|
||||
textarea.style.height = 'auto';
|
||||
let newHeight = textarea.scrollHeight + borderAddOn;
|
||||
|
||||
|
@ -196,6 +200,12 @@ export function autosize(textarea: HTMLTextAreaElement, {viewportMarginBottom =
|
|||
textarea.style.height = `${newHeight}px`;
|
||||
lastStyleHeight = textarea.style.height;
|
||||
} finally {
|
||||
// restore previous margin
|
||||
if (previousMargin) {
|
||||
textarea.style.marginBottom = previousMargin;
|
||||
} else {
|
||||
textarea.style.removeProperty('margin-bottom');
|
||||
}
|
||||
// ensure that the textarea is fully scrolled to the end, when the cursor
|
||||
// is at the end during an input event
|
||||
if (textarea.selectionStart === textarea.selectionEnd &&
|
||||
|
|
Loading…
Reference in New Issue