Merge branch 'main' into label_delete_event_fix

pull/34316/head
Sumit 2025-05-21 22:38:50 +05:30 committed by GitHub
commit 6aa8ba38cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
99 changed files with 2018 additions and 344 deletions

View File

@ -80,6 +80,11 @@ wiki, issues, labels, releases, release_assets, milestones, pull_requests, comme
} }
func runDumpRepository(ctx *cli.Context) error { 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() stdCtx, cancel := installSignals()
defer cancel() defer cancel()

View File

@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1739214665, "lastModified": 1747179050,
"narHash": "sha256-26L8VAu3/1YRxS8MHgBOyOM8xALdo6N0I04PgorE7UM=", "narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "64e75cd44acf21c7933d61d7721e812eac1b5a0a", "rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
"type": "github" "type": "github"
}, },
"original": { "original": {

2
go.mod
View File

@ -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/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 // 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 replace github.com/charmbracelet/git-lfs-transfer => gitea.com/gitea/git-lfs-transfer v0.2.0

4
go.sum
View File

@ -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= 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 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 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.6 h1:CjZwKOyejonNFDmsXOw3wGm5Vet573hHM6VMLsxtvPY=
gitea.com/gitea/act v0.261.4/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok= 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 h1:baHaNoBSRaeq/xKayEXwiDQtlIjps4Ac/Ll4KqLMB40=
gitea.com/gitea/git-lfs-transfer v0.2.0/go.mod h1:UrXUCm3xLQkq15fu7qlXHUMlrhdlXHoi13KH2Dfiits= 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= gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4=

View File

@ -16,6 +16,7 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "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 { func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error {
_, err := db.GetEngine(ctx).ID(repo.ID). _, err := db.GetEngine(ctx).ID(repo.ID).
NoAutoTime().
SetExpr("num_action_runs", SetExpr("num_action_runs",
builder.Select("count(*)").From("action_run"). builder.Select("count(*)").From("action_run").
Where(builder.Eq{"repo_id": repo.ID}), Where(builder.Eq{"repo_id": repo.ID}),
@ -342,13 +344,13 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork
return committer.Commit() 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 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 { if err != nil {
return nil, err return nil, err
} else if !has { } 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 return &run, nil
@ -419,18 +421,11 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error {
if run.Status != 0 || slices.Contains(cols, "status") { if run.Status != 0 || slices.Contains(cols, "status") {
if run.RepoID == 0 { if run.RepoID == 0 {
run, err = GetRunByID(ctx, run.ID) setting.PanicInDevOrTesting("RepoID should not be 0")
if err != nil { }
if err = run.LoadRepo(ctx); err != nil {
return err return err
} }
}
if run.Repo == nil {
repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID)
if err != nil {
return err
}
run.Repo = repo
}
if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil { if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil {
return err return err
} }

View File

@ -51,7 +51,7 @@ func (job *ActionRunJob) Duration() time.Duration {
func (job *ActionRunJob) LoadRun(ctx context.Context) error { func (job *ActionRunJob) LoadRun(ctx context.Context) error {
if job.Run == nil { if job.Run == nil {
run, err := GetRunByID(ctx, job.RunID) run, err := GetRunByRepoAndID(ctx, job.RepoID, job.RunID)
if err != nil { if err != nil {
return err 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. // 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. // 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 { if err != nil {
return 0, err return 0, err
} }

View File

@ -5,6 +5,7 @@ package actions
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@ -298,6 +299,23 @@ func DeleteRunner(ctx context.Context, id int64) error {
return err 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. // CreateRunner creates new runner.
func CreateRunner(ctx context.Context, t *ActionRunner) error { func CreateRunner(ctx context.Context, t *ActionRunner) error {
if t.OwnerID != 0 && t.RepoID != 0 { if t.OwnerID != 0 && t.RepoID != 0 {

View File

@ -336,6 +336,11 @@ func UpdateTask(ctx context.Context, task *ActionTask, cols ...string) error {
sess.Cols(cols...) sess.Cols(cols...)
} }
_, err := sess.Update(task) _, 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 return err
} }

View File

@ -48,6 +48,7 @@ func (tasks TaskList) LoadAttributes(ctx context.Context) error {
type FindTaskOptions struct { type FindTaskOptions struct {
db.ListOptions db.ListOptions
RepoID int64 RepoID int64
JobID int64
OwnerID int64 OwnerID int64
CommitSHA string CommitSHA string
Status Status Status Status
@ -61,6 +62,9 @@ func (opts FindTaskOptions) ToConds() builder.Cond {
if opts.RepoID > 0 { if opts.RepoID > 0 {
cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) 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 { if opts.OwnerID > 0 {
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
} }

View File

@ -82,3 +82,22 @@ func calculateDuration(started, stopped timeutil.TimeStamp, status Status) time.
} }
return timeSince(s).Truncate(time.Second) 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,
}
}

View File

@ -530,7 +530,7 @@ func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.
if opts.RequestedTeam != nil { if opts.RequestedTeam != nil {
env := repo_model.AccessibleTeamReposEnv(organization.OrgFromUser(opts.RequestedUser), opts.RequestedTeam) 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 { if err != nil {
return nil, fmt.Errorf("GetTeamRepositories: %w", err) return nil, fmt.Errorf("GetTeamRepositories: %w", err)
} }

View File

@ -105,3 +105,39 @@
created_unix: 1730330775 created_unix: 1730330775
updated_unix: 1730330775 updated_unix: 1730330775
expired_unix: 1738106775 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

View File

@ -48,7 +48,7 @@
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
event: "push" event: "push"
is_fork_pull_request: 0 is_fork_pull_request: 0
status: 1 status: 6 # running
started: 1683636528 started: 1683636528
stopped: 1683636626 stopped: 1683636626
created: 1683636108 created: 1683636108
@ -74,3 +74,23 @@
updated: 1683636626 updated: 1683636626
need_approval: 0 need_approval: 0
approved_by: 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

View File

@ -69,3 +69,33 @@
status: 5 status: 5
started: 1683636528 started: 1683636528
stopped: 1683636626 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

View File

@ -38,3 +38,14 @@
repo_id: 0 repo_id: 0
description: "This runner is going to be deleted" description: "This runner is going to be deleted"
agent_labels: '["runner_to_be_deleted","linux"]' 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"]'

View File

@ -117,3 +117,63 @@
log_length: 707 log_length: 707
log_size: 90179 log_size: 90179
log_expired: 0 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

View File

@ -14,5 +14,8 @@ func AddIndexToActionTaskStoppedLogExpired(x *xorm.Engine) error {
Stopped timeutil.TimeStamp `xorm:"index(stopped_log_expired)"` Stopped timeutil.TimeStamp `xorm:"index(stopped_log_expired)"`
LogExpired bool `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
} }

View File

@ -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))
}

View File

@ -9,5 +9,8 @@ func AddIndexForReleaseSha1(x *xorm.Engine) error {
type Release struct { type Release struct {
Sha1 string `xorm:"INDEX VARCHAR(64)"` Sha1 string `xorm:"INDEX VARCHAR(64)"`
} }
return x.Sync(new(Release)) _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
}, new(Release))
return err
} }

View File

@ -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))
}

View File

@ -334,7 +334,7 @@ func TestAccessibleReposEnv_RepoIDs(t *testing.T) {
testSuccess := func(userID int64, expectedRepoIDs []int64) { testSuccess := func(userID int64, expectedRepoIDs []int64) {
env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID) env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID)
assert.NoError(t, err) assert.NoError(t, err)
repoIDs, err := env.RepoIDs(db.DefaultContext, 1, 100) repoIDs, err := env.RepoIDs(db.DefaultContext)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expectedRepoIDs, repoIDs) assert.Equal(t, expectedRepoIDs, repoIDs)
} }
@ -342,25 +342,6 @@ func TestAccessibleReposEnv_RepoIDs(t *testing.T) {
testSuccess(4, []int64{3, 32}) 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) { func TestAccessibleReposEnv_MirrorRepos(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})

View File

@ -48,8 +48,7 @@ func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (Repo
// accessible to a particular user // accessible to a particular user
type AccessibleReposEnvironment interface { type AccessibleReposEnvironment interface {
CountRepos(ctx context.Context) (int64, error) CountRepos(ctx context.Context) (int64, error)
RepoIDs(ctx context.Context, page, pageSize int) ([]int64, error) RepoIDs(ctx context.Context) ([]int64, error)
Repos(ctx context.Context, page, pageSize int) (RepositoryList, error)
MirrorRepos(ctx context.Context) (RepositoryList, error) MirrorRepos(ctx context.Context) (RepositoryList, error)
AddKeyword(keyword string) AddKeyword(keyword string)
SetSort(db.SearchOrderBy) SetSort(db.SearchOrderBy)
@ -132,40 +131,18 @@ func (env *accessibleReposEnv) CountRepos(ctx context.Context) (int64, error) {
return repoCount, nil return repoCount, nil
} }
func (env *accessibleReposEnv) RepoIDs(ctx context.Context, page, pageSize int) ([]int64, error) { func (env *accessibleReposEnv) RepoIDs(ctx context.Context) ([]int64, error) {
if page <= 0 { var repoIDs []int64
page = 1
}
repoIDs := make([]int64, 0, pageSize)
return repoIDs, db.GetEngine(ctx). return repoIDs, db.GetEngine(ctx).
Table("repository"). Table("repository").
Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id"). Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id").
Where(env.cond()). 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)). OrderBy(string(env.orderBy)).
Limit(pageSize, (page-1)*pageSize).
Cols("`repository`.id"). Cols("`repository`.id").
Find(&repoIDs) 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) { func (env *accessibleReposEnv) MirrorRepoIDs(ctx context.Context) ([]int64, error) {
repoIDs := make([]int64, 0, 10) repoIDs := make([]int64, 0, 10)
return repoIDs, db.GetEngine(ctx). return repoIDs, db.GetEngine(ctx).

View File

@ -249,7 +249,7 @@ func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_m
} }
repo.Status = RepositoryPendingTransfer repo.Status = RepositoryPendingTransfer
if err := UpdateRepositoryCols(ctx, repo, "status"); err != nil { if err := UpdateRepositoryColsNoAutoTime(ctx, repo, "status"); err != nil {
return err return err
} }

View File

@ -25,7 +25,7 @@ func UpdateRepositoryOwnerNames(ctx context.Context, ownerID int64, ownerName st
} }
defer committer.Close() 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, OwnerName: ownerName,
}); err != nil { }); err != nil {
return err return err
@ -40,8 +40,8 @@ func UpdateRepositoryUpdatedTime(ctx context.Context, repoID int64, updateTime t
return err return err
} }
// UpdateRepositoryCols updates repository's columns // UpdateRepositoryColsWithAutoTime updates repository's columns
func UpdateRepositoryCols(ctx context.Context, repo *Repository, cols ...string) error { func UpdateRepositoryColsWithAutoTime(ctx context.Context, repo *Repository, cols ...string) error {
_, err := db.GetEngine(ctx).ID(repo.ID).Cols(cols...).Update(repo) _, err := db.GetEngine(ctx).ID(repo.ID).Cols(cols...).Update(repo)
return err return err
} }

View File

@ -166,6 +166,8 @@ type CommitsCountOptions struct {
Not string Not string
Revision []string Revision []string
RelPath []string RelPath []string
Since string
Until string
} }
// CommitsCount returns number of total commits of until given revision. // 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 // 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) { func (c *Commit) CommitsByRange(page, pageSize int, not, since, until string) ([]*Commit, error) {
return c.repo.commitsByRange(c.ID, page, pageSize, not) return c.repo.commitsByRangeWithTime(c.ID, page, pageSize, not, since, until)
} }
// CommitsBefore returns all the commits before current revision // CommitsBefore returns all the commits before current revision

View File

@ -89,7 +89,8 @@ func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) {
return commits[0], nil 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"). cmd := NewCommand("log").
AddOptionFormat("--skip=%d", (page-1)*pageSize). AddOptionFormat("--skip=%d", (page-1)*pageSize).
AddOptionFormat("--max-count=%d", pageSize). AddOptionFormat("--max-count=%d", pageSize).
@ -99,6 +100,12 @@ func (repo *Repository) commitsByRange(id ObjectID, page, pageSize int, not stri
if not != "" { if not != "" {
cmd.AddOptionValues("--not", 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}) stdout, _, err := cmd.RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path})
if err != nil { if err != nil {
@ -212,6 +219,8 @@ type CommitsByFileAndRangeOptions struct {
File string File string
Not string Not string
Page int Page int
Since string
Until string
} }
// CommitsByFileAndRange return the commits according revision file and the page // CommitsByFileAndRange return the commits according revision file and the page
@ -231,6 +240,12 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions)
if opts.Not != "" { if opts.Not != "" {
gitCmd.AddOptionValues("--not", 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) gitCmd.AddDashesAndList(opts.File)
err := gitCmd.Run(repo.Ctx, &RunOpts{ err := gitCmd.Run(repo.Ctx, &RunOpts{

View File

@ -40,7 +40,9 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string)
since := fromTime.Format(time.RFC3339) 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 { if runErr != nil {
return nil, runErr return nil, runErr
} }
@ -60,7 +62,8 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string)
_ = stdoutWriter.Close() _ = 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 { if len(branch) == 0 {
gitCmd.AddArguments("--branches=*") gitCmd.AddArguments("--branches=*")
} else { } else {

View File

@ -200,5 +200,3 @@ func TestListToPushCommits(t *testing.T) {
assert.Equal(t, now, pushCommits.Commits[1].Timestamp) assert.Equal(t, now, pushCommits.Commits[1].Timestamp)
} }
} }
// TODO TestPushUpdate

View File

@ -101,6 +101,8 @@ type Repository struct {
AllowSquash bool `json:"allow_squash_merge"` AllowSquash bool `json:"allow_squash_merge"`
AllowFastForwardOnly bool `json:"allow_fast_forward_only_merge"` AllowFastForwardOnly bool `json:"allow_fast_forward_only_merge"`
AllowRebaseUpdate bool `json:"allow_rebase_update"` 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"` DefaultDeleteBranchAfterMerge bool `json:"default_delete_branch_after_merge"`
DefaultMergeStyle string `json:"default_merge_style"` DefaultMergeStyle string `json:"default_merge_style"`
DefaultAllowMaintainerEdit bool `json:"default_allow_maintainer_edit"` DefaultAllowMaintainerEdit bool `json:"default_allow_maintainer_edit"`

View File

@ -66,6 +66,7 @@ Piotr Orzechowski <piotr AT orzechowski DOT tech>
Richard Bukovansky <richard DOT bukovansky AT gmail DOT com> Richard Bukovansky <richard DOT bukovansky AT gmail DOT com>
Robert Nuske <robert DOT nuske AT web DOT de> Robert Nuske <robert DOT nuske AT web DOT de>
Robin Hübner <profan AT prfn DOT se> Robin Hübner <profan AT prfn DOT se>
Ryo Hanafusa <ryo7gumi AT gmail DOT com>
SeongJae Park <sj38 DOT park AT gmail DOT com> SeongJae Park <sj38 DOT park AT gmail DOT com>
Thiago Avelino <thiago AT avelino DOT xxx> Thiago Avelino <thiago AT avelino DOT xxx>
Thomas Fanninger <gogs DOT thomas AT fanninger DOT at> Thomas Fanninger <gogs DOT thomas AT fanninger DOT at>

View File

@ -1228,6 +1228,7 @@ migrate.migrating_issues = Migrating Issues
migrate.migrating_pulls = Migrating Pull Requests migrate.migrating_pulls = Migrating Pull Requests
migrate.cancel_migrating_title = Cancel Migration migrate.cancel_migrating_title = Cancel Migration
migrate.cancel_migrating_confirm = Do you want to cancel this migration? migrate.cancel_migrating_confirm = Do you want to cancel this migration?
migrating_status = Migrating status
mirror_from = mirror of mirror_from = mirror of
forked_from = forked from 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.no_runs = The workflow has no runs yet.
runs.empty_commit_message = (empty commit message) runs.empty_commit_message = (empty commit message)
runs.expire_log_message = Logs have been purged because they were too old. 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 = Disable Workflow
workflow.disable_success = Workflow '%s' disabled successfully. workflow.disable_success = Workflow '%s' disabled successfully.

View File

@ -3728,7 +3728,11 @@ creation.name_placeholder=Caractères alphanumériques ou tirets bas uniquement,
creation.value_placeholder=Entrez nimporte quoi. Les blancs cernant seront taillés. creation.value_placeholder=Entrez nimporte quoi. Les blancs cernant seront taillés.
creation.description_placeholder=Décrire brièvement votre dépôt (optionnel). creation.description_placeholder=Décrire brièvement votre dépôt (optionnel).
save_success=Le secret « %s » a été enregistré.
save_failed=Impossible denregistrer le secret.
add_secret=Ajouter un secret
edit_secret=Modifier le secret
deletion=Supprimer le secret deletion=Supprimer le secret
deletion.description=La suppression d'un secret est permanente et irréversible. Continuer ? deletion.description=La suppression d'un secret est permanente et irréversible. Continuer ?
deletion.success=Le secret a été supprimé. deletion.success=Le secret a été supprimé.
@ -3806,6 +3810,9 @@ runs.no_workflows.documentation=Pour plus dinformations sur les actions Gitea
runs.no_runs=Le flux de travail n'a pas encore d'exécution. runs.no_runs=Le flux de travail n'a pas encore d'exécution.
runs.empty_commit_message=(message de révision vide) 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.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 nest pas terminée.
workflow.disable=Désactiver le flux de travail workflow.disable=Désactiver le flux de travail
workflow.disable_success=Le flux de travail « %s » a bien été désactivé. workflow.disable_success=Le flux de travail « %s » a bien été désactivé.

View File

@ -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.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). 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=Bain rún
deletion.description=Is buan rún a bhaint agus ní féidir é a chealú. Lean ort? deletion.description=Is buan rún a bhaint agus ní féidir é a chealú. Lean ort?
deletion.success=Tá an rún bainte. 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.no_runs=Níl aon rith ag an sreabhadh oibre fós.
runs.empty_commit_message=(teachtaireacht tiomantas folamh) runs.empty_commit_message=(teachtaireacht tiomantas folamh)
runs.expire_log_message=Glanadh logaí toisc go raibh siad ró-sean. 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=Díchumasaigh sreabhadh oibre
workflow.disable_success=D'éirigh le sreabhadh oibre '%s' a dhíchumasú. workflow.disable_success=D'éirigh le sreabhadh oibre '%s' a dhíchumasú.

View File

@ -450,6 +450,7 @@ use_scratch_code=スクラッチコードを使う
twofa_scratch_used=あなたはスクラッチコードを使用しました。 2要素認証の設定ページにリダイレクトしましたので、デバイスの登録を解除するか、新しいスクラッチコードを生成しましょう。 twofa_scratch_used=あなたはスクラッチコードを使用しました。 2要素認証の設定ページにリダイレクトしましたので、デバイスの登録を解除するか、新しいスクラッチコードを生成しましょう。
twofa_passcode_incorrect=パスコードが正しくありません。デバイスを紛失した場合は、スクラッチコードを使ってサインインしてください。 twofa_passcode_incorrect=パスコードが正しくありません。デバイスを紛失した場合は、スクラッチコードを使ってサインインしてください。
twofa_scratch_token_incorrect=スクラッチコードが正しくありません。 twofa_scratch_token_incorrect=スクラッチコードが正しくありません。
twofa_required=リポジトリにアクセスするには2段階認証を設定するか、再度ログインしてください。
login_userpass=サインイン login_userpass=サインイン
login_openid=OpenID login_openid=OpenID
oauth_signup_tab=新規アカウント登録 oauth_signup_tab=新規アカウント登録
@ -1878,6 +1879,7 @@ pulls.add_prefix=先頭に <strong>%s</strong> を追加
pulls.remove_prefix=先頭の <strong>%s</strong> を除去 pulls.remove_prefix=先頭の <strong>%s</strong> を除去
pulls.data_broken=このプルリクエストは、フォークの情報が見つからないため壊れています。 pulls.data_broken=このプルリクエストは、フォークの情報が見つからないため壊れています。
pulls.files_conflicted=このプルリクエストは、ターゲットブランチと競合する変更を含んでいます。 pulls.files_conflicted=このプルリクエストは、ターゲットブランチと競合する変更を含んでいます。
pulls.is_checking=マージの競合を確認しています...
pulls.is_ancestor=このブランチは既にターゲットブランチに含まれています。マージするものはありません。 pulls.is_ancestor=このブランチは既にターゲットブランチに含まれています。マージするものはありません。
pulls.is_empty=このブランチの変更は既にターゲットブランチにあります。これは空のコミットになります。 pulls.is_empty=このブランチの変更は既にターゲットブランチにあります。これは空のコミットになります。
pulls.required_status_check_failed=いくつかの必要なステータスチェックが成功していません。 pulls.required_status_check_failed=いくつかの必要なステータスチェックが成功していません。
@ -3725,7 +3727,11 @@ creation.name_placeholder=大文字小文字の区別なし、英数字とアン
creation.value_placeholder=内容を入力してください。前後の空白は除去されます。 creation.value_placeholder=内容を入力してください。前後の空白は除去されます。
creation.description_placeholder=簡単な説明を入力してください。 (オプション) creation.description_placeholder=簡単な説明を入力してください。 (オプション)
save_success=シークレット "%s" を保存しました。
save_failed=シークレットの保存に失敗しました。
add_secret=シークレットを追加
edit_secret=シークレットを編集
deletion=シークレットの削除 deletion=シークレットの削除
deletion.description=シークレットの削除は恒久的で元に戻すことはできません。 続行しますか? deletion.description=シークレットの削除は恒久的で元に戻すことはできません。 続行しますか?
deletion.success=シークレットを削除しました。 deletion.success=シークレットを削除しました。
@ -3842,6 +3848,8 @@ deleted.display_name=削除されたプロジェクト
type-1.display_name=個人プロジェクト type-1.display_name=個人プロジェクト
type-2.display_name=リポジトリ プロジェクト type-2.display_name=リポジトリ プロジェクト
type-3.display_name=組織プロジェクト type-3.display_name=組織プロジェクト
enter_fullscreen=フルスクリーン
exit_fullscreen=フルスクリーンを終了
[git.filemode] [git.filemode]
changed_filemode=%[1]s → %[2]s changed_filemode=%[1]s → %[2]s

View File

@ -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.no_runs=A sequência de trabalho ainda não foi executada.
runs.empty_commit_message=(mensagem de cometimento vazia) runs.empty_commit_message=(mensagem de cometimento vazia)
runs.expire_log_message=Os registros foram removidos porque eram muito antigos. 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=Desabilitar sequência de trabalho
workflow.disable_success=A sequência de trabalho '%s' foi desabilitada com sucesso. workflow.disable_success=A sequência de trabalho '%s' foi desabilitada com sucesso.

View File

@ -113,9 +113,11 @@ copy_type_unsupported=Bu dosya türü kopyalanamaz
write=Yaz write=Yaz
preview=Önizleme preview=Önizleme
loading=Yükleniyor… loading=Yükleniyor…
files=Dosyalar
error=Hata error=Hata
error404=Ulaşmaya çalıştığınız sayfa <strong>mevcut değil</strong> veya <strong>görüntüleme yetkiniz yok</strong>. 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 go_back=Geri Git
invalid_data=Geçersiz veri: %v invalid_data=Geçersiz veri: %v
@ -128,6 +130,7 @@ pin=Sabitle
unpin=Sabitlemeyi kaldır unpin=Sabitlemeyi kaldır
artifacts=Yapılar artifacts=Yapılar
expired=Süresi doldu
confirm_delete_artifact=%s yapısını silmek istediğinizden emin misiniz? confirm_delete_artifact=%s yapısını silmek istediğinizden emin misiniz?
archived=Arşivlenmiş archived=Arşivlenmiş
@ -169,6 +172,10 @@ search=Ara...
type_tooltip=Arama türü type_tooltip=Arama türü
fuzzy=Bulanık fuzzy=Bulanık
fuzzy_tooltip=Arama terimine benzeyen sonuçları da içer 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=Tam
exact_tooltip=Sadece arama terimiyle tamamen eşleşen sonuçları içer exact_tooltip=Sadece arama terimiyle tamamen eşleşen sonuçları içer
repo_kind=Depoları ara... repo_kind=Depoları ara...
@ -235,13 +242,17 @@ network_error=Ağ hatası
[startpage] [startpage]
app_desc=Zahmetsiz, kendi sunucunuzda barındırabileceğiniz Git servisi app_desc=Zahmetsiz, kendi sunucunuzda barındırabileceğiniz Git servisi
install=Kurulumu kolay 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=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=Hafif
lightweight_desc=Gitea'nın minimal gereksinimleri çok düşüktür ve ucuz bir Raspberry Pi üzerinde çalışabilmektedir. Makine enerjinizden tasarruf edin! lightweight_desc=Gitea'nın minimal gereksinimleri çok düşüktür ve ucuz bir Raspberry Pi üzerinde çalışabilmektedir. Makine enerjinizden tasarruf edin!
license=ık Kaynak license=ı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]
install=Kurulum install=Kurulum
installing_desc=Şimdi kuruluyor, lütfen bekleyin...
title=Başlangıç Yapılandırması 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. 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. 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. 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=Ortam Yapılandırma
env_config_keys_prompt=Aşağıdaki ortam değişkenleri de yapılandırma dosyanıza eklenecektir: 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] [home]
nav_menu=Gezinti Menüsü 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 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] [explore]
repos=Depolar 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_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_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_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_userpass=Oturum Aç
login_openid=ık Kimlik login_openid=ık Kimlik
oauth_signup_tab=Yeni Hesap Oluştur 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_tab=Mevcut Hesaba Bağla
oauth_signin_title=Bağlantılı Hesabı Yetkilendirmek için Giriş Yapın oauth_signin_title=Bağlantılı Hesabı Yetkilendirmek için Giriş Yapın
oauth_signin_submit=Hesabı Bağla 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.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.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. 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_redirect_notice=Bu uygulamayı yetkilendirirseniz %s adresine yönlendirileceksiniz.
authorize_application_created_by=Bu uygulama %s tarafından oluşturuldu. 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_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? authorize_title=Hesabınıza erişmesi için "%s" yetkilendirilsin mi?
authorization_failed=Yetkilendirme başarısız oldu 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. 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 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ı password_pwned_err=HaveIBeenPwned'e yapılan istek tamamlanamadı
last_admin=Son yöneticiyi silemezsiniz. En azından bir yönetici olmalıdır. last_admin=Son yöneticiyi silemezsiniz. En azından bir yönetici olmalıdır.
signin_passkey=Bir parola anahtarı ile oturum aç 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_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. 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 username_has_not_been_changed=Kullanıcı adı değişmedi
repo_name_been_taken=Depo adı zaten kullanılıyor. repo_name_been_taken=Depo adı zaten kullanılıyor.
repository_force_private=Gizliyi Zorla devrede: gizli depolar herkese açık yapılamaz. 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. 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_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. 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 orgs=Organizasyonları Yönet
repos=Depolar repos=Depolar
delete=Hesabı Sil delete=Hesabı Sil
twofa=İki Aşamalı Kimlik Doğrulama (TOTP)
account_link=Bağlı Hesaplar account_link=Bağlı Hesaplar
organization=Organizasyonlar organization=Organizasyonlar
uid=UID uid=UID
webauthn=İki-Aşamalı Kimlik Doğrulama (Güvenlik Anahtarları)
public_profile=Herkese Açık Profil public_profile=Herkese Açık Profil
biography_placeholder=Bize kendiniz hakkında birşeyler söyleyin! (Markdown kullanabilirsiniz) biography_placeholder=Bize kendiniz hakkında birşeyler söyleyin! (Markdown kullanabilirsiniz)
location_placeholder=Yaklaşık konumunuzu başkalarıyla paylaşın 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. 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 full_name=Ad Soyad
website=Web Sitesi website=Web Sitesi
location=Konum 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. 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_avatar_success=Profil resminiz değiştirildi.
update_user_avatar_success=Kullanıcının avatarı güncellendi. 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 change_password=Parolayı Güncelle
old_password=Mevcut Parola 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ı. email_preference_set_success=E-posta tercihi başarıyla ayarlandı.
add_openid_success=Yeni OpenID adresi eklendi. add_openid_success=Yeni OpenID adresi eklendi.
keep_email_private=E-posta Adresini Gizle 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. 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 manage_ssh_keys=SSH Anahtarlarını Yönet
@ -898,6 +929,9 @@ permission_not_set=Ayarlanmadı
permission_no_access=Erişim Yok permission_no_access=Erişim Yok
permission_read=Okunmuş permission_read=Okunmuş
permission_write=Okuma ve Yazma 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. 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 at_least_one_permission=Bir token oluşturmak için en azından bir izin seçmelisiniz
permissions_list=İzinler: 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_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_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_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=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. 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_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ı. 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_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_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_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_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_enroll=İki Faktörlü Kimlik Doğrulamaya Kaydolun
twofa_disable_note=Gerekirse iki faktörlü kimlik doğrulamayı devre dışı bırakabilirsiniz. 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? 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ı. 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: scan_this_image=Kim doğrulama uygulamanızla bu görüntüyü tarayın:
or_enter_secret=Veya gizli şeyi girin: %s 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=Sahibi
owner_helper=Bazı organizasyonlar, en çok depo sayısı sınırı nedeniyle açılır menüde görünmeyebilir. 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=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 repo_size=Depo Boyutu
template=Şablon template=Şablon
template_select=Bir şablon seçin. 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_visibility_helper=Çatallanmış bir deponun görünürlüğü değiştirilemez.
fork_branch=Çatala klonlanacak dal fork_branch=Çatala klonlanacak dal
all_branches=Tüm dallar 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_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. fork.blocked_user=Depo çatallanamıyor, depo sahibi tarafından engellenmişsiniz.
use_template=Bu şablonu kullan use_template=Bu şablonu kullan
@ -1022,6 +1065,8 @@ generate_repo=Depo Oluştur
generate_from=Şuradan Oluştur generate_from=Şuradan Oluştur
repo_desc=ıklama repo_desc=ıklama
repo_desc_helper=Kısa açıklama girin (isteğe bağlı) 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=.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. 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 issue_labels=Konu Etiketleri
@ -1029,6 +1074,7 @@ issue_labels_helper=Bir konu etiket seti seçin.
license=Lisans license=Lisans
license_helper=Bir lisans dosyası seçin. 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 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=Nesne Biçimi
object_format_helper=Deponun nesne biçimi. Daha sonra değiştirilemez. SHA1 en uyumlu olandır. object_format_helper=Deponun nesne biçimi. Daha sonra değiştirilemez. SHA1 en uyumlu olandır.
readme=README 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_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=<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ı. 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=Aktarımı Kabul Et
transfer.accept_desc=`"%s" tarafına aktar`
transfer.reject=Aktarımı Reddet 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_accept=Bu aktarımı kabul etme izniniz yok.
transfer.no_permission_to_reject=Bu aktarımı reddetme izniniz yok. transfer.no_permission_to_reject=Bu aktarımı reddetme izniniz yok.
desc.private=Özel desc.private=Özel
desc.public=Genel desc.public=Genel
desc.public_access=Herkese Açık Erişim
desc.template=Şablon desc.template=Şablon
desc.internal=Dahili desc.internal=Dahili
desc.archived=Arşivlenmiş 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.onedev.description=Code.onedev.io ve diğer OneDev sunucularından veri aktar.
migrate.codebase.description=Codebasehq.com sitesinden veri aktar. migrate.codebase.description=Codebasehq.com sitesinden veri aktar.
migrate.gitbucket.description=GitBucket sunucularından 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_git=Git Verilerini Taşıma
migrate.migrating_topics=Konuları Taşıma migrate.migrating_topics=Konuları Taşıma
migrate.migrating_milestones=Kilometre Taşlarını 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 push_exist_repo=Komut satırından mevcut bir depo itiliyor
empty_message=Bu depoda herhangi bir içerik yok. 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. 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=Kod
code.desc=Kaynak koda, dosyalara, işlemelere ve dallara eriş. 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.cancel=İptal
editor.filename_cannot_be_empty=Dosya adı boş olamaz. editor.filename_cannot_be_empty=Dosya adı boş olamaz.
editor.filename_is_invalid=Dosya adı geçersiz: "%s". 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_does_not_exist=Bu depoda "%s" dalı yok.
editor.branch_already_exists=Bu depoda "%s" dalı zaten var. 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. 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.gpg_key_id=GPG Anahtar Kimliği
commits.ssh_key_fingerprint=SSH Anahtar Parmak İzi commits.ssh_key_fingerprint=SSH Anahtar Parmak İzi
commits.view_path=Geçmişte bu noktayı görüntüle 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.operations=İşlemler
commit.revert=Geri Al commit.revert=Geri Al
@ -1410,6 +1469,8 @@ issues.filter_milestones=Kilometre Taşı Süzgeci
issues.filter_projects=Projeyi Süz issues.filter_projects=Projeyi Süz
issues.filter_labels=Etiket Süzgeci issues.filter_labels=Etiket Süzgeci
issues.filter_reviewers=Gözden Geçiren 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=Yeni Konu
issues.new.title_empty=Başlık boş olamaz issues.new.title_empty=Başlık boş olamaz
issues.new.labels=Etiketler issues.new.labels=Etiketler
@ -1427,6 +1488,7 @@ issues.new.clear_milestone=Kilometre Taşlarını Temizle
issues.new.assignees=Atananlar issues.new.assignees=Atananlar
issues.new.clear_assignees=Atamaları Temizle issues.new.clear_assignees=Atamaları Temizle
issues.new.no_assignees=Atanan Kişi Yok 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.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.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. 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_all=Tüm projeler
issues.filter_project_none=Proje yok issues.filter_project_none=Proje yok
issues.filter_assignee=Atanan issues.filter_assignee=Atanan
issues.filter_assignee_no_assignee=Hiç kimseye atanmamış
issues.filter_poster=Yazar issues.filter_poster=Yazar
issues.filter_type=Tür issues.filter_type=Tür
issues.filter_type.all_issues=Tüm konular issues.filter_type.all_issues=Tüm konular
@ -2029,6 +2092,7 @@ contributors.contribution_type.deletions=Silmeler
settings=Ayarlar settings=Ayarlar
settings.desc=Ayarlar, deponun ayarlarını yönetebileceğiniz yerdir settings.desc=Ayarlar, deponun ayarlarını yönetebileceğiniz yerdir
settings.options=Depo settings.options=Depo
settings.public_access=Herkese Açık Erişim
settings.collaboration=Katkıcılar settings.collaboration=Katkıcılar
settings.collaboration.admin=Yönetici settings.collaboration.admin=Yönetici
settings.collaboration.write=Yazma settings.collaboration.write=Yazma

View File

@ -117,6 +117,7 @@ files=文件
error=错误 error=错误
error404=您正尝试访问的页面 <strong>不存在</strong> 或 <strong>您尚未被授权</strong> 查看该页面。 error404=您正尝试访问的页面 <strong>不存在</strong> 或 <strong>您尚未被授权</strong> 查看该页面。
error503=服务器无法完成您的请求,请稍后重试。
go_back=返回 go_back=返回
invalid_data=无效数据: %v invalid_data=无效数据: %v
@ -129,6 +130,7 @@ pin=固定
unpin=取消置顶 unpin=取消置顶
artifacts=制品 artifacts=制品
expired=已过期
confirm_delete_artifact=您确定要删除制品'%s'吗? confirm_delete_artifact=您确定要删除制品'%s'吗?
archived=已归档 archived=已归档
@ -449,6 +451,7 @@ use_scratch_code=使用验证口令
twofa_scratch_used=你已经使用了你的验证口令。你将会转到两步验证设置页面以便移除你的注册设备或者重新生成新的验证口令。 twofa_scratch_used=你已经使用了你的验证口令。你将会转到两步验证设置页面以便移除你的注册设备或者重新生成新的验证口令。
twofa_passcode_incorrect=你的验证码不正确。如果你丢失了你的设备,请使用你的验证口令。 twofa_passcode_incorrect=你的验证码不正确。如果你丢失了你的设备,请使用你的验证口令。
twofa_scratch_token_incorrect=你的验证口令不正确。 twofa_scratch_token_incorrect=你的验证口令不正确。
twofa_required=您必须设置两步验证来访问仓库,或者尝试重新登录。
login_userpass=登录 login_userpass=登录
login_openid=OpenID login_openid=OpenID
oauth_signup_tab=注册帐号 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_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.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_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_milestone=(已删除)
issues.deleted_project=`(已删除)` issues.deleted_project=`(已删除)`
issues.self_assign_at=`于 %s 指派给自己` issues.self_assign_at=`于 %s 指派给自己`
@ -1877,6 +1880,7 @@ pulls.add_prefix=添加 <strong>%s</strong> 前缀
pulls.remove_prefix=删除 <strong>%s</strong> 前缀 pulls.remove_prefix=删除 <strong>%s</strong> 前缀
pulls.data_broken=此合并请求因为派生仓库信息缺失而中断。 pulls.data_broken=此合并请求因为派生仓库信息缺失而中断。
pulls.files_conflicted=此合并请求有变更与目标分支冲突。 pulls.files_conflicted=此合并请求有变更与目标分支冲突。
pulls.is_checking=正在进行合并冲突检测 ...
pulls.is_ancestor=此分支已经包含在目标分支中,没有什么可以合并。 pulls.is_ancestor=此分支已经包含在目标分支中,没有什么可以合并。
pulls.is_empty=此分支上的更改已经在目标分支上。这将是一个空提交。 pulls.is_empty=此分支上的更改已经在目标分支上。这将是一个空提交。
pulls.required_status_check_failed=一些必要的检查没有成功 pulls.required_status_check_failed=一些必要的检查没有成功
@ -3724,7 +3728,11 @@ creation.name_placeholder=不区分大小写,字母数字或下划线不能以
creation.value_placeholder=输入任何内容,开头和结尾的空白都会被省略 creation.value_placeholder=输入任何内容,开头和结尾的空白都会被省略
creation.description_placeholder=输入简短描述(可选)。 creation.description_placeholder=输入简短描述(可选)。
save_success=密钥 '%s' 保存成功。
save_failed=密钥保存失败。
add_secret=添加密钥
edit_secret=编辑密钥
deletion=删除密钥 deletion=删除密钥
deletion.description=删除密钥是永久性的,无法撤消。继续吗? deletion.description=删除密钥是永久性的,无法撤消。继续吗?
deletion.success=此Secret已被删除。 deletion.success=此Secret已被删除。
@ -3841,6 +3849,8 @@ deleted.display_name=已删除项目
type-1.display_name=个人项目 type-1.display_name=个人项目
type-2.display_name=仓库项目 type-2.display_name=仓库项目
type-3.display_name=组织项目 type-3.display_name=组织项目
enter_fullscreen=全屏
exit_fullscreen=退出全屏
[git.filemode] [git.filemode]
changed_filemode=%[1]s -> %[2]s changed_filemode=%[1]s -> %[2]s

View File

@ -1279,7 +1279,10 @@ func Routes() *web.Router {
}, reqToken(), reqAdmin()) }, reqToken(), reqAdmin())
m.Group("/actions", func() { m.Group("/actions", func() {
m.Get("/tasks", repo.ListActionTasks) 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.Get("/artifacts", repo.GetArtifacts)
m.Group("/artifacts/{artifact_id}", func() { m.Group("/artifacts/{artifact_id}", func() {
m.Get("", repo.GetArtifact) m.Get("", repo.GetArtifact)

View File

@ -1061,6 +1061,58 @@ func GetArtifactsOfRun(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, &res) 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. // GetArtifacts Lists all artifacts for a repository.
func GetArtifacts(ctx *context.APIContext) { func GetArtifacts(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/artifacts repository getArtifacts // swagger:operation GET /repos/{owner}/{repo}/actions/artifacts repository getArtifacts

View File

@ -8,6 +8,7 @@ import (
"math" "math"
"net/http" "net/http"
"strconv" "strconv"
"time"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -116,6 +117,16 @@ func GetAllCommits(ctx *context.APIContext) {
// in: query // in: query
// description: filepath of a file/dir // description: filepath of a file/dir
// type: string // 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 // - name: stat
// in: query // in: query
// description: include diff stats for every commit (disable for speedup, default 'true') // description: include diff stats for every commit (disable for speedup, default 'true')
@ -148,6 +159,23 @@ func GetAllCommits(ctx *context.APIContext) {
// "409": // "409":
// "$ref": "#/responses/EmptyRepository" // "$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 { if ctx.Repo.Repository.IsEmpty {
ctx.JSON(http.StatusConflict, api.APIError{ ctx.JSON(http.StatusConflict, api.APIError{
Message: "Git Repository is empty.", Message: "Git Repository is empty.",
@ -198,6 +226,8 @@ func GetAllCommits(ctx *context.APIContext) {
RepoPath: ctx.Repo.GitRepo.Path, RepoPath: ctx.Repo.GitRepo.Path,
Not: not, Not: not,
Revision: []string{baseCommit.ID.String()}, Revision: []string{baseCommit.ID.String()},
Since: since,
Until: until,
}) })
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
@ -205,7 +235,7 @@ func GetAllCommits(ctx *context.APIContext) {
} }
// Query commits // 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 { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
@ -221,6 +251,8 @@ func GetAllCommits(ctx *context.APIContext) {
Not: not, Not: not,
Revision: []string{sha}, Revision: []string{sha},
RelPath: []string{path}, RelPath: []string{path},
Since: since,
Until: until,
}) })
if err != nil { if err != nil {
@ -237,6 +269,8 @@ func GetAllCommits(ctx *context.APIContext) {
File: path, File: path,
Not: not, Not: not,
Page: listOptions.Page, Page: listOptions.Page,
Since: since,
Until: until,
}) })
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)

View File

@ -895,6 +895,15 @@ func EditIssue(ctx *context.APIContext) {
issue.MilestoneID != *form.Milestone { issue.MilestoneID != *form.Milestone {
oldMilestoneID := issue.MilestoneID oldMilestoneID := issue.MilestoneID
issue.MilestoneID = *form.Milestone 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 { if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return

View File

@ -609,6 +609,7 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
return return
} }
if form.Body != comment.Content {
oldContent := comment.Content oldContent := comment.Content
comment.Content = form.Body comment.Content = form.Body
if err := issue_service.UpdateComment(ctx, comment, comment.ContentVersion, ctx.Doer, oldContent); err != nil { if err := issue_service.UpdateComment(ctx, comment, comment.ContentVersion, ctx.Doer, oldContent); err != nil {
@ -619,6 +620,7 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
} }
return return
} }
}
ctx.JSON(http.StatusOK, convert.ToAPIComment(ctx, ctx.Repo.Repository, comment)) ctx.JSON(http.StatusOK, convert.ToAPIComment(ctx, ctx.Repo.Repository, comment))
} }

View File

@ -706,6 +706,11 @@ func EditPullRequest(ctx *context.APIContext) {
issue.MilestoneID != form.Milestone { issue.MilestoneID != form.Milestone {
oldMilestoneID := issue.MilestoneID oldMilestoneID := issue.MilestoneID
issue.MilestoneID = form.Milestone 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 { if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return

View File

@ -67,6 +67,28 @@ func ListRunners(ctx *context.APIContext, ownerID, repoID int64) {
ctx.JSON(http.StatusOK, &res) 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 // 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 including global runners
// ownerID == 0 and repoID != 0 means any runner for the given repo // 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 { if ownerID != 0 && repoID != 0 {
setting.PanicInDevOrTesting("ownerID and repoID should not be both set") setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
} }
runner, err := actions_model.GetRunnerByID(ctx, runnerID) runner, ok := getRunnerByID(ctx, ownerID, repoID, runnerID)
if err != nil { if !ok {
ctx.APIErrorNotFound(err)
return
}
if !runner.EditableInContext(ownerID, repoID) {
ctx.APIErrorNotFound("No permission to get this runner")
return return
} }
ctx.JSON(http.StatusOK, convert.ToActionRunner(ctx, runner)) 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 // ownerID != 0 and repoID != 0 undefined behavior
// Access rights are checked at the API route level // Access rights are checked at the API route level
func DeleteRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) { func DeleteRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) {
if ownerID != 0 && repoID != 0 { runner, ok := getRunnerByID(ctx, ownerID, repoID, runnerID)
setting.PanicInDevOrTesting("ownerID and repoID should not be both set") if !ok {
}
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")
return return
} }
err = actions_model.DeleteRunner(ctx, runner.ID) err := actions_model.DeleteRunner(ctx, runner.ID)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return

View File

@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation"
webhook_module "code.gitea.io/gitea/modules/webhook" webhook_module "code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
webhook_service "code.gitea.io/gitea/services/webhook" 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") ctx.APIError(http.StatusUnprocessableEntity, "Invalid content type")
return false return false
} }
if !validation.IsValidURL(form.Config["url"]) {
ctx.APIError(http.StatusUnprocessableEntity, "Invalid url")
return false
}
return true 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) 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 // addHook add the hook specified by `form`, `ownerID` and `repoID`. If there is
// an error, write to `ctx` accordingly. Return (webhook, ok) // an error, write to `ctx` accordingly. Return (webhook, ok)
func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoID int64) (*webhook.Webhook, bool) { 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 return nil, false
} }
if len(form.Events) == 0 {
form.Events = []string{"push"}
}
if form.Config["is_system_webhook"] != "" { if form.Config["is_system_webhook"] != "" {
sw, err := strconv.ParseBool(form.Config["is_system_webhook"]) sw, err := strconv.ParseBool(form.Config["is_system_webhook"])
if err != nil { if err != nil {
@ -183,31 +220,7 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
IsSystemWebhook: isSystemWebhook, IsSystemWebhook: isSystemWebhook,
HookEvent: &webhook_module.HookEvent{ HookEvent: &webhook_module.HookEvent{
ChooseEvents: true, ChooseEvents: true,
HookEvents: webhook_module.HookEvents{ HookEvents: updateHookEvents(form.Events),
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),
},
BranchFilter: form.BranchFilter, BranchFilter: form.BranchFilter,
}, },
IsActive: form.Active, 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 { func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webhook) bool {
if form.Config != nil { if form.Config != nil {
if url, ok := form.Config["url"]; ok { if url, ok := form.Config["url"]; ok {
if !validation.IsValidURL(url) {
ctx.APIError(http.StatusUnprocessableEntity, "Invalid url")
return false
}
w.URL = url w.URL = url
} }
if ct, ok := form.Config["content_type"]; ok { 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 // Update events
if len(form.Events) == 0 { w.HookEvents = updateHookEvents(form.Events)
form.Events = []string{"push"}
}
w.PushOnly = false w.PushOnly = false
w.SendEverything = false w.SendEverything = false
w.ChooseEvents = true 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 w.BranchFilter = form.BranchFilter
err := w.SetHeaderAuthorization(form.AuthorizationHeader) err := w.SetHeaderAuthorization(form.AuthorizationHeader)
@ -373,23 +381,6 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
return false 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 { if err := w.UpdateEvent(); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return false return false

View File

@ -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())
})
}

View File

@ -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()
},
})
}

View File

@ -220,7 +220,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
} }
if len(cols) > 0 { 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) log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err), Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),

View File

@ -15,7 +15,7 @@ import (
// ShowBranchFeed shows tags and/or releases on the repo as RSS / Atom feed // ShowBranchFeed shows tags and/or releases on the repo as RSS / Atom feed
func ShowBranchFeed(ctx *context.Context, repo *repo.Repository, formatType string) { 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 { if err != nil {
ctx.ServerError("ShowBranchFeed", err) ctx.ServerError("ShowBranchFeed", err)
return return

View File

@ -317,6 +317,8 @@ func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
pager.AddParamFromRequest(ctx.Req) pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager ctx.Data["Page"] = pager
ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0 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. // loadIsRefDeleted loads the IsRefDeleted field for each run in the list.

View File

@ -577,6 +577,33 @@ func Approve(ctx *context_module.Context) {
ctx.JSON(http.StatusOK, struct{}{}) 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. // getRunJobs gets the jobs of runIndex, and returns jobs[jobIndex], jobs.
// Any error will be written to the ctx. // 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. // 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) run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
if err != nil { if err != nil {
if errors.Is(err, util.ErrNotExist) { if errors.Is(err, util.ErrNotExist) {
ctx.HTTPError(http.StatusNotFound, err.Error()) ctx.NotFound(nil)
return nil, nil return nil, nil
} }
ctx.HTTPError(http.StatusInternalServerError, err.Error()) ctx.ServerError("GetRunByIndex", err)
return nil, nil return nil, nil
} }
run.Repo = ctx.Repo.Repository run.Repo = ctx.Repo.Repository
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID) jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
if err != nil { if err != nil {
ctx.HTTPError(http.StatusInternalServerError, err.Error()) ctx.ServerError("GetRunJobsByRunID", err)
return nil, nil return nil, nil
} }
if len(jobs) == 0 { if len(jobs) == 0 {
ctx.HTTPError(http.StatusNotFound) ctx.NotFound(nil)
return nil, nil return nil, nil
} }

View File

@ -264,7 +264,7 @@ func MergeUpstream(ctx *context.Context) {
_, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName) _, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName)
if err != nil { if err != nil {
if errors.Is(err, util.ErrNotExist) { if errors.Is(err, util.ErrNotExist) {
ctx.JSONError(ctx.Tr("error.not_found")) ctx.JSONErrorNotFound()
return return
} else if pull_service.IsErrMergeConflicts(err) { } else if pull_service.IsErrMergeConflicts(err) {
ctx.JSONError(ctx.Tr("repo.pulls.merge_conflict")) ctx.JSONError(ctx.Tr("repo.pulls.merge_conflict"))

View File

@ -78,7 +78,7 @@ func Commits(ctx *context.Context) {
} }
// Both `git log branchName` and `git log commitId` work. // 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 { if err != nil {
ctx.ServerError("CommitsByRange", err) ctx.ServerError("CommitsByRange", err)
return return

View File

@ -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) redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath)
} }
@ -790,7 +784,7 @@ func UploadFilePost(ctx *context.Context) {
if ctx.Repo.Repository.IsEmpty { if ctx.Repo.Repository.IsEmpty {
if isEmpty, err := ctx.Repo.GitRepo.IsEmpty(); err == nil && !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")
} }
} }

View File

@ -418,6 +418,16 @@ func UpdateIssueMilestone(ctx *context.Context) {
continue continue
} }
issue.MilestoneID = milestoneID 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 { if err := issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
ctx.ServerError("ChangeMilestoneAssign", err) ctx.ServerError("ChangeMilestoneAssign", err)
return return

View File

@ -239,12 +239,18 @@ func UpdateCommentContent(ctx *context.Context) {
return return
} }
oldContent := comment.Content
newContent := ctx.FormString("content") newContent := ctx.FormString("content")
contentVersion := ctx.FormInt("content_version") contentVersion := ctx.FormInt("content_version")
if contentVersion != comment.ContentVersion {
ctx.JSONError(ctx.Tr("repo.comments.edit.already_changed"))
return
}
if newContent != comment.Content {
// allow to save empty content // allow to save empty content
oldContent := comment.Content
comment.Content = newContent comment.Content = newContent
if err = issue_service.UpdateComment(ctx, comment, contentVersion, ctx.Doer, oldContent); err != nil { if err = issue_service.UpdateComment(ctx, comment, contentVersion, ctx.Doer, oldContent); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) { if errors.Is(err, user_model.ErrBlockedUser) {
ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user")) ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
@ -255,6 +261,7 @@ func UpdateCommentContent(ctx *context.Context) {
} }
return return
} }
}
if err := comment.LoadAttachments(ctx); err != nil { if err := comment.LoadAttachments(ctx); err != nil {
ctx.ServerError("LoadAttachments", err) ctx.ServerError("LoadAttachments", err)

View File

@ -21,6 +21,7 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
giturl "code.gitea.io/gitea/modules/git/url" 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/httplib"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository" 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) { func handleRepoEmptyOrBroken(ctx *context.Context) {
showEmpty := true 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 { if ctx.Repo.GitRepo != nil {
reallyEmpty, err := ctx.Repo.GitRepo.IsEmpty() reallyEmpty, err := ctx.Repo.GitRepo.IsEmpty()
if err != nil { if err != nil {
@ -396,10 +401,8 @@ func Home(ctx *context.Context) {
return return
} }
prepareHomeTreeSideBarSwitch(ctx)
title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name 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 title += ": " + ctx.Repo.Repository.Description
} }
ctx.Data["Title"] = title ctx.Data["Title"] = title
@ -412,6 +415,8 @@ func Home(ctx *context.Context) {
return return
} }
prepareHomeTreeSideBarSwitch(ctx)
// get the current git entry which doer user is currently looking at. // get the current git entry which doer user is currently looking at.
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
if err != nil { if err != nil {

View File

@ -109,7 +109,7 @@ func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, err
return wikiGitRepo, nil, errBranch return wikiGitRepo, nil, errBranch
} }
// update the default branch in the database // 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 { if errDb != nil {
return wikiGitRepo, nil, errDb return wikiGitRepo, nil, errDb
} }

View File

@ -245,7 +245,7 @@ func TestDefaultWikiBranch(t *testing.T) {
assert.NoError(t, wiki_service.ChangeDefaultWikiBranch(db.DefaultContext, repoWithNoWiki, "main")) assert.NoError(t, wiki_service.ChangeDefaultWikiBranch(db.DefaultContext, repoWithNoWiki, "main"))
// repo with wiki // 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, _ := contexttest.MockContext(t, "user2/repo1/wiki")
ctx.SetPathParam("*", "Home") ctx.SetPathParam("*", "Home")

View File

@ -1447,6 +1447,7 @@ func registerWebRoutes(m *web.Router) {
}) })
m.Post("/cancel", reqRepoActionsWriter, actions.Cancel) m.Post("/cancel", reqRepoActionsWriter, actions.Cancel)
m.Post("/approve", reqRepoActionsWriter, actions.Approve) m.Post("/approve", reqRepoActionsWriter, actions.Approve)
m.Post("/delete", reqRepoActionsWriter, actions.Delete)
m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView) m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView)
m.Delete("/artifacts/{artifact_name}", reqRepoActionsWriter, actions.ArtifactsDeleteView) m.Delete("/artifacts/{artifact_name}", reqRepoActionsWriter, actions.ArtifactsDeleteView)
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun) m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)

View File

@ -5,12 +5,14 @@ package actions
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"time" "time"
actions_model "code.gitea.io/gitea/models/actions" actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
actions_module "code.gitea.io/gitea/modules/actions" 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/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/storage"
@ -27,7 +29,7 @@ func Cleanup(ctx context.Context) error {
} }
// clean up old logs // clean up old logs
if err := CleanupLogs(ctx); err != nil { if err := CleanupExpiredLogs(ctx); err != nil {
return fmt.Errorf("cleanup logs: %w", err) return fmt.Errorf("cleanup logs: %w", err)
} }
@ -98,8 +100,15 @@ func cleanNeedDeleteArtifacts(taskCtx context.Context) error {
const deleteLogBatchSize = 100 const deleteLogBatchSize = 100
// CleanupLogs removes logs which are older than the configured retention time func removeTaskLog(ctx context.Context, task *actions_model.ActionTask) {
func CleanupLogs(ctx context.Context) error { 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) olderThan := timeutil.TimeStampNow().AddDuration(-time.Duration(setting.Actions.LogRetentionDays) * 24 * time.Hour)
count := 0 count := 0
@ -109,10 +118,7 @@ func CleanupLogs(ctx context.Context) error {
return fmt.Errorf("find old tasks: %w", err) return fmt.Errorf("find old tasks: %w", err)
} }
for _, task := range tasks { for _, task := range tasks {
if err := actions_module.RemoveLogs(ctx, task.LogInStorage, task.LogFilename); err != nil { removeTaskLog(ctx, task)
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
}
task.LogIndexes = nil // clear log indexes since it's a heavy field task.LogIndexes = nil // clear log indexes since it's a heavy field
task.LogExpired = true task.LogExpired = true
if err := actions_model.UpdateTask(ctx, task, "log_indexes", "log_expired"); err != nil { 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) log.Info("Removed %d runners", affected)
return nil 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
}

View File

@ -15,11 +15,15 @@ import (
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting" "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 // GenerateGiteaContext generate the gitea context without token and gitea_runtime_token
// job can be nil when generating a context for parsing workflow-level expressions // 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{} event := map[string]any{}
_ = json.Unmarshal([]byte(run.EventPayload), &event) _ = json.Unmarshal([]byte(run.EventPayload), &event)
@ -42,7 +46,7 @@ func GenerateGiteaContext(run *actions_model.ActionRun, job *actions_model.Actio
refName := git.RefName(ref) 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 // 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": "", // 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. "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 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),
}
}

View File

@ -302,9 +302,11 @@ func handleWorkflows(
run := &actions_model.ActionRun{ run := &actions_model.ActionRun{
Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0], Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0],
RepoID: input.Repo.ID, RepoID: input.Repo.ID,
Repo: input.Repo,
OwnerID: input.Repo.OwnerID, OwnerID: input.Repo.OwnerID,
WorkflowID: dwf.EntryName, WorkflowID: dwf.EntryName,
TriggerUserID: input.Doer.ID, TriggerUserID: input.Doer.ID,
TriggerUser: input.Doer,
Ref: ref, Ref: ref,
CommitSHA: commit.ID.String(), CommitSHA: commit.ID.String(),
IsForkPullRequest: isForkPullRequest, IsForkPullRequest: isForkPullRequest,
@ -333,12 +335,18 @@ func handleWorkflows(
continue 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 { if err != nil {
log.Error("jobparser.Parse: %v", err) log.Error("jobparser.Parse: %v", err)
continue 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 // cancel running jobs if the event is push or pull_request_sync
if run.Event == webhook_module.HookEventPush || if run.Event == webhook_module.HookEventPush ||
run.Event == webhook_module.HookEventPullRequestSync { run.Event == webhook_module.HookEventPullRequestSync {
@ -508,9 +516,11 @@ func handleSchedules(
run := &actions_model.ActionSchedule{ run := &actions_model.ActionSchedule{
Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0], Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0],
RepoID: input.Repo.ID, RepoID: input.Repo.ID,
Repo: input.Repo,
OwnerID: input.Repo.OwnerID, OwnerID: input.Repo.OwnerID,
WorkflowID: dwf.EntryName, WorkflowID: dwf.EntryName,
TriggerUserID: user_model.ActionsUserID, TriggerUserID: user_model.ActionsUserID,
TriggerUser: user_model.NewActionsUser(),
Ref: ref, Ref: ref,
CommitSHA: commit.ID.String(), CommitSHA: commit.ID.String(),
Event: input.Event, Event: input.Event,
@ -518,6 +528,25 @@ func handleSchedules(
Specs: schedules, Specs: schedules,
Content: dwf.Content, 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) crons = append(crons, run)
} }

View File

@ -192,20 +192,53 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
// find workflow from commit // find workflow from commit
var workflows []*jobparser.SingleWorkflow var workflows []*jobparser.SingleWorkflow
for _, entry := range entries { var entry *git.TreeEntry
if entry.Name() != workflowID {
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 continue
} }
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) content, err := actions.GetContentFromEntry(entry)
if err != nil { if err != nil {
return err return err
} }
workflows, err = jobparser.Parse(content)
giteaCtx := GenerateGiteaContext(run, nil)
workflows, err = jobparser.Parse(content, jobparser.WithGitContext(giteaCtx.ToGitHubContext()))
if err != nil { if err != nil {
return err return err
} }
break
if len(workflows) > 0 && workflows[0].RunName != "" {
run.Title = workflows[0].RunName
} }
if len(workflows) == 0 { if len(workflows) == 0 {
@ -236,25 +269,12 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
Inputs: inputsWithDefaults, Inputs: inputsWithDefaults,
Sender: convert.ToUserWithAccessMode(ctx, doer, perm.AccessModeNone), Sender: convert.ToUserWithAccessMode(ctx, doer, perm.AccessModeNone),
} }
var eventPayload []byte var eventPayload []byte
if eventPayload, err = workflowDispatchPayload.JSONPayload(); err != nil { if eventPayload, err = workflowDispatchPayload.JSONPayload(); err != nil {
return fmt.Errorf("JSONPayload: %w", err) return fmt.Errorf("JSONPayload: %w", err)
} }
run.EventPayload = string(eventPayload)
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,
}
// cancel running jobs of the same workflow // cancel running jobs of the same workflow
if err := CancelPreviousJobs( if err := CancelPreviousJobs(
@ -280,6 +300,5 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
for _, job := range allJobs { for _, job := range allJobs {
notify_service.WorkflowJobStatusUpdate(ctx, repo, doer, job, nil) notify_service.WorkflowJobStatusUpdate(ctx, repo, doer, job, nil)
} }
return nil return nil
} }

View File

@ -245,7 +245,7 @@ func APIContexter() func(http.Handler) http.Handler {
// APIErrorNotFound handles 404s for APIContext // APIErrorNotFound handles 404s for APIContext
// String will replace message, errors will be added to a slice // String will replace message, errors will be added to a slice
func (ctx *APIContext) APIErrorNotFound(objs ...any) { func (ctx *APIContext) APIErrorNotFound(objs ...any) {
message := ctx.Locale.TrString("error.not_found") var message string
var errs []string var errs []string
for _, obj := range objs { for _, obj := range objs {
// Ignore nil // Ignore nil
@ -259,9 +259,8 @@ func (ctx *APIContext) APIErrorNotFound(objs ...any) {
message = obj.(string) message = obj.(string)
} }
} }
ctx.JSON(http.StatusNotFound, map[string]any{ 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, "url": setting.API.SwaggerURL,
"errors": errs, "errors": errs,
}) })

View File

@ -23,6 +23,7 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
web_types "code.gitea.io/gitea/modules/web/types" 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)) 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"})
}

View File

@ -795,8 +795,8 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) {
return func(ctx *Context) { return func(ctx *Context) {
var err error var err error
refType := detectRefType refType := detectRefType
if ctx.Repo.Repository.IsBeingCreated() { 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" 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. // Empty repository does not have reference information.
if ctx.Repo.Repository.IsEmpty { if ctx.Repo.Repository.IsEmpty {

View File

@ -98,6 +98,8 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
allowSquash := false allowSquash := false
allowFastForwardOnly := false allowFastForwardOnly := false
allowRebaseUpdate := false allowRebaseUpdate := false
allowManualMerge := true
autodetectManualMerge := false
defaultDeleteBranchAfterMerge := false defaultDeleteBranchAfterMerge := false
defaultMergeStyle := repo_model.MergeStyleMerge defaultMergeStyle := repo_model.MergeStyleMerge
defaultAllowMaintainerEdit := false defaultAllowMaintainerEdit := false
@ -111,6 +113,8 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
allowSquash = config.AllowSquash allowSquash = config.AllowSquash
allowFastForwardOnly = config.AllowFastForwardOnly allowFastForwardOnly = config.AllowFastForwardOnly
allowRebaseUpdate = config.AllowRebaseUpdate allowRebaseUpdate = config.AllowRebaseUpdate
allowManualMerge = config.AllowManualMerge
autodetectManualMerge = config.AutodetectManualMerge
defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge
defaultMergeStyle = config.GetDefaultMergeStyle() defaultMergeStyle = config.GetDefaultMergeStyle()
defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit
@ -235,6 +239,8 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
AllowSquash: allowSquash, AllowSquash: allowSquash,
AllowFastForwardOnly: allowFastForwardOnly, AllowFastForwardOnly: allowFastForwardOnly,
AllowRebaseUpdate: allowRebaseUpdate, AllowRebaseUpdate: allowRebaseUpdate,
AllowManualMerge: allowManualMerge,
AutodetectManualMerge: autodetectManualMerge,
DefaultDeleteBranchAfterMerge: defaultDeleteBranchAfterMerge, DefaultDeleteBranchAfterMerge: defaultDeleteBranchAfterMerge,
DefaultMergeStyle: string(defaultMergeStyle), DefaultMergeStyle: string(defaultMergeStyle),
DefaultAllowMaintainerEdit: defaultAllowMaintainerEdit, DefaultAllowMaintainerEdit: defaultAllowMaintainerEdit,

View File

@ -148,7 +148,7 @@ func (g *GiteaLocalUploader) CreateRepo(ctx context.Context, repo *base.Reposito
return err return err
} }
g.repo.ObjectFormatName = objectFormat.Name() 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 // Close closes this uploader
@ -975,7 +975,7 @@ func (g *GiteaLocalUploader) Finish(ctx context.Context) error {
} }
g.repo.Status = repo_model.RepositoryReady 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 { func (g *GiteaLocalUploader) remapUser(ctx context.Context, source user_model.ExternalUserMigrated, target user_model.ExternalUserRemappable) error {

View File

@ -71,7 +71,7 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error
// erase authentication before storing in database // erase authentication before storing in database
u.User = nil u.User = nil
m.Repo.OriginalURL = u.String() 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. // 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 m.Repo.IsEmpty = false
// Update the is empty and default_branch columns // 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) 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) desc := fmt.Sprintf("Failed to update default branch of repository '%s': %v", m.Repo.RepoPath(), err)
if err = system_model.CreateRepositoryNotice(desc); err != nil { if err = system_model.CreateRepositoryNotice(desc); err != nil {

View File

@ -64,10 +64,11 @@ func RemoveOrgUser(ctx context.Context, org *organization.Organization, user *us
if err != nil { if err != nil {
return fmt.Errorf("AccessibleReposEnv: %w", err) return fmt.Errorf("AccessibleReposEnv: %w", err)
} }
repoIDs, err := env.RepoIDs(ctx, 1, org.NumRepos) repoIDs, err := env.RepoIDs(ctx)
if err != nil { if err != nil {
return fmt.Errorf("GetUserRepositories [%d]: %w", user.ID, err) return fmt.Errorf("GetUserRepositories [%d]: %w", user.ID, err)
} }
for _, repoID := range repoIDs { for _, repoID := range repoIDs {
repo, err := repo_model.GetRepositoryByID(ctx, repoID) repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil { if err != nil {

View File

@ -100,7 +100,7 @@ func AdoptRepository(ctx context.Context, doer, owner *user_model.User, opts Cre
// 4 - update repository status // 4 - update repository status
repo.Status = repo_model.RepositoryReady 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) return nil, fmt.Errorf("UpdateRepositoryCols: %w", err)
} }

View File

@ -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 // 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 // Then repo will be removed - only it avatar file will be removed
repo.Avatar = newAvatar 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) 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() defer committer.Close()
repo.Avatar = "" 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) 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 err
} }
return repo_model.UpdateRepositoryCols(ctx, generateRepo, "avatar") return repo_model.UpdateRepositoryColsNoAutoTime(ctx, generateRepo, "avatar")
} }

View File

@ -321,7 +321,7 @@ func CreateRepositoryDirectly(ctx context.Context, doer, owner *user_model.User,
// 7 - update repository status to be ready // 7 - update repository status to be ready
if needsUpdateToReady { if needsUpdateToReady {
repo.Status = repo_model.RepositoryReady 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) return nil, fmt.Errorf("UpdateRepositoryCols: %w", err)
} }
} }

View File

@ -27,6 +27,7 @@ import (
"code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/storage"
actions_service "code.gitea.io/gitea/services/actions"
asymkey_service "code.gitea.io/gitea/services/asymkey" asymkey_service "code.gitea.io/gitea/services/asymkey"
"xorm.io/builder" "xorm.io/builder"
@ -133,6 +134,14 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
return err 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, if err := db.DeleteBeans(ctx,
&access_model.Access{RepoID: repo.ID}, &access_model.Access{RepoID: repo.ID},
&activities_model.Action{RepoID: repo.ID}, &activities_model.Action{RepoID: repo.ID},

View File

@ -306,7 +306,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
if repo.IsEmpty { if repo.IsEmpty {
if isEmpty, err := gitRepo.IsEmpty(); err == nil && !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")
} }
} }

View File

@ -198,7 +198,7 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
// 8 - update repository status to be ready // 8 - update repository status to be ready
repo.Status = repo_model.RepositoryReady 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) return nil, fmt.Errorf("UpdateRepositoryCols: %w", err)
} }

View File

@ -283,7 +283,7 @@ func pushNewBranch(ctx context.Context, repo *repo_model.Repository, pusher *use
} }
} }
// Update the is empty and default_branch columns // 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) return nil, fmt.Errorf("UpdateRepositoryCols: %w", err)
} }
} }

View File

@ -205,7 +205,7 @@ func updateRepository(ctx context.Context, repo *repo_model.Repository, visibili
e := db.GetEngine(ctx) 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) return fmt.Errorf("update: %w", err)
} }

View File

@ -184,7 +184,7 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ
// 6 - update repository status to be ready // 6 - update repository status to be ready
generateRepo.Status = repo_model.RepositoryReady 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) return nil, fmt.Errorf("UpdateRepositoryCols: %w", err)
} }

View File

@ -160,7 +160,7 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
repo.OwnerName = newOwner.Name repo.OwnerName = newOwner.Name
// Update repository. // 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) 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) return fmt.Errorf("deleteRepositoryTransfer: %w", err)
} }
repo.Status = repo_model.RepositoryReady 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 return err
} }
@ -495,7 +495,7 @@ func RejectRepositoryTransfer(ctx context.Context, repo *repo_model.Repository,
} }
repo.Status = repo_model.RepositoryReady 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 return err
} }
@ -543,7 +543,7 @@ func CancelRepositoryTransfer(ctx context.Context, repoTransfer *repo_model.Repo
} }
repoTransfer.Repo.Status = repo_model.RepositoryReady 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 return err
} }

View File

@ -305,7 +305,7 @@ func parseHookPullRequestEventType(event webhook_module.HookEventType) (string,
case webhook_module.HookEventPullRequestReviewApproved: case webhook_module.HookEventPullRequestReviewApproved:
return "approved", nil return "approved", nil
case webhook_module.HookEventPullRequestReviewRejected: case webhook_module.HookEventPullRequestReviewRejected:
return "rejected", nil return "requested changes", nil
case webhook_module.HookEventPullRequestReviewComment: case webhook_module.HookEventPullRequestReviewComment:
return "comment", nil return "comment", nil
default: default:

View File

@ -365,7 +365,7 @@ func ChangeDefaultWikiBranch(ctx context.Context, repo *repo_model.Repository, n
} }
return db.WithTx(ctx, func(ctx context.Context) error { return db.WithTx(ctx, func(ctx context.Context) error {
repo.DefaultWikiBranch = newBranch 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) return fmt.Errorf("unable to update database: %w", err)
} }

View File

@ -5,37 +5,46 @@
<h2>{{if $.IsFiltered}}{{ctx.Locale.Tr "actions.runs.no_results"}}{{else}}{{ctx.Locale.Tr "actions.runs.no_runs"}}{{end}}</h2> <h2>{{if $.IsFiltered}}{{ctx.Locale.Tr "actions.runs.no_results"}}{{else}}{{ctx.Locale.Tr "actions.runs.no_runs"}}{{end}}</h2>
</div> </div>
{{end}} {{end}}
{{range .Runs}} {{range $run := .Runs}}
<div class="flex-item tw-items-center"> <div class="flex-item tw-items-center">
<div class="flex-item-leading"> <div class="flex-item-leading">
{{template "repo/actions/status" (dict "status" .Status.String)}} {{template "repo/actions/status" (dict "status" $run.Status.String)}}
</div> </div>
<div class="flex-item-main"> <div class="flex-item-main">
<a class="flex-item-title" title="{{.Title}}" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}"> <a class="flex-item-title" title="{{$run.Title}}" href="{{$run.Link}}">
{{if .Title}}{{.Title}}{{else}}{{ctx.Locale.Tr "actions.runs.empty_commit_message"}}{{end}} {{or $run.Title (ctx.Locale.Tr "actions.runs.empty_commit_message")}}
</a> </a>
<div class="flex-item-body"> <div class="flex-item-body">
<span><b>{{if not $.CurWorkflow}}{{.WorkflowID}} {{end}}#{{.Index}}</b>:</span> <span><b>{{if not $.CurWorkflow}}{{$run.WorkflowID}} {{end}}#{{$run.Index}}</b>:</span>
{{- if .ScheduleID -}} {{- if $run.ScheduleID -}}
{{ctx.Locale.Tr "actions.runs.scheduled"}} {{ctx.Locale.Tr "actions.runs.scheduled"}}
{{- else -}} {{- else -}}
{{ctx.Locale.Tr "actions.runs.commit"}} {{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"}} {{ctx.Locale.Tr "actions.runs.pushed_by"}}
<a href="{{.TriggerUser.HomeLink}}">{{.TriggerUser.GetDisplayName}}</a> <a href="{{$run.TriggerUser.HomeLink}}">{{$run.TriggerUser.GetDisplayName}}</a>
{{- end -}} {{- end -}}
</div> </div>
</div> </div>
<div class="flex-item-trailing"> <div class="flex-item-trailing">
{{if .IsRefDeleted}} {{if $run.IsRefDeleted}}
<span class="ui label run-list-ref gt-ellipsis tw-line-through" data-tooltip-content="{{.PrettyRef}}">{{.PrettyRef}}</span> <span class="ui label run-list-ref gt-ellipsis tw-line-through" data-tooltip-content="{{$run.PrettyRef}}">{{$run.PrettyRef}}</span>
{{else}} {{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}} {{end}}
<div class="run-list-item-right"> <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-calendar" 16}}{{DateUtils.TimeSince $run.Updated}}</div>
<div class="run-list-meta">{{svg "octicon-stopwatch" 16}}{{.Duration}}</div> <div class="run-list-meta">{{svg "octicon-stopwatch" 16}}{{$run.Duration}}</div>
</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>
</div> </div>
{{end}} {{end}}

View File

@ -226,11 +226,19 @@
</a> </a>
{{end}} {{end}}
</div> </div>
{{else if .Permission.IsAdmin}} {{else}}
<div class="overflow-menu-items"> <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"> <a class="{{if .PageIsRepoSettings}}active {{end}} item" href="{{.RepoLink}}/settings">
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}} {{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
</a> </a>
{{end}}
</div> </div>
{{end}} {{end}}
</overflow-menu> </overflow-menu>

View File

@ -4,7 +4,7 @@
<div class="flex-item"> <div class="flex-item">
<div class="flex-item-main"> <div class="flex-item-main">
<div class="flex-item-title"> <div class="flex-item-title">
<a class="item muted" href="{{.Link}}/releases"> <a class="item muted" href="{{.RepoLink}}/releases">
{{ctx.Locale.Tr "repo.releases"}} {{ctx.Locale.Tr "repo.releases"}}
<span class="ui small label">{{.NumReleases}}</span> <span class="ui small label">{{.NumReleases}}</span>
</a> </a>

View File

@ -37,7 +37,7 @@
<span class="color-text-light-2"> <span class="color-text-light-2">
{{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .CreatedUnix)}} {{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .CreatedUnix)}}
</span> </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="#add-secret-modal"
data-modal-form.action="{{$.Link}}" data-modal-form.action="{{$.Link}}"
data-modal-header="{{ctx.Locale.Tr "secrets.edit_secret"}}" data-modal-header="{{ctx.Locale.Tr "secrets.edit_secret"}}"
@ -49,7 +49,7 @@
> >
{{svg "octicon-pencil"}} {{svg "octicon-pencil"}}
</button> </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-url="{{$.Link}}/delete?id={{.ID}}"
data-modal-confirm="{{ctx.Locale.Tr "secrets.deletion.description"}}" data-modal-confirm="{{ctx.Locale.Tr "secrets.deletion.description"}}"
data-tooltip-content="{{ctx.Locale.Tr "secrets.deletion"}}" data-tooltip-content="{{ctx.Locale.Tr "secrets.deletion"}}"

View File

@ -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": { "/repos/{owner}/{repo}/actions/runs/{run}/artifacts": {
"get": { "get": {
"produces": [ "produces": [
@ -6558,6 +6604,20 @@
"name": "path", "name": "path",
"in": "query" "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", "type": "boolean",
"description": "include diff stats for every commit (disable for speedup, default 'true')", "description": "include diff stats for every commit (disable for speedup, default 'true')",
@ -26138,6 +26198,10 @@
"type": "boolean", "type": "boolean",
"x-go-name": "AllowFastForwardOnly" "x-go-name": "AllowFastForwardOnly"
}, },
"allow_manual_merge": {
"type": "boolean",
"x-go-name": "AllowManualMerge"
},
"allow_merge_commits": { "allow_merge_commits": {
"type": "boolean", "type": "boolean",
"x-go-name": "AllowMerge" "x-go-name": "AllowMerge"
@ -26167,6 +26231,10 @@
"format": "date-time", "format": "date-time",
"x-go-name": "ArchivedAt" "x-go-name": "ArchivedAt"
}, },
"autodetect_manual_merge": {
"type": "boolean",
"x-go-name": "AutodetectManualMerge"
},
"avatar_url": { "avatar_url": {
"type": "string", "type": "string",
"x-go-name": "AvatarURL" "x-go-name": "AvatarURL"

View File

@ -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)
}
})
}

View File

@ -1156,6 +1156,7 @@ jobs:
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
Title: "add workflow", Title: "add workflow",
RepoID: repo.ID, RepoID: repo.ID,
Repo: repo,
Event: "workflow_dispatch", Event: "workflow_dispatch",
Ref: "refs/heads/dispatch", Ref: "refs/heads/dispatch",
WorkflowID: "dispatch.yml", WorkflowID: "dispatch.yml",
@ -1448,3 +1449,157 @@ jobs:
assert.Equal(t, pullRequest.MergedCommitID, actionRun.CommitSHA) 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)
})
}

View File

@ -4,6 +4,7 @@
package integration package integration
import ( import (
"fmt"
"net/http" "net/http"
"strconv" "strconv"
"testing" "testing"
@ -72,12 +73,37 @@ func TestAdminDeleteUser(t *testing.T) {
session := loginUser(t, "user1") session := loginUser(t, "user1")
usersToDelete := []struct {
userID int64
purge bool
}{
{
userID: 2,
purge: true,
},
{
userID: 8,
},
}
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) csrf := GetUserCSRFToken(t, session)
req := NewRequestWithValues(t, "POST", "/-/admin/users/8/delete", map[string]string{ req := NewRequestWithValues(t, "POST", fmt.Sprintf("/-/admin/users/%d/delete%s", entry.userID, query), map[string]string{
"_csrf": csrf, "_csrf": csrf,
}) })
session.MakeRequest(t, req, http.StatusSeeOther) session.MakeRequest(t, req, http.StatusSeeOther)
assertUserDeleted(t, 8) assertUserDeleted(t, entry.userID)
unittest.CheckConsistencyFor(t, &user_model.User{}) unittest.CheckConsistencyFor(t, &user_model.User{})
})
}
} }

View File

@ -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)
}

View File

@ -41,8 +41,6 @@ func testActionsRunnerAdmin(t *testing.T) {
runnerList := api.ActionRunnersResponse{} runnerList := api.ActionRunnersResponse{}
DecodeJSON(t, runnerListResp, &runnerList) DecodeJSON(t, runnerListResp, &runnerList)
assert.Len(t, runnerList.Entries, 4)
idx := slices.IndexFunc(runnerList.Entries, func(e *api.ActionRunner) bool { return e.ID == 34349 }) idx := slices.IndexFunc(runnerList.Entries, func(e *api.ActionRunner) bool { return e.ID == 34349 })
require.NotEqual(t, -1, idx) require.NotEqual(t, -1, idx)
expectedRunner := runnerList.Entries[idx] expectedRunner := runnerList.Entries[idx]
@ -160,16 +158,20 @@ func testActionsRunnerOwner(t *testing.T) {
runnerList := api.ActionRunnersResponse{} runnerList := api.ActionRunnersResponse{}
DecodeJSON(t, runnerListResp, &runnerList) DecodeJSON(t, runnerListResp, &runnerList)
assert.Len(t, runnerList.Entries, 1) idx := slices.IndexFunc(runnerList.Entries, func(e *api.ActionRunner) bool { return e.ID == 34347 })
assert.Equal(t, "runner_to_be_deleted-org", runnerList.Entries[0].Name) require.NotEqual(t, -1, idx)
assert.Equal(t, int64(34347), runnerList.Entries[0].ID) expectedRunner := runnerList.Entries[idx]
assert.False(t, runnerList.Entries[0].Ephemeral)
assert.Len(t, runnerList.Entries[0].Labels, 2) require.NotNil(t, expectedRunner)
assert.Equal(t, "runner_to_be_deleted", runnerList.Entries[0].Labels[0].Name) assert.Equal(t, "runner_to_be_deleted-org", expectedRunner.Name)
assert.Equal(t, "linux", runnerList.Entries[0].Labels[1].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 // 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) runnerResp := MakeRequest(t, req, http.StatusOK)
runner := api.ActionRunner{} runner := api.ActionRunner{}
@ -183,11 +185,11 @@ func testActionsRunnerOwner(t *testing.T) {
assert.Equal(t, "linux", runner.Labels[1].Name) assert.Equal(t, "linux", runner.Labels[1].Name)
// Verify delete the runner by id // 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) MakeRequest(t, req, http.StatusNoContent)
// Verify runner deletion // 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) 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) req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34349)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound) 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)
})
} }

View File

@ -23,6 +23,7 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -100,13 +101,16 @@ func TestEmptyRepoAddFile(t *testing.T) {
assert.Contains(t, resp.Body.String(), "test-file.md") 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) // if the repo is in incorrect state, it should be able to self-heal (recover to correct state)
testEmptyOrBrokenRecover := func(t *testing.T, isEmpty, isBroken bool) {
user30EmptyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 30, Name: "empty"}) user30EmptyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 30, Name: "empty"})
user30EmptyRepo.IsEmpty = true user30EmptyRepo.IsEmpty = isEmpty
user30EmptyRepo.Status = util.Iif(isBroken, repo_model.RepositoryBroken, repo_model.RepositoryReady)
user30EmptyRepo.DefaultBranch = "no-such" user30EmptyRepo.DefaultBranch = "no-such"
_, err := db.GetEngine(db.DefaultContext).ID(user30EmptyRepo.ID).Cols("is_empty", "default_branch").Update(user30EmptyRepo) _, err := db.GetEngine(db.DefaultContext).ID(user30EmptyRepo.ID).Cols("is_empty", "status", "default_branch").Update(user30EmptyRepo)
require.NoError(t, err) require.NoError(t, err)
user30EmptyRepo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 30, Name: "empty"}) user30EmptyRepo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 30, Name: "empty"})
assert.True(t, user30EmptyRepo.IsEmpty) assert.Equal(t, isEmpty, user30EmptyRepo.IsEmpty)
assert.Equal(t, isBroken, user30EmptyRepo.Status == repo_model.RepositoryBroken)
req = NewRequest(t, "GET", "/user30/empty") req = NewRequest(t, "GET", "/user30/empty")
resp = session.MakeRequest(t, req, http.StatusSeeOther) resp = session.MakeRequest(t, req, http.StatusSeeOther)
@ -117,6 +121,10 @@ func TestEmptyRepoAddFile(t *testing.T) {
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), "test-file.md") 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) { func TestEmptyRepoUploadFile(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()

View File

@ -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)
}

View File

@ -184,6 +184,15 @@ func testIssueAddComment(t *testing.T, session *TestSession, issueURL, content,
return int64(id) 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) { func TestNewIssue(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2") session := loginUser(t, "user2")

View File

@ -56,8 +56,12 @@ 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) 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{ req := NewRequestWithJSON(t, "POST", "/api/v1/repos/"+userName+"/"+repoName+"/hooks", api.CreateHookOption{
Type: "gitea", Type: "gitea",
Config: api.CreateHookOptionConfig{ Config: api.CreateHookOptionConfig{
@ -66,6 +70,7 @@ func testAPICreateWebhookForRepo(t *testing.T, session *TestSession, userName, r
}, },
Events: []string{event}, Events: []string{event},
Active: true, Active: true,
BranchFilter: branchFilterString,
}).AddTokenAuth(token) }).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated) MakeRequest(t, req, http.StatusCreated)
} }
@ -241,6 +246,7 @@ func Test_WebhookIssueComment(t *testing.T) {
testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "issue_comment") testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "issue_comment")
t.Run("create comment", func(t *testing.T) {
// 2. trigger the webhook // 2. trigger the webhook
issueURL := testNewIssue(t, session, "user2", "repo1", "Title2", "Description2") issueURL := testNewIssue(t, session, "user2", "repo1", "Title2", "Description2")
testIssueAddComment(t, session, issueURL, "issue title2 comment1", "") testIssueAddComment(t, session, issueURL, "issue title2 comment1", "")
@ -255,6 +261,54 @@ func Test_WebhookIssueComment(t *testing.T) {
assert.Equal(t, "Description2", payloads[0].Issue.Body) assert.Equal(t, "Description2", payloads[0].Issue.Body)
assert.Equal(t, "issue title2 comment1", payloads[0].Comment.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)
})
})
} }
func Test_WebhookRelease(t *testing.T) { func Test_WebhookRelease(t *testing.T) {
@ -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) { func Test_WebhookIssue(t *testing.T) {
var payloads []api.IssuePayload var payloads []api.IssuePayload
var triggeredEvent string 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) { func Test_WebhookPullRequest(t *testing.T) {
var payloads []api.PullRequestPayload var payloads []api.PullRequestPayload
var triggeredEvent string var triggeredEvent string

View File

@ -10,7 +10,7 @@
.project-header { .project-header {
padding: 0.5em 0; 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 { .project-header h2 {
@ -101,17 +101,11 @@
opacity: 0; opacity: 0;
} }
.fullscreen.projects-view .project-header { .fullscreen.projects-view {
position: fixed; position: absolute;
z-index: 1000; inset: 0;
top: 0; display: flex;
left: 0; flex-direction: column;
right: 0;
padding: 0.5em;
width: 100%;
max-width: 100%;
background-color: var(--color-body);
border-bottom: 1px solid var(--color-secondary);
} }
/* Hide project-description in full-screen due to its variable height. No need to show it for better space use. */ /* 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 { .fullscreen.projects-view #project-board {
position: absolute; flex: 1;
top: 60px; max-height: unset;
left: 0; padding-bottom: 0.5em;
right: 0;
max-height: calc(100vh - 70px);
} }

View File

@ -161,6 +161,7 @@ export function autosize(textarea: HTMLTextAreaElement, {viewportMarginBottom =
function resizeToFit() { function resizeToFit() {
if (isUserResized) return; if (isUserResized) return;
if (textarea.offsetWidth <= 0 && textarea.offsetHeight <= 0) return; if (textarea.offsetWidth <= 0 && textarea.offsetHeight <= 0) return;
const previousMargin = textarea.style.marginBottom;
try { try {
const {top, bottom} = overflowOffset(); const {top, bottom} = overflowOffset();
@ -176,6 +177,9 @@ export function autosize(textarea: HTMLTextAreaElement, {viewportMarginBottom =
const curHeight = parseFloat(computedStyle.height); const curHeight = parseFloat(computedStyle.height);
const maxHeight = curHeight + bottom - adjustedViewportMarginBottom; 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'; textarea.style.height = 'auto';
let newHeight = textarea.scrollHeight + borderAddOn; let newHeight = textarea.scrollHeight + borderAddOn;
@ -196,6 +200,12 @@ export function autosize(textarea: HTMLTextAreaElement, {viewportMarginBottom =
textarea.style.height = `${newHeight}px`; textarea.style.height = `${newHeight}px`;
lastStyleHeight = textarea.style.height; lastStyleHeight = textarea.style.height;
} finally { } 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 // ensure that the textarea is fully scrolled to the end, when the cursor
// is at the end during an input event // is at the end during an input event
if (textarea.selectionStart === textarea.selectionEnd && if (textarea.selectionStart === textarea.selectionEnd &&