mirror of https://github.com/go-gitea/gitea.git
Merge branch 'go-gitea:main' into main
This commit is contained in:
commit
29f084e495
|
|
@ -7,7 +7,7 @@
|
|||
"version": "lts"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/git-lfs:1.2.2": {},
|
||||
"ghcr.io/devcontainers-contrib/features/poetry:2": {},
|
||||
"ghcr.io/devcontainers-extra/features/poetry:2": {},
|
||||
"ghcr.io/devcontainers/features/python:1": {
|
||||
"version": "3.12"
|
||||
},
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -51,7 +51,7 @@ require (
|
|||
github.com/gliderlabs/ssh v0.3.8
|
||||
github.com/go-ap/activitypub v0.0.0-20250409143848-7113328b1f3d
|
||||
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
|
||||
github.com/go-chi/chi/v5 v5.2.1
|
||||
github.com/go-chi/chi/v5 v5.2.2
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/go-co-op/gocron v1.37.0
|
||||
github.com/go-enry/go-enry/v2 v2.9.2
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -289,8 +289,8 @@ github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5La
|
|||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
|
||||
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=
|
||||
|
|
|
|||
|
|
@ -166,6 +166,17 @@ func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, err
|
|||
return nil, fmt.Errorf("event %s is not a pull request event", run.Event)
|
||||
}
|
||||
|
||||
func (run *ActionRun) GetWorkflowRunEventPayload() (*api.WorkflowRunPayload, error) {
|
||||
if run.Event == webhook_module.HookEventWorkflowRun {
|
||||
var payload api.WorkflowRunPayload
|
||||
if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &payload, nil
|
||||
}
|
||||
return nil, fmt.Errorf("event %s is not a workflow run event", run.Event)
|
||||
}
|
||||
|
||||
func (run *ActionRun) IsSchedule() bool {
|
||||
return run.ScheduleID > 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,22 +80,31 @@ type FindRunJobOptions struct {
|
|||
func (opts FindRunJobOptions) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.RunID > 0 {
|
||||
cond = cond.And(builder.Eq{"run_id": opts.RunID})
|
||||
cond = cond.And(builder.Eq{"`action_run_job`.run_id": opts.RunID})
|
||||
}
|
||||
if opts.RepoID > 0 {
|
||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||
}
|
||||
if opts.OwnerID > 0 {
|
||||
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
||||
cond = cond.And(builder.Eq{"`action_run_job`.repo_id": opts.RepoID})
|
||||
}
|
||||
if opts.CommitSHA != "" {
|
||||
cond = cond.And(builder.Eq{"commit_sha": opts.CommitSHA})
|
||||
cond = cond.And(builder.Eq{"`action_run_job`.commit_sha": opts.CommitSHA})
|
||||
}
|
||||
if len(opts.Statuses) > 0 {
|
||||
cond = cond.And(builder.In("status", opts.Statuses))
|
||||
cond = cond.And(builder.In("`action_run_job`.status", opts.Statuses))
|
||||
}
|
||||
if opts.UpdatedBefore > 0 {
|
||||
cond = cond.And(builder.Lt{"updated": opts.UpdatedBefore})
|
||||
cond = cond.And(builder.Lt{"`action_run_job`.updated": opts.UpdatedBefore})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
func (opts FindRunJobOptions) ToJoins() []db.JoinFunc {
|
||||
if opts.OwnerID > 0 {
|
||||
return []db.JoinFunc{
|
||||
func(sess db.Engine) error {
|
||||
sess.Join("INNER", "repository", "repository.id = repo_id AND repository.owner_id = ?", opts.OwnerID)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,39 +72,50 @@ type FindRunOptions struct {
|
|||
TriggerEvent webhook_module.HookEventType
|
||||
Approved bool // not util.OptionalBool, it works only when it's true
|
||||
Status []Status
|
||||
CommitSHA string
|
||||
}
|
||||
|
||||
func (opts FindRunOptions) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.RepoID > 0 {
|
||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||
}
|
||||
if opts.OwnerID > 0 {
|
||||
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
||||
cond = cond.And(builder.Eq{"`action_run`.repo_id": opts.RepoID})
|
||||
}
|
||||
if opts.WorkflowID != "" {
|
||||
cond = cond.And(builder.Eq{"workflow_id": opts.WorkflowID})
|
||||
cond = cond.And(builder.Eq{"`action_run`.workflow_id": opts.WorkflowID})
|
||||
}
|
||||
if opts.TriggerUserID > 0 {
|
||||
cond = cond.And(builder.Eq{"trigger_user_id": opts.TriggerUserID})
|
||||
cond = cond.And(builder.Eq{"`action_run`.trigger_user_id": opts.TriggerUserID})
|
||||
}
|
||||
if opts.Approved {
|
||||
cond = cond.And(builder.Gt{"approved_by": 0})
|
||||
cond = cond.And(builder.Gt{"`action_run`.approved_by": 0})
|
||||
}
|
||||
if len(opts.Status) > 0 {
|
||||
cond = cond.And(builder.In("status", opts.Status))
|
||||
cond = cond.And(builder.In("`action_run`.status", opts.Status))
|
||||
}
|
||||
if opts.Ref != "" {
|
||||
cond = cond.And(builder.Eq{"ref": opts.Ref})
|
||||
cond = cond.And(builder.Eq{"`action_run`.ref": opts.Ref})
|
||||
}
|
||||
if opts.TriggerEvent != "" {
|
||||
cond = cond.And(builder.Eq{"trigger_event": opts.TriggerEvent})
|
||||
cond = cond.And(builder.Eq{"`action_run`.trigger_event": opts.TriggerEvent})
|
||||
}
|
||||
if opts.CommitSHA != "" {
|
||||
cond = cond.And(builder.Eq{"`action_run`.commit_sha": opts.CommitSHA})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
func (opts FindRunOptions) ToJoins() []db.JoinFunc {
|
||||
if opts.OwnerID > 0 {
|
||||
return []db.JoinFunc{func(sess db.Engine) error {
|
||||
sess.Join("INNER", "repository", "repository.id = repo_id AND repository.owner_id = ?", opts.OwnerID)
|
||||
return nil
|
||||
}}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (opts FindRunOptions) ToOrders() string {
|
||||
return "`id` DESC"
|
||||
return "`action_run`.`id` DESC"
|
||||
}
|
||||
|
||||
type StatusInfo struct {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
ref: "refs/heads/master"
|
||||
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
|
||||
event: "push"
|
||||
trigger_event: "push"
|
||||
is_fork_pull_request: 0
|
||||
status: 1
|
||||
started: 1683636528
|
||||
|
|
@ -28,6 +29,7 @@
|
|||
ref: "refs/heads/master"
|
||||
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
|
||||
event: "push"
|
||||
trigger_event: "push"
|
||||
is_fork_pull_request: 0
|
||||
status: 1
|
||||
started: 1683636528
|
||||
|
|
@ -47,6 +49,7 @@
|
|||
ref: "refs/heads/master"
|
||||
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
|
||||
event: "push"
|
||||
trigger_event: "push"
|
||||
is_fork_pull_request: 0
|
||||
status: 6 # running
|
||||
started: 1683636528
|
||||
|
|
@ -66,6 +69,47 @@
|
|||
ref: "refs/heads/test"
|
||||
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
|
||||
event: "push"
|
||||
trigger_event: "push"
|
||||
is_fork_pull_request: 0
|
||||
status: 1
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
created: 1683636108
|
||||
updated: 1683636626
|
||||
need_approval: 0
|
||||
approved_by: 0
|
||||
-
|
||||
id: 802
|
||||
title: "workflow run list"
|
||||
repo_id: 5
|
||||
owner_id: 3
|
||||
workflow_id: "test.yaml"
|
||||
index: 191
|
||||
trigger_user_id: 1
|
||||
ref: "refs/heads/test"
|
||||
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
|
||||
event: "push"
|
||||
trigger_event: "push"
|
||||
is_fork_pull_request: 0
|
||||
status: 1
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
created: 1683636108
|
||||
updated: 1683636626
|
||||
need_approval: 0
|
||||
approved_by: 0
|
||||
-
|
||||
id: 803
|
||||
title: "workflow run list for user"
|
||||
repo_id: 2
|
||||
owner_id: 0
|
||||
workflow_id: "test.yaml"
|
||||
index: 192
|
||||
trigger_user_id: 1
|
||||
ref: "refs/heads/test"
|
||||
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
|
||||
event: "push"
|
||||
trigger_event: "push"
|
||||
is_fork_pull_request: 0
|
||||
status: 1
|
||||
started: 1683636528
|
||||
|
|
@ -86,6 +130,7 @@
|
|||
ref: "refs/heads/test"
|
||||
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
|
||||
event: "push"
|
||||
trigger_event: "push"
|
||||
is_fork_pull_request: 0
|
||||
status: 2
|
||||
started: 1683636528
|
||||
|
|
|
|||
|
|
@ -99,3 +99,33 @@
|
|||
status: 2
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
-
|
||||
id: 203
|
||||
run_id: 802
|
||||
repo_id: 5
|
||||
owner_id: 0
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
is_fork_pull_request: 0
|
||||
name: job2
|
||||
attempt: 1
|
||||
job_id: job2
|
||||
needs: '["job1"]'
|
||||
task_id: 51
|
||||
status: 5
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
-
|
||||
id: 204
|
||||
run_id: 803
|
||||
repo_id: 2
|
||||
owner_id: 0
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
is_fork_pull_request: 0
|
||||
name: job2
|
||||
attempt: 1
|
||||
job_id: job2
|
||||
needs: '["job1"]'
|
||||
task_id: 51
|
||||
status: 5
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
|
|
|
|||
|
|
@ -112,7 +112,6 @@ type LFSMetaObject struct {
|
|||
ID int64 `xorm:"pk autoincr"`
|
||||
lfs.Pointer `xorm:"extends"`
|
||||
RepositoryID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
|
||||
Existing bool `xorm:"-"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
}
|
||||
|
|
@ -146,7 +145,6 @@ func NewLFSMetaObject(ctx context.Context, repoID int64, p lfs.Pointer) (*LFSMet
|
|||
if err != nil {
|
||||
return nil, err
|
||||
} else if exist {
|
||||
m.Existing = true
|
||||
return m, committer.Commit()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ func TestWebhook_EventsArray(t *testing.T) {
|
|||
"pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone",
|
||||
"pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected",
|
||||
"pull_request_review_comment", "pull_request_sync", "pull_request_review_request", "wiki", "repository", "release",
|
||||
"package", "status", "workflow_job",
|
||||
"package", "status", "workflow_run", "workflow_job",
|
||||
},
|
||||
(&Webhook{
|
||||
HookEvent: &webhook_module.HookEvent{SendEverything: true},
|
||||
|
|
|
|||
|
|
@ -246,6 +246,10 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
|
|||
webhook_module.HookEventPackage:
|
||||
return matchPackageEvent(payload.(*api.PackagePayload), evt)
|
||||
|
||||
case // workflow_run
|
||||
webhook_module.HookEventWorkflowRun:
|
||||
return matchWorkflowRunEvent(payload.(*api.WorkflowRunPayload), evt)
|
||||
|
||||
default:
|
||||
log.Warn("unsupported event %q", triggedEvent)
|
||||
return false
|
||||
|
|
@ -691,3 +695,53 @@ func matchPackageEvent(payload *api.PackagePayload, evt *jobparser.Event) bool {
|
|||
}
|
||||
return matchTimes == len(evt.Acts())
|
||||
}
|
||||
|
||||
func matchWorkflowRunEvent(payload *api.WorkflowRunPayload, evt *jobparser.Event) bool {
|
||||
// with no special filter parameters
|
||||
if len(evt.Acts()) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
matchTimes := 0
|
||||
// all acts conditions should be satisfied
|
||||
for cond, vals := range evt.Acts() {
|
||||
switch cond {
|
||||
case "types":
|
||||
action := payload.Action
|
||||
for _, val := range vals {
|
||||
if glob.MustCompile(val, '/').Match(action) {
|
||||
matchTimes++
|
||||
break
|
||||
}
|
||||
}
|
||||
case "workflows":
|
||||
workflow := payload.Workflow
|
||||
patterns, err := workflowpattern.CompilePatterns(vals...)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !workflowpattern.Skip(patterns, []string{workflow.Name}, &workflowpattern.EmptyTraceWriter{}) {
|
||||
matchTimes++
|
||||
}
|
||||
case "branches":
|
||||
patterns, err := workflowpattern.CompilePatterns(vals...)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !workflowpattern.Skip(patterns, []string{payload.WorkflowRun.HeadBranch}, &workflowpattern.EmptyTraceWriter{}) {
|
||||
matchTimes++
|
||||
}
|
||||
case "branches-ignore":
|
||||
patterns, err := workflowpattern.CompilePatterns(vals...)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !workflowpattern.Filter(patterns, []string{payload.WorkflowRun.HeadBranch}, &workflowpattern.EmptyTraceWriter{}) {
|
||||
matchTimes++
|
||||
}
|
||||
default:
|
||||
log.Warn("workflow run event unsupported condition %q", cond)
|
||||
}
|
||||
}
|
||||
return matchTimes == len(evt.Acts())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,8 +63,11 @@ func processNodeA(ctx *RenderContext, node *html.Node) {
|
|||
|
||||
func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) {
|
||||
next = img.NextSibling
|
||||
attrSrc, hasLazy := "", false
|
||||
for i, imgAttr := range img.Attr {
|
||||
hasLazy = hasLazy || imgAttr.Key == "loading" && imgAttr.Val == "lazy"
|
||||
if imgAttr.Key != "src" {
|
||||
attrSrc = imgAttr.Val
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -72,8 +75,8 @@ func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) {
|
|||
isLinkable := imgSrcOrigin != "" && !strings.HasPrefix(imgSrcOrigin, "data:")
|
||||
|
||||
// By default, the "<img>" tag should also be clickable,
|
||||
// because frontend use `<img>` to paste the re-scaled image into the markdown,
|
||||
// so it must match the default markdown image behavior.
|
||||
// because frontend uses `<img>` to paste the re-scaled image into the Markdown,
|
||||
// so it must match the default Markdown image behavior.
|
||||
cnt := 0
|
||||
for p := img.Parent; isLinkable && p != nil && cnt < 2; p = p.Parent {
|
||||
if hasParentAnchor := p.Type == html.ElementNode && p.Data == "a"; hasParentAnchor {
|
||||
|
|
@ -98,6 +101,9 @@ func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) {
|
|||
imgAttr.Val = camoHandleLink(imgAttr.Val)
|
||||
img.Attr[i] = imgAttr
|
||||
}
|
||||
if !RenderBehaviorForTesting.DisableAdditionalAttributes && !hasLazy && !strings.HasPrefix(attrSrc, "data:") {
|
||||
img.Attr = append(img.Attr, html.Attribute{Key: "loading", Val: "lazy"})
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ func TestRender_StandardLinks(t *testing.T) {
|
|||
func TestRender_Images(t *testing.T) {
|
||||
setting.AppURL = AppURL
|
||||
|
||||
test := func(input, expected string) {
|
||||
render := func(input, expected string) {
|
||||
buffer, err := markdown.RenderString(markup.NewTestRenderContext(FullURL), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
|
||||
|
|
@ -59,27 +59,32 @@ func TestRender_Images(t *testing.T) {
|
|||
result := util.URLJoin(FullURL, url)
|
||||
// hint: With Markdown v2.5.2, there is a new syntax: [link](URL){:target="_blank"} , but we do not support it now
|
||||
|
||||
test(
|
||||
render(
|
||||
"",
|
||||
`<p><a href="`+result+`" target="_blank" rel="nofollow noopener"><img src="`+result+`" alt="`+title+`"/></a></p>`)
|
||||
|
||||
test(
|
||||
render(
|
||||
"[["+title+"|"+url+"]]",
|
||||
`<p><a href="`+result+`" rel="nofollow"><img src="`+result+`" title="`+title+`" alt="`+title+`"/></a></p>`)
|
||||
test(
|
||||
render(
|
||||
"[]("+href+")",
|
||||
`<p><a href="`+href+`" rel="nofollow"><img src="`+result+`" alt="`+title+`"/></a></p>`)
|
||||
|
||||
test(
|
||||
render(
|
||||
"",
|
||||
`<p><a href="`+result+`" target="_blank" rel="nofollow noopener"><img src="`+result+`" alt="`+title+`"/></a></p>`)
|
||||
|
||||
test(
|
||||
render(
|
||||
"[["+title+"|"+url+"]]",
|
||||
`<p><a href="`+result+`" rel="nofollow"><img src="`+result+`" title="`+title+`" alt="`+title+`"/></a></p>`)
|
||||
test(
|
||||
render(
|
||||
"[]("+href+")",
|
||||
`<p><a href="`+href+`" rel="nofollow"><img src="`+result+`" alt="`+title+`"/></a></p>`)
|
||||
|
||||
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, false)()
|
||||
render(
|
||||
"<a><img src='a.jpg'></a>", // by the way, empty "a" tag will be removed
|
||||
`<p dir="auto"><img src="http://localhost:3000/user13/repo11/a.jpg" loading="lazy"/></p>`)
|
||||
}
|
||||
|
||||
func TestTotal_RenderString(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
|
|||
|
||||
policy.AllowAttrs("src", "autoplay", "controls").OnElements("video")
|
||||
|
||||
policy.AllowAttrs("loading").OnElements("img")
|
||||
|
||||
// Allow generally safe attributes (reference: https://github.com/jch/html-pipeline)
|
||||
generalSafeAttrs := []string{
|
||||
"abbr", "accept", "accept-charset",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
|
@ -72,20 +71,39 @@ type Manifest struct {
|
|||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
func IsMediaTypeValid(mt string) bool {
|
||||
return strings.HasPrefix(mt, "application/vnd.docker.") || strings.HasPrefix(mt, "application/vnd.oci.")
|
||||
}
|
||||
|
||||
func IsMediaTypeImageManifest(mt string) bool {
|
||||
return strings.EqualFold(mt, oci.MediaTypeImageManifest) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.v2+json")
|
||||
}
|
||||
|
||||
func IsMediaTypeImageIndex(mt string) bool {
|
||||
return strings.EqualFold(mt, oci.MediaTypeImageIndex) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.list.v2+json")
|
||||
}
|
||||
|
||||
// ParseImageConfig parses the metadata of an image config
|
||||
func ParseImageConfig(mt string, r io.Reader) (*Metadata, error) {
|
||||
if strings.EqualFold(mt, helm.ConfigMediaType) {
|
||||
func ParseImageConfig(mediaType string, r io.Reader) (*Metadata, error) {
|
||||
if strings.EqualFold(mediaType, helm.ConfigMediaType) {
|
||||
return parseHelmConfig(r)
|
||||
}
|
||||
|
||||
// fallback to OCI Image Config
|
||||
return parseOCIImageConfig(r)
|
||||
// FIXME: this fallback is not right, we should strictly check the media type in the future
|
||||
metadata, err := parseOCIImageConfig(r)
|
||||
if err != nil {
|
||||
if !IsMediaTypeImageManifest(mediaType) {
|
||||
return &Metadata{Platform: "unknown/unknown"}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
func parseOCIImageConfig(r io.Reader) (*Metadata, error) {
|
||||
var image oci.Image
|
||||
// EOF means empty input, still use the default data
|
||||
if err := json.NewDecoder(r).Decode(&image); err != nil && !errors.Is(err, io.EOF) {
|
||||
if err := json.NewDecoder(r).Decode(&image); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,10 +59,8 @@ func TestParseImageConfig(t *testing.T) {
|
|||
assert.ElementsMatch(t, []string{author}, metadata.Authors)
|
||||
assert.Equal(t, projectURL, metadata.ProjectURL)
|
||||
assert.Equal(t, repositoryURL, metadata.RepositoryURL)
|
||||
}
|
||||
|
||||
func TestParseOCIImageConfig(t *testing.T) {
|
||||
metadata, err := parseOCIImageConfig(strings.NewReader(""))
|
||||
metadata, err = ParseImageConfig("anything-unknown", strings.NewReader(""))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, &Metadata{Type: TypeOCI, Platform: DefaultPlatform, ImageLayers: []string{}}, metadata)
|
||||
assert.Equal(t, &Metadata{Platform: "unknown/unknown"}, metadata)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -470,6 +470,22 @@ func (p *CommitStatusPayload) JSONPayload() ([]byte, error) {
|
|||
return json.MarshalIndent(p, "", " ")
|
||||
}
|
||||
|
||||
// WorkflowRunPayload represents a payload information of workflow run event.
|
||||
type WorkflowRunPayload struct {
|
||||
Action string `json:"action"`
|
||||
Workflow *ActionWorkflow `json:"workflow"`
|
||||
WorkflowRun *ActionWorkflowRun `json:"workflow_run"`
|
||||
PullRequest *PullRequest `json:"pull_request,omitempty"`
|
||||
Organization *Organization `json:"organization,omitempty"`
|
||||
Repo *Repository `json:"repository"`
|
||||
Sender *User `json:"sender"`
|
||||
}
|
||||
|
||||
// JSONPayload implements Payload
|
||||
func (p *WorkflowRunPayload) JSONPayload() ([]byte, error) {
|
||||
return json.MarshalIndent(p, "", " ")
|
||||
}
|
||||
|
||||
// WorkflowJobPayload represents a payload information of workflow job event.
|
||||
type WorkflowJobPayload struct {
|
||||
Action string `json:"action"`
|
||||
|
|
|
|||
|
|
@ -86,9 +86,39 @@ type ActionArtifact struct {
|
|||
|
||||
// ActionWorkflowRun represents a WorkflowRun
|
||||
type ActionWorkflowRun struct {
|
||||
ID int64 `json:"id"`
|
||||
RepositoryID int64 `json:"repository_id"`
|
||||
HeadSha string `json:"head_sha"`
|
||||
ID int64 `json:"id"`
|
||||
URL string `json:"url"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
DisplayTitle string `json:"display_title"`
|
||||
Path string `json:"path"`
|
||||
Event string `json:"event"`
|
||||
RunAttempt int64 `json:"run_attempt"`
|
||||
RunNumber int64 `json:"run_number"`
|
||||
RepositoryID int64 `json:"repository_id,omitempty"`
|
||||
HeadSha string `json:"head_sha"`
|
||||
HeadBranch string `json:"head_branch,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Actor *User `json:"actor,omitempty"`
|
||||
TriggerActor *User `json:"trigger_actor,omitempty"`
|
||||
Repository *Repository `json:"repository,omitempty"`
|
||||
HeadRepository *Repository `json:"head_repository,omitempty"`
|
||||
Conclusion string `json:"conclusion,omitempty"`
|
||||
// swagger:strfmt date-time
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
// swagger:strfmt date-time
|
||||
CompletedAt time.Time `json:"completed_at"`
|
||||
}
|
||||
|
||||
// ActionWorkflowRunsResponse returns ActionWorkflowRuns
|
||||
type ActionWorkflowRunsResponse struct {
|
||||
Entries []*ActionWorkflowRun `json:"workflow_runs"`
|
||||
TotalCount int64 `json:"total_count"`
|
||||
}
|
||||
|
||||
// ActionWorkflowJobsResponse returns ActionWorkflowJobs
|
||||
type ActionWorkflowJobsResponse struct {
|
||||
Entries []*ActionWorkflowJob `json:"jobs"`
|
||||
TotalCount int64 `json:"total_count"`
|
||||
}
|
||||
|
||||
// ActionArtifactsResponse returns ActionArtifacts
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ type UpdateBranchProtectionPriories struct {
|
|||
|
||||
type MergeUpstreamRequest struct {
|
||||
Branch string `json:"branch"`
|
||||
FfOnly bool `json:"ff_only"`
|
||||
}
|
||||
|
||||
type MergeUpstreamResponse struct {
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ func (o *UpdateFileOptions) Branch() string {
|
|||
return o.FileOptions.BranchName
|
||||
}
|
||||
|
||||
// FIXME: ChangeFileOperation.SHA is NOT required for update or delete if last commit is provided in the options.
|
||||
|
||||
// ChangeFileOperation for creating, updating or deleting a file
|
||||
type ChangeFileOperation struct {
|
||||
// indicates what to do with the file
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ const (
|
|||
HookEventPullRequestReview HookEventType = "pull_request_review"
|
||||
// Actions event only
|
||||
HookEventSchedule HookEventType = "schedule"
|
||||
HookEventWorkflowRun HookEventType = "workflow_run"
|
||||
HookEventWorkflowJob HookEventType = "workflow_job"
|
||||
)
|
||||
|
||||
|
|
@ -67,6 +68,7 @@ func AllEvents() []HookEventType {
|
|||
HookEventRelease,
|
||||
HookEventPackage,
|
||||
HookEventStatus,
|
||||
HookEventWorkflowRun,
|
||||
HookEventWorkflowJob,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1354,7 +1354,7 @@ editor.update = Update %s
|
|||
editor.delete = Delete %s
|
||||
editor.patch = Apply Patch
|
||||
editor.patching = Patching:
|
||||
editor.fail_to_apply_patch = Unable to apply patch "%s"
|
||||
editor.fail_to_apply_patch = Unable to apply patch
|
||||
editor.new_patch = New Patch
|
||||
editor.commit_message_desc = Add an optional extended description…
|
||||
editor.signoff_desc = Add a Signed-off-by trailer by the committer at the end of the commit log message.
|
||||
|
|
@ -1374,8 +1374,7 @@ editor.branch_already_exists = Branch "%s" already exists in this repository.
|
|||
editor.directory_is_a_file = Directory name "%s" is already used as a filename in this repository.
|
||||
editor.file_is_a_symlink = `"%s" is a symbolic link. Symbolic links cannot be edited in the web editor`
|
||||
editor.filename_is_a_directory = Filename "%s" is already used as a directory name in this repository.
|
||||
editor.file_editing_no_longer_exists = The file being edited, "%s", no longer exists in this repository.
|
||||
editor.file_deleting_no_longer_exists = The file being deleted, "%s", no longer exists in this repository.
|
||||
editor.file_modifying_no_longer_exists = The file being modified, "%s", no longer exists in this repository.
|
||||
editor.file_changed_while_editing = The file contents have changed since you started editing. <a target="_blank" rel="noopener noreferrer" href="%s">Click here</a> to see them or <strong>Commit Changes again</strong> to overwrite them.
|
||||
editor.file_already_exists = A file named "%s" already exists in this repository.
|
||||
editor.commit_id_not_matching = The Commit ID does not match the ID when you began editing. Commit into a patch branch and then merge.
|
||||
|
|
@ -1383,8 +1382,6 @@ editor.push_out_of_date = The push appears to be out of date.
|
|||
editor.commit_empty_file_header = Commit an empty file
|
||||
editor.commit_empty_file_text = The file you're about to commit is empty. Proceed?
|
||||
editor.no_changes_to_show = There are no changes to show.
|
||||
editor.fail_to_update_file = Failed to update/create file "%s".
|
||||
editor.fail_to_update_file_summary = Error Message:
|
||||
editor.push_rejected_no_message = The change was rejected by the server without a message. Please check Git Hooks.
|
||||
editor.push_rejected = The change was rejected by the server. Please check Git Hooks.
|
||||
editor.push_rejected_summary = Full Rejection Message:
|
||||
|
|
@ -1398,6 +1395,8 @@ editor.user_no_push_to_branch = User cannot push to branch
|
|||
editor.require_signed_commit = Branch requires a signed commit
|
||||
editor.cherry_pick = Cherry-pick %s onto:
|
||||
editor.revert = Revert %s onto:
|
||||
editor.failed_to_commit = Failed to commit changes.
|
||||
editor.failed_to_commit_summary = Error Message:
|
||||
|
||||
commits.desc = Browse source code change history.
|
||||
commits.commits = Commits
|
||||
|
|
@ -2403,6 +2402,8 @@ settings.event_pull_request_review_request_desc = Pull request review requested
|
|||
settings.event_pull_request_approvals = Pull Request Approvals
|
||||
settings.event_pull_request_merge = Pull Request Merge
|
||||
settings.event_header_workflow = Workflow Events
|
||||
settings.event_workflow_run = Workflow Run
|
||||
settings.event_workflow_run_desc = Gitea Actions Workflow run queued, waiting, in progress, or completed.
|
||||
settings.event_workflow_job = Workflow Jobs
|
||||
settings.event_workflow_job_desc = Gitea Actions Workflow job queued, waiting, in progress, or completed.
|
||||
settings.event_package = Package
|
||||
|
|
|
|||
|
|
@ -1332,7 +1332,9 @@ editor.upload_file=Uaslódáil Comhad
|
|||
editor.edit_file=Cuir Comhad in eagar
|
||||
editor.preview_changes=Athruithe Réamhamhar
|
||||
editor.cannot_edit_lfs_files=Ní féidir comhaid LFS a chur in eagar sa chomhéadan gréasáin.
|
||||
editor.cannot_edit_too_large_file=Tá an comhad rómhór le cur in eagar.
|
||||
editor.cannot_edit_non_text_files=Ní féidir comhaid dhénártha a chur in eagar sa chomhéadan gréasáin.
|
||||
editor.file_not_editable_hint=Ach is féidir leat é a athainmniú nó a bhogadh fós.
|
||||
editor.edit_this_file=Cuir Comhad in eagar
|
||||
editor.this_file_locked=Tá an comhad faoi ghlas
|
||||
editor.must_be_on_a_branch=Caithfidh tú a bheith ar bhrainse chun athruithe a dhéanamh nó a mholadh ar an gcomhad seo.
|
||||
|
|
|
|||
|
|
@ -425,8 +425,7 @@ account_activated=Обліковий запис активовано
|
|||
prohibit_login=Вхід заборонено
|
||||
prohibit_login_desc=Ваш обліковий запис заблоковано для входу, зверніться до адміністратора сайту.
|
||||
resent_limit_prompt=Ви вже надсилали запит на активацію нещодавно. Зачекайте 3 хвилини і спробуйте ще раз.
|
||||
has_unconfirmed_mail=
|
||||
Привіт %s, у вас непідтверджена адреса електронної пошти (<b>%s</b>). Якщо ви не отримали листа з підтвердженням або вам потрібно надіслати новий, будь ласка, натисніть кнопку нижче.
|
||||
has_unconfirmed_mail=Привіт %s, у вас непідтверджена адреса електронної пошти (<b>%s</b>). Якщо ви не отримали листа з підтвердженням або вам потрібно надіслати новий, будь ласка, натисніть кнопку нижче.
|
||||
change_unconfirmed_mail_address=Якщо ваша адреса електронної пошти для реєстрації невірна, ви можете змінити її тут і надіслати новий лист з підтвердженням.
|
||||
resend_mail=Натисніть тут, щоб повторно надіслати лист з активацією
|
||||
email_not_associate=Ця адреса електронної пошти не пов'язана з жодним обліковим записом.
|
||||
|
|
|
|||
|
|
@ -1332,7 +1332,9 @@ editor.upload_file=上传文件
|
|||
editor.edit_file=编辑文件
|
||||
editor.preview_changes=预览变更
|
||||
editor.cannot_edit_lfs_files=无法在 web 界面中编辑 lfs 文件。
|
||||
editor.cannot_edit_too_large_file=文件过大,无法编辑。
|
||||
editor.cannot_edit_non_text_files=网页不能编辑二进制文件。
|
||||
editor.file_not_editable_hint=但您仍然可以重命名或移动它。
|
||||
editor.edit_this_file=编辑文件
|
||||
editor.this_file_locked=文件已锁定
|
||||
editor.must_be_on_a_branch=您必须在某个分支上才能对此文件进行修改操作。
|
||||
|
|
|
|||
|
|
@ -693,6 +693,8 @@ func ContainerRoutes() *web.Router {
|
|||
&container.Auth{},
|
||||
})
|
||||
|
||||
// TODO: Content Discovery / References (not implemented yet)
|
||||
|
||||
r.Get("", container.ReqContainerAccess, container.DetermineSupport)
|
||||
r.Group("/token", func() {
|
||||
r.Get("", container.Authenticate)
|
||||
|
|
|
|||
|
|
@ -320,6 +320,7 @@ func PostBlobsUploads(ctx *context.Context) {
|
|||
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
|
||||
func GetBlobsUpload(ctx *context.Context) {
|
||||
image := ctx.PathParam("image")
|
||||
uuid := ctx.PathParam("uuid")
|
||||
|
||||
upload, err := packages_model.GetBlobUploadByID(ctx, uuid)
|
||||
|
|
@ -334,6 +335,7 @@ func GetBlobsUpload(ctx *context.Context) {
|
|||
|
||||
// FIXME: undefined behavior when the uploaded content is empty: https://github.com/opencontainers/distribution-spec/issues/578
|
||||
respHeaders := &containerHeaders{
|
||||
Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, upload.ID),
|
||||
UploadUUID: upload.ID,
|
||||
Status: http.StatusNoContent,
|
||||
}
|
||||
|
|
@ -386,7 +388,7 @@ func PatchBlobsUpload(ctx *context.Context) {
|
|||
UploadUUID: uploader.ID,
|
||||
Status: http.StatusAccepted,
|
||||
}
|
||||
if contentRange != "" {
|
||||
if uploader.Size() > 0 {
|
||||
respHeaders.Range = fmt.Sprintf("0-%d", uploader.Size()-1)
|
||||
}
|
||||
setResponseHeaders(ctx.Resp, respHeaders)
|
||||
|
|
|
|||
|
|
@ -29,18 +29,6 @@ import (
|
|||
oci "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
func isMediaTypeValid(mt string) bool {
|
||||
return strings.HasPrefix(mt, "application/vnd.docker.") || strings.HasPrefix(mt, "application/vnd.oci.")
|
||||
}
|
||||
|
||||
func isMediaTypeImageManifest(mt string) bool {
|
||||
return strings.EqualFold(mt, oci.MediaTypeImageManifest) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.v2+json")
|
||||
}
|
||||
|
||||
func isMediaTypeImageIndex(mt string) bool {
|
||||
return strings.EqualFold(mt, oci.MediaTypeImageIndex) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.list.v2+json")
|
||||
}
|
||||
|
||||
// manifestCreationInfo describes a manifest to create
|
||||
type manifestCreationInfo struct {
|
||||
MediaType string
|
||||
|
|
@ -66,16 +54,16 @@ func processManifest(ctx context.Context, mci *manifestCreationInfo, buf *packag
|
|||
return "", err
|
||||
}
|
||||
|
||||
if !isMediaTypeValid(mci.MediaType) {
|
||||
if !container_module.IsMediaTypeValid(mci.MediaType) {
|
||||
mci.MediaType = index.MediaType
|
||||
if !isMediaTypeValid(mci.MediaType) {
|
||||
if !container_module.IsMediaTypeValid(mci.MediaType) {
|
||||
return "", errManifestInvalid.WithMessage("MediaType not recognized")
|
||||
}
|
||||
}
|
||||
|
||||
if isMediaTypeImageManifest(mci.MediaType) {
|
||||
if container_module.IsMediaTypeImageManifest(mci.MediaType) {
|
||||
return processOciImageManifest(ctx, mci, buf)
|
||||
} else if isMediaTypeImageIndex(mci.MediaType) {
|
||||
} else if container_module.IsMediaTypeImageIndex(mci.MediaType) {
|
||||
return processOciImageIndex(ctx, mci, buf)
|
||||
}
|
||||
return "", errManifestInvalid
|
||||
|
|
@ -201,7 +189,7 @@ func processOciImageIndex(ctx context.Context, mci *manifestCreationInfo, buf *p
|
|||
}
|
||||
|
||||
for _, manifest := range index.Manifests {
|
||||
if !isMediaTypeImageManifest(manifest.MediaType) {
|
||||
if !container_module.IsMediaTypeImageManifest(manifest.MediaType) {
|
||||
return errManifestInvalid
|
||||
}
|
||||
|
||||
|
|
@ -336,7 +324,7 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if isMediaTypeImageIndex(mci.MediaType) {
|
||||
if container_module.IsMediaTypeImageIndex(mci.MediaType) {
|
||||
if pv.CreatedUnix.AsTime().Before(time.Now().Add(-24 * time.Hour)) {
|
||||
if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/routers/api/v1/shared"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// ListWorkflowJobs Lists all jobs
|
||||
func ListWorkflowJobs(ctx *context.APIContext) {
|
||||
// swagger:operation GET /admin/actions/jobs admin listAdminWorkflowJobs
|
||||
// ---
|
||||
// summary: Lists all jobs
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: status
|
||||
// in: query
|
||||
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/WorkflowJobsList"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
shared.ListJobs(ctx, 0, 0, 0)
|
||||
}
|
||||
|
||||
// ListWorkflowRuns Lists all runs
|
||||
func ListWorkflowRuns(ctx *context.APIContext) {
|
||||
// swagger:operation GET /admin/actions/runs admin listAdminWorkflowRuns
|
||||
// ---
|
||||
// summary: Lists all runs
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: event
|
||||
// in: query
|
||||
// description: workflow event name
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: branch
|
||||
// in: query
|
||||
// description: workflow branch
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: status
|
||||
// in: query
|
||||
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: actor
|
||||
// in: query
|
||||
// description: triggered by user
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: head_sha
|
||||
// in: query
|
||||
// description: triggering sha of the workflow run
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/WorkflowRunsList"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
shared.ListRuns(ctx, 0, 0)
|
||||
}
|
||||
|
|
@ -942,6 +942,8 @@ func Routes() *web.Router {
|
|||
m.Get("/{runner_id}", reqToken(), reqChecker, act.GetRunner)
|
||||
m.Delete("/{runner_id}", reqToken(), reqChecker, act.DeleteRunner)
|
||||
})
|
||||
m.Get("/runs", reqToken(), reqChecker, act.ListWorkflowRuns)
|
||||
m.Get("/jobs", reqToken(), reqChecker, act.ListWorkflowJobs)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -1078,6 +1080,9 @@ func Routes() *web.Router {
|
|||
m.Get("/{runner_id}", reqToken(), user.GetRunner)
|
||||
m.Delete("/{runner_id}", reqToken(), user.DeleteRunner)
|
||||
})
|
||||
|
||||
m.Get("/runs", reqToken(), user.ListWorkflowRuns)
|
||||
m.Get("/jobs", reqToken(), user.ListWorkflowJobs)
|
||||
})
|
||||
|
||||
m.Get("/followers", user.ListMyFollowers)
|
||||
|
|
@ -1202,6 +1207,7 @@ func Routes() *web.Router {
|
|||
}, context.ReferencesGitRepo(), reqToken(), reqRepoReader(unit.TypeActions))
|
||||
|
||||
m.Group("/actions/jobs", func() {
|
||||
m.Get("/{job_id}", repo.GetWorkflowJob)
|
||||
m.Get("/{job_id}/logs", repo.DownloadActionsRunJobLogs)
|
||||
}, reqToken(), reqRepoReader(unit.TypeActions))
|
||||
|
||||
|
|
@ -1280,9 +1286,13 @@ func Routes() *web.Router {
|
|||
}, reqToken(), reqAdmin())
|
||||
m.Group("/actions", func() {
|
||||
m.Get("/tasks", repo.ListActionTasks)
|
||||
m.Group("/runs/{run}", func() {
|
||||
m.Get("/artifacts", repo.GetArtifactsOfRun)
|
||||
m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun)
|
||||
m.Group("/runs", func() {
|
||||
m.Group("/{run}", func() {
|
||||
m.Get("", repo.GetWorkflowRun)
|
||||
m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun)
|
||||
m.Get("/jobs", repo.ListWorkflowRunJobs)
|
||||
m.Get("/artifacts", repo.GetArtifactsOfRun)
|
||||
})
|
||||
})
|
||||
m.Get("/artifacts", repo.GetArtifacts)
|
||||
m.Group("/artifacts/{artifact_id}", func() {
|
||||
|
|
@ -1734,11 +1744,15 @@ func Routes() *web.Router {
|
|||
Patch(bind(api.EditHookOption{}), admin.EditHook).
|
||||
Delete(admin.DeleteHook)
|
||||
})
|
||||
m.Group("/actions/runners", func() {
|
||||
m.Get("", admin.ListRunners)
|
||||
m.Post("/registration-token", admin.CreateRegistrationToken)
|
||||
m.Get("/{runner_id}", admin.GetRunner)
|
||||
m.Delete("/{runner_id}", admin.DeleteRunner)
|
||||
m.Group("/actions", func() {
|
||||
m.Group("/runners", func() {
|
||||
m.Get("", admin.ListRunners)
|
||||
m.Post("/registration-token", admin.CreateRegistrationToken)
|
||||
m.Get("/{runner_id}", admin.GetRunner)
|
||||
m.Delete("/{runner_id}", admin.DeleteRunner)
|
||||
})
|
||||
m.Get("/runs", admin.ListWorkflowRuns)
|
||||
m.Get("/jobs", admin.ListWorkflowJobs)
|
||||
})
|
||||
m.Group("/runners", func() {
|
||||
m.Get("/registration-token", admin.GetRegistrationToken)
|
||||
|
|
|
|||
|
|
@ -570,6 +570,96 @@ func (Action) DeleteRunner(ctx *context.APIContext) {
|
|||
shared.DeleteRunner(ctx, ctx.Org.Organization.ID, 0, ctx.PathParamInt64("runner_id"))
|
||||
}
|
||||
|
||||
func (Action) ListWorkflowJobs(ctx *context.APIContext) {
|
||||
// swagger:operation GET /orgs/{org}/actions/jobs organization getOrgWorkflowJobs
|
||||
// ---
|
||||
// summary: Get org-level workflow jobs
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: status
|
||||
// in: query
|
||||
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/WorkflowJobsList"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
shared.ListJobs(ctx, ctx.Org.Organization.ID, 0, 0)
|
||||
}
|
||||
|
||||
func (Action) ListWorkflowRuns(ctx *context.APIContext) {
|
||||
// swagger:operation GET /orgs/{org}/actions/runs organization getOrgWorkflowRuns
|
||||
// ---
|
||||
// summary: Get org-level workflow runs
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: event
|
||||
// in: query
|
||||
// description: workflow event name
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: branch
|
||||
// in: query
|
||||
// description: workflow branch
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: status
|
||||
// in: query
|
||||
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: actor
|
||||
// in: query
|
||||
// description: triggered by user
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: head_sha
|
||||
// in: query
|
||||
// description: triggering sha of the workflow run
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/WorkflowRunsList"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
shared.ListRuns(ctx, ctx.Org.Organization.ID, 0)
|
||||
}
|
||||
|
||||
var _ actions_service.API = new(Action)
|
||||
|
||||
// Action implements actions_service.API
|
||||
|
|
|
|||
|
|
@ -650,6 +650,114 @@ func (Action) DeleteRunner(ctx *context.APIContext) {
|
|||
shared.DeleteRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id"))
|
||||
}
|
||||
|
||||
// GetWorkflowRunJobs Lists all jobs for a workflow run.
|
||||
func (Action) ListWorkflowJobs(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/actions/jobs repository listWorkflowJobs
|
||||
// ---
|
||||
// summary: Lists all jobs for a repository
|
||||
// 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: status
|
||||
// in: query
|
||||
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/WorkflowJobsList"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
repoID := ctx.Repo.Repository.ID
|
||||
|
||||
shared.ListJobs(ctx, 0, repoID, 0)
|
||||
}
|
||||
|
||||
// ListWorkflowRuns Lists all runs for a repository run.
|
||||
func (Action) ListWorkflowRuns(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/actions/runs repository getWorkflowRuns
|
||||
// ---
|
||||
// summary: Lists all runs for a repository 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: event
|
||||
// in: query
|
||||
// description: workflow event name
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: branch
|
||||
// in: query
|
||||
// description: workflow branch
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: status
|
||||
// in: query
|
||||
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: actor
|
||||
// in: query
|
||||
// description: triggered by user
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: head_sha
|
||||
// in: query
|
||||
// description: triggering sha of the workflow run
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ArtifactsList"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
repoID := ctx.Repo.Repository.ID
|
||||
|
||||
shared.ListRuns(ctx, 0, repoID)
|
||||
}
|
||||
|
||||
var _ actions_service.API = new(Action)
|
||||
|
||||
// Action implements actions_service.API
|
||||
|
|
@ -756,7 +864,7 @@ func ActionsListRepositoryWorkflows(ctx *context.APIContext) {
|
|||
// "500":
|
||||
// "$ref": "#/responses/error"
|
||||
|
||||
workflows, err := actions_service.ListActionWorkflows(ctx)
|
||||
workflows, err := convert.ListActionWorkflows(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
|
|
@ -802,7 +910,7 @@ func ActionsGetWorkflow(ctx *context.APIContext) {
|
|||
// "$ref": "#/responses/error"
|
||||
|
||||
workflowID := ctx.PathParam("workflow_id")
|
||||
workflow, err := actions_service.GetActionWorkflow(ctx, workflowID)
|
||||
workflow, err := convert.GetActionWorkflow(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository, workflowID)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.APIError(http.StatusNotFound, err)
|
||||
|
|
@ -992,6 +1100,157 @@ func ActionsEnableWorkflow(ctx *context.APIContext) {
|
|||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// GetWorkflowRun Gets a specific workflow run.
|
||||
func GetWorkflowRun(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run} repository GetWorkflowRun
|
||||
// ---
|
||||
// summary: Gets a specific 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: id of the run
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/WorkflowRun"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
runID := ctx.PathParamInt64("run")
|
||||
job, _, err := db.GetByID[actions_model.ActionRun](ctx, runID)
|
||||
|
||||
if err != nil || job.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.APIError(http.StatusNotFound, util.ErrNotExist)
|
||||
}
|
||||
|
||||
convertedArtifact, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, job)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convertedArtifact)
|
||||
}
|
||||
|
||||
// ListWorkflowRunJobs Lists all jobs for a workflow run.
|
||||
func ListWorkflowRunJobs(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/jobs repository listWorkflowRunJobs
|
||||
// ---
|
||||
// summary: Lists all jobs for 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
|
||||
// - name: status
|
||||
// in: query
|
||||
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/WorkflowJobsList"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
repoID := ctx.Repo.Repository.ID
|
||||
|
||||
runID := ctx.PathParamInt64("run")
|
||||
|
||||
// Avoid the list all jobs functionality for this api route to be used with a runID == 0.
|
||||
if runID <= 0 {
|
||||
ctx.APIError(http.StatusBadRequest, util.NewInvalidArgumentErrorf("runID must be a positive integer"))
|
||||
return
|
||||
}
|
||||
|
||||
// runID is used as an additional filter next to repoID to ensure that we only list jobs for the specified repoID and runID.
|
||||
// no additional checks for runID are needed here
|
||||
shared.ListJobs(ctx, 0, repoID, runID)
|
||||
}
|
||||
|
||||
// GetWorkflowJob Gets a specific workflow job for a workflow run.
|
||||
func GetWorkflowJob(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/actions/jobs/{job_id} repository getWorkflowJob
|
||||
// ---
|
||||
// summary: Gets a specific workflow job for 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: job_id
|
||||
// in: path
|
||||
// description: id of the job
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/WorkflowJob"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
jobID := ctx.PathParamInt64("job_id")
|
||||
job, _, err := db.GetByID[actions_model.ActionRunJob](ctx, jobID)
|
||||
|
||||
if err != nil || job.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.APIError(http.StatusNotFound, util.ErrNotExist)
|
||||
}
|
||||
|
||||
convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, nil, job)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convertedWorkflowJob)
|
||||
}
|
||||
|
||||
// GetArtifacts Lists all artifacts for a repository.
|
||||
func GetArtifactsOfRun(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/artifacts repository getArtifactsOfRun
|
||||
|
|
|
|||
|
|
@ -1181,7 +1181,7 @@ func MergeUpstream(ctx *context.APIContext) {
|
|||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
form := web.GetForm(ctx).(*api.MergeUpstreamRequest)
|
||||
mergeStyle, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, form.Branch)
|
||||
mergeStyle, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, form.Branch, form.FfOnly)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.APIError(http.StatusBadRequest, err)
|
||||
|
|
|
|||
|
|
@ -470,6 +470,9 @@ func ChangeFiles(ctx *context.APIContext) {
|
|||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
// FIXME: actually now we support more operations like "rename", "upload"
|
||||
// FIXME: ChangeFileOperation.SHA is NOT required for update or delete if last commit is provided in the options.
|
||||
// Need to fully fix them in API
|
||||
changeRepoFile := &files_service.ChangeRepoFile{
|
||||
Operation: file.Operation,
|
||||
TreePath: file.Path,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,187 @@
|
|||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package shared
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/webhook"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
)
|
||||
|
||||
// ListJobs lists jobs for api route validated ownerID and repoID
|
||||
// ownerID == 0 and repoID == 0 means all jobs
|
||||
// ownerID == 0 and repoID != 0 means all jobs for the given repo
|
||||
// ownerID != 0 and repoID == 0 means all jobs for the given user/org
|
||||
// ownerID != 0 and repoID != 0 undefined behavior
|
||||
// runID == 0 means all jobs
|
||||
// runID is used as an additional filter together with ownerID and repoID to only return jobs for the given run
|
||||
// Access rights are checked at the API route level
|
||||
func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64) {
|
||||
if ownerID != 0 && repoID != 0 {
|
||||
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
|
||||
}
|
||||
opts := actions_model.FindRunJobOptions{
|
||||
OwnerID: ownerID,
|
||||
RepoID: repoID,
|
||||
RunID: runID,
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
}
|
||||
for _, status := range ctx.FormStrings("status") {
|
||||
values, err := convertToInternal(status)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusBadRequest, fmt.Errorf("Invalid status %s", status))
|
||||
return
|
||||
}
|
||||
opts.Statuses = append(opts.Statuses, values...)
|
||||
}
|
||||
|
||||
jobs, total, err := db.FindAndCount[actions_model.ActionRunJob](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
res := new(api.ActionWorkflowJobsResponse)
|
||||
res.TotalCount = total
|
||||
|
||||
res.Entries = make([]*api.ActionWorkflowJob, len(jobs))
|
||||
|
||||
isRepoLevel := repoID != 0 && ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == repoID
|
||||
for i := range jobs {
|
||||
var repository *repo_model.Repository
|
||||
if isRepoLevel {
|
||||
repository = ctx.Repo.Repository
|
||||
} else {
|
||||
repository, err = repo_model.GetRepositoryByID(ctx, jobs[i].RepoID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, repository, nil, jobs[i])
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
res.Entries[i] = convertedWorkflowJob
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, &res)
|
||||
}
|
||||
|
||||
func convertToInternal(s string) ([]actions_model.Status, error) {
|
||||
switch s {
|
||||
case "pending", "waiting", "requested", "action_required":
|
||||
return []actions_model.Status{actions_model.StatusBlocked}, nil
|
||||
case "queued":
|
||||
return []actions_model.Status{actions_model.StatusWaiting}, nil
|
||||
case "in_progress":
|
||||
return []actions_model.Status{actions_model.StatusRunning}, nil
|
||||
case "completed":
|
||||
return []actions_model.Status{
|
||||
actions_model.StatusSuccess,
|
||||
actions_model.StatusFailure,
|
||||
actions_model.StatusSkipped,
|
||||
actions_model.StatusCancelled,
|
||||
}, nil
|
||||
case "failure":
|
||||
return []actions_model.Status{actions_model.StatusFailure}, nil
|
||||
case "success":
|
||||
return []actions_model.Status{actions_model.StatusSuccess}, nil
|
||||
case "skipped", "neutral":
|
||||
return []actions_model.Status{actions_model.StatusSkipped}, nil
|
||||
case "cancelled", "timed_out":
|
||||
return []actions_model.Status{actions_model.StatusCancelled}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid status %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
// ListRuns lists jobs for api route validated ownerID and repoID
|
||||
// ownerID == 0 and repoID == 0 means all runs
|
||||
// ownerID == 0 and repoID != 0 means all runs for the given repo
|
||||
// ownerID != 0 and repoID == 0 means all runs for the given user/org
|
||||
// ownerID != 0 and repoID != 0 undefined behavior
|
||||
// Access rights are checked at the API route level
|
||||
func ListRuns(ctx *context.APIContext, ownerID, repoID int64) {
|
||||
if ownerID != 0 && repoID != 0 {
|
||||
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
|
||||
}
|
||||
opts := actions_model.FindRunOptions{
|
||||
OwnerID: ownerID,
|
||||
RepoID: repoID,
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
}
|
||||
|
||||
if event := ctx.FormString("event"); event != "" {
|
||||
opts.TriggerEvent = webhook.HookEventType(event)
|
||||
}
|
||||
if branch := ctx.FormString("branch"); branch != "" {
|
||||
opts.Ref = string(git.RefNameFromBranch(branch))
|
||||
}
|
||||
for _, status := range ctx.FormStrings("status") {
|
||||
values, err := convertToInternal(status)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusBadRequest, fmt.Errorf("Invalid status %s", status))
|
||||
return
|
||||
}
|
||||
opts.Status = append(opts.Status, values...)
|
||||
}
|
||||
if actor := ctx.FormString("actor"); actor != "" {
|
||||
user, err := user_model.GetUserByName(ctx, actor)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
opts.TriggerUserID = user.ID
|
||||
}
|
||||
if headSHA := ctx.FormString("head_sha"); headSHA != "" {
|
||||
opts.CommitSHA = headSHA
|
||||
}
|
||||
|
||||
runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
res := new(api.ActionWorkflowRunsResponse)
|
||||
res.TotalCount = total
|
||||
|
||||
res.Entries = make([]*api.ActionWorkflowRun, len(runs))
|
||||
isRepoLevel := repoID != 0 && ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == repoID
|
||||
for i := range runs {
|
||||
var repository *repo_model.Repository
|
||||
if isRepoLevel {
|
||||
repository = ctx.Repo.Repository
|
||||
} else {
|
||||
repository, err = repo_model.GetRepositoryByID(ctx, runs[i].RepoID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
convertedRun, err := convert.ToActionWorkflowRun(ctx, repository, runs[i])
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
res.Entries[i] = convertedRun
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, &res)
|
||||
}
|
||||
|
|
@ -443,6 +443,34 @@ type swaggerRepoTasksList struct {
|
|||
Body api.ActionTaskResponse `json:"body"`
|
||||
}
|
||||
|
||||
// WorkflowRunsList
|
||||
// swagger:response WorkflowRunsList
|
||||
type swaggerActionWorkflowRunsResponse struct {
|
||||
// in:body
|
||||
Body api.ActionWorkflowRunsResponse `json:"body"`
|
||||
}
|
||||
|
||||
// WorkflowRun
|
||||
// swagger:response WorkflowRun
|
||||
type swaggerWorkflowRun struct {
|
||||
// in:body
|
||||
Body api.ActionWorkflowRun `json:"body"`
|
||||
}
|
||||
|
||||
// WorkflowJobsList
|
||||
// swagger:response WorkflowJobsList
|
||||
type swaggerActionWorkflowJobsResponse struct {
|
||||
// in:body
|
||||
Body api.ActionWorkflowJobsResponse `json:"body"`
|
||||
}
|
||||
|
||||
// WorkflowJob
|
||||
// swagger:response WorkflowJob
|
||||
type swaggerWorkflowJob struct {
|
||||
// in:body
|
||||
Body api.ActionWorkflowJob `json:"body"`
|
||||
}
|
||||
|
||||
// ArtifactsList
|
||||
// swagger:response ArtifactsList
|
||||
type swaggerRepoArtifactsList struct {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/shared"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
actions_service "code.gitea.io/gitea/services/actions"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
|
|
@ -356,3 +357,86 @@ func ListVariables(ctx *context.APIContext) {
|
|||
ctx.SetTotalCountHeader(count)
|
||||
ctx.JSON(http.StatusOK, variables)
|
||||
}
|
||||
|
||||
// ListWorkflowRuns lists workflow runs
|
||||
func ListWorkflowRuns(ctx *context.APIContext) {
|
||||
// swagger:operation GET /user/actions/runs user getUserWorkflowRuns
|
||||
// ---
|
||||
// summary: Get workflow runs
|
||||
// parameters:
|
||||
// - name: event
|
||||
// in: query
|
||||
// description: workflow event name
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: branch
|
||||
// in: query
|
||||
// description: workflow branch
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: status
|
||||
// in: query
|
||||
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: actor
|
||||
// in: query
|
||||
// description: triggered by user
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: head_sha
|
||||
// in: query
|
||||
// description: triggering sha of the workflow run
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// produces:
|
||||
// - application/json
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/WorkflowRunsList"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
shared.ListRuns(ctx, ctx.Doer.ID, 0)
|
||||
}
|
||||
|
||||
// ListWorkflowJobs lists workflow jobs
|
||||
func ListWorkflowJobs(ctx *context.APIContext) {
|
||||
// swagger:operation GET /user/actions/jobs user getUserWorkflowJobs
|
||||
// ---
|
||||
// summary: Get workflow jobs
|
||||
// parameters:
|
||||
// - name: status
|
||||
// in: query
|
||||
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// produces:
|
||||
// - application/json
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/WorkflowJobsList"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
shared.ListJobs(ctx, ctx.Doer.ID, 0, 0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,6 +173,7 @@ func updateHookEvents(events []string) webhook_module.HookEvents {
|
|||
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.HookEventWorkflowRun] = util.SliceContainsString(events, string(webhook_module.HookEventWorkflowRun), true)
|
||||
hookEvents[webhook_module.HookEventWorkflowJob] = util.SliceContainsString(events, string(webhook_module.HookEventWorkflowJob), true)
|
||||
|
||||
// Issues
|
||||
|
|
|
|||
|
|
@ -304,7 +304,7 @@ func ViewPost(ctx *context_module.Context) {
|
|||
if task != nil {
|
||||
steps, logs, err := convertToViewModel(ctx, req.LogCursors, task)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
||||
ctx.ServerError("convertToViewModel", err)
|
||||
return
|
||||
}
|
||||
resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, steps...)
|
||||
|
|
@ -408,7 +408,7 @@ func Rerun(ctx *context_module.Context) {
|
|||
|
||||
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
||||
ctx.ServerError("GetRunByIndex", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -426,7 +426,7 @@ func Rerun(ctx *context_module.Context) {
|
|||
run.Started = 0
|
||||
run.Stopped = 0
|
||||
if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration"); err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
||||
ctx.ServerError("UpdateRun", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -441,7 +441,7 @@ func Rerun(ctx *context_module.Context) {
|
|||
// if the job has needs, it should be set to "blocked" status to wait for other jobs
|
||||
shouldBlock := len(j.Needs) > 0
|
||||
if err := rerunJob(ctx, j, shouldBlock); err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
||||
ctx.ServerError("RerunJob", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -455,7 +455,7 @@ func Rerun(ctx *context_module.Context) {
|
|||
// jobs other than the specified one should be set to "blocked" status
|
||||
shouldBlock := j.JobID != job.JobID
|
||||
if err := rerunJob(ctx, j, shouldBlock); err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
||||
ctx.ServerError("RerunJob", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -485,7 +485,7 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shou
|
|||
}
|
||||
|
||||
actions_service.CreateCommitStatus(ctx, job)
|
||||
_ = job.LoadAttributes(ctx)
|
||||
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
|
||||
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
||||
|
||||
return nil
|
||||
|
|
@ -547,7 +547,7 @@ func Cancel(ctx *context_module.Context) {
|
|||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
||||
ctx.ServerError("StopTask", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -557,7 +557,11 @@ func Cancel(ctx *context_module.Context) {
|
|||
_ = job.LoadAttributes(ctx)
|
||||
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
||||
}
|
||||
|
||||
if len(updatedjobs) > 0 {
|
||||
job := updatedjobs[0]
|
||||
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
|
||||
notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
|
||||
}
|
||||
ctx.JSON(http.StatusOK, struct{}{})
|
||||
}
|
||||
|
||||
|
|
@ -593,12 +597,18 @@ func Approve(ctx *context_module.Context) {
|
|||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
||||
ctx.ServerError("UpdateRunJob", err)
|
||||
return
|
||||
}
|
||||
|
||||
actions_service.CreateCommitStatus(ctx, jobs...)
|
||||
|
||||
if len(updatedjobs) > 0 {
|
||||
job := updatedjobs[0]
|
||||
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
|
||||
notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
|
||||
}
|
||||
|
||||
for _, job := range updatedjobs {
|
||||
_ = job.LoadAttributes(ctx)
|
||||
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
||||
|
|
@ -680,7 +690,7 @@ func ArtifactsDeleteView(ctx *context_module.Context) {
|
|||
return
|
||||
}
|
||||
if err = actions_model.SetArtifactNeedDelete(ctx, run.ID, artifactName); err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
||||
ctx.ServerError("SetArtifactNeedDelete", err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, struct{}{})
|
||||
|
|
@ -696,7 +706,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
|
|||
ctx.HTTPError(http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
||||
ctx.ServerError("GetRunByIndex", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -705,7 +715,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
|
|||
ArtifactName: artifactName,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
||||
ctx.ServerError("FindArtifacts", err)
|
||||
return
|
||||
}
|
||||
if len(artifacts) == 0 {
|
||||
|
|
@ -726,7 +736,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
|
|||
if len(artifacts) == 1 && actions.IsArtifactV4(artifacts[0]) {
|
||||
err := actions.DownloadArtifactV4(ctx.Base, artifacts[0])
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
||||
ctx.ServerError("DownloadArtifactV4", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
|
|
@ -739,7 +749,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
|
|||
for _, art := range artifacts {
|
||||
f, err := storage.ActionsArtifacts.Open(art.StoragePath)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
||||
ctx.ServerError("ActionsArtifacts.Open", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -747,7 +757,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
|
|||
if art.ContentEncoding == "gzip" {
|
||||
r, err = gzip.NewReader(f)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
||||
ctx.ServerError("gzip.NewReader", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
|
|
@ -757,11 +767,11 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
|
|||
|
||||
w, err := writer.Create(art.ArtifactPath)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
||||
ctx.ServerError("writer.Create", err)
|
||||
return
|
||||
}
|
||||
if _, err := io.Copy(w, r); err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
||||
ctx.ServerError("io.Copy", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ func CreateBranch(ctx *context.Context) {
|
|||
|
||||
func MergeUpstream(ctx *context.Context) {
|
||||
branchName := ctx.FormString("branch")
|
||||
_, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName)
|
||||
_, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName, false)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.JSONErrorNotFound()
|
||||
|
|
|
|||
|
|
@ -1,193 +0,0 @@
|
|||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"code.gitea.io/gitea/services/repository/files"
|
||||
)
|
||||
|
||||
var tplCherryPick templates.TplName = "repo/editor/cherry_pick"
|
||||
|
||||
// CherryPick handles cherrypick GETs
|
||||
func CherryPick(ctx *context.Context) {
|
||||
ctx.Data["SHA"] = ctx.PathParam("sha")
|
||||
cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(ctx.PathParam("sha"))
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
ctx.NotFound(err)
|
||||
return
|
||||
}
|
||||
ctx.ServerError("GetCommit", err)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.FormString("cherry-pick-type") == "revert" {
|
||||
ctx.Data["CherryPickType"] = "revert"
|
||||
ctx.Data["commit_summary"] = "revert " + ctx.PathParam("sha")
|
||||
ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message()
|
||||
} else {
|
||||
ctx.Data["CherryPickType"] = "cherry-pick"
|
||||
splits := strings.SplitN(cherryPickCommit.Message(), "\n", 2)
|
||||
ctx.Data["commit_summary"] = splits[0]
|
||||
ctx.Data["commit_message"] = splits[1]
|
||||
}
|
||||
|
||||
canCommit := renderCommitRights(ctx)
|
||||
ctx.Data["TreePath"] = ""
|
||||
|
||||
if canCommit {
|
||||
ctx.Data["commit_choice"] = frmCommitChoiceDirect
|
||||
} else {
|
||||
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
|
||||
}
|
||||
ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
|
||||
ctx.Data["last_commit"] = ctx.Repo.CommitID
|
||||
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
|
||||
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
|
||||
|
||||
ctx.HTML(http.StatusOK, tplCherryPick)
|
||||
}
|
||||
|
||||
// CherryPickPost handles cherrypick POSTs
|
||||
func CherryPickPost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.CherryPickForm)
|
||||
|
||||
sha := ctx.PathParam("sha")
|
||||
ctx.Data["SHA"] = sha
|
||||
if form.Revert {
|
||||
ctx.Data["CherryPickType"] = "revert"
|
||||
} else {
|
||||
ctx.Data["CherryPickType"] = "cherry-pick"
|
||||
}
|
||||
|
||||
canCommit := renderCommitRights(ctx)
|
||||
branchName := ctx.Repo.BranchName
|
||||
if form.CommitChoice == frmCommitChoiceNewBranch {
|
||||
branchName = form.NewBranchName
|
||||
}
|
||||
ctx.Data["commit_summary"] = form.CommitSummary
|
||||
ctx.Data["commit_message"] = form.CommitMessage
|
||||
ctx.Data["commit_choice"] = form.CommitChoice
|
||||
ctx.Data["new_branch_name"] = form.NewBranchName
|
||||
ctx.Data["last_commit"] = ctx.Repo.CommitID
|
||||
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
|
||||
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, tplCherryPick)
|
||||
return
|
||||
}
|
||||
|
||||
// Cannot commit to a an existing branch if user doesn't have rights
|
||||
if branchName == ctx.Repo.BranchName && !canCommit {
|
||||
ctx.Data["Err_NewBranchName"] = true
|
||||
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
|
||||
ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplCherryPick, &form)
|
||||
return
|
||||
}
|
||||
|
||||
message := strings.TrimSpace(form.CommitSummary)
|
||||
if message == "" {
|
||||
if form.Revert {
|
||||
message = ctx.Locale.TrString("repo.commit.revert-header", sha)
|
||||
} else {
|
||||
message = ctx.Locale.TrString("repo.commit.cherry-pick-header", sha)
|
||||
}
|
||||
}
|
||||
|
||||
form.CommitMessage = strings.TrimSpace(form.CommitMessage)
|
||||
if len(form.CommitMessage) > 0 {
|
||||
message += "\n\n" + form.CommitMessage
|
||||
}
|
||||
|
||||
gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail)
|
||||
if !valid {
|
||||
ctx.Data["Err_CommitEmail"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplCherryPick, &form)
|
||||
return
|
||||
}
|
||||
opts := &files.ApplyDiffPatchOptions{
|
||||
LastCommitID: form.LastCommit,
|
||||
OldBranch: ctx.Repo.BranchName,
|
||||
NewBranch: branchName,
|
||||
Message: message,
|
||||
Author: gitCommitter,
|
||||
Committer: gitCommitter,
|
||||
}
|
||||
|
||||
// First lets try the simple plain read-tree -m approach
|
||||
opts.Content = sha
|
||||
if _, err := files.CherryPick(ctx, ctx.Repo.Repository, ctx.Doer, form.Revert, opts); err != nil {
|
||||
if git_model.IsErrBranchAlreadyExists(err) {
|
||||
// User has specified a branch that already exists
|
||||
branchErr := err.(git_model.ErrBranchAlreadyExists)
|
||||
ctx.Data["Err_NewBranchName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form)
|
||||
return
|
||||
} else if files.IsErrCommitIDDoesNotMatch(err) {
|
||||
ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form)
|
||||
return
|
||||
}
|
||||
// Drop through to the apply technique
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
if form.Revert {
|
||||
if err := git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), sha, buf); err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
ctx.NotFound(errors.New("commit " + ctx.PathParam("sha") + " does not exist."))
|
||||
return
|
||||
}
|
||||
ctx.ServerError("GetRawDiff", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := git.GetRawDiff(ctx.Repo.GitRepo, sha, git.RawDiffType("patch"), buf); err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
ctx.NotFound(errors.New("commit " + ctx.PathParam("sha") + " does not exist."))
|
||||
return
|
||||
}
|
||||
ctx.ServerError("GetRawDiff", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
opts.Content = buf.String()
|
||||
ctx.Data["FileContent"] = opts.Content
|
||||
|
||||
if _, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
|
||||
if git_model.IsErrBranchAlreadyExists(err) {
|
||||
// User has specified a branch that already exists
|
||||
branchErr := err.(git_model.ErrBranchAlreadyExists)
|
||||
ctx.Data["Err_NewBranchName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form)
|
||||
return
|
||||
} else if files.IsErrCommitIDDoesNotMatch(err) {
|
||||
ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form)
|
||||
return
|
||||
}
|
||||
ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_apply_patch", err), tplPatchFile, &form)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) {
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
|
||||
} else {
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName))
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"code.gitea.io/gitea/services/repository/files"
|
||||
)
|
||||
|
||||
func NewDiffPatch(ctx *context.Context) {
|
||||
prepareEditorCommitFormOptions(ctx, "_diffpatch")
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["PageIsPatch"] = true
|
||||
ctx.HTML(http.StatusOK, tplPatchFile)
|
||||
}
|
||||
|
||||
// NewDiffPatchPost response for sending patch page
|
||||
func NewDiffPatchPost(ctx *context.Context) {
|
||||
parsed := parseEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
defaultCommitMessage := ctx.Locale.TrString("repo.editor.patch")
|
||||
_, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{
|
||||
LastCommitID: parsed.form.LastCommit,
|
||||
OldBranch: ctx.Repo.BranchName,
|
||||
NewBranch: parsed.TargetBranchName,
|
||||
Message: parsed.GetCommitMessage(defaultCommitMessage),
|
||||
Content: strings.ReplaceAll(parsed.form.Content.Value(), "\r\n", "\n"),
|
||||
Author: parsed.GitCommitter,
|
||||
Committer: parsed.GitCommitter,
|
||||
})
|
||||
if err != nil {
|
||||
err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch")
|
||||
}
|
||||
if err != nil {
|
||||
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
|
||||
return
|
||||
}
|
||||
redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"code.gitea.io/gitea/services/repository/files"
|
||||
)
|
||||
|
||||
func CherryPick(ctx *context.Context) {
|
||||
prepareEditorCommitFormOptions(ctx, "_cherrypick")
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
fromCommitID := ctx.PathParam("sha")
|
||||
ctx.Data["FromCommitID"] = fromCommitID
|
||||
cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(fromCommitID)
|
||||
if err != nil {
|
||||
HandleGitError(ctx, "GetCommit", err)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.FormString("cherry-pick-type") == "revert" {
|
||||
ctx.Data["CherryPickType"] = "revert"
|
||||
ctx.Data["commit_summary"] = "revert " + ctx.PathParam("sha")
|
||||
ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message()
|
||||
} else {
|
||||
ctx.Data["CherryPickType"] = "cherry-pick"
|
||||
splits := strings.SplitN(cherryPickCommit.Message(), "\n", 2)
|
||||
ctx.Data["commit_summary"] = splits[0]
|
||||
ctx.Data["commit_message"] = splits[1]
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplCherryPick)
|
||||
}
|
||||
|
||||
func CherryPickPost(ctx *context.Context) {
|
||||
fromCommitID := ctx.PathParam("sha")
|
||||
parsed := parseEditorCommitSubmittedForm[*forms.CherryPickForm](ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
defaultCommitMessage := util.Iif(parsed.form.Revert, ctx.Locale.TrString("repo.commit.revert-header", fromCommitID), ctx.Locale.TrString("repo.commit.cherry-pick-header", fromCommitID))
|
||||
opts := &files.ApplyDiffPatchOptions{
|
||||
LastCommitID: parsed.form.LastCommit,
|
||||
OldBranch: ctx.Repo.BranchName,
|
||||
NewBranch: parsed.TargetBranchName,
|
||||
Message: parsed.GetCommitMessage(defaultCommitMessage),
|
||||
Author: parsed.GitCommitter,
|
||||
Committer: parsed.GitCommitter,
|
||||
}
|
||||
|
||||
// First try the simple plain read-tree -m approach
|
||||
opts.Content = fromCommitID
|
||||
if _, err := files.CherryPick(ctx, ctx.Repo.Repository, ctx.Doer, parsed.form.Revert, opts); err != nil {
|
||||
// Drop through to the "apply" method
|
||||
buf := &bytes.Buffer{}
|
||||
if parsed.form.Revert {
|
||||
err = git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), fromCommitID, buf)
|
||||
} else {
|
||||
err = git.GetRawDiff(ctx.Repo.GitRepo, fromCommitID, "patch", buf)
|
||||
}
|
||||
if err == nil {
|
||||
opts.Content = buf.String()
|
||||
_, err = files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts)
|
||||
if err != nil {
|
||||
err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch")
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
// Copyright 2025 Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/utils"
|
||||
context_service "code.gitea.io/gitea/services/context"
|
||||
files_service "code.gitea.io/gitea/services/repository/files"
|
||||
)
|
||||
|
||||
func errorAs[T error](v error) (e T, ok bool) {
|
||||
if errors.As(v, &e) {
|
||||
return e, true
|
||||
}
|
||||
return e, false
|
||||
}
|
||||
|
||||
func editorHandleFileOperationErrorRender(ctx *context_service.Context, message, summary, details string) {
|
||||
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
|
||||
"Message": message,
|
||||
"Summary": summary,
|
||||
"Details": utils.SanitizeFlashErrorString(details),
|
||||
})
|
||||
if err == nil {
|
||||
ctx.JSONError(flashError)
|
||||
} else {
|
||||
log.Error("RenderToHTML: %v", err)
|
||||
ctx.JSONError(message + "\n" + summary + "\n" + utils.SanitizeFlashErrorString(details))
|
||||
}
|
||||
}
|
||||
|
||||
func editorHandleFileOperationError(ctx *context_service.Context, targetBranchName string, err error) {
|
||||
if errAs := util.ErrorAsLocale(err); errAs != nil {
|
||||
ctx.JSONError(ctx.Tr(errAs.TrKey, errAs.TrArgs...))
|
||||
} else if errAs, ok := errorAs[git.ErrNotExist](err); ok {
|
||||
ctx.JSONError(ctx.Tr("repo.editor.file_modifying_no_longer_exists", errAs.RelPath))
|
||||
} else if errAs, ok := errorAs[git_model.ErrLFSFileLocked](err); ok {
|
||||
ctx.JSONError(ctx.Tr("repo.editor.upload_file_is_locked", errAs.Path, errAs.UserName))
|
||||
} else if errAs, ok := errorAs[files_service.ErrFilenameInvalid](err); ok {
|
||||
ctx.JSONError(ctx.Tr("repo.editor.filename_is_invalid", errAs.Path))
|
||||
} else if errAs, ok := errorAs[files_service.ErrFilePathInvalid](err); ok {
|
||||
switch errAs.Type {
|
||||
case git.EntryModeSymlink:
|
||||
ctx.JSONError(ctx.Tr("repo.editor.file_is_a_symlink", errAs.Path))
|
||||
case git.EntryModeTree:
|
||||
ctx.JSONError(ctx.Tr("repo.editor.filename_is_a_directory", errAs.Path))
|
||||
case git.EntryModeBlob:
|
||||
ctx.JSONError(ctx.Tr("repo.editor.directory_is_a_file", errAs.Path))
|
||||
default:
|
||||
ctx.JSONError(ctx.Tr("repo.editor.filename_is_invalid", errAs.Path))
|
||||
}
|
||||
} else if errAs, ok := errorAs[files_service.ErrRepoFileAlreadyExists](err); ok {
|
||||
ctx.JSONError(ctx.Tr("repo.editor.file_already_exists", errAs.Path))
|
||||
} else if errAs, ok := errorAs[git.ErrBranchNotExist](err); ok {
|
||||
ctx.JSONError(ctx.Tr("repo.editor.branch_does_not_exist", errAs.Name))
|
||||
} else if errAs, ok := errorAs[git_model.ErrBranchAlreadyExists](err); ok {
|
||||
ctx.JSONError(ctx.Tr("repo.editor.branch_already_exists", errAs.BranchName))
|
||||
} else if files_service.IsErrCommitIDDoesNotMatch(err) {
|
||||
ctx.JSONError(ctx.Tr("repo.editor.commit_id_not_matching"))
|
||||
} else if files_service.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) {
|
||||
ctx.JSONError(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(ctx.Repo.CommitID)+"..."+util.PathEscapeSegments(targetBranchName)))
|
||||
} else if errAs, ok := errorAs[*git.ErrPushRejected](err); ok {
|
||||
if errAs.Message == "" {
|
||||
ctx.JSONError(ctx.Tr("repo.editor.push_rejected_no_message"))
|
||||
} else {
|
||||
editorHandleFileOperationErrorRender(ctx, ctx.Locale.TrString("repo.editor.push_rejected"), ctx.Locale.TrString("repo.editor.push_rejected_summary"), errAs.Message)
|
||||
}
|
||||
} else if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.JSONError(ctx.Tr("error.not_found"))
|
||||
} else {
|
||||
setting.PanicInDevOrTesting("unclear err %T: %v", err, err)
|
||||
editorHandleFileOperationErrorRender(ctx, ctx.Locale.TrString("repo.editor.failed_to_commit"), ctx.Locale.TrString("repo.editor.failed_to_commit_summary"), err.Error())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/services/context"
|
||||
files_service "code.gitea.io/gitea/services/repository/files"
|
||||
)
|
||||
|
||||
func DiffPreviewPost(ctx *context.Context) {
|
||||
content := ctx.FormString("content")
|
||||
treePath := files_service.CleanGitTreePath(ctx.Repo.TreePath)
|
||||
if treePath == "" {
|
||||
ctx.HTTPError(http.StatusBadRequest, "file name to diff is invalid")
|
||||
return
|
||||
}
|
||||
|
||||
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetTreeEntryByPath", err)
|
||||
return
|
||||
} else if entry.IsDir() {
|
||||
ctx.HTTPError(http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, content)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetDiffPreview", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(diff.Files) != 0 {
|
||||
ctx.Data["File"] = diff.Files[0]
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplEditDiffPreview)
|
||||
}
|
||||
|
|
@ -6,76 +6,27 @@ package repo
|
|||
import (
|
||||
"testing"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/services/contexttest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCleanUploadName(t *testing.T) {
|
||||
func TestEditorUtils(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
|
||||
kases := map[string]string{
|
||||
".git/refs/master": "",
|
||||
"/root/abc": "root/abc",
|
||||
"./../../abc": "abc",
|
||||
"a/../.git": "",
|
||||
"a/../../../abc": "abc",
|
||||
"../../../acd": "acd",
|
||||
"../../.git/abc": "",
|
||||
"..\\..\\.git/abc": "..\\..\\.git/abc",
|
||||
"..\\../.git/abc": "",
|
||||
"..\\../.git": "",
|
||||
"abc/../def": "def",
|
||||
".drone.yml": ".drone.yml",
|
||||
".abc/def/.drone.yml": ".abc/def/.drone.yml",
|
||||
"..drone.yml.": "..drone.yml.",
|
||||
"..a.dotty...name...": "..a.dotty...name...",
|
||||
"..a.dotty../.folder../.name...": "..a.dotty../.folder../.name...",
|
||||
}
|
||||
for k, v := range kases {
|
||||
assert.Equal(t, cleanUploadFileName(k), v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUniquePatchBranchName(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
ctx, _ := contexttest.MockContext(t, "user2/repo1")
|
||||
ctx.SetPathParam("id", "1")
|
||||
contexttest.LoadRepo(t, ctx, 1)
|
||||
contexttest.LoadRepoCommit(t, ctx)
|
||||
contexttest.LoadUser(t, ctx, 2)
|
||||
contexttest.LoadGitRepo(t, ctx)
|
||||
defer ctx.Repo.GitRepo.Close()
|
||||
|
||||
expectedBranchName := "user2-patch-1"
|
||||
branchName := GetUniquePatchBranchName(ctx)
|
||||
assert.Equal(t, expectedBranchName, branchName)
|
||||
}
|
||||
|
||||
func TestGetClosestParentWithFiles(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
ctx, _ := contexttest.MockContext(t, "user2/repo1")
|
||||
ctx.SetPathParam("id", "1")
|
||||
contexttest.LoadRepo(t, ctx, 1)
|
||||
contexttest.LoadRepoCommit(t, ctx)
|
||||
contexttest.LoadUser(t, ctx, 2)
|
||||
contexttest.LoadGitRepo(t, ctx)
|
||||
defer ctx.Repo.GitRepo.Close()
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
branch := repo.DefaultBranch
|
||||
gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo)
|
||||
defer gitRepo.Close()
|
||||
commit, _ := gitRepo.GetBranchCommit(branch)
|
||||
var expectedTreePath string // Should return the root dir, empty string, since there are no subdirs in this repo
|
||||
for _, deletedFile := range []string{
|
||||
"dir1/dir2/dir3/file.txt",
|
||||
"file.txt",
|
||||
} {
|
||||
treePath := GetClosestParentWithFiles(deletedFile, commit)
|
||||
assert.Equal(t, expectedTreePath, treePath)
|
||||
}
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
t.Run("getUniquePatchBranchName", func(t *testing.T) {
|
||||
branchName := getUniquePatchBranchName(t.Context(), "user2", repo)
|
||||
assert.Equal(t, "user2-patch-1", branchName)
|
||||
})
|
||||
t.Run("getClosestParentWithFiles", func(t *testing.T) {
|
||||
gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo)
|
||||
defer gitRepo.Close()
|
||||
treePath := getClosestParentWithFiles(gitRepo, "sub-home-md-img-check", "docs/foo/bar")
|
||||
assert.Equal(t, "docs", treePath)
|
||||
treePath = getClosestParentWithFiles(gitRepo, "sub-home-md-img-check", "any/other")
|
||||
assert.Empty(t, treePath)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/context/upload"
|
||||
files_service "code.gitea.io/gitea/services/repository/files"
|
||||
)
|
||||
|
||||
// UploadFileToServer upload file to server file dir not git
|
||||
func UploadFileToServer(ctx *context.Context) {
|
||||
file, header, err := ctx.Req.FormFile("file")
|
||||
if err != nil {
|
||||
ctx.ServerError("FormFile", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
n, _ := util.ReadAtMost(file, buf)
|
||||
if n > 0 {
|
||||
buf = buf[:n]
|
||||
}
|
||||
|
||||
err = upload.Verify(buf, header.Filename, setting.Repository.Upload.AllowedTypes)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
name := files_service.CleanGitTreePath(header.Filename)
|
||||
if len(name) == 0 {
|
||||
ctx.HTTPError(http.StatusBadRequest, "Upload file name is invalid")
|
||||
return
|
||||
}
|
||||
|
||||
uploaded, err := repo_model.NewUpload(ctx, name, buf, file)
|
||||
if err != nil {
|
||||
ctx.ServerError("NewUpload", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]string{"uuid": uploaded.UUID})
|
||||
}
|
||||
|
||||
// RemoveUploadFileFromServer remove file from server file dir
|
||||
func RemoveUploadFileFromServer(ctx *context.Context) {
|
||||
fileUUID := ctx.FormString("file")
|
||||
if err := repo_model.DeleteUploadByUUID(ctx, fileUUID); err != nil {
|
||||
ctx.ServerError("DeleteUploadByUUID", err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
context_service "code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// getUniquePatchBranchName Gets a unique branch name for a new patch branch
|
||||
// It will be in the form of <username>-patch-<num> where <num> is the first branch of this format
|
||||
// that doesn't already exist. If we exceed 1000 tries or an error is thrown, we just return "" so the user has to
|
||||
// type in the branch name themselves (will be an empty field)
|
||||
func getUniquePatchBranchName(ctx context.Context, prefixName string, repo *repo_model.Repository) string {
|
||||
prefix := prefixName + "-patch-"
|
||||
for i := 1; i <= 1000; i++ {
|
||||
branchName := fmt.Sprintf("%s%d", prefix, i)
|
||||
if exist, err := git_model.IsBranchExist(ctx, repo.ID, branchName); err != nil {
|
||||
log.Error("getUniquePatchBranchName: %v", err)
|
||||
return ""
|
||||
} else if !exist {
|
||||
return branchName
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getClosestParentWithFiles Recursively gets the closest path of parent in a tree that has files when a file in a tree is
|
||||
// deleted. It returns "" for the tree root if no parents other than the root have files.
|
||||
func getClosestParentWithFiles(gitRepo *git.Repository, branchName, originTreePath string) string {
|
||||
var f func(treePath string, commit *git.Commit) string
|
||||
f = func(treePath string, commit *git.Commit) string {
|
||||
if treePath == "" || treePath == "." {
|
||||
return ""
|
||||
}
|
||||
// see if the tree has entries
|
||||
if tree, err := commit.SubTree(treePath); err != nil {
|
||||
return f(path.Dir(treePath), commit) // failed to get the tree, going up a dir
|
||||
} else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 {
|
||||
return f(path.Dir(treePath), commit) // no files in this dir, going up a dir
|
||||
}
|
||||
return treePath
|
||||
}
|
||||
commit, err := gitRepo.GetBranchCommit(branchName) // must get the commit again to get the latest change
|
||||
if err != nil {
|
||||
log.Error("GetBranchCommit: %v", err)
|
||||
return ""
|
||||
}
|
||||
return f(originTreePath, commit)
|
||||
}
|
||||
|
||||
// getContextRepoEditorConfig returns the editorconfig JSON string for given treePath or "null"
|
||||
func getContextRepoEditorConfig(ctx *context_service.Context, treePath string) string {
|
||||
ec, _, err := ctx.Repo.GetEditorconfig()
|
||||
if err == nil {
|
||||
def, err := ec.GetDefinitionForFilename(treePath)
|
||||
if err == nil {
|
||||
jsonStr, _ := json.Marshal(def)
|
||||
return string(jsonStr)
|
||||
}
|
||||
}
|
||||
return "null"
|
||||
}
|
||||
|
||||
// getParentTreeFields returns list of parent tree names and corresponding tree paths based on given treePath.
|
||||
// eg: []{"a", "b", "c"}, []{"a", "a/b", "a/b/c"}
|
||||
// or: []{""}, []{""} for the root treePath
|
||||
func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
|
||||
treeNames = strings.Split(treePath, "/")
|
||||
treePaths = make([]string, len(treeNames))
|
||||
for i := range treeNames {
|
||||
treePaths[i] = strings.Join(treeNames[:i+1], "/")
|
||||
}
|
||||
return treeNames, treePaths
|
||||
}
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"code.gitea.io/gitea/services/repository/files"
|
||||
)
|
||||
|
||||
const (
|
||||
tplPatchFile templates.TplName = "repo/editor/patch"
|
||||
)
|
||||
|
||||
// NewDiffPatch render create patch page
|
||||
func NewDiffPatch(ctx *context.Context) {
|
||||
canCommit := renderCommitRights(ctx)
|
||||
|
||||
ctx.Data["PageIsPatch"] = true
|
||||
|
||||
ctx.Data["commit_summary"] = ""
|
||||
ctx.Data["commit_message"] = ""
|
||||
if canCommit {
|
||||
ctx.Data["commit_choice"] = frmCommitChoiceDirect
|
||||
} else {
|
||||
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
|
||||
}
|
||||
ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
|
||||
ctx.Data["last_commit"] = ctx.Repo.CommitID
|
||||
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
|
||||
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
|
||||
|
||||
ctx.HTML(http.StatusOK, tplPatchFile)
|
||||
}
|
||||
|
||||
// NewDiffPatchPost response for sending patch page
|
||||
func NewDiffPatchPost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.EditRepoFileForm)
|
||||
|
||||
canCommit := renderCommitRights(ctx)
|
||||
branchName := ctx.Repo.BranchName
|
||||
if form.CommitChoice == frmCommitChoiceNewBranch {
|
||||
branchName = form.NewBranchName
|
||||
}
|
||||
ctx.Data["PageIsPatch"] = true
|
||||
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
|
||||
ctx.Data["FileContent"] = form.Content
|
||||
ctx.Data["commit_summary"] = form.CommitSummary
|
||||
ctx.Data["commit_message"] = form.CommitMessage
|
||||
ctx.Data["commit_choice"] = form.CommitChoice
|
||||
ctx.Data["new_branch_name"] = form.NewBranchName
|
||||
ctx.Data["last_commit"] = ctx.Repo.CommitID
|
||||
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, tplPatchFile)
|
||||
return
|
||||
}
|
||||
|
||||
// Cannot commit to an existing branch if user doesn't have rights
|
||||
if branchName == ctx.Repo.BranchName && !canCommit {
|
||||
ctx.Data["Err_NewBranchName"] = true
|
||||
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
|
||||
ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form)
|
||||
return
|
||||
}
|
||||
|
||||
// CommitSummary is optional in the web form, if empty, give it a default message based on add or update
|
||||
// `message` will be both the summary and message combined
|
||||
message := strings.TrimSpace(form.CommitSummary)
|
||||
if len(message) == 0 {
|
||||
message = ctx.Locale.TrString("repo.editor.patch")
|
||||
}
|
||||
|
||||
form.CommitMessage = strings.TrimSpace(form.CommitMessage)
|
||||
if len(form.CommitMessage) > 0 {
|
||||
message += "\n\n" + form.CommitMessage
|
||||
}
|
||||
|
||||
gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail)
|
||||
if !valid {
|
||||
ctx.Data["Err_CommitEmail"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplPatchFile, &form)
|
||||
return
|
||||
}
|
||||
|
||||
fileResponse, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{
|
||||
LastCommitID: form.LastCommit,
|
||||
OldBranch: ctx.Repo.BranchName,
|
||||
NewBranch: branchName,
|
||||
Message: message,
|
||||
Content: strings.ReplaceAll(form.Content.Value(), "\r", ""),
|
||||
Author: gitCommitter,
|
||||
Committer: gitCommitter,
|
||||
})
|
||||
if err != nil {
|
||||
if git_model.IsErrBranchAlreadyExists(err) {
|
||||
// User has specified a branch that already exists
|
||||
branchErr := err.(git_model.ErrBranchAlreadyExists)
|
||||
ctx.Data["Err_NewBranchName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form)
|
||||
return
|
||||
} else if files.IsErrCommitIDDoesNotMatch(err) {
|
||||
ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form)
|
||||
return
|
||||
}
|
||||
ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_apply_patch", err), tplPatchFile, &form)
|
||||
return
|
||||
}
|
||||
|
||||
if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) {
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
|
||||
} else {
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/commit/" + fileResponse.Commit.SHA)
|
||||
}
|
||||
}
|
||||
|
|
@ -381,7 +381,7 @@ func NewRelease(ctx *context.Context) {
|
|||
|
||||
ctx.Data["ShowCreateTagOnlyButton"] = false
|
||||
ctx.Data["tag_name"] = rel.TagName
|
||||
ctx.Data["tag_target"] = rel.Target
|
||||
ctx.Data["tag_target"] = util.IfZero(rel.Target, ctx.Repo.Repository.DefaultBranch)
|
||||
ctx.Data["title"] = rel.Title
|
||||
ctx.Data["content"] = rel.Note
|
||||
ctx.Data["attachments"] = rel.Attachments
|
||||
|
|
@ -537,7 +537,7 @@ func EditRelease(ctx *context.Context) {
|
|||
}
|
||||
ctx.Data["ID"] = rel.ID
|
||||
ctx.Data["tag_name"] = rel.TagName
|
||||
ctx.Data["tag_target"] = rel.Target
|
||||
ctx.Data["tag_target"] = util.IfZero(rel.Target, ctx.Repo.Repository.DefaultBranch)
|
||||
ctx.Data["title"] = rel.Title
|
||||
ctx.Data["content"] = rel.Note
|
||||
ctx.Data["prerelease"] = rel.IsPrerelease
|
||||
|
|
@ -583,7 +583,7 @@ func EditReleasePost(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
ctx.Data["tag_name"] = rel.TagName
|
||||
ctx.Data["tag_target"] = rel.Target
|
||||
ctx.Data["tag_target"] = util.IfZero(rel.Target, ctx.Repo.Repository.DefaultBranch)
|
||||
ctx.Data["title"] = rel.Title
|
||||
ctx.Data["content"] = rel.Note
|
||||
ctx.Data["prerelease"] = rel.IsPrerelease
|
||||
|
|
|
|||
|
|
@ -185,6 +185,7 @@ func ParseHookEvent(form forms.WebhookForm) *webhook_module.HookEvent {
|
|||
webhook_module.HookEventRepository: form.Repository,
|
||||
webhook_module.HookEventPackage: form.Package,
|
||||
webhook_module.HookEventStatus: form.Status,
|
||||
webhook_module.HookEventWorkflowRun: form.WorkflowRun,
|
||||
webhook_module.HookEventWorkflowJob: form.WorkflowJob,
|
||||
},
|
||||
BranchFilter: form.BranchFilter,
|
||||
|
|
|
|||
|
|
@ -1315,11 +1315,11 @@ func registerWebRoutes(m *web.Router) {
|
|||
m.Group("/{username}/{reponame}", func() { // repo code
|
||||
m.Group("", func() {
|
||||
m.Group("", func() {
|
||||
m.Post("/_preview/*", web.Bind(forms.EditPreviewDiffForm{}), repo.DiffPreviewPost)
|
||||
m.Combo("/_edit/*").Get(repo.EditFile).
|
||||
m.Post("/_preview/*", repo.DiffPreviewPost)
|
||||
m.Combo("/{editor_action:_edit}/*").Get(repo.EditFile).
|
||||
Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost)
|
||||
m.Combo("/{editor_action:_new}/*").Get(repo.EditFile).
|
||||
Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost)
|
||||
m.Combo("/_new/*").Get(repo.NewFile).
|
||||
Post(web.Bind(forms.EditRepoFileForm{}), repo.NewFilePost)
|
||||
m.Combo("/_delete/*").Get(repo.DeleteFile).
|
||||
Post(web.Bind(forms.DeleteRepoFileForm{}), repo.DeleteFilePost)
|
||||
m.Combo("/_upload/*", repo.MustBeAbleToUpload).Get(repo.UploadFile).
|
||||
|
|
@ -1331,7 +1331,7 @@ func registerWebRoutes(m *web.Router) {
|
|||
}, context.RepoRefByType(git.RefTypeBranch), context.CanWriteToBranch(), repo.WebGitOperationCommonData)
|
||||
m.Group("", func() {
|
||||
m.Post("/upload-file", repo.UploadFileToServer)
|
||||
m.Post("/upload-remove", web.Bind(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer)
|
||||
m.Post("/upload-remove", repo.RemoveUploadFileFromServer)
|
||||
}, repo.MustBeAbleToUpload, reqRepoCodeWriter)
|
||||
}, repo.MustBeEditable, context.RepoMustNotBeArchived())
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ func notifyWorkflowJobStatusUpdate(ctx context.Context, jobs []*actions_model.Ac
|
|||
_ = job.LoadAttributes(ctx)
|
||||
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
||||
}
|
||||
if len(jobs) > 0 {
|
||||
job := jobs[0]
|
||||
notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -123,7 +127,7 @@ func CancelAbandonedJobs(ctx context.Context) error {
|
|||
}
|
||||
CreateCommitStatus(ctx, job)
|
||||
if updated {
|
||||
_ = job.LoadAttributes(ctx)
|
||||
NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
|
||||
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,4 +33,8 @@ type API interface {
|
|||
GetRunner(*context.APIContext)
|
||||
// DeleteRunner delete runner
|
||||
DeleteRunner(*context.APIContext)
|
||||
// ListWorkflowJobs list jobs
|
||||
ListWorkflowJobs(*context.APIContext)
|
||||
// ListWorkflowRuns list runs
|
||||
ListWorkflowRuns(*context.APIContext)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/queue"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
|
||||
|
|
@ -78,9 +79,30 @@ func checkJobsOfRun(ctx context.Context, runID int64) error {
|
|||
_ = job.LoadAttributes(ctx)
|
||||
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
||||
}
|
||||
if len(jobs) > 0 {
|
||||
runUpdated := true
|
||||
for _, job := range jobs {
|
||||
if !job.Status.IsDone() {
|
||||
runUpdated = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if runUpdated {
|
||||
NotifyWorkflowRunStatusUpdateWithReload(ctx, jobs[0])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NotifyWorkflowRunStatusUpdateWithReload(ctx context.Context, job *actions_model.ActionRunJob) {
|
||||
job.Run = nil
|
||||
if err := job.LoadAttributes(ctx); err != nil {
|
||||
log.Error("LoadAttributes: %v", err)
|
||||
return
|
||||
}
|
||||
notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
|
||||
}
|
||||
|
||||
type jobStatusResolver struct {
|
||||
statuses map[int64]actions_model.Status
|
||||
needs map[int64][]int64
|
||||
|
|
|
|||
|
|
@ -6,13 +6,16 @@ package actions
|
|||
import (
|
||||
"context"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
perm_model "code.gitea.io/gitea/models/perm"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
|
@ -762,3 +765,41 @@ func (n *actionsNotifier) MigrateRepository(ctx context.Context, doer, u *user_m
|
|||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
}).Notify(ctx)
|
||||
}
|
||||
|
||||
func (n *actionsNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) {
|
||||
ctx = withMethod(ctx, "WorkflowRunStatusUpdate")
|
||||
|
||||
var org *api.Organization
|
||||
if repo.Owner.IsOrganization() {
|
||||
org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner))
|
||||
}
|
||||
|
||||
status := convert.ToWorkflowRunAction(run.Status)
|
||||
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
|
||||
if err != nil {
|
||||
log.Error("OpenRepository: %v", err)
|
||||
return
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
convertedWorkflow, err := convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID)
|
||||
if err != nil {
|
||||
log.Error("GetActionWorkflow: %v", err)
|
||||
return
|
||||
}
|
||||
convertedRun, err := convert.ToActionWorkflowRun(ctx, repo, run)
|
||||
if err != nil {
|
||||
log.Error("ToActionWorkflowRun: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
newNotifyInput(repo, sender, webhook_module.HookEventWorkflowRun).WithPayload(&api.WorkflowRunPayload{
|
||||
Action: status,
|
||||
Workflow: convertedWorkflow,
|
||||
WorkflowRun: convertedRun,
|
||||
Organization: org,
|
||||
Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeOwner}),
|
||||
Sender: convert.ToUser(ctx, sender, nil),
|
||||
}).Notify(ctx)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ func notify(ctx context.Context, input *notifyInput) error {
|
|||
return fmt.Errorf("gitRepo.GetCommit: %w", err)
|
||||
}
|
||||
|
||||
if skipWorkflows(input, commit) {
|
||||
if skipWorkflows(ctx, input, commit) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -243,7 +243,7 @@ func notify(ctx context.Context, input *notifyInput) error {
|
|||
return handleWorkflows(ctx, detectedWorkflows, commit, input, ref.String())
|
||||
}
|
||||
|
||||
func skipWorkflows(input *notifyInput, commit *git.Commit) bool {
|
||||
func skipWorkflows(ctx context.Context, input *notifyInput, commit *git.Commit) bool {
|
||||
// skip workflow runs with a configured skip-ci string in commit message or pr title if the event is push or pull_request(_sync)
|
||||
// https://docs.github.com/en/actions/managing-workflow-runs/skipping-workflow-runs
|
||||
skipWorkflowEvents := []webhook_module.HookEventType{
|
||||
|
|
@ -263,6 +263,27 @@ func skipWorkflows(input *notifyInput, commit *git.Commit) bool {
|
|||
}
|
||||
}
|
||||
}
|
||||
if input.Event == webhook_module.HookEventWorkflowRun {
|
||||
wrun, ok := input.Payload.(*api.WorkflowRunPayload)
|
||||
for i := 0; i < 5 && ok && wrun.WorkflowRun != nil; i++ {
|
||||
if wrun.WorkflowRun.Event != "workflow_run" {
|
||||
return false
|
||||
}
|
||||
r, err := actions_model.GetRunByRepoAndID(ctx, input.Repo.ID, wrun.WorkflowRun.ID)
|
||||
if err != nil {
|
||||
log.Error("GetRunByRepoAndID: %v", err)
|
||||
return true
|
||||
}
|
||||
wrun, err = r.GetWorkflowRunEventPayload()
|
||||
if err != nil {
|
||||
log.Error("GetWorkflowRunEventPayload: %v", err)
|
||||
return true
|
||||
}
|
||||
}
|
||||
// skip workflow runs events exceeding the maxiumum of 5 recursive events
|
||||
log.Debug("repo %s: skipped workflow_run because of recursive event of 5", input.Repo.RepoPath())
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -372,6 +393,15 @@ func handleWorkflows(
|
|||
continue
|
||||
}
|
||||
CreateCommitStatus(ctx, alljobs...)
|
||||
if len(alljobs) > 0 {
|
||||
job := alljobs[0]
|
||||
err := job.LoadRun(ctx)
|
||||
if err != nil {
|
||||
log.Error("LoadRun: %v", err)
|
||||
continue
|
||||
}
|
||||
notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
|
||||
}
|
||||
for _, job := range alljobs {
|
||||
notify_service.WorkflowJobStatusUpdate(ctx, input.Repo, input.Doer, job, nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,6 +157,7 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule)
|
|||
if err != nil {
|
||||
log.Error("LoadAttributes: %v", err)
|
||||
}
|
||||
notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run)
|
||||
for _, job := range allJobs {
|
||||
notify_service.WorkflowJobStatusUpdate(ctx, run.Repo, run.TriggerUser, job, nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,6 @@ package actions
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
|
|
@ -31,51 +28,8 @@ import (
|
|||
"github.com/nektos/act/pkg/model"
|
||||
)
|
||||
|
||||
func getActionWorkflowEntry(ctx *context.APIContext, commit *git.Commit, folder string, entry *git.TreeEntry) *api.ActionWorkflow {
|
||||
cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
|
||||
cfg := cfgUnit.ActionsConfig()
|
||||
|
||||
defaultBranch, _ := commit.GetBranchName()
|
||||
|
||||
workflowURL := fmt.Sprintf("%s/actions/workflows/%s", ctx.Repo.Repository.APIURL(), url.PathEscape(entry.Name()))
|
||||
workflowRepoURL := fmt.Sprintf("%s/src/branch/%s/%s/%s", ctx.Repo.Repository.HTMLURL(ctx), util.PathEscapeSegments(defaultBranch), util.PathEscapeSegments(folder), url.PathEscape(entry.Name()))
|
||||
badgeURL := fmt.Sprintf("%s/actions/workflows/%s/badge.svg?branch=%s", ctx.Repo.Repository.HTMLURL(ctx), url.PathEscape(entry.Name()), url.QueryEscape(ctx.Repo.Repository.DefaultBranch))
|
||||
|
||||
// See https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#get-a-workflow
|
||||
// State types:
|
||||
// - active
|
||||
// - deleted
|
||||
// - disabled_fork
|
||||
// - disabled_inactivity
|
||||
// - disabled_manually
|
||||
state := "active"
|
||||
if cfg.IsWorkflowDisabled(entry.Name()) {
|
||||
state = "disabled_manually"
|
||||
}
|
||||
|
||||
// The CreatedAt and UpdatedAt fields currently reflect the timestamp of the latest commit, which can later be refined
|
||||
// by retrieving the first and last commits for the file history. The first commit would indicate the creation date,
|
||||
// while the last commit would represent the modification date. The DeletedAt could be determined by identifying
|
||||
// the last commit where the file existed. However, this implementation has not been done here yet, as it would likely
|
||||
// cause a significant performance degradation.
|
||||
createdAt := commit.Author.When
|
||||
updatedAt := commit.Author.When
|
||||
|
||||
return &api.ActionWorkflow{
|
||||
ID: entry.Name(),
|
||||
Name: entry.Name(),
|
||||
Path: path.Join(folder, entry.Name()),
|
||||
State: state,
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: updatedAt,
|
||||
URL: workflowURL,
|
||||
HTMLURL: workflowRepoURL,
|
||||
BadgeURL: badgeURL,
|
||||
}
|
||||
}
|
||||
|
||||
func EnableOrDisableWorkflow(ctx *context.APIContext, workflowID string, isEnable bool) error {
|
||||
workflow, err := GetActionWorkflow(ctx, workflowID)
|
||||
workflow, err := convert.GetActionWorkflow(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository, workflowID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -92,42 +46,6 @@ func EnableOrDisableWorkflow(ctx *context.APIContext, workflowID string, isEnabl
|
|||
return repo_model.UpdateRepoUnit(ctx, cfgUnit)
|
||||
}
|
||||
|
||||
func ListActionWorkflows(ctx *context.APIContext) ([]*api.ActionWorkflow, error) {
|
||||
defaultBranchCommit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
folder, entries, err := actions.ListWorkflows(defaultBranchCommit)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusNotFound, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
workflows := make([]*api.ActionWorkflow, len(entries))
|
||||
for i, entry := range entries {
|
||||
workflows[i] = getActionWorkflowEntry(ctx, defaultBranchCommit, folder, entry)
|
||||
}
|
||||
|
||||
return workflows, nil
|
||||
}
|
||||
|
||||
func GetActionWorkflow(ctx *context.APIContext, workflowID string) (*api.ActionWorkflow, error) {
|
||||
entries, err := ListActionWorkflows(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.Name == workflowID {
|
||||
return entry, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, util.NewNotExistErrorf("workflow %q not found", workflowID)
|
||||
}
|
||||
|
||||
func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, workflowID, ref string, processInputs func(model *model.WorkflowDispatch, inputs map[string]any) error) error {
|
||||
if workflowID == "" {
|
||||
return util.ErrorWrapLocale(
|
||||
|
|
@ -285,6 +203,15 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
|
|||
log.Error("FindRunJobs: %v", err)
|
||||
}
|
||||
CreateCommitStatus(ctx, allJobs...)
|
||||
if len(allJobs) > 0 {
|
||||
job := allJobs[0]
|
||||
err := job.LoadRun(ctx)
|
||||
if err != nil {
|
||||
log.Error("LoadRun: %v", err)
|
||||
} else {
|
||||
notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
|
||||
}
|
||||
}
|
||||
for _, job := range allJobs {
|
||||
notify_service.WorkflowJobStatusUpdate(ctx, repo, doer, job, nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import (
|
|||
)
|
||||
|
||||
// FormString returns the first value matching the provided key in the form as a string
|
||||
// It works the same as http.Request.FormValue:
|
||||
// try urlencoded request body first, then query string, then multipart form body
|
||||
func (b *Base) FormString(key string, def ...string) string {
|
||||
s := b.Req.FormValue(key)
|
||||
if s == "" {
|
||||
|
|
@ -20,7 +22,7 @@ func (b *Base) FormString(key string, def ...string) string {
|
|||
return s
|
||||
}
|
||||
|
||||
// FormStrings returns a string slice for the provided key from the form
|
||||
// FormStrings returns a values for the key in the form (including query parameters), similar to FormString
|
||||
func (b *Base) FormStrings(key string) []string {
|
||||
if b.Req.Form == nil {
|
||||
if err := b.Req.ParseMultipartForm(32 << 20); err != nil {
|
||||
|
|
|
|||
|
|
@ -94,24 +94,22 @@ func RepoMustNotBeArchived() func(ctx *Context) {
|
|||
}
|
||||
}
|
||||
|
||||
// CanCommitToBranchResults represents the results of CanCommitToBranch
|
||||
type CanCommitToBranchResults struct {
|
||||
CanCommitToBranch bool
|
||||
EditorEnabled bool
|
||||
UserCanPush bool
|
||||
RequireSigned bool
|
||||
WillSign bool
|
||||
SigningKey *git.SigningKey
|
||||
WontSignReason string
|
||||
type CommitFormBehaviors struct {
|
||||
CanCommitToBranch bool
|
||||
EditorEnabled bool
|
||||
UserCanPush bool
|
||||
RequireSigned bool
|
||||
WillSign bool
|
||||
SigningKey *git.SigningKey
|
||||
WontSignReason string
|
||||
CanCreatePullRequest bool
|
||||
CanCreateBasePullRequest bool
|
||||
}
|
||||
|
||||
// CanCommitToBranch returns true if repository is editable and user has proper access level
|
||||
//
|
||||
// and branch is not protected for push
|
||||
func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.User) (CanCommitToBranchResults, error) {
|
||||
func (r *Repository) PrepareCommitFormBehaviors(ctx *Context, doer *user_model.User) (*CommitFormBehaviors, error) {
|
||||
protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, r.Repository.ID, r.BranchName)
|
||||
if err != nil {
|
||||
return CanCommitToBranchResults{}, err
|
||||
return nil, err
|
||||
}
|
||||
userCanPush := true
|
||||
requireSigned := false
|
||||
|
|
@ -138,7 +136,10 @@ func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.Use
|
|||
}
|
||||
}
|
||||
|
||||
return CanCommitToBranchResults{
|
||||
canCreateBasePullRequest := ctx.Repo.Repository.BaseRepo != nil && ctx.Repo.Repository.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests)
|
||||
canCreatePullRequest := ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypePullRequests) || canCreateBasePullRequest
|
||||
|
||||
return &CommitFormBehaviors{
|
||||
CanCommitToBranch: canCommit,
|
||||
EditorEnabled: canEnableEditor,
|
||||
UserCanPush: userCanPush,
|
||||
|
|
@ -146,6 +147,9 @@ func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.Use
|
|||
WillSign: sign,
|
||||
SigningKey: keyID,
|
||||
WontSignReason: wontSignReason,
|
||||
|
||||
CanCreatePullRequest: canCreatePullRequest,
|
||||
CanCreateBasePullRequest: canCreateBasePullRequest,
|
||||
}, err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -113,5 +113,7 @@ func AddUploadContext(ctx *context.Context, uploadType string) {
|
|||
ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Upload.AllowedTypes, "|", ",")
|
||||
ctx.Data["UploadMaxFiles"] = setting.Repository.Upload.MaxFiles
|
||||
ctx.Data["UploadMaxSize"] = setting.Repository.Upload.FileMaxSize
|
||||
default:
|
||||
setting.PanicInDevOrTesting("Invalid upload type: %s", uploadType)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,11 @@
|
|||
package convert
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -14,6 +17,7 @@ import (
|
|||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
|
|
@ -22,6 +26,7 @@ import (
|
|||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/actions"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
|
@ -32,6 +37,7 @@ import (
|
|||
"code.gitea.io/gitea/services/gitdiff"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
)
|
||||
|
||||
// ToEmail convert models.EmailAddress to api.Email
|
||||
|
|
@ -241,6 +247,242 @@ func ToActionTask(ctx context.Context, t *actions_model.ActionTask) (*api.Action
|
|||
}, nil
|
||||
}
|
||||
|
||||
func ToActionWorkflowRun(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun) (*api.ActionWorkflowRun, error) {
|
||||
err := run.LoadAttributes(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
status, conclusion := ToActionsStatus(run.Status)
|
||||
return &api.ActionWorkflowRun{
|
||||
ID: run.ID,
|
||||
URL: fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(), run.ID),
|
||||
HTMLURL: run.HTMLURL(),
|
||||
RunNumber: run.Index,
|
||||
StartedAt: run.Started.AsLocalTime(),
|
||||
CompletedAt: run.Stopped.AsLocalTime(),
|
||||
Event: string(run.Event),
|
||||
DisplayTitle: run.Title,
|
||||
HeadBranch: git.RefName(run.Ref).BranchName(),
|
||||
HeadSha: run.CommitSHA,
|
||||
Status: status,
|
||||
Conclusion: conclusion,
|
||||
Path: fmt.Sprintf("%s@%s", run.WorkflowID, run.Ref),
|
||||
Repository: ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeNone}),
|
||||
TriggerActor: ToUser(ctx, run.TriggerUser, nil),
|
||||
// We do not have a way to get a different User for the actor than the trigger user
|
||||
Actor: ToUser(ctx, run.TriggerUser, nil),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ToWorkflowRunAction(status actions_model.Status) string {
|
||||
var action string
|
||||
switch status {
|
||||
case actions_model.StatusWaiting, actions_model.StatusBlocked:
|
||||
action = "requested"
|
||||
case actions_model.StatusRunning:
|
||||
action = "in_progress"
|
||||
}
|
||||
if status.IsDone() {
|
||||
action = "completed"
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
func ToActionsStatus(status actions_model.Status) (string, string) {
|
||||
var action string
|
||||
var conclusion string
|
||||
switch status {
|
||||
// This is a naming conflict of the webhook between Gitea and GitHub Actions
|
||||
case actions_model.StatusWaiting:
|
||||
action = "queued"
|
||||
case actions_model.StatusBlocked:
|
||||
action = "waiting"
|
||||
case actions_model.StatusRunning:
|
||||
action = "in_progress"
|
||||
}
|
||||
if status.IsDone() {
|
||||
action = "completed"
|
||||
switch status {
|
||||
case actions_model.StatusSuccess:
|
||||
conclusion = "success"
|
||||
case actions_model.StatusCancelled:
|
||||
conclusion = "cancelled"
|
||||
case actions_model.StatusFailure:
|
||||
conclusion = "failure"
|
||||
case actions_model.StatusSkipped:
|
||||
conclusion = "skipped"
|
||||
}
|
||||
}
|
||||
return action, conclusion
|
||||
}
|
||||
|
||||
// ToActionWorkflowJob convert a actions_model.ActionRunJob to an api.ActionWorkflowJob
|
||||
// task is optional and can be nil
|
||||
func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, task *actions_model.ActionTask, job *actions_model.ActionRunJob) (*api.ActionWorkflowJob, error) {
|
||||
err := job.LoadAttributes(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jobIndex := 0
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, job.RunID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, j := range jobs {
|
||||
if j.ID == job.ID {
|
||||
jobIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
status, conclusion := ToActionsStatus(job.Status)
|
||||
var runnerID int64
|
||||
var runnerName string
|
||||
var steps []*api.ActionWorkflowStep
|
||||
|
||||
if job.TaskID != 0 {
|
||||
if task == nil {
|
||||
task, _, err = db.GetByID[actions_model.ActionTask](ctx, job.TaskID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
runnerID = task.RunnerID
|
||||
if runner, ok, _ := db.GetByID[actions_model.ActionRunner](ctx, runnerID); ok {
|
||||
runnerName = runner.Name
|
||||
}
|
||||
for i, step := range task.Steps {
|
||||
stepStatus, stepConclusion := ToActionsStatus(job.Status)
|
||||
steps = append(steps, &api.ActionWorkflowStep{
|
||||
Name: step.Name,
|
||||
Number: int64(i),
|
||||
Status: stepStatus,
|
||||
Conclusion: stepConclusion,
|
||||
StartedAt: step.Started.AsTime().UTC(),
|
||||
CompletedAt: step.Stopped.AsTime().UTC(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &api.ActionWorkflowJob{
|
||||
ID: job.ID,
|
||||
// missing api endpoint for this location
|
||||
URL: fmt.Sprintf("%s/actions/jobs/%d", repo.APIURL(), job.ID),
|
||||
HTMLURL: fmt.Sprintf("%s/jobs/%d", job.Run.HTMLURL(), jobIndex),
|
||||
RunID: job.RunID,
|
||||
// Missing api endpoint for this location, artifacts are available under a nested url
|
||||
RunURL: fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(), job.RunID),
|
||||
Name: job.Name,
|
||||
Labels: job.RunsOn,
|
||||
RunAttempt: job.Attempt,
|
||||
HeadSha: job.Run.CommitSHA,
|
||||
HeadBranch: git.RefName(job.Run.Ref).BranchName(),
|
||||
Status: status,
|
||||
Conclusion: conclusion,
|
||||
RunnerID: runnerID,
|
||||
RunnerName: runnerName,
|
||||
Steps: steps,
|
||||
CreatedAt: job.Created.AsTime().UTC(),
|
||||
StartedAt: job.Started.AsTime().UTC(),
|
||||
CompletedAt: job.Stopped.AsTime().UTC(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getActionWorkflowEntry(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, folder string, entry *git.TreeEntry) *api.ActionWorkflow {
|
||||
cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions)
|
||||
cfg := cfgUnit.ActionsConfig()
|
||||
|
||||
defaultBranch, _ := commit.GetBranchName()
|
||||
|
||||
workflowURL := fmt.Sprintf("%s/actions/workflows/%s", repo.APIURL(), util.PathEscapeSegments(entry.Name()))
|
||||
workflowRepoURL := fmt.Sprintf("%s/src/branch/%s/%s/%s", repo.HTMLURL(ctx), util.PathEscapeSegments(defaultBranch), util.PathEscapeSegments(folder), util.PathEscapeSegments(entry.Name()))
|
||||
badgeURL := fmt.Sprintf("%s/actions/workflows/%s/badge.svg?branch=%s", repo.HTMLURL(ctx), util.PathEscapeSegments(entry.Name()), url.QueryEscape(repo.DefaultBranch))
|
||||
|
||||
// See https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#get-a-workflow
|
||||
// State types:
|
||||
// - active
|
||||
// - deleted
|
||||
// - disabled_fork
|
||||
// - disabled_inactivity
|
||||
// - disabled_manually
|
||||
state := "active"
|
||||
if cfg.IsWorkflowDisabled(entry.Name()) {
|
||||
state = "disabled_manually"
|
||||
}
|
||||
|
||||
// The CreatedAt and UpdatedAt fields currently reflect the timestamp of the latest commit, which can later be refined
|
||||
// by retrieving the first and last commits for the file history. The first commit would indicate the creation date,
|
||||
// while the last commit would represent the modification date. The DeletedAt could be determined by identifying
|
||||
// the last commit where the file existed. However, this implementation has not been done here yet, as it would likely
|
||||
// cause a significant performance degradation.
|
||||
createdAt := commit.Author.When
|
||||
updatedAt := commit.Author.When
|
||||
|
||||
content, err := actions.GetContentFromEntry(entry)
|
||||
name := entry.Name()
|
||||
if err == nil {
|
||||
workflow, err := model.ReadWorkflow(bytes.NewReader(content))
|
||||
if err == nil {
|
||||
// Only use the name when specified in the workflow file
|
||||
if workflow.Name != "" {
|
||||
name = workflow.Name
|
||||
}
|
||||
} else {
|
||||
log.Error("getActionWorkflowEntry: Failed to parse workflow: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Error("getActionWorkflowEntry: Failed to get content from entry: %v", err)
|
||||
}
|
||||
|
||||
return &api.ActionWorkflow{
|
||||
ID: entry.Name(),
|
||||
Name: name,
|
||||
Path: path.Join(folder, entry.Name()),
|
||||
State: state,
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: updatedAt,
|
||||
URL: workflowURL,
|
||||
HTMLURL: workflowRepoURL,
|
||||
BadgeURL: badgeURL,
|
||||
}
|
||||
}
|
||||
|
||||
func ListActionWorkflows(ctx context.Context, gitrepo *git.Repository, repo *repo_model.Repository) ([]*api.ActionWorkflow, error) {
|
||||
defaultBranchCommit, err := gitrepo.GetBranchCommit(repo.DefaultBranch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
folder, entries, err := actions.ListWorkflows(defaultBranchCommit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
workflows := make([]*api.ActionWorkflow, len(entries))
|
||||
for i, entry := range entries {
|
||||
workflows[i] = getActionWorkflowEntry(ctx, repo, defaultBranchCommit, folder, entry)
|
||||
}
|
||||
|
||||
return workflows, nil
|
||||
}
|
||||
|
||||
func GetActionWorkflow(ctx context.Context, gitrepo *git.Repository, repo *repo_model.Repository, workflowID string) (*api.ActionWorkflow, error) {
|
||||
entries, err := ListActionWorkflows(ctx, gitrepo, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.ID == workflowID {
|
||||
return entry, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, util.NewNotExistErrorf("workflow %q not found", workflowID)
|
||||
}
|
||||
|
||||
// ToActionArtifact convert a actions_model.ActionArtifact to an api.ActionArtifact
|
||||
func ToActionArtifact(repo *repo_model.Repository, art *actions_model.ActionArtifact) (*api.ActionArtifact, error) {
|
||||
url := fmt.Sprintf("%s/actions/artifacts/%d", repo.APIURL(), art.ID)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import (
|
|||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
|
|
@ -234,6 +233,7 @@ type WebhookForm struct {
|
|||
Release bool
|
||||
Package bool
|
||||
Status bool
|
||||
WorkflowRun bool
|
||||
WorkflowJob bool
|
||||
Active bool
|
||||
BranchFilter string `binding:"GlobPattern"`
|
||||
|
|
@ -680,129 +680,6 @@ func (f *NewWikiForm) Validate(req *http.Request, errs binding.Errors) binding.E
|
|||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// ___________ .___.__ __
|
||||
// \_ _____/ __| _/|__|/ |_
|
||||
// | __)_ / __ | | \ __\
|
||||
// | \/ /_/ | | || |
|
||||
// /_______ /\____ | |__||__|
|
||||
// \/ \/
|
||||
|
||||
// EditRepoFileForm form for changing repository file
|
||||
type EditRepoFileForm struct {
|
||||
TreePath string `binding:"Required;MaxSize(500)"`
|
||||
Content optional.Option[string]
|
||||
CommitSummary string `binding:"MaxSize(100)"`
|
||||
CommitMessage string
|
||||
CommitChoice string `binding:"Required;MaxSize(50)"`
|
||||
NewBranchName string `binding:"GitRefName;MaxSize(100)"`
|
||||
LastCommit string
|
||||
Signoff bool
|
||||
CommitEmail string
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
func (f *EditRepoFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||
ctx := context.GetValidateContext(req)
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// EditPreviewDiffForm form for changing preview diff
|
||||
type EditPreviewDiffForm struct {
|
||||
Content string
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
func (f *EditPreviewDiffForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||
ctx := context.GetValidateContext(req)
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// _________ .__ __________.__ __
|
||||
// \_ ___ \| |__ __________________ ___.__. \______ \__| ____ | | __
|
||||
// / \ \/| | \_/ __ \_ __ \_ __ < | | | ___/ |/ ___\| |/ /
|
||||
// \ \___| Y \ ___/| | \/| | \/\___ | | | | \ \___| <
|
||||
// \______ /___| /\___ >__| |__| / ____| |____| |__|\___ >__|_ \
|
||||
// \/ \/ \/ \/ \/ \/
|
||||
|
||||
// CherryPickForm form for changing repository file
|
||||
type CherryPickForm struct {
|
||||
CommitSummary string `binding:"MaxSize(100)"`
|
||||
CommitMessage string
|
||||
CommitChoice string `binding:"Required;MaxSize(50)"`
|
||||
NewBranchName string `binding:"GitRefName;MaxSize(100)"`
|
||||
LastCommit string
|
||||
Revert bool
|
||||
Signoff bool
|
||||
CommitEmail string
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
func (f *CherryPickForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||
ctx := context.GetValidateContext(req)
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// ____ ___ .__ .___
|
||||
// | | \______ | | _________ __| _/
|
||||
// | | /\____ \| | / _ \__ \ / __ |
|
||||
// | | / | |_> > |_( <_> ) __ \_/ /_/ |
|
||||
// |______/ | __/|____/\____(____ /\____ |
|
||||
// |__| \/ \/
|
||||
//
|
||||
|
||||
// UploadRepoFileForm form for uploading repository file
|
||||
type UploadRepoFileForm struct {
|
||||
TreePath string `binding:"MaxSize(500)"`
|
||||
CommitSummary string `binding:"MaxSize(100)"`
|
||||
CommitMessage string
|
||||
CommitChoice string `binding:"Required;MaxSize(50)"`
|
||||
NewBranchName string `binding:"GitRefName;MaxSize(100)"`
|
||||
Files []string
|
||||
Signoff bool
|
||||
CommitEmail string
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
func (f *UploadRepoFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||
ctx := context.GetValidateContext(req)
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// RemoveUploadFileForm form for removing uploaded file
|
||||
type RemoveUploadFileForm struct {
|
||||
File string `binding:"Required;MaxSize(50)"`
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
func (f *RemoveUploadFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||
ctx := context.GetValidateContext(req)
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// ________ .__ __
|
||||
// \______ \ ____ | | _____/ |_ ____
|
||||
// | | \_/ __ \| | _/ __ \ __\/ __ \
|
||||
// | ` \ ___/| |_\ ___/| | \ ___/
|
||||
// /_______ /\___ >____/\___ >__| \___ >
|
||||
// \/ \/ \/ \/
|
||||
|
||||
// DeleteRepoFileForm form for deleting repository file
|
||||
type DeleteRepoFileForm struct {
|
||||
CommitSummary string `binding:"MaxSize(100)"`
|
||||
CommitMessage string
|
||||
CommitChoice string `binding:"Required;MaxSize(50)"`
|
||||
NewBranchName string `binding:"GitRefName;MaxSize(100)"`
|
||||
LastCommit string
|
||||
Signoff bool
|
||||
CommitEmail string
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
func (f *DeleteRepoFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||
ctx := context.GetValidateContext(req)
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// ___________.__ ___________ __
|
||||
// \__ ___/|__| _____ ____ \__ ___/___________ ____ | | __ ___________
|
||||
// | | | |/ \_/ __ \ | | \_ __ \__ \ _/ ___\| |/ // __ \_ __ \
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package forms
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
|
||||
"gitea.com/go-chi/binding"
|
||||
)
|
||||
|
||||
type CommitCommonForm struct {
|
||||
TreePath string `binding:"MaxSize(500)"`
|
||||
CommitSummary string `binding:"MaxSize(100)"`
|
||||
CommitMessage string
|
||||
CommitChoice string `binding:"Required;MaxSize(50)"`
|
||||
NewBranchName string `binding:"GitRefName;MaxSize(100)"`
|
||||
LastCommit string
|
||||
Signoff bool
|
||||
CommitEmail string
|
||||
}
|
||||
|
||||
func (f *CommitCommonForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||
ctx := context.GetValidateContext(req)
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
type CommitCommonFormInterface interface {
|
||||
GetCommitCommonForm() *CommitCommonForm
|
||||
}
|
||||
|
||||
func (f *CommitCommonForm) GetCommitCommonForm() *CommitCommonForm {
|
||||
return f
|
||||
}
|
||||
|
||||
type EditRepoFileForm struct {
|
||||
CommitCommonForm
|
||||
Content optional.Option[string]
|
||||
}
|
||||
|
||||
type DeleteRepoFileForm struct {
|
||||
CommitCommonForm
|
||||
}
|
||||
|
||||
type UploadRepoFileForm struct {
|
||||
CommitCommonForm
|
||||
Files []string
|
||||
}
|
||||
|
||||
type CherryPickForm struct {
|
||||
CommitCommonForm
|
||||
Revert bool
|
||||
}
|
||||
|
|
@ -528,7 +528,7 @@ func TestEmbedBase64Images(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
mailBody := msgs[0].Body
|
||||
assert.Regexp(t, `MSG-BEFORE <a[^>]+><img src="data:image/png;base64,iVBORw0KGgo="/></a> MSG-AFTER`, mailBody)
|
||||
assert.Regexp(t, `MSG-BEFORE <a[^>]+><img src="data:image/png;base64,iVBORw0KGgo=".*/></a> MSG-AFTER`, mailBody)
|
||||
})
|
||||
|
||||
t.Run("EmbedInstanceImageSkipExternalImage", func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -79,5 +79,7 @@ type Notifier interface {
|
|||
|
||||
CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus)
|
||||
|
||||
WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun)
|
||||
|
||||
WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -376,6 +376,12 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit
|
|||
}
|
||||
}
|
||||
|
||||
func WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) {
|
||||
for _, notifier := range notifiers {
|
||||
notifier.WorkflowRunStatusUpdate(ctx, repo, sender, run)
|
||||
}
|
||||
}
|
||||
|
||||
func WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask) {
|
||||
for _, notifier := range notifiers {
|
||||
notifier.WorkflowJobStatusUpdate(ctx, repo, sender, job, task)
|
||||
|
|
|
|||
|
|
@ -214,5 +214,8 @@ func (*NullNotifier) ChangeDefaultBranch(ctx context.Context, repo *repo_model.R
|
|||
func (*NullNotifier) CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) {
|
||||
}
|
||||
|
||||
func (*NullNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) {
|
||||
}
|
||||
|
||||
func (*NullNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ func GetContentsOrList(ctx context.Context, repo *repo_model.Repository, refComm
|
|||
}
|
||||
|
||||
// Check that the path given in opts.treePath is valid (not a git path)
|
||||
cleanTreePath := CleanUploadFileName(treePath)
|
||||
cleanTreePath := CleanGitTreePath(treePath)
|
||||
if cleanTreePath == "" && treePath != "" {
|
||||
return nil, ErrFilenameInvalid{
|
||||
Path: treePath,
|
||||
|
|
@ -103,7 +103,7 @@ func GetObjectTypeFromTreeEntry(entry *git.TreeEntry) ContentType {
|
|||
// GetContents gets the metadata on a file's contents. Ref can be a branch, commit or tag
|
||||
func GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *utils.RefCommit, treePath string, forList bool) (*api.ContentsResponse, error) {
|
||||
// Check that the path given in opts.treePath is valid (not a git path)
|
||||
cleanTreePath := CleanUploadFileName(treePath)
|
||||
cleanTreePath := CleanGitTreePath(treePath)
|
||||
if cleanTreePath == "" && treePath != "" {
|
||||
return nil, ErrFilenameInvalid{
|
||||
Path: treePath,
|
||||
|
|
|
|||
|
|
@ -134,9 +134,8 @@ func (err ErrFilenameInvalid) Unwrap() error {
|
|||
return util.ErrInvalidArgument
|
||||
}
|
||||
|
||||
// CleanUploadFileName Trims a filename and returns empty string if it is a .git directory
|
||||
func CleanUploadFileName(name string) string {
|
||||
// Rebase the filename
|
||||
// CleanGitTreePath cleans a tree path for git, it returns an empty string the path is invalid (e.g.: contains ".git" part)
|
||||
func CleanGitTreePath(name string) string {
|
||||
name = util.PathJoinRel(name)
|
||||
// Git disallows any filenames to have a .git directory in them.
|
||||
for part := range strings.SplitSeq(name, "/") {
|
||||
|
|
@ -144,5 +143,8 @@ func CleanUploadFileName(name string) string {
|
|||
return ""
|
||||
}
|
||||
}
|
||||
if name == "." {
|
||||
name = ""
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,17 +10,9 @@ import (
|
|||
)
|
||||
|
||||
func TestCleanUploadFileName(t *testing.T) {
|
||||
t.Run("Clean regular file", func(t *testing.T) {
|
||||
name := "this/is/test"
|
||||
cleanName := CleanUploadFileName(name)
|
||||
expectedCleanName := name
|
||||
assert.Equal(t, expectedCleanName, cleanName)
|
||||
})
|
||||
|
||||
t.Run("Clean a .git path", func(t *testing.T) {
|
||||
name := "this/is/test/.git"
|
||||
cleanName := CleanUploadFileName(name)
|
||||
expectedCleanName := ""
|
||||
assert.Equal(t, expectedCleanName, cleanName)
|
||||
})
|
||||
assert.Equal(t, "", CleanGitTreePath("")) //nolint
|
||||
assert.Equal(t, "", CleanGitTreePath(".")) //nolint
|
||||
assert.Equal(t, "a/b", CleanGitTreePath("a/b"))
|
||||
assert.Equal(t, "", CleanGitTreePath(".git/b")) //nolint
|
||||
assert.Equal(t, "", CleanGitTreePath("a/.git")) //nolint
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,8 +88,26 @@ func (err ErrRepoFileDoesNotExist) Unwrap() error {
|
|||
return util.ErrNotExist
|
||||
}
|
||||
|
||||
type LazyReadSeeker interface {
|
||||
io.ReadSeeker
|
||||
io.Closer
|
||||
OpenLazyReader() error
|
||||
}
|
||||
|
||||
// ChangeRepoFiles adds, updates or removes multiple files in the given repository
|
||||
func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *ChangeRepoFilesOptions) (*structs.FilesResponse, error) {
|
||||
func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *ChangeRepoFilesOptions) (_ *structs.FilesResponse, errRet error) {
|
||||
var addedLfsPointers []lfs.Pointer
|
||||
defer func() {
|
||||
if errRet != nil {
|
||||
for _, lfsPointer := range addedLfsPointers {
|
||||
_, err := git_model.RemoveLFSMetaObjectByOid(ctx, repo.ID, lfsPointer.Oid)
|
||||
if err != nil {
|
||||
log.Error("ChangeRepoFiles: RemoveLFSMetaObjectByOid failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err := repo.MustNotBeArchived()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -127,14 +145,14 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
|
|||
}
|
||||
|
||||
// Check that the path given in opts.treePath is valid (not a git path)
|
||||
treePath := CleanUploadFileName(file.TreePath)
|
||||
treePath := CleanGitTreePath(file.TreePath)
|
||||
if treePath == "" {
|
||||
return nil, ErrFilenameInvalid{
|
||||
Path: file.TreePath,
|
||||
}
|
||||
}
|
||||
// If there is a fromTreePath (we are copying it), also clean it up
|
||||
fromTreePath := CleanUploadFileName(file.FromTreePath)
|
||||
fromTreePath := CleanGitTreePath(file.FromTreePath)
|
||||
if fromTreePath == "" && file.FromTreePath != "" {
|
||||
return nil, ErrFilenameInvalid{
|
||||
Path: file.FromTreePath,
|
||||
|
|
@ -241,10 +259,14 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
|
|||
lfsContentStore := lfs.NewContentStore()
|
||||
for _, file := range opts.Files {
|
||||
switch file.Operation {
|
||||
case "create", "update", "rename":
|
||||
if err = CreateUpdateRenameFile(ctx, t, file, lfsContentStore, repo.ID, hasOldBranch); err != nil {
|
||||
case "create", "update", "rename", "upload":
|
||||
addedLfsPointer, err := modifyFile(ctx, t, file, lfsContentStore, repo.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if addedLfsPointer != nil {
|
||||
addedLfsPointers = append(addedLfsPointers, *addedLfsPointer)
|
||||
}
|
||||
case "delete":
|
||||
if err = t.RemoveFilesFromIndex(ctx, file.TreePath); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -366,18 +388,29 @@ func (err ErrSHAOrCommitIDNotProvided) Error() string {
|
|||
|
||||
// handles the check for various issues for ChangeRepoFiles
|
||||
func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRepoFilesOptions) error {
|
||||
if file.Operation == "update" || file.Operation == "delete" || file.Operation == "rename" {
|
||||
fromEntry, err := commit.GetTreeEntryByPath(file.Options.fromTreePath)
|
||||
if err != nil {
|
||||
return err
|
||||
// check old entry (fromTreePath/fromEntry)
|
||||
if file.Operation == "update" || file.Operation == "upload" || file.Operation == "delete" || file.Operation == "rename" {
|
||||
var fromEntryIDString string
|
||||
{
|
||||
fromEntry, err := commit.GetTreeEntryByPath(file.Options.fromTreePath)
|
||||
if file.Operation == "upload" && git.IsErrNotExist(err) {
|
||||
fromEntry = nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if fromEntry != nil {
|
||||
fromEntryIDString = fromEntry.ID.String()
|
||||
file.Options.executable = fromEntry.IsExecutable() // FIXME: legacy hacky approach, it shouldn't prepare the "Options" in the "check" function
|
||||
}
|
||||
}
|
||||
|
||||
if file.SHA != "" {
|
||||
// If the SHA given doesn't match the SHA of the fromTreePath, throw error
|
||||
if file.SHA != fromEntry.ID.String() {
|
||||
if file.SHA != fromEntryIDString {
|
||||
return pull_service.ErrSHADoesNotMatch{
|
||||
Path: file.Options.treePath,
|
||||
GivenSHA: file.SHA,
|
||||
CurrentSHA: fromEntry.ID.String(),
|
||||
CurrentSHA: fromEntryIDString,
|
||||
}
|
||||
}
|
||||
} else if opts.LastCommitID != "" {
|
||||
|
|
@ -399,11 +432,10 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep
|
|||
// haven't been made. We throw an error if one wasn't provided.
|
||||
return ErrSHAOrCommitIDNotProvided{}
|
||||
}
|
||||
// FIXME: legacy hacky approach, it shouldn't prepare the "Options" in the "check" function
|
||||
file.Options.executable = fromEntry.IsExecutable()
|
||||
}
|
||||
|
||||
if file.Operation == "create" || file.Operation == "update" || file.Operation == "rename" {
|
||||
// check new entry (treePath/treeEntry)
|
||||
if file.Operation == "create" || file.Operation == "update" || file.Operation == "upload" || file.Operation == "rename" {
|
||||
// For operation's target path, we need to make sure no parts of the path are existing files or links
|
||||
// except for the last item in the path (which is the file name).
|
||||
// And that shouldn't exist IF it is a new file OR is being moved to a new path.
|
||||
|
|
@ -454,18 +486,23 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep
|
|||
return nil
|
||||
}
|
||||
|
||||
func CreateUpdateRenameFile(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile, contentStore *lfs.ContentStore, repoID int64, hasOldBranch bool) error {
|
||||
func modifyFile(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile, contentStore *lfs.ContentStore, repoID int64) (addedLfsPointer *lfs.Pointer, _ error) {
|
||||
if rd, ok := file.ContentReader.(LazyReadSeeker); ok {
|
||||
if err := rd.OpenLazyReader(); err != nil {
|
||||
return nil, fmt.Errorf("OpenLazyReader: %w", err)
|
||||
}
|
||||
defer rd.Close()
|
||||
}
|
||||
|
||||
// Get the two paths (might be the same if not moving) from the index if they exist
|
||||
filesInIndex, err := t.LsFiles(ctx, file.TreePath, file.FromTreePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("UpdateRepoFile: %w", err)
|
||||
return nil, fmt.Errorf("LsFiles: %w", err)
|
||||
}
|
||||
// If is a new file (not updating) then the given path shouldn't exist
|
||||
if file.Operation == "create" {
|
||||
if slices.Contains(filesInIndex, file.TreePath) {
|
||||
return ErrRepoFileAlreadyExists{
|
||||
Path: file.TreePath,
|
||||
}
|
||||
return nil, ErrRepoFileAlreadyExists{Path: file.TreePath}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -474,7 +511,7 @@ func CreateUpdateRenameFile(ctx context.Context, t *TemporaryUploadRepository, f
|
|||
for _, indexFile := range filesInIndex {
|
||||
if indexFile == file.Options.fromTreePath {
|
||||
if err = t.RemoveFilesFromIndex(ctx, file.FromTreePath); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -482,45 +519,46 @@ func CreateUpdateRenameFile(ctx context.Context, t *TemporaryUploadRepository, f
|
|||
|
||||
var writeObjectRet *writeRepoObjectRet
|
||||
switch file.Operation {
|
||||
case "create", "update":
|
||||
writeObjectRet, err = writeRepoObjectForCreateOrUpdate(ctx, t, file)
|
||||
case "create", "update", "upload":
|
||||
writeObjectRet, err = writeRepoObjectForModify(ctx, t, file)
|
||||
case "rename":
|
||||
writeObjectRet, err = writeRepoObjectForRename(ctx, t, file)
|
||||
default:
|
||||
return util.NewInvalidArgumentErrorf("unknown file modification operation: '%s'", file.Operation)
|
||||
return nil, util.NewInvalidArgumentErrorf("unknown file modification operation: '%s'", file.Operation)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add the object to the index, the "file.Options.executable" is set in handleCheckErrors by the caller (legacy hacky approach)
|
||||
if err = t.AddObjectToIndex(ctx, util.Iif(file.Options.executable, "100755", "100644"), writeObjectRet.ObjectHash, file.Options.treePath); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if writeObjectRet.LfsContent == nil {
|
||||
return nil // No LFS pointer, so nothing to do
|
||||
return nil, nil // No LFS pointer, so nothing to do
|
||||
}
|
||||
defer writeObjectRet.LfsContent.Close()
|
||||
|
||||
// Now we must store the content into an LFS object
|
||||
lfsMetaObject, err := git_model.NewLFSMetaObject(ctx, repoID, writeObjectRet.LfsPointer)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if exist, err := contentStore.Exists(lfsMetaObject.Pointer); err != nil {
|
||||
return err
|
||||
} else if exist {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = contentStore.Put(lfsMetaObject.Pointer, writeObjectRet.LfsContent)
|
||||
exist, err := contentStore.Exists(lfsMetaObject.Pointer)
|
||||
if err != nil {
|
||||
if _, errRemove := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); errRemove != nil {
|
||||
return fmt.Errorf("unable to remove failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, errRemove, err)
|
||||
return nil, err
|
||||
}
|
||||
if !exist {
|
||||
err = contentStore.Put(lfsMetaObject.Pointer, writeObjectRet.LfsContent)
|
||||
if err != nil {
|
||||
if _, errRemove := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); errRemove != nil {
|
||||
return nil, fmt.Errorf("unable to remove failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, errRemove, err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return err
|
||||
return &lfsMetaObject.Pointer, nil
|
||||
}
|
||||
|
||||
func checkIsLfsFileInGitAttributes(ctx context.Context, t *TemporaryUploadRepository, paths []string) (ret []bool, err error) {
|
||||
|
|
@ -544,8 +582,8 @@ type writeRepoObjectRet struct {
|
|||
LfsPointer lfs.Pointer
|
||||
}
|
||||
|
||||
// writeRepoObjectForCreateOrUpdate hashes the git object for create or update operations
|
||||
func writeRepoObjectForCreateOrUpdate(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile) (ret *writeRepoObjectRet, err error) {
|
||||
// writeRepoObjectForModify hashes the git object for create or update operations
|
||||
func writeRepoObjectForModify(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile) (ret *writeRepoObjectRet, err error) {
|
||||
ret = &writeRepoObjectRet{}
|
||||
treeObjectContentReader := file.ContentReader
|
||||
if setting.LFS.StartServer {
|
||||
|
|
@ -574,7 +612,7 @@ func writeRepoObjectForCreateOrUpdate(ctx context.Context, t *TemporaryUploadRep
|
|||
return ret, nil
|
||||
}
|
||||
|
||||
// writeRepoObjectForRename the same as writeRepoObjectForCreateOrUpdate buf for "rename"
|
||||
// writeRepoObjectForRename the same as writeRepoObjectForModify buf for "rename"
|
||||
func writeRepoObjectForRename(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile) (ret *writeRepoObjectRet, err error) {
|
||||
lastCommitID, err := t.GetLastCommit(ctx)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -8,15 +8,11 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/git/attribute"
|
||||
"code.gitea.io/gitea/modules/lfs"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
// UploadRepoFileOptions contains the uploaded repository file options
|
||||
|
|
@ -32,23 +28,48 @@ type UploadRepoFileOptions struct {
|
|||
Committer *IdentityOptions
|
||||
}
|
||||
|
||||
type uploadInfo struct {
|
||||
upload *repo_model.Upload
|
||||
lfsMetaObject *git_model.LFSMetaObject
|
||||
type lazyLocalFileReader struct {
|
||||
*os.File
|
||||
localFilename string
|
||||
counter int
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func cleanUpAfterFailure(ctx context.Context, infos *[]uploadInfo, t *TemporaryUploadRepository, original error) error {
|
||||
for _, info := range *infos {
|
||||
if info.lfsMetaObject == nil {
|
||||
continue
|
||||
}
|
||||
if !info.lfsMetaObject.Existing {
|
||||
if _, err := git_model.RemoveLFSMetaObjectByOid(ctx, t.repo.ID, info.lfsMetaObject.Oid); err != nil {
|
||||
original = fmt.Errorf("%w, %v", original, err) // We wrap the original error - as this is the underlying error that required the fallback
|
||||
var _ LazyReadSeeker = (*lazyLocalFileReader)(nil)
|
||||
|
||||
func (l *lazyLocalFileReader) Close() error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
if l.counter > 0 {
|
||||
l.counter--
|
||||
if l.counter == 0 {
|
||||
if err := l.File.Close(); err != nil {
|
||||
return fmt.Errorf("close file %s: %w", l.localFilename, err)
|
||||
}
|
||||
l.File = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return original
|
||||
return fmt.Errorf("file %s already closed", l.localFilename)
|
||||
}
|
||||
|
||||
func (l *lazyLocalFileReader) OpenLazyReader() error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
if l.File != nil {
|
||||
l.counter++
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := os.Open(l.localFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.File = file
|
||||
l.counter = 1
|
||||
return nil
|
||||
}
|
||||
|
||||
// UploadRepoFiles uploads files to the given repository
|
||||
|
|
@ -62,178 +83,29 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
|
|||
return fmt.Errorf("GetUploadsByUUIDs [uuids: %v]: %w", opts.Files, err)
|
||||
}
|
||||
|
||||
names := make([]string, len(uploads))
|
||||
infos := make([]uploadInfo, len(uploads))
|
||||
for i, upload := range uploads {
|
||||
// Check file is not lfs locked, will return nil if lock setting not enabled
|
||||
filepath := path.Join(opts.TreePath, upload.Name)
|
||||
lfsLock, err := git_model.GetTreePathLock(ctx, repo.ID, filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if lfsLock != nil && lfsLock.OwnerID != doer.ID {
|
||||
u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return git_model.ErrLFSFileLocked{RepoID: repo.ID, Path: filepath, UserName: u.Name}
|
||||
}
|
||||
|
||||
names[i] = upload.Name
|
||||
infos[i] = uploadInfo{upload: upload}
|
||||
changeOpts := &ChangeRepoFilesOptions{
|
||||
LastCommitID: opts.LastCommitID,
|
||||
OldBranch: opts.OldBranch,
|
||||
NewBranch: opts.NewBranch,
|
||||
Message: opts.Message,
|
||||
Signoff: opts.Signoff,
|
||||
Author: opts.Author,
|
||||
Committer: opts.Committer,
|
||||
}
|
||||
|
||||
t, err := NewTemporaryUploadRepository(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer t.Close()
|
||||
|
||||
hasOldBranch := true
|
||||
if err = t.Clone(ctx, opts.OldBranch, true); err != nil {
|
||||
if !git.IsErrBranchNotExist(err) || !repo.IsEmpty {
|
||||
return err
|
||||
}
|
||||
if err = t.Init(ctx, repo.ObjectFormatName); err != nil {
|
||||
return err
|
||||
}
|
||||
hasOldBranch = false
|
||||
opts.LastCommitID = ""
|
||||
}
|
||||
if hasOldBranch {
|
||||
if err = t.SetDefaultIndex(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var attributesMap map[string]*attribute.Attributes
|
||||
// when uploading to an empty repo, the old branch doesn't exist, but some "global gitattributes" or "info/attributes" may exist
|
||||
if setting.LFS.StartServer {
|
||||
attributesMap, err = attribute.CheckAttributes(ctx, t.gitRepo, "" /* use temp repo's working dir */, attribute.CheckAttributeOpts{
|
||||
Attributes: []string{attribute.Filter},
|
||||
Filenames: names,
|
||||
for _, upload := range uploads {
|
||||
changeOpts.Files = append(changeOpts.Files, &ChangeRepoFile{
|
||||
Operation: "upload",
|
||||
TreePath: path.Join(opts.TreePath, upload.Name),
|
||||
ContentReader: &lazyLocalFileReader{localFilename: upload.LocalPath()},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Copy uploaded files into repository.
|
||||
// TODO: there is a small problem: when uploading LFS files with ".gitattributes", the "check-attr" runs before this loop,
|
||||
// so LFS files are not able to be added as LFS objects. Ideally we need to do in 3 steps in the future:
|
||||
// 1. Add ".gitattributes" to git index
|
||||
// 2. Run "check-attr" (the previous attribute.CheckAttributes call)
|
||||
// 3. Add files to git index (this loop)
|
||||
// This problem is trivial so maybe no need to spend too much time on it at the moment.
|
||||
for i := range infos {
|
||||
if err := copyUploadedLFSFileIntoRepository(ctx, &infos[i], attributesMap, t, opts.TreePath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Now write the tree
|
||||
treeHash, err := t.WriteTree(ctx)
|
||||
_, err = ChangeRepoFiles(ctx, repo, doer, changeOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Now commit the tree
|
||||
commitOpts := &CommitTreeUserOptions{
|
||||
ParentCommitID: opts.LastCommitID,
|
||||
TreeHash: treeHash,
|
||||
CommitMessage: opts.Message,
|
||||
SignOff: opts.Signoff,
|
||||
DoerUser: doer,
|
||||
AuthorIdentity: opts.Author,
|
||||
CommitterIdentity: opts.Committer,
|
||||
}
|
||||
commitHash, err := t.CommitTree(ctx, commitOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Now deal with LFS objects
|
||||
for i := range infos {
|
||||
if infos[i].lfsMetaObject == nil {
|
||||
continue
|
||||
}
|
||||
infos[i].lfsMetaObject, err = git_model.NewLFSMetaObject(ctx, infos[i].lfsMetaObject.RepositoryID, infos[i].lfsMetaObject.Pointer)
|
||||
if err != nil {
|
||||
// OK Now we need to cleanup
|
||||
return cleanUpAfterFailure(ctx, &infos, t, err)
|
||||
}
|
||||
// Don't move the files yet - we need to ensure that
|
||||
// everything can be inserted first
|
||||
}
|
||||
|
||||
// OK now we can insert the data into the store - there's no way to clean up the store
|
||||
// once it's in there, it's in there.
|
||||
contentStore := lfs.NewContentStore()
|
||||
for _, info := range infos {
|
||||
if err := uploadToLFSContentStore(info, contentStore); err != nil {
|
||||
return cleanUpAfterFailure(ctx, &infos, t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Then push this tree to NewBranch
|
||||
if err := t.Push(ctx, doer, commitHash, opts.NewBranch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return repo_model.DeleteUploads(ctx, uploads...)
|
||||
}
|
||||
|
||||
func copyUploadedLFSFileIntoRepository(ctx context.Context, info *uploadInfo, attributesMap map[string]*attribute.Attributes, t *TemporaryUploadRepository, treePath string) error {
|
||||
file, err := os.Open(info.upload.LocalPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var objectHash string
|
||||
if setting.LFS.StartServer && attributesMap[info.upload.Name] != nil && attributesMap[info.upload.Name].Get(attribute.Filter).ToString().Value() == "lfs" {
|
||||
// Handle LFS
|
||||
// FIXME: Inefficient! this should probably happen in models.Upload
|
||||
pointer, err := lfs.GeneratePointer(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
info.lfsMetaObject = &git_model.LFSMetaObject{Pointer: pointer, RepositoryID: t.repo.ID}
|
||||
|
||||
if objectHash, err = t.HashObjectAndWrite(ctx, strings.NewReader(pointer.StringContent())); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if objectHash, err = t.HashObjectAndWrite(ctx, file); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add the object to the index
|
||||
return t.AddObjectToIndex(ctx, "100644", objectHash, path.Join(treePath, info.upload.Name))
|
||||
}
|
||||
|
||||
func uploadToLFSContentStore(info uploadInfo, contentStore *lfs.ContentStore) error {
|
||||
if info.lfsMetaObject == nil {
|
||||
return nil
|
||||
}
|
||||
exist, err := contentStore.Exists(info.lfsMetaObject.Pointer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exist {
|
||||
file, err := os.Open(info.upload.LocalPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
// FIXME: Put regenerates the hash and copies the file over.
|
||||
// I guess this strictly ensures the soundness of the store but this is inefficient.
|
||||
if err := contentStore.Put(info.lfsMetaObject.Pointer, file); err != nil {
|
||||
// OK Now we need to cleanup
|
||||
// Can't clean up the store, once uploaded there they're there.
|
||||
return err
|
||||
}
|
||||
if err := repo_model.DeleteUploads(ctx, uploads...); err != nil {
|
||||
log.Error("DeleteUploads: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import (
|
|||
)
|
||||
|
||||
// MergeUpstream merges the base repository's default branch into the fork repository's current branch.
|
||||
func MergeUpstream(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, branch string) (mergeStyle string, err error) {
|
||||
func MergeUpstream(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, branch string, ffOnly bool) (mergeStyle string, err error) {
|
||||
if err = repo.MustNotBeArchived(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -45,6 +45,11 @@ func MergeUpstream(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_
|
|||
return "", err
|
||||
}
|
||||
|
||||
// If ff_only is requested and fast-forward failed, return error
|
||||
if ffOnly {
|
||||
return "", util.NewInvalidArgumentErrorf("fast-forward merge not possible: branch has diverged")
|
||||
}
|
||||
|
||||
// TODO: FakePR: it is somewhat hacky, but it is the only way to "merge" at the moment
|
||||
// ideally in the future the "merge" functions should be refactored to decouple from the PullRequest
|
||||
fakeIssue := &issue_model.Issue{
|
||||
|
|
|
|||
|
|
@ -176,6 +176,12 @@ func (dc dingtalkConvertor) Status(p *api.CommitStatusPayload) (DingtalkPayload,
|
|||
return createDingtalkPayload(text, text, "Status Changed", p.TargetURL), nil
|
||||
}
|
||||
|
||||
func (dingtalkConvertor) WorkflowRun(p *api.WorkflowRunPayload) (DingtalkPayload, error) {
|
||||
text, _ := getWorkflowRunPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return createDingtalkPayload(text, text, "Workflow Run", p.WorkflowRun.HTMLURL), nil
|
||||
}
|
||||
|
||||
func (dingtalkConvertor) WorkflowJob(p *api.WorkflowJobPayload) (DingtalkPayload, error) {
|
||||
text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
|
|
|
|||
|
|
@ -278,6 +278,12 @@ func (d discordConvertor) Status(p *api.CommitStatusPayload) (DiscordPayload, er
|
|||
return d.createPayload(p.Sender, text, "", p.TargetURL, color), nil
|
||||
}
|
||||
|
||||
func (d discordConvertor) WorkflowRun(p *api.WorkflowRunPayload) (DiscordPayload, error) {
|
||||
text, color := getWorkflowRunPayloadInfo(p, noneLinkFormatter, false)
|
||||
|
||||
return d.createPayload(p.Sender, text, "", p.WorkflowRun.HTMLURL, color), nil
|
||||
}
|
||||
|
||||
func (d discordConvertor) WorkflowJob(p *api.WorkflowJobPayload) (DiscordPayload, error) {
|
||||
text, color := getWorkflowJobPayloadInfo(p, noneLinkFormatter, false)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,13 @@ package webhook
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
webhook_model "code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
|
|
@ -16,10 +20,12 @@ import (
|
|||
)
|
||||
|
||||
type (
|
||||
// FeishuPayload represents
|
||||
// FeishuPayload represents the payload for Feishu webhook
|
||||
FeishuPayload struct {
|
||||
MsgType string `json:"msg_type"` // text / post / image / share_chat / interactive / file /audio / media
|
||||
Content struct {
|
||||
Timestamp int64 `json:"timestamp,omitempty"` // Unix timestamp for signature verification
|
||||
Sign string `json:"sign,omitempty"` // Signature for verification
|
||||
MsgType string `json:"msg_type"` // text / post / image / share_chat / interactive / file /audio / media
|
||||
Content struct {
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
}
|
||||
|
|
@ -172,15 +178,41 @@ func (fc feishuConvertor) Status(p *api.CommitStatusPayload) (FeishuPayload, err
|
|||
return newFeishuTextPayload(text), nil
|
||||
}
|
||||
|
||||
func (feishuConvertor) WorkflowRun(p *api.WorkflowRunPayload) (FeishuPayload, error) {
|
||||
text, _ := getWorkflowRunPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return newFeishuTextPayload(text), nil
|
||||
}
|
||||
|
||||
func (feishuConvertor) WorkflowJob(p *api.WorkflowJobPayload) (FeishuPayload, error) {
|
||||
text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return newFeishuTextPayload(text), nil
|
||||
}
|
||||
|
||||
// feishuGenSign generates a signature for Feishu webhook
|
||||
// https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot
|
||||
func feishuGenSign(secret string, timestamp int64) string {
|
||||
// key="{timestamp}\n{secret}", then hmac-sha256, then base64 encode
|
||||
stringToSign := fmt.Sprintf("%d\n%s", timestamp, secret)
|
||||
h := hmac.New(sha256.New, []byte(stringToSign))
|
||||
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func newFeishuRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
||||
var pc payloadConvertor[FeishuPayload] = feishuConvertor{}
|
||||
return newJSONRequest(pc, w, t, true)
|
||||
payload, err := newPayload(feishuConvertor{}, []byte(t.PayloadContent), t.EventType)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Add timestamp and signature if secret is provided
|
||||
if w.Secret != "" {
|
||||
timestamp := time.Now().Unix()
|
||||
payload.Timestamp = timestamp
|
||||
payload.Sign = feishuGenSign(w.Secret, timestamp)
|
||||
}
|
||||
|
||||
return prepareJSONRequest(payload, w, t, false /* no default headers */)
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ func TestFeishuJSONPayload(t *testing.T) {
|
|||
URL: "https://feishu.example.com/",
|
||||
Meta: `{}`,
|
||||
HTTPMethod: "POST",
|
||||
Secret: "secret",
|
||||
}
|
||||
task := &webhook_model.HookTask{
|
||||
HookID: hook.ID,
|
||||
|
|
@ -183,10 +184,13 @@ func TestFeishuJSONPayload(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "POST", req.Method)
|
||||
assert.Equal(t, "https://feishu.example.com/", req.URL.String())
|
||||
assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
|
||||
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
|
||||
var body FeishuPayload
|
||||
err = json.NewDecoder(req.Body).Decode(&body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "[test/repo:test] \r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.Content.Text)
|
||||
assert.Equal(t, feishuGenSign(hook.Secret, body.Timestamp), body.Sign)
|
||||
|
||||
// a separate sign test, the result is generated by official python code, so the algo must be correct
|
||||
assert.Equal(t, "rWZ84lcag1x9aBFhn1gtV4ZN+4gme3pilfQNMk86vKg=", feishuGenSign("a", 1))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -327,6 +327,37 @@ func getStatusPayloadInfo(p *api.CommitStatusPayload, linkFormatter linkFormatte
|
|||
return text, color
|
||||
}
|
||||
|
||||
func getWorkflowRunPayloadInfo(p *api.WorkflowRunPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
|
||||
description := p.WorkflowRun.Conclusion
|
||||
if description == "" {
|
||||
description = p.WorkflowRun.Status
|
||||
}
|
||||
refLink := linkFormatter(p.WorkflowRun.HTMLURL, fmt.Sprintf("%s(#%d)", p.WorkflowRun.DisplayTitle, p.WorkflowRun.ID)+"["+base.ShortSha(p.WorkflowRun.HeadSha)+"]:"+description)
|
||||
|
||||
text = fmt.Sprintf("Workflow Run %s: %s", p.Action, refLink)
|
||||
switch description {
|
||||
case "waiting":
|
||||
color = orangeColor
|
||||
case "queued":
|
||||
color = orangeColorLight
|
||||
case "success":
|
||||
color = greenColor
|
||||
case "failure":
|
||||
color = redColor
|
||||
case "cancelled":
|
||||
color = yellowColor
|
||||
case "skipped":
|
||||
color = purpleColor
|
||||
default:
|
||||
color = greyColor
|
||||
}
|
||||
if withSender {
|
||||
text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
|
||||
}
|
||||
|
||||
return text, color
|
||||
}
|
||||
|
||||
func getWorkflowJobPayloadInfo(p *api.WorkflowJobPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
|
||||
description := p.WorkflowJob.Conclusion
|
||||
if description == "" {
|
||||
|
|
|
|||
|
|
@ -252,6 +252,12 @@ func (m matrixConvertor) Status(p *api.CommitStatusPayload) (MatrixPayload, erro
|
|||
return m.newPayload(text)
|
||||
}
|
||||
|
||||
func (m matrixConvertor) WorkflowRun(p *api.WorkflowRunPayload) (MatrixPayload, error) {
|
||||
text, _ := getWorkflowRunPayloadInfo(p, htmlLinkFormatter, true)
|
||||
|
||||
return m.newPayload(text)
|
||||
}
|
||||
|
||||
func (m matrixConvertor) WorkflowJob(p *api.WorkflowJobPayload) (MatrixPayload, error) {
|
||||
text, _ := getWorkflowJobPayloadInfo(p, htmlLinkFormatter, true)
|
||||
|
||||
|
|
|
|||
|
|
@ -318,6 +318,20 @@ func (m msteamsConvertor) Status(p *api.CommitStatusPayload) (MSTeamsPayload, er
|
|||
), nil
|
||||
}
|
||||
|
||||
func (msteamsConvertor) WorkflowRun(p *api.WorkflowRunPayload) (MSTeamsPayload, error) {
|
||||
title, color := getWorkflowRunPayloadInfo(p, noneLinkFormatter, false)
|
||||
|
||||
return createMSTeamsPayload(
|
||||
p.Repo,
|
||||
p.Sender,
|
||||
title,
|
||||
"",
|
||||
p.WorkflowRun.HTMLURL,
|
||||
color,
|
||||
&MSTeamsFact{"WorkflowRun:", p.WorkflowRun.DisplayTitle},
|
||||
), nil
|
||||
}
|
||||
|
||||
func (msteamsConvertor) WorkflowJob(p *api.WorkflowJobPayload) (MSTeamsPayload, error) {
|
||||
title, color := getWorkflowJobPayloadInfo(p, noneLinkFormatter, false)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,8 @@ package webhook
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
|
|
@ -18,6 +16,7 @@ import (
|
|||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/repository"
|
||||
|
|
@ -956,72 +955,17 @@ func (*webhookNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_
|
|||
org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner))
|
||||
}
|
||||
|
||||
err := job.LoadAttributes(ctx)
|
||||
status, _ := convert.ToActionsStatus(job.Status)
|
||||
|
||||
convertedJob, err := convert.ToActionWorkflowJob(ctx, repo, task, job)
|
||||
if err != nil {
|
||||
log.Error("Error loading job attributes: %v", err)
|
||||
log.Error("ToActionWorkflowJob: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
jobIndex := 0
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, job.RunID)
|
||||
if err != nil {
|
||||
log.Error("Error loading getting run jobs: %v", err)
|
||||
return
|
||||
}
|
||||
for i, j := range jobs {
|
||||
if j.ID == job.ID {
|
||||
jobIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
status, conclusion := toActionStatus(job.Status)
|
||||
var runnerID int64
|
||||
var runnerName string
|
||||
var steps []*api.ActionWorkflowStep
|
||||
|
||||
if task != nil {
|
||||
runnerID = task.RunnerID
|
||||
if runner, ok, _ := db.GetByID[actions_model.ActionRunner](ctx, runnerID); ok {
|
||||
runnerName = runner.Name
|
||||
}
|
||||
for i, step := range task.Steps {
|
||||
stepStatus, stepConclusion := toActionStatus(job.Status)
|
||||
steps = append(steps, &api.ActionWorkflowStep{
|
||||
Name: step.Name,
|
||||
Number: int64(i),
|
||||
Status: stepStatus,
|
||||
Conclusion: stepConclusion,
|
||||
StartedAt: step.Started.AsTime().UTC(),
|
||||
CompletedAt: step.Stopped.AsTime().UTC(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if err := PrepareWebhooks(ctx, source, webhook_module.HookEventWorkflowJob, &api.WorkflowJobPayload{
|
||||
Action: status,
|
||||
WorkflowJob: &api.ActionWorkflowJob{
|
||||
ID: job.ID,
|
||||
// missing api endpoint for this location
|
||||
URL: fmt.Sprintf("%s/actions/runs/%d/jobs/%d", repo.APIURL(), job.RunID, job.ID),
|
||||
HTMLURL: fmt.Sprintf("%s/jobs/%d", job.Run.HTMLURL(), jobIndex),
|
||||
RunID: job.RunID,
|
||||
// Missing api endpoint for this location, artifacts are available under a nested url
|
||||
RunURL: fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(), job.RunID),
|
||||
Name: job.Name,
|
||||
Labels: job.RunsOn,
|
||||
RunAttempt: job.Attempt,
|
||||
HeadSha: job.Run.CommitSHA,
|
||||
HeadBranch: git.RefName(job.Run.Ref).BranchName(),
|
||||
Status: status,
|
||||
Conclusion: conclusion,
|
||||
RunnerID: runnerID,
|
||||
RunnerName: runnerName,
|
||||
Steps: steps,
|
||||
CreatedAt: job.Created.AsTime().UTC(),
|
||||
StartedAt: job.Started.AsTime().UTC(),
|
||||
CompletedAt: job.Stopped.AsTime().UTC(),
|
||||
},
|
||||
Action: status,
|
||||
WorkflowJob: convertedJob,
|
||||
Organization: org,
|
||||
Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}),
|
||||
Sender: convert.ToUser(ctx, sender, nil),
|
||||
|
|
@ -1030,28 +974,46 @@ func (*webhookNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_
|
|||
}
|
||||
}
|
||||
|
||||
func toActionStatus(status actions_model.Status) (string, string) {
|
||||
var action string
|
||||
var conclusion string
|
||||
switch status {
|
||||
// This is a naming conflict of the webhook between Gitea and GitHub Actions
|
||||
case actions_model.StatusWaiting:
|
||||
action = "queued"
|
||||
case actions_model.StatusBlocked:
|
||||
action = "waiting"
|
||||
case actions_model.StatusRunning:
|
||||
action = "in_progress"
|
||||
func (*webhookNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) {
|
||||
source := EventSource{
|
||||
Repository: repo,
|
||||
Owner: repo.Owner,
|
||||
}
|
||||
if status.IsDone() {
|
||||
action = "completed"
|
||||
switch status {
|
||||
case actions_model.StatusSuccess:
|
||||
conclusion = "success"
|
||||
case actions_model.StatusCancelled:
|
||||
conclusion = "cancelled"
|
||||
case actions_model.StatusFailure:
|
||||
conclusion = "failure"
|
||||
}
|
||||
|
||||
var org *api.Organization
|
||||
if repo.Owner.IsOrganization() {
|
||||
org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner))
|
||||
}
|
||||
|
||||
status := convert.ToWorkflowRunAction(run.Status)
|
||||
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
|
||||
if err != nil {
|
||||
log.Error("OpenRepository: %v", err)
|
||||
return
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
convertedWorkflow, err := convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID)
|
||||
if err != nil {
|
||||
log.Error("GetActionWorkflow: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
convertedRun, err := convert.ToActionWorkflowRun(ctx, repo, run)
|
||||
if err != nil {
|
||||
log.Error("ToActionWorkflowRun: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := PrepareWebhooks(ctx, source, webhook_module.HookEventWorkflowRun, &api.WorkflowRunPayload{
|
||||
Action: status,
|
||||
Workflow: convertedWorkflow,
|
||||
WorkflowRun: convertedRun,
|
||||
Organization: org,
|
||||
Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}),
|
||||
Sender: convert.ToUser(ctx, sender, nil),
|
||||
}); err != nil {
|
||||
log.Error("PrepareWebhooks: %v", err)
|
||||
}
|
||||
return action, conclusion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,6 +114,10 @@ func (pc packagistConvertor) Status(_ *api.CommitStatusPayload) (PackagistPayloa
|
|||
return PackagistPayload{}, nil
|
||||
}
|
||||
|
||||
func (pc packagistConvertor) WorkflowRun(_ *api.WorkflowRunPayload) (PackagistPayload, error) {
|
||||
return PackagistPayload{}, nil
|
||||
}
|
||||
|
||||
func (pc packagistConvertor) WorkflowJob(_ *api.WorkflowJobPayload) (PackagistPayload, error) {
|
||||
return PackagistPayload{}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ type payloadConvertor[T any] interface {
|
|||
Wiki(*api.WikiPayload) (T, error)
|
||||
Package(*api.PackagePayload) (T, error)
|
||||
Status(*api.CommitStatusPayload) (T, error)
|
||||
WorkflowRun(*api.WorkflowRunPayload) (T, error)
|
||||
WorkflowJob(*api.WorkflowJobPayload) (T, error)
|
||||
}
|
||||
|
||||
|
|
@ -81,6 +82,8 @@ func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module
|
|||
return convertUnmarshalledJSON(rc.Package, data)
|
||||
case webhook_module.HookEventStatus:
|
||||
return convertUnmarshalledJSON(rc.Status, data)
|
||||
case webhook_module.HookEventWorkflowRun:
|
||||
return convertUnmarshalledJSON(rc.WorkflowRun, data)
|
||||
case webhook_module.HookEventWorkflowJob:
|
||||
return convertUnmarshalledJSON(rc.WorkflowJob, data)
|
||||
}
|
||||
|
|
@ -92,7 +95,10 @@ func newJSONRequest[T any](pc payloadConvertor[T], w *webhook_model.Webhook, t *
|
|||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return prepareJSONRequest(payload, w, t, withDefaultHeaders)
|
||||
}
|
||||
|
||||
func prepareJSONRequest[T any](payload T, w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) {
|
||||
body, err := json.MarshalIndent(payload, "", " ")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
|
|
|||
|
|
@ -173,6 +173,12 @@ func (s slackConvertor) Status(p *api.CommitStatusPayload) (SlackPayload, error)
|
|||
return s.createPayload(text, nil), nil
|
||||
}
|
||||
|
||||
func (s slackConvertor) WorkflowRun(p *api.WorkflowRunPayload) (SlackPayload, error) {
|
||||
text, _ := getWorkflowRunPayloadInfo(p, SlackLinkFormatter, true)
|
||||
|
||||
return s.createPayload(text, nil), nil
|
||||
}
|
||||
|
||||
func (s slackConvertor) WorkflowJob(p *api.WorkflowJobPayload) (SlackPayload, error) {
|
||||
text, _ := getWorkflowJobPayloadInfo(p, SlackLinkFormatter, true)
|
||||
|
||||
|
|
|
|||
|
|
@ -180,6 +180,12 @@ func (t telegramConvertor) Status(p *api.CommitStatusPayload) (TelegramPayload,
|
|||
return createTelegramPayloadHTML(text), nil
|
||||
}
|
||||
|
||||
func (telegramConvertor) WorkflowRun(p *api.WorkflowRunPayload) (TelegramPayload, error) {
|
||||
text, _ := getWorkflowRunPayloadInfo(p, htmlLinkFormatter, true)
|
||||
|
||||
return createTelegramPayloadHTML(text), nil
|
||||
}
|
||||
|
||||
func (telegramConvertor) WorkflowJob(p *api.WorkflowJobPayload) (TelegramPayload, error) {
|
||||
text, _ := getWorkflowJobPayloadInfo(p, htmlLinkFormatter, true)
|
||||
|
||||
|
|
|
|||
|
|
@ -181,6 +181,12 @@ func (wc wechatworkConvertor) Status(p *api.CommitStatusPayload) (WechatworkPayl
|
|||
return newWechatworkMarkdownPayload(text), nil
|
||||
}
|
||||
|
||||
func (wc wechatworkConvertor) WorkflowRun(p *api.WorkflowRunPayload) (WechatworkPayload, error) {
|
||||
text, _ := getWorkflowRunPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return newWechatworkMarkdownPayload(text), nil
|
||||
}
|
||||
|
||||
func (wc wechatworkConvertor) WorkflowJob(p *api.WorkflowJobPayload) (WechatworkPayload, error) {
|
||||
text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,15 +3,13 @@
|
|||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
<form class="ui edit form" method="post" action="{{.RepoLink}}/_cherrypick/{{.SHA}}/{{.BranchName | PathEscapeSegments}}">
|
||||
<form class="ui edit form form-fetch-action" method="post" action="{{.RepoLink}}/_cherrypick/{{.FromCommitID}}/{{.BranchName | PathEscapeSegments}}">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="last_commit" value="{{.last_commit}}">
|
||||
<input type="hidden" name="page_has_posted" value="true">
|
||||
<input type="hidden" name="revert" value="{{if eq .CherryPickType "revert"}}true{{else}}false{{end}}">
|
||||
<div class="repo-editor-header">
|
||||
<div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}">
|
||||
{{$shaurl := printf "%s/commit/%s" $.RepoLink (PathEscape .SHA)}}
|
||||
{{$shalink := HTMLFormat `<a class="ui primary sha label" href="%s">%s</a>` $shaurl (ShortSha .SHA)}}
|
||||
<div class="breadcrumb">
|
||||
{{$shaurl := printf "%s/commit/%s" $.RepoLink (PathEscape .FromCommitID)}}
|
||||
{{$shalink := HTMLFormat `<a class="ui primary sha label" href="%s">%s</a>` $shaurl (ShortSha .FromCommitID)}}
|
||||
{{if eq .CherryPickType "revert"}}
|
||||
{{ctx.Locale.Tr "repo.editor.revert" $shalink}}
|
||||
{{else}}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<div class="commit-form-wrapper">
|
||||
{{ctx.AvatarUtils.Avatar .SignedUser 40 "commit-avatar"}}
|
||||
<div class="commit-form">
|
||||
<h3>{{- if .CanCommitToBranch.WillSign}}
|
||||
<span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CanCommitToBranch.SigningKey}}">{{svg "octicon-lock" 24}}</span>
|
||||
<h3>{{- if .CommitFormBehaviors.WillSign}}
|
||||
<span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CommitFormBehaviors.SigningKey}}">{{svg "octicon-lock" 24}}</span>
|
||||
{{ctx.Locale.Tr "repo.editor.commit_signed_changes"}}
|
||||
{{- else}}
|
||||
<span title="{{ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .CanCommitToBranch.WontSignReason)}}">{{svg "octicon-unlock" 24}}</span>
|
||||
<span title="{{ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .CommitFormBehaviors.WontSignReason)}}">{{svg "octicon-unlock" 24}}</span>
|
||||
{{ctx.Locale.Tr "repo.editor.commit_changes"}}
|
||||
{{- end}}</h3>
|
||||
<div class="field">
|
||||
|
|
@ -22,17 +22,17 @@
|
|||
</div>
|
||||
<div class="quick-pull-choice js-quick-pull-choice">
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox {{if not .CanCommitToBranch.CanCommitToBranch}}disabled{{end}}">
|
||||
<div class="ui radio checkbox {{if not .CommitFormBehaviors.CanCommitToBranch}}disabled{{end}}">
|
||||
<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="direct" data-button-text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "direct"}}checked{{end}}>
|
||||
<label>
|
||||
{{svg "octicon-git-commit"}}
|
||||
{{ctx.Locale.Tr "repo.editor.commit_directly_to_this_branch" .BranchName}}
|
||||
{{if not .CanCommitToBranch.CanCommitToBranch}}
|
||||
{{if not .CommitFormBehaviors.CanCommitToBranch}}
|
||||
<div class="ui visible small warning message">
|
||||
{{ctx.Locale.Tr "repo.editor.no_commit_to_branch"}}
|
||||
<ul>
|
||||
{{if not .CanCommitToBranch.UserCanPush}}<li>{{ctx.Locale.Tr "repo.editor.user_no_push_to_branch"}}</li>{{end}}
|
||||
{{if and .CanCommitToBranch.RequireSigned (not .CanCommitToBranch.WillSign)}}<li>{{ctx.Locale.Tr "repo.editor.require_signed_commit"}}</li>{{end}}
|
||||
{{if not .CommitFormBehaviors.UserCanPush}}<li>{{ctx.Locale.Tr "repo.editor.user_no_push_to_branch"}}</li>{{end}}
|
||||
{{if and .CommitFormBehaviors.RequireSigned (not .CommitFormBehaviors.WillSign)}}<li>{{ctx.Locale.Tr "repo.editor.require_signed_commit"}}</li>{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
@ -42,14 +42,14 @@
|
|||
{{if and (not .Repository.IsEmpty) (not .IsEditingFileOnly)}}
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
{{if .CanCreatePullRequest}}
|
||||
{{if .CommitFormBehaviors.CanCreatePullRequest}}
|
||||
<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" data-button-text="{{ctx.Locale.Tr "repo.editor.propose_file_change"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}>
|
||||
{{else}}
|
||||
<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" data-button-text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}>
|
||||
{{end}}
|
||||
<label>
|
||||
{{svg "octicon-git-pull-request"}}
|
||||
{{if .CanCreatePullRequest}}
|
||||
{{if .CommitFormBehaviors.CanCreatePullRequest}}
|
||||
{{ctx.Locale.Tr "repo.editor.create_new_branch"}}
|
||||
{{else}}
|
||||
{{ctx.Locale.Tr "repo.editor.create_new_branch_np"}}
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="quick-pull-branch-name {{if not (eq .commit_choice "commit-to-new-branch")}}tw-hidden{{end}}">
|
||||
<div class="new-branch-name-input field {{if .Err_NewBranchName}}error{{end}}">
|
||||
<div class="new-branch-name-input field">
|
||||
{{svg "octicon-git-branch"}}
|
||||
<input type="text" name="new_branch_name" maxlength="100" value="{{.new_branch_name}}" class="input-contrast tw-mr-1 js-quick-pull-new-branch-name" placeholder="{{ctx.Locale.Tr "repo.editor.new_branch_name_desc"}}" {{if eq .commit_choice "commit-to-new-branch"}}required{{end}} title="{{ctx.Locale.Tr "repo.editor.new_branch_name"}}">
|
||||
<span class="text-muted js-quick-pull-normalization-info"></span>
|
||||
|
|
@ -67,7 +67,7 @@
|
|||
{{end}}
|
||||
</div>
|
||||
{{if and .CommitCandidateEmails (gt (len .CommitCandidateEmails) 1)}}
|
||||
<div class="field {{if .Err_CommitEmail}}error{{end}}">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.editor.commit_email"}}</label>
|
||||
<select class="ui selection dropdown" name="commit_email">
|
||||
{{- range $email := .CommitCandidateEmails -}}
|
||||
|
|
@ -77,7 +77,8 @@
|
|||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<button id="commit-button" type="submit" class="ui primary button" {{if .PageIsEdit}}disabled{{end}}>
|
||||
<input type="hidden" name="last_commit" value="{{.last_commit}}">
|
||||
<button id="commit-button" type="submit" class="ui primary button">
|
||||
{{if eq .commit_choice "commit-to-new-branch"}}{{ctx.Locale.Tr "repo.editor.propose_file_change"}}{{else}}{{ctx.Locale.Tr "repo.editor.commit_changes"}}{{end}}
|
||||
</button>
|
||||
<a class="ui button red" href="{{if .ReturnURI}}{{.ReturnURI}}{{else}}{{$.BranchLink}}/{{PathEscapeSegments .TreePath}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel"}}</a>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
<div class="breadcrumb">
|
||||
<a class="section" href="{{$.BranchLink}}">{{.Repository.Name}}</a>
|
||||
{{$n := len .TreeNames}}
|
||||
{{$l := Eval $n "-" 1}}
|
||||
{{range $i, $v := .TreeNames}}
|
||||
<div class="breadcrumb-divider">/</div>
|
||||
{{if eq $i $l}}
|
||||
<input id="file-name" maxlength="255" value="{{$v}}" placeholder="{{ctx.Locale.Tr (Iif $.PageIsUpload "repo.editor.add_subdir" "repo.editor.name_your_file")}}" data-editorconfig="{{$.EditorconfigJson}}" required autofocus>
|
||||
<span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span>
|
||||
{{else}}
|
||||
<span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{or .ReturnURI (print $.BranchLink "/" (PathEscapeSegments .TreePath))}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span>
|
||||
<input type="hidden" id="tree_path" name="tree_path" value="{{.TreePath}}">
|
||||
</div>
|
||||
|
|
@ -3,9 +3,8 @@
|
|||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
<form class="ui form" method="post">
|
||||
<form class="ui form form-fetch-action" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="last_commit" value="{{.last_commit}}">
|
||||
{{template "repo/editor/commit_form" .}}
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,30 +3,13 @@
|
|||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
<form class="ui edit form" method="post"
|
||||
<form class="ui edit form form-fetch-action" method="post"
|
||||
data-text-empty-confirm-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}"
|
||||
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
|
||||
>
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="last_commit" value="{{.last_commit}}">
|
||||
<input type="hidden" name="page_has_posted" value="{{.PageHasPosted}}">
|
||||
<div class="repo-editor-header">
|
||||
<div class="ui breadcrumb field{{if .Err_TreePath}} error{{end}}">
|
||||
<a class="section" href="{{$.BranchLink}}">{{.Repository.Name}}</a>
|
||||
{{$n := len .TreeNames}}
|
||||
{{$l := Eval $n "-" 1}}
|
||||
{{range $i, $v := .TreeNames}}
|
||||
<div class="breadcrumb-divider">/</div>
|
||||
{{if eq $i $l}}
|
||||
<input id="file-name" maxlength="255" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.name_your_file"}}" data-editorconfig="{{$.EditorconfigJson}}" required autofocus>
|
||||
<span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span>
|
||||
{{else}}
|
||||
<span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{if .ReturnURI}}{{.ReturnURI}}{{else}}{{$.BranchLink}}{{if not .IsNewFile}}/{{PathEscapeSegments .TreePath}}{{end}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span>
|
||||
<input type="hidden" id="tree_path" name="tree_path" value="{{.TreePath}}" required>
|
||||
</div>
|
||||
{{template "repo/editor/common_breadcrumb" .}}
|
||||
</div>
|
||||
{{if not .NotEditableReason}}
|
||||
<div class="field">
|
||||
|
|
|
|||
|
|
@ -3,15 +3,13 @@
|
|||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
<form class="ui edit form" method="post" action="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}"
|
||||
<form class="ui edit form form-fetch-action" method="post" action="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}"
|
||||
data-text-empty-confirm-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}"
|
||||
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
|
||||
>
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="last_commit" value="{{.last_commit}}">
|
||||
<input type="hidden" name="page_has_posted" value="{{.PageHasPosted}}">
|
||||
<div class="repo-editor-header">
|
||||
<div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}">
|
||||
<div class="breadcrumb">
|
||||
{{ctx.Locale.Tr "repo.editor.patching"}}
|
||||
<a class="section" href="{{$.RepoLink}}">{{.Repository.FullName}}</a>
|
||||
<div class="breadcrumb-divider">:</div>
|
||||
|
|
|
|||
|
|
@ -3,25 +3,10 @@
|
|||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
<form class="ui comment form" method="post">
|
||||
<form class="ui comment form form-fetch-action" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="repo-editor-header">
|
||||
<div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}">
|
||||
<a class="section" href="{{$.BranchLink}}">{{.Repository.Name}}</a>
|
||||
{{$n := len .TreeNames}}
|
||||
{{$l := Eval $n "-" 1}}
|
||||
{{range $i, $v := .TreeNames}}
|
||||
<div class="breadcrumb-divider">/</div>
|
||||
{{if eq $i $l}}
|
||||
<input type="text" id="file-name" maxlength="255" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.add_subdir"}}" autofocus>
|
||||
<span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span>
|
||||
{{else}}
|
||||
<span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{$.BranchLink}}{{if not .IsNewFile}}/{{.TreePath | PathEscapeSegments}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span>
|
||||
<input type="hidden" id="tree_path" name="tree_path" value="{{.TreePath}}" required>
|
||||
</div>
|
||||
{{template "repo/editor/common_breadcrumb" .}}
|
||||
</div>
|
||||
<div class="field">
|
||||
{{template "repo/upload" .}}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
{{if $attachments}}
|
||||
<div class="card-attachment-images">
|
||||
{{range $attachments}}
|
||||
<img src="{{.DownloadURL}}" alt="{{.Name}}" />
|
||||
<img loading="lazy" src="{{.DownloadURL}}" alt="{{.Name}}" />
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
{{if FilenameIsImage .Name}}
|
||||
{{if not (StringUtils.Contains (StringUtils.ToString $.RenderedContent) .UUID)}}
|
||||
<a target="_blank" rel="noopener noreferrer" href="{{.DownloadURL}}">
|
||||
<img alt="{{.Name}}" src="{{.DownloadURL}}" title="{{ctx.Locale.Tr "repo.issues.attachment.open_tab" .Name}}">
|
||||
<img loading="lazy" alt="{{.Name}}" src="{{.DownloadURL}}" title="{{ctx.Locale.Tr "repo.issues.attachment.open_tab" .Name}}">
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
|
|
|||
|
|
@ -615,7 +615,7 @@
|
|||
<div class="timeline-item-group">
|
||||
<div class="timeline-item event" id="{{.HashTag}}">
|
||||
<a class="timeline-avatar"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}>
|
||||
<img alt src="{{.Poster.AvatarLink ctx}}" width="40" height="40">
|
||||
<img loading="lazy" alt src="{{.Poster.AvatarLink ctx}}" width="40" height="40">
|
||||
</a>
|
||||
<span class="badge grey">{{svg "octicon-x" 16}}</span>
|
||||
<span class="text grey muted-links">
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
{{else if not .IsTextFile}}
|
||||
<div class="view-raw">
|
||||
{{if .IsImageFile}}
|
||||
<img alt="{{$.RawFileLink}}" src="{{$.RawFileLink}}">
|
||||
<img loading="lazy" alt="{{$.RawFileLink}}" src="{{$.RawFileLink}}">
|
||||
{{else if .IsVideoFile}}
|
||||
<video controls src="{{$.RawFileLink}}">
|
||||
<strong>{{ctx.Locale.Tr "repo.video_not_supported_in_browser"}}</strong>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue