diff --git a/models/actions/run.go b/models/actions/run.go index 37064520a213a..aa3861c475477 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -74,6 +74,7 @@ func (run *ActionRun) Link() string { return fmt.Sprintf("%s/actions/runs/%d", run.Repo.Link(), run.Index) } +// WorkflowLink return the url to the actions list filtered for this runs workflow func (run *ActionRun) WorkflowLink() string { if run.Repo == nil { return "" diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 0d2b0dd9194d9..ed8e60c6476dd 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -5,9 +5,11 @@ package actions import ( "bytes" + "fmt" "io" "strings" + actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" @@ -69,6 +71,42 @@ func ListWorkflows(commit *git.Commit) (git.Entries, error) { return ret, nil } +// GetRunsWorkflowFileLink return url for the source of the workflow file for an ActionRun +func GetRunsWorkflowFileLink(run *actions_model.ActionRun, gitRepo *git.Repository) string { + if run.Repo == nil || run.CommitSHA == "" { + return "" + } + + commit, err := gitRepo.GetCommit(run.CommitSHA) + if err != nil { + return "" + } + + entries, err := ListWorkflows(commit) + if err != nil { + return "" + } + + var workflowEntry *git.TreeEntry + for _, entry := range entries { + if entry.Name() == run.WorkflowID { + workflowEntry = entry + break + } + } + + if workflowEntry == nil { + return "" + } + + workflowFilePath := workflowEntry.GetPathInRepo() + if workflowFilePath == "" { + return "" + } + + return fmt.Sprintf("%s/src/commit/%s/%s", run.Repo.Link(), run.CommitSHA, workflowFilePath) +} + func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) { f, err := entry.Blob().DataAsync() if err != nil { diff --git a/modules/git/tree_entry.go b/modules/git/tree_entry.go index 95131214872e6..eb992ba538cb7 100644 --- a/modules/git/tree_entry.go +++ b/modules/git/tree_entry.go @@ -8,6 +8,8 @@ import ( "io" "sort" "strings" + + "code.gitea.io/gitea/modules/log" ) // Type returns the type of the entry (commit, tree, blob) @@ -179,3 +181,30 @@ func (tes Entries) Sort() { func (tes Entries) CustomSort(cmp func(s1, s2 string) bool) { sort.Sort(customSortableEntries{cmp, tes}) } + +// GetPathInRepo returns the relative path in the tree to this entry +func (te *TreeEntry) GetPathInRepo() string { + if te == nil { + return "" + } + + path := te.Name() + current := te.ptree + + for current != nil && current.ptree != nil { + entries, err := current.ptree.ListEntries() + if err != nil { + log.Error("Failed to climb git tree %v", err) + return "" + } + for _, entry := range entries { + if entry.ID.String() == current.ID.String() { + path = entry.Name() + "/" + path + break + } + } + current = current.ptree + } + + return path +} diff --git a/modules/git/tree_entry_gogit_test.go b/modules/git/tree_entry_gogit_test.go new file mode 100644 index 0000000000000..b01a692b086e5 --- /dev/null +++ b/modules/git/tree_entry_gogit_test.go @@ -0,0 +1,55 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +//go:build gogit + +package git + +import ( + "testing" + + "github.com/go-git/go-git/v5/plumbing/filemode" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/stretchr/testify/assert" +) + +func getTestEntries() Entries { + return Entries{ + &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v1.0", Mode: filemode.Dir}}, + &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.0", Mode: filemode.Dir}}, + &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.1", Mode: filemode.Dir}}, + &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.12", Mode: filemode.Dir}}, + &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.2", Mode: filemode.Dir}}, + &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v12.0", Mode: filemode.Dir}}, + &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "abc", Mode: filemode.Regular}}, + &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "bcd", Mode: filemode.Regular}}, + } +} + +func TestEntriesSort(t *testing.T) { + entries := getTestEntries() + entries.Sort() + assert.Equal(t, "v1.0", entries[0].Name()) + assert.Equal(t, "v12.0", entries[1].Name()) + assert.Equal(t, "v2.0", entries[2].Name()) + assert.Equal(t, "v2.1", entries[3].Name()) + assert.Equal(t, "v2.12", entries[4].Name()) + assert.Equal(t, "v2.2", entries[5].Name()) + assert.Equal(t, "abc", entries[6].Name()) + assert.Equal(t, "bcd", entries[7].Name()) +} + +func TestEntriesCustomSort(t *testing.T) { + entries := getTestEntries() + entries.CustomSort(func(s1, s2 string) bool { + return s1 > s2 + }) + assert.Equal(t, "v2.2", entries[0].Name()) + assert.Equal(t, "v2.12", entries[1].Name()) + assert.Equal(t, "v2.1", entries[2].Name()) + assert.Equal(t, "v2.0", entries[3].Name()) + assert.Equal(t, "v12.0", entries[4].Name()) + assert.Equal(t, "v1.0", entries[5].Name()) + assert.Equal(t, "bcd", entries[6].Name()) + assert.Equal(t, "abc", entries[7].Name()) +} diff --git a/modules/git/tree_entry_test.go b/modules/git/tree_entry_test.go index 30eee13669e43..61afea59a243a 100644 --- a/modules/git/tree_entry_test.go +++ b/modules/git/tree_entry_test.go @@ -1,59 +1,14 @@ // Copyright 2017 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -//go:build gogit - package git import ( "testing" - "github.com/go-git/go-git/v5/plumbing/filemode" - "github.com/go-git/go-git/v5/plumbing/object" "github.com/stretchr/testify/assert" ) -func getTestEntries() Entries { - return Entries{ - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v1.0", Mode: filemode.Dir}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.0", Mode: filemode.Dir}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.1", Mode: filemode.Dir}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.12", Mode: filemode.Dir}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.2", Mode: filemode.Dir}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v12.0", Mode: filemode.Dir}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "abc", Mode: filemode.Regular}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "bcd", Mode: filemode.Regular}}, - } -} - -func TestEntriesSort(t *testing.T) { - entries := getTestEntries() - entries.Sort() - assert.Equal(t, "v1.0", entries[0].Name()) - assert.Equal(t, "v12.0", entries[1].Name()) - assert.Equal(t, "v2.0", entries[2].Name()) - assert.Equal(t, "v2.1", entries[3].Name()) - assert.Equal(t, "v2.12", entries[4].Name()) - assert.Equal(t, "v2.2", entries[5].Name()) - assert.Equal(t, "abc", entries[6].Name()) - assert.Equal(t, "bcd", entries[7].Name()) -} - -func TestEntriesCustomSort(t *testing.T) { - entries := getTestEntries() - entries.CustomSort(func(s1, s2 string) bool { - return s1 > s2 - }) - assert.Equal(t, "v2.2", entries[0].Name()) - assert.Equal(t, "v2.12", entries[1].Name()) - assert.Equal(t, "v2.1", entries[2].Name()) - assert.Equal(t, "v2.0", entries[3].Name()) - assert.Equal(t, "v12.0", entries[4].Name()) - assert.Equal(t, "v1.0", entries[5].Name()) - assert.Equal(t, "bcd", entries[6].Name()) - assert.Equal(t, "abc", entries[7].Name()) -} - func TestFollowLink(t *testing.T) { r, err := openRepositoryWithDefaultContext("tests/repos/repo1_bare") assert.NoError(t, err) @@ -100,3 +55,30 @@ func TestFollowLink(t *testing.T) { _, err = target.FollowLink() assert.EqualError(t, err, "link_short: broken link") } + +func TestGetPathInRepo(t *testing.T) { + r, err := openRepositoryWithDefaultContext("tests/repos/repo1_bare") + assert.NoError(t, err) + defer r.Close() + + commit, err := r.GetCommit("37991dec2c8e592043f47155ce4808d4580f9123") + assert.NoError(t, err) + + // nested entry + entry, err := commit.Tree.GetTreeEntryByPath("foo/bar/link_to_hello") + assert.NoError(t, err) + path := entry.GetPathInRepo() + assert.Equal(t, "foo/bar/link_to_hello", path) + + // folder + entry, err = commit.Tree.GetTreeEntryByPath("foo/bar") + assert.NoError(t, err) + path = entry.GetPathInRepo() + assert.Equal(t, "foo/bar", path) + + // top level file + entry, err = commit.Tree.GetTreeEntryByPath("file2.txt") + assert.NoError(t, err) + path = entry.GetPathInRepo() + assert.Equal(t, "file2.txt", path) +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 8d51864d3d12d..eecb0ab2ac4a0 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3697,6 +3697,7 @@ runs.no_workflows.documentation = For more information on Gitea Actions, see diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index 7adc29ad4148b..56634e5fe4e63 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -46,6 +46,7 @@ const sfc = { done: false, workflowID: '', workflowLink: '', + workflowFileLink: '', isSchedule: false, jobs: [ // { @@ -350,10 +351,12 @@ export function initRepositoryActionView() { artifactsTitle: el.getAttribute('data-locale-artifacts-title'), areYouSure: el.getAttribute('data-locale-are-you-sure'), confirmDeleteArtifact: el.getAttribute('data-locale-confirm-delete-artifact'), + runDetails: el.getAttribute('data-locale-runs-details'), showTimeStamps: el.getAttribute('data-locale-show-timestamps'), showLogSeconds: el.getAttribute('data-locale-show-log-seconds'), showFullScreen: el.getAttribute('data-locale-show-full-screen'), downloadLogs: el.getAttribute('data-locale-download-logs'), + workflowFile: el.getAttribute('data-locale-workflow-file'), status: { unknown: el.getAttribute('data-locale-status-unknown'), waiting: el.getAttribute('data-locale-status-waiting'), @@ -421,17 +424,29 @@ export function initRepositoryActionView() { -