From 188e0ee8e40ad0b32f9db33a0a217043cfdf3610 Mon Sep 17 00:00:00 2001 From: Kemal Zebari <60799661+kemzeb@users.noreply.github.com> Date: Fri, 3 Jan 2025 13:09:47 -0800 Subject: [PATCH 01/37] Use `Project-URL` metadata field to get a PyPI package's homepage URL (#33089) Resolves #33085. --- routers/api/packages/pypi/pypi.go | 47 ++++++++++++-- routers/api/packages/pypi/pypi_test.go | 10 +++ tests/integration/api_packages_pypi_test.go | 69 +++++++++++++++++++-- 3 files changed, 116 insertions(+), 10 deletions(-) diff --git a/routers/api/packages/pypi/pypi.go b/routers/api/packages/pypi/pypi.go index 5ea86071a9..19aa54628d 100644 --- a/routers/api/packages/pypi/pypi.go +++ b/routers/api/packages/pypi/pypi.go @@ -10,6 +10,7 @@ import ( "regexp" "sort" "strings" + "unicode" packages_model "code.gitea.io/gitea/models/packages" packages_module "code.gitea.io/gitea/modules/packages" @@ -139,9 +140,30 @@ func UploadPackageFile(ctx *context.Context) { return } - projectURL := ctx.Req.FormValue("home_page") - if !validation.IsValidURL(projectURL) { - projectURL = "" + // Ensure ctx.Req.Form exists. + _ = ctx.Req.ParseForm() + + var homepageURL string + projectURLs := ctx.Req.Form["project_urls"] + for _, purl := range projectURLs { + label, url, found := strings.Cut(purl, ",") + if !found { + continue + } + if normalizeLabel(label) != "homepage" { + continue + } + homepageURL = strings.TrimSpace(url) + break + } + + if len(homepageURL) == 0 { + // TODO: Home-page is a deprecated metadata field. Remove this branch once it's no longer apart of the spec. + homepageURL = ctx.Req.FormValue("home_page") + } + + if !validation.IsValidURL(homepageURL) { + homepageURL = "" } _, _, err = packages_service.CreatePackageOrAddFileToExisting( @@ -160,7 +182,7 @@ func UploadPackageFile(ctx *context.Context) { Description: ctx.Req.FormValue("description"), LongDescription: ctx.Req.FormValue("long_description"), Summary: ctx.Req.FormValue("summary"), - ProjectURL: projectURL, + ProjectURL: homepageURL, License: ctx.Req.FormValue("license"), RequiresPython: ctx.Req.FormValue("requires_python"), }, @@ -189,6 +211,23 @@ func UploadPackageFile(ctx *context.Context) { ctx.Status(http.StatusCreated) } +// Normalizes a Project-URL label. +// See https://packaging.python.org/en/latest/specifications/well-known-project-urls/#label-normalization. +func normalizeLabel(label string) string { + var builder strings.Builder + + // "A label is normalized by deleting all ASCII punctuation and whitespace, and then converting the result + // to lowercase." + for _, r := range label { + if unicode.IsPunct(r) || unicode.IsSpace(r) { + continue + } + builder.WriteRune(unicode.ToLower(r)) + } + + return builder.String() +} + func isValidNameAndVersion(packageName, packageVersion string) bool { return nameMatcher.MatchString(packageName) && versionMatcher.MatchString(packageVersion) } diff --git a/routers/api/packages/pypi/pypi_test.go b/routers/api/packages/pypi/pypi_test.go index 3023692177..786105693f 100644 --- a/routers/api/packages/pypi/pypi_test.go +++ b/routers/api/packages/pypi/pypi_test.go @@ -36,3 +36,13 @@ func TestIsValidNameAndVersion(t *testing.T) { assert.False(t, isValidNameAndVersion("test-name", "1.0.1aa")) assert.False(t, isValidNameAndVersion("test-name", "1.0.0-alpha.beta")) } + +func TestNormalizeLabel(t *testing.T) { + // Cases fetched from https://packaging.python.org/en/latest/specifications/well-known-project-urls/#label-normalization. + assert.Equal(t, "homepage", normalizeLabel("Homepage")) + assert.Equal(t, "homepage", normalizeLabel("Home-page")) + assert.Equal(t, "homepage", normalizeLabel("Home page")) + assert.Equal(t, "changelog", normalizeLabel("Change_Log")) + assert.Equal(t, "whatsnew", normalizeLabel("What's New?")) + assert.Equal(t, "github", normalizeLabel("github")) +} diff --git a/tests/integration/api_packages_pypi_test.go b/tests/integration/api_packages_pypi_test.go index e973f6a52a..2dabb5005b 100644 --- a/tests/integration/api_packages_pypi_test.go +++ b/tests/integration/api_packages_pypi_test.go @@ -32,15 +32,16 @@ func TestPackagePyPI(t *testing.T) { packageVersion := "1!1.0.1+r1234" packageAuthor := "KN4CK3R" packageDescription := "Test Description" + projectURL := "https://example.com" content := "test" hashSHA256 := "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" root := fmt.Sprintf("/api/packages/%s/pypi", user.Name) - uploadFile := func(t *testing.T, filename, content string, expectedStatus int) { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) + createBasicMultipartFile := func(filename, packageName, content string) (body *bytes.Buffer, writer *multipart.Writer, closer func() error) { + body = &bytes.Buffer{} + writer = multipart.NewWriter(body) part, _ := writer.CreateFormFile("content", filename) _, _ = io.Copy(part, strings.NewReader(content)) @@ -52,14 +53,27 @@ func TestPackagePyPI(t *testing.T) { writer.WriteField("sha256_digest", hashSHA256) writer.WriteField("requires_python", "3.6") - _ = writer.Close() + return body, writer, writer.Close + } + uploadHelper := func(t *testing.T, body *bytes.Buffer, contentType string, expectedStatus int) { req := NewRequestWithBody(t, "POST", root, body). - SetHeader("Content-Type", writer.FormDataContentType()). + SetHeader("Content-Type", contentType). AddBasicAuth(user.Name) MakeRequest(t, req, expectedStatus) } + uploadFile := func(t *testing.T, filename, content string, expectedStatus int) { + body, writer, closeFunc := createBasicMultipartFile(filename, packageName, content) + + writer.WriteField("project_urls", "DOCUMENTATION , https://readthedocs.org") + writer.WriteField("project_urls", fmt.Sprintf("Home-page, %s", projectURL)) + + _ = closeFunc() + + uploadHelper(t, body, writer.FormDataContentType(), expectedStatus) + } + t.Run("Upload", func(t *testing.T) { defer tests.PrintCurrentTest(t)() @@ -74,6 +88,7 @@ func TestPackagePyPI(t *testing.T) { assert.NoError(t, err) assert.Nil(t, pd.SemVer) assert.IsType(t, &pypi.Metadata{}, pd.Metadata) + assert.Equal(t, projectURL, pd.Metadata.(*pypi.Metadata).ProjectURL) assert.Equal(t, packageName, pd.Package.Name) assert.Equal(t, packageVersion, pd.Version.Version) @@ -133,6 +148,48 @@ func TestPackagePyPI(t *testing.T) { uploadFile(t, "test.tar.gz", content, http.StatusConflict) }) + t.Run("UploadUsingDeprecatedHomepageMetadata", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + pkgName := "homepage-package" + body, writer, closeFunc := createBasicMultipartFile("test.whl", pkgName, content) + + writer.WriteField("home_page", projectURL) + + _ = closeFunc() + + uploadHelper(t, body, writer.FormDataContentType(), http.StatusCreated) + + pvs, err := packages.GetVersionsByPackageName(db.DefaultContext, user.ID, packages.TypePyPI, pkgName) + assert.NoError(t, err) + assert.Len(t, pvs, 1) + + pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) + assert.NoError(t, err) + assert.IsType(t, &pypi.Metadata{}, pd.Metadata) + assert.Equal(t, projectURL, pd.Metadata.(*pypi.Metadata).ProjectURL) + }) + + t.Run("UploadWithoutAnyHomepageURLMetadata", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + pkgName := "no-project-url-or-homepage-package" + body, writer, closeFunc := createBasicMultipartFile("test.whl", pkgName, content) + + _ = closeFunc() + + uploadHelper(t, body, writer.FormDataContentType(), http.StatusCreated) + + pvs, err := packages.GetVersionsByPackageName(db.DefaultContext, user.ID, packages.TypePyPI, pkgName) + assert.NoError(t, err) + assert.Len(t, pvs, 1) + + pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) + assert.NoError(t, err) + assert.IsType(t, &pypi.Metadata{}, pd.Metadata) + assert.Empty(t, pd.Metadata.(*pypi.Metadata).ProjectURL) + }) + t.Run("Download", func(t *testing.T) { defer tests.PrintCurrentTest(t)() @@ -147,7 +204,7 @@ func TestPackagePyPI(t *testing.T) { downloadFile("test.whl") downloadFile("test.tar.gz") - pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypePyPI) + pvs, err := packages.GetVersionsByPackageName(db.DefaultContext, user.ID, packages.TypePyPI, packageName) assert.NoError(t, err) assert.Len(t, pvs, 1) assert.Equal(t, int64(2), pvs[0].DownloadCount) From 2b064b8637dee3904e882baada99221204f4f874 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 4 Jan 2025 10:56:07 +0800 Subject: [PATCH 02/37] Refactor legacy line-number and scroll code (#33094) 1. remove jquery 2. rewrite the "line number selection", fix various edge cases 3. fix the scroll --- web_src/css/base.css | 9 +- web_src/js/features/repo-code.test.ts | 17 --- web_src/js/features/repo-code.ts | 158 ++++++++++---------------- web_src/js/features/repo-diff.ts | 5 + web_src/js/features/repo-issue.ts | 21 +--- 5 files changed, 71 insertions(+), 139 deletions(-) delete mode 100644 web_src/js/features/repo-code.test.ts diff --git a/web_src/css/base.css b/web_src/css/base.css index 49d5743158..a1ee7044ec 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -336,8 +336,13 @@ a.label, border-color: var(--color-secondary); } +.ui.dropdown .menu > .header { + text-transform: none; /* reset fomantic's "uppercase" */ +} + .ui.dropdown .menu > .header:not(.ui) { color: var(--color-text); + font-size: 0.95em; /* reset fomantic's small font-size */ } .ui.dropdown .menu > .item { @@ -691,10 +696,6 @@ input:-webkit-autofill:active, box-shadow: 0 6px 18px var(--color-shadow) !important; } -.ui.dropdown .menu > .header { - font-size: 0.8em; -} - .ui .text.left { text-align: left !important; } diff --git a/web_src/js/features/repo-code.test.ts b/web_src/js/features/repo-code.test.ts deleted file mode 100644 index 27554aa847..0000000000 --- a/web_src/js/features/repo-code.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {singleAnchorRegex, rangeAnchorRegex} from './repo-code.ts'; - -test('singleAnchorRegex', () => { - expect(singleAnchorRegex.test('#L0')).toEqual(false); - expect(singleAnchorRegex.test('#L1')).toEqual(true); - expect(singleAnchorRegex.test('#L01')).toEqual(false); - expect(singleAnchorRegex.test('#n0')).toEqual(false); - expect(singleAnchorRegex.test('#n1')).toEqual(true); - expect(singleAnchorRegex.test('#n01')).toEqual(false); -}); - -test('rangeAnchorRegex', () => { - expect(rangeAnchorRegex.test('#L0-L10')).toEqual(false); - expect(rangeAnchorRegex.test('#L1-L10')).toEqual(true); - expect(rangeAnchorRegex.test('#L01-L10')).toEqual(false); - expect(rangeAnchorRegex.test('#L1-L01')).toEqual(false); -}); diff --git a/web_src/js/features/repo-code.ts b/web_src/js/features/repo-code.ts index a8d6e8f97d..207022ca42 100644 --- a/web_src/js/features/repo-code.ts +++ b/web_src/js/features/repo-code.ts @@ -1,12 +1,8 @@ -import $ from 'jquery'; import {svg} from '../svg.ts'; -import {invertFileFolding} from './file-fold.ts'; import {createTippy} from '../modules/tippy.ts'; import {clippie} from 'clippie'; import {toAbsoluteUrl} from '../utils.ts'; - -export const singleAnchorRegex = /^#(L|n)([1-9][0-9]*)$/; -export const rangeAnchorRegex = /^#(L[1-9][0-9]*)-(L[1-9][0-9]*)$/; +import {addDelegatedEventListener} from '../utils/dom.ts'; function changeHash(hash: string) { if (window.history.pushState) { @@ -16,20 +12,11 @@ function changeHash(hash: string) { } } -function isBlame() { - return Boolean(document.querySelector('div.blame')); -} +// it selects the code lines defined by range: `L1-L3` (3 lines) or `L2` (singe line) +function selectRange(range: string): Element { + for (const el of document.querySelectorAll('.code-view tr.active')) el.classList.remove('active'); + const elLineNums = document.querySelectorAll(`.code-view td.lines-num span[data-line-number]`); -function getLineEls() { - return document.querySelectorAll(`.code-view td.lines-code${isBlame() ? '.blame-code' : ''}`); -} - -function selectRange($linesEls, $selectionEndEl, $selectionStartEls?) { - for (const el of $linesEls) { - el.closest('tr').classList.remove('active'); - } - - // add hashchange to permalink const refInNewIssue = document.querySelector('a.ref-in-new-issue'); const copyPermalink = document.querySelector('a.copy-line-permalink'); const viewGitBlame = document.querySelector('a.view_git_blame'); @@ -59,37 +46,30 @@ function selectRange($linesEls, $selectionEndEl, $selectionStartEls?) { copyPermalink.setAttribute('data-url', link); }; - if ($selectionStartEls) { - let a = parseInt($selectionEndEl[0].getAttribute('rel').slice(1)); - let b = parseInt($selectionStartEls[0].getAttribute('rel').slice(1)); - let c; - if (a !== b) { - if (a > b) { - c = a; - a = b; - b = c; - } - const classes = []; - for (let i = a; i <= b; i++) { - classes.push(`[rel=L${i}]`); - } - $linesEls.filter(classes.join(',')).each(function () { - this.closest('tr').classList.add('active'); - }); - changeHash(`#L${a}-L${b}`); + const rangeFields = range ? range.split('-') : []; + const start = rangeFields[0] ?? ''; + if (!start) return null; + const stop = rangeFields[1] || start; - updateIssueHref(`L${a}-L${b}`); - updateViewGitBlameFragment(`L${a}-L${b}`); - updateCopyPermalinkUrl(`L${a}-L${b}`); - return; - } + // format is i.e. 'L14-L26' + let startLineNum = parseInt(start.substring(1)); + let stopLineNum = parseInt(stop.substring(1)); + if (startLineNum > stopLineNum) { + const tmp = startLineNum; + startLineNum = stopLineNum; + stopLineNum = tmp; + range = `${stop}-${start}`; } - $selectionEndEl[0].closest('tr').classList.add('active'); - changeHash(`#${$selectionEndEl[0].getAttribute('rel')}`); - updateIssueHref($selectionEndEl[0].getAttribute('rel')); - updateViewGitBlameFragment($selectionEndEl[0].getAttribute('rel')); - updateCopyPermalinkUrl($selectionEndEl[0].getAttribute('rel')); + const first = elLineNums[startLineNum - 1] ?? null; + for (let i = startLineNum - 1; i <= stopLineNum - 1 && i < elLineNums.length; i++) { + elLineNums[i].closest('tr').classList.add('active'); + } + changeHash(`#${range}`); + updateIssueHref(range); + updateViewGitBlameFragment(range); + updateCopyPermalinkUrl(range); + return first; } function showLineButton() { @@ -103,6 +83,8 @@ function showLineButton() { // find active row and add button const tr = document.querySelector('.code-view tr.active'); + if (!tr) return; + const td = tr.querySelector('td.lines-num'); const btn = document.createElement('button'); btn.classList.add('code-line-button', 'ui', 'basic', 'button'); @@ -128,62 +110,36 @@ function showLineButton() { } export function initRepoCodeView() { - if ($('.code-view .lines-num').length > 0) { - $(document).on('click', '.lines-num span', function (e) { - const linesEls = getLineEls(); - const selectedEls = Array.from(linesEls).filter((el) => { - return el.matches(`[rel=${this.getAttribute('id')}]`); - }); + if (!document.querySelector('.code-view .lines-num')) return; - let from; - if (e.shiftKey) { - from = Array.from(linesEls).filter((el) => { - return el.closest('tr').classList.contains('active'); - }); - } - selectRange($(linesEls), $(selectedEls), from ? $(from) : null); - window.getSelection().removeAllRanges(); - showLineButton(); - }); - - $(window).on('hashchange', () => { - let m = rangeAnchorRegex.exec(window.location.hash); - const $linesEls = $(getLineEls()); - let $first; - if (m) { - $first = $linesEls.filter(`[rel=${m[1]}]`); - if ($first.length) { - selectRange($linesEls, $first, $linesEls.filter(`[rel=${m[2]}]`)); - - // show code view menu marker (don't show in blame page) - if (!isBlame()) { - showLineButton(); - } - - $('html, body').scrollTop($first.offset().top - 200); - return; - } - } - m = singleAnchorRegex.exec(window.location.hash); - if (m) { - $first = $linesEls.filter(`[rel=L${m[2]}]`); - if ($first.length) { - selectRange($linesEls, $first); - - // show code view menu marker (don't show in blame page) - if (!isBlame()) { - showLineButton(); - } - - $('html, body').scrollTop($first.offset().top - 200); - } - } - }).trigger('hashchange'); - } - $(document).on('click', '.fold-file', ({currentTarget}) => { - invertFileFolding(currentTarget.closest('.file-content'), currentTarget); + let selRangeStart: string; + addDelegatedEventListener(document, 'click', '.lines-num span', (el: HTMLElement, e: KeyboardEvent) => { + if (!selRangeStart || !e.shiftKey) { + selRangeStart = el.getAttribute('id'); + selectRange(selRangeStart); + } else { + const selRangeStop = el.getAttribute('id'); + selectRange(`${selRangeStart}-${selRangeStop}`); + } + window.getSelection().removeAllRanges(); + showLineButton(); }); - $(document).on('click', '.copy-line-permalink', async ({currentTarget}) => { - await clippie(toAbsoluteUrl(currentTarget.getAttribute('data-url'))); + + const onHashChange = () => { + if (!window.location.hash) return; + const range = window.location.hash.substring(1); + const first = selectRange(range); + if (first) { + // set scrollRestoration to 'manual' when there is a hash in url, so that the scroll position will not be remembered after refreshing + if (window.history.scrollRestoration !== 'manual') window.history.scrollRestoration = 'manual'; + first.scrollIntoView({block: 'start'}); + showLineButton(); + } + }; + onHashChange(); + window.addEventListener('hashchange', onHashChange); + + addDelegatedEventListener(document, 'click', '.copy-line-permalink', (el) => { + clippie(toAbsoluteUrl(el.getAttribute('data-url'))); }); } diff --git a/web_src/js/features/repo-diff.ts b/web_src/js/features/repo-diff.ts index 2b405abb9b..0cb2e566c0 100644 --- a/web_src/js/features/repo-diff.ts +++ b/web_src/js/features/repo-diff.ts @@ -19,6 +19,7 @@ import { import {POST, GET} from '../modules/fetch.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; import {createTippy} from '../modules/tippy.ts'; +import {invertFileFolding} from './file-fold.ts'; const {pageData, i18n} = window.config; @@ -244,4 +245,8 @@ export function initRepoDiffView() { initRepoDiffFileViewToggle(); initViewedCheckboxListenerFor(); initExpandAndCollapseFilesButton(); + + addDelegatedEventListener(document, 'click', '.fold-file', (el) => { + invertFileFolding(el.closest('.file-content'), el); + }); } diff --git a/web_src/js/features/repo-issue.ts b/web_src/js/features/repo-issue.ts index a9dda39a7f..d74d3f7700 100644 --- a/web_src/js/features/repo-issue.ts +++ b/web_src/js/features/repo-issue.ts @@ -373,10 +373,6 @@ export async function handleReply(el) { export function initRepoPullRequestReview() { if (window.location.hash && window.location.hash.startsWith('#issuecomment-')) { - // set scrollRestoration to 'manual' when there is a hash in url, so that the scroll position will not be remembered after refreshing - if (window.history.scrollRestoration !== 'manual') { - window.history.scrollRestoration = 'manual'; - } const commentDiv = document.querySelector(window.location.hash); if (commentDiv) { // get the name of the parent id @@ -384,14 +380,6 @@ export function initRepoPullRequestReview() { if (groupID && groupID.startsWith('code-comments-')) { const id = groupID.slice(14); const ancestorDiffBox = commentDiv.closest('.diff-file-box'); - // on pages like conversation, there is no diff header - const diffHeader = ancestorDiffBox?.querySelector('.diff-file-header'); - - // offset is for scrolling - let offset = 30; - if (diffHeader) { - offset += $('.diff-detail-box').outerHeight() + $(diffHeader).outerHeight(); - } hideElem(`#show-outdated-${id}`); showElem(`#code-comments-${id}, #code-preview-${id}, #hide-outdated-${id}`); @@ -399,12 +387,11 @@ export function initRepoPullRequestReview() { if (ancestorDiffBox?.getAttribute('data-folded') === 'true') { setFileFolding(ancestorDiffBox, ancestorDiffBox.querySelector('.fold-file'), false); } - - window.scrollTo({ - top: $(commentDiv).offset().top - offset, - behavior: 'instant', - }); } + // set scrollRestoration to 'manual' when there is a hash in url, so that the scroll position will not be remembered after refreshing + if (window.history.scrollRestoration !== 'manual') window.history.scrollRestoration = 'manual'; + // wait for a while because some elements (eg: image, editor, etc.) may change the viewport's height. + setTimeout(() => commentDiv.scrollIntoView({block: 'start'}), 100); } } From 3d544a3ad35f221d48591757ae3beaab0820e4ff Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 4 Jan 2025 18:47:24 +0800 Subject: [PATCH 03/37] Fix empty git repo handling logic (#33101) Fix #33092 --- models/repo/repo.go | 2 ++ options/locale/locale_en-US.ini | 1 + routers/web/repo/view_home.go | 46 +++++++++++++++++++++------------ services/context/repo.go | 3 --- templates/repo/empty.tmpl | 21 +++++++-------- templates/repo/header.tmpl | 2 +- 6 files changed, 43 insertions(+), 32 deletions(-) diff --git a/models/repo/repo.go b/models/repo/repo.go index 5ef4d470c3..af4a1f7fb5 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -279,6 +279,8 @@ func (repo *Repository) IsBroken() bool { } // MarkAsBrokenEmpty marks the repo as broken and empty +// FIXME: the status "broken" and "is_empty" were abused, +// The code always set them together, no way to distinguish whether a repo is really "empty" or "broken" func (repo *Repository) MarkAsBrokenEmpty() { repo.Status = RepositoryBroken repo.IsEmpty = true diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 6029d49ade..07c9ffa9fc 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1235,6 +1235,7 @@ create_new_repo_command = Creating a new repository on the command line push_exist_repo = Pushing an existing repository from the command line empty_message = This repository does not contain any content. broken_message = The Git data underlying this repository cannot be read. Contact the administrator of this instance or delete this repository. +no_branch = This repository doesn’t have any branches. code = Code code.desc = Access source code, files, commits and branches. diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go index 70ba07f9a8..3fcd7bba8e 100644 --- a/routers/web/repo/view_home.go +++ b/routers/web/repo/view_home.go @@ -223,16 +223,37 @@ func prepareRecentlyPushedNewBranches(ctx *context.Context) { } } +func updateContextRepoEmptyAndStatus(ctx *context.Context, empty bool, status repo_model.RepositoryStatus) { + ctx.Repo.Repository.IsEmpty = empty + if ctx.Repo.Repository.Status == repo_model.RepositoryReady || ctx.Repo.Repository.Status == repo_model.RepositoryBroken { + ctx.Repo.Repository.Status = status // only handle ready and broken status, leave other status as-is + } + if err := repo_model.UpdateRepositoryCols(ctx, ctx.Repo.Repository, "is_empty", "status"); err != nil { + ctx.ServerError("updateContextRepoEmptyAndStatus: UpdateRepositoryCols", err) + return + } +} + func handleRepoEmptyOrBroken(ctx *context.Context) { showEmpty := true - var err error if ctx.Repo.GitRepo != nil { - showEmpty, err = ctx.Repo.GitRepo.IsEmpty() + reallyEmpty, err := ctx.Repo.GitRepo.IsEmpty() if err != nil { + showEmpty = true // the repo is broken + updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryBroken) log.Error("GitRepo.IsEmpty: %v", err) - ctx.Repo.Repository.Status = repo_model.RepositoryBroken - showEmpty = true ctx.Flash.Error(ctx.Tr("error.occurred"), true) + } else if reallyEmpty { + showEmpty = true // the repo is really empty + updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryReady) + } else if ctx.Repo.Commit == nil { + showEmpty = true // it is not really empty, but there is no branch + // at the moment, other repo units like "actions" are not able to handle such case, + // so we just mark the repo as empty to prevent from displaying these units. + updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryReady) + } else { + // the repo is actually not empty and has branches, need to update the database later + showEmpty = false } } if showEmpty { @@ -240,18 +261,11 @@ func handleRepoEmptyOrBroken(ctx *context.Context) { return } - // the repo is not really empty, so we should update the modal in database - // such problem may be caused by: - // 1) an error occurs during pushing/receiving. 2) the user replaces an empty git repo manually - // and even more: the IsEmpty flag is deeply broken and should be removed with the UI changed to manage to cope with empty repos. - // it's possible for a repository to be non-empty by that flag but still 500 - // because there are no branches - only tags -or the default branch is non-extant as it has been 0-pushed. - ctx.Repo.Repository.IsEmpty = false - if err = repo_model.UpdateRepositoryCols(ctx, ctx.Repo.Repository, "is_empty"); err != nil { - ctx.ServerError("UpdateRepositoryCols", err) - return - } - if err = repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil { + // The repo is not really empty, so we should update the model in database, such problem may be caused by: + // 1) an error occurs during pushing/receiving. + // 2) the user replaces an empty git repo manually. + updateContextRepoEmptyAndStatus(ctx, false, repo_model.RepositoryReady) + if err := repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil { ctx.ServerError("UpdateRepoSize", err) return } diff --git a/services/context/repo.go b/services/context/repo.go index 2a473f4a54..63529e1d81 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -897,10 +897,8 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func refName = brs[0].Name } else if len(brs) == 0 { log.Error("No branches in non-empty repository %s", ctx.Repo.GitRepo.Path) - ctx.Repo.Repository.MarkAsBrokenEmpty() } else { log.Error("GetBranches error: %v", err) - ctx.Repo.Repository.MarkAsBrokenEmpty() } } ctx.Repo.RefName = refName @@ -911,7 +909,6 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func } else if strings.Contains(err.Error(), "fatal: not a git repository") || strings.Contains(err.Error(), "object does not exist") { // if the repository is broken, we can continue to the handler code, to show "Settings -> Delete Repository" for end users log.Error("GetBranchCommit: %v", err) - ctx.Repo.Repository.MarkAsBrokenEmpty() } else { ctx.ServerError("GetBranchCommit", err) return diff --git a/templates/repo/empty.tmpl b/templates/repo/empty.tmpl index 7170fe3602..dfda5b7b2b 100644 --- a/templates/repo/empty.tmpl +++ b/templates/repo/empty.tmpl @@ -14,14 +14,13 @@ {{end}} {{end}} + {{if .Repository.IsBroken}} -
- {{ctx.Locale.Tr "repo.broken_message"}} -
+
{{ctx.Locale.Tr "repo.broken_message"}}
+ {{else if .Repository.IsEmpty}} +
{{ctx.Locale.Tr "repo.no_branch"}}
{{else if .CanWriteCode}} -

- {{ctx.Locale.Tr "repo.quick_guide"}} -

+

{{ctx.Locale.Tr "repo.quick_guide"}}

{{ctx.Locale.Tr "repo.clone_this_repo"}} {{ctx.Locale.Tr "repo.clone_helper" "http://git-scm.com/book/en/v2/Git-Basics-Getting-a-Git-Repository"}}

@@ -66,12 +65,10 @@ git push -u origin {{.Repository.DefaultBranch}}
{{end}} - {{else}} -
- {{ctx.Locale.Tr "repo.empty_message"}} -
- {{end}} - + + {{else}} +
{{ctx.Locale.Tr "repo.empty_message"}}
+ {{end}} diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index c3ae697f31..e187ef1a87 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -162,7 +162,7 @@ {{end}} - {{if and .EnableActions (.Permission.CanRead ctx.Consts.RepoUnitTypeActions)}} + {{if and .EnableActions (.Permission.CanRead ctx.Consts.RepoUnitTypeActions) (not .IsEmptyRepo)}} {{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}} {{if .Repository.NumOpenActionRuns}} From df9d1fe8c5a628f57b74c04fed91e7dafa5eb1a5 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 5 Jan 2025 06:25:50 +0800 Subject: [PATCH 04/37] Trivial fixes (#33103) 1. remove `gock` dependency, it is not needed 2. fix a regression from org private profile readme --- go.mod | 2 -- go.sum | 6 ----- modules/auth/password/pwn/pwn_test.go | 33 ++++++++++++++++++--------- templates/user/profile.tmpl | 2 +- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/go.mod b/go.mod index dc80d2ca2b..084b294609 100644 --- a/go.mod +++ b/go.mod @@ -71,7 +71,6 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/feeds v1.2.0 github.com/gorilla/sessions v1.4.0 - github.com/h2non/gock v1.2.0 github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/huandu/xstrings v1.5.0 @@ -230,7 +229,6 @@ require ( github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect diff --git a/go.sum b/go.sum index b5e64321b5..40add64289 100644 --- a/go.sum +++ b/go.sum @@ -452,10 +452,6 @@ github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= -github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= -github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -599,8 +595,6 @@ github.com/msteinert/pam v1.2.0 h1:mYfjlvN2KYs2Pb9G6nb/1f/nPfAttT/Jee5Sq9r3bGE= github.com/msteinert/pam v1.2.0/go.mod h1:d2n0DCUK8rGecChV3JzvmsDjOY4R7AYbsNxAT+ftQl0= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= -github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek= github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o= github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= diff --git a/modules/auth/password/pwn/pwn_test.go b/modules/auth/password/pwn/pwn_test.go index b3e7734c3f..ae03fabc57 100644 --- a/modules/auth/password/pwn/pwn_test.go +++ b/modules/auth/password/pwn/pwn_test.go @@ -4,46 +4,57 @@ package pwn import ( + "errors" + "io" "net/http" + "strings" "testing" - "time" - "github.com/h2non/gock" "github.com/stretchr/testify/assert" ) -var client = New(WithHTTP(&http.Client{ - Timeout: time.Second * 2, -})) +type mockTransport struct{} + +func (mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if req.URL.Host != "api.pwnedpasswords.com" { + return nil, errors.New("unsupported host") + } + respMap := map[string]string{ + "/range/5c1d8": "EAF2F254732680E8AC339B84F3266ECCBB5:1\r\nFC446EB88938834178CB9322C1EE273C2A7:2", + "/range/ba189": "FD4CB34F0378BCB15D23F6FFD28F0775C9E:3\r\nFDF342FCD8C3611DAE4D76E8A992A3E4169:4", + "/range/a1733": "C4CE0F1F0062B27B9E2F41AF0C08218017C:1\r\nFC446EB88938834178CB9322C1EE273C2A7:2\r\nFE81480327C992FE62065A827429DD1318B:0", + "/range/5617b": "FD4CB34F0378BCB15D23F6FFD28F0775C9E:3\r\nFDF342FCD8C3611DAE4D76E8A992A3E4169:4\r\nFE81480327C992FE62065A827429DD1318B:0", + "/range/79082": "FDF342FCD8C3611DAE4D76E8A992A3E4169:4\r\nFE81480327C992FE62065A827429DD1318B:0\r\nAFEF386F56EB0B4BE314E07696E5E6E6536:0", + } + if resp, ok := respMap[req.URL.Path]; ok { + return &http.Response{Request: req, Body: io.NopCloser(strings.NewReader(resp))}, nil + } + return nil, errors.New("unsupported path") +} func TestPassword(t *testing.T) { - defer gock.Off() + client := New(WithHTTP(&http.Client{Transport: mockTransport{}})) count, err := client.CheckPassword("", false) assert.ErrorIs(t, err, ErrEmptyPassword, "blank input should return ErrEmptyPassword") assert.Equal(t, -1, count) - gock.New("https://api.pwnedpasswords.com").Get("/range/5c1d8").Times(1).Reply(200).BodyString("EAF2F254732680E8AC339B84F3266ECCBB5:1\r\nFC446EB88938834178CB9322C1EE273C2A7:2") count, err = client.CheckPassword("pwned", false) assert.NoError(t, err) assert.Equal(t, 1, count) - gock.New("https://api.pwnedpasswords.com").Get("/range/ba189").Times(1).Reply(200).BodyString("FD4CB34F0378BCB15D23F6FFD28F0775C9E:3\r\nFDF342FCD8C3611DAE4D76E8A992A3E4169:4") count, err = client.CheckPassword("notpwned", false) assert.NoError(t, err) assert.Equal(t, 0, count) - gock.New("https://api.pwnedpasswords.com").Get("/range/a1733").Times(1).Reply(200).BodyString("C4CE0F1F0062B27B9E2F41AF0C08218017C:1\r\nFC446EB88938834178CB9322C1EE273C2A7:2\r\nFE81480327C992FE62065A827429DD1318B:0") count, err = client.CheckPassword("paddedpwned", true) assert.NoError(t, err) assert.Equal(t, 1, count) - gock.New("https://api.pwnedpasswords.com").Get("/range/5617b").Times(1).Reply(200).BodyString("FD4CB34F0378BCB15D23F6FFD28F0775C9E:3\r\nFDF342FCD8C3611DAE4D76E8A992A3E4169:4\r\nFE81480327C992FE62065A827429DD1318B:0") count, err = client.CheckPassword("paddednotpwned", true) assert.NoError(t, err) assert.Equal(t, 0, count) - gock.New("https://api.pwnedpasswords.com").Get("/range/79082").Times(1).Reply(200).BodyString("FDF342FCD8C3611DAE4D76E8A992A3E4169:4\r\nFE81480327C992FE62065A827429DD1318B:0\r\nAFEF386F56EB0B4BE314E07696E5E6E6536:0") count, err = client.CheckPassword("paddednotpwnedzero", true) assert.NoError(t, err) assert.Equal(t, 0, count) diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl index 2c83ce97cd..345872b00d 100644 --- a/templates/user/profile.tmpl +++ b/templates/user/profile.tmpl @@ -26,7 +26,7 @@ {{else if eq .TabName "followers"}} {{template "repo/user_cards" .}} {{else if eq .TabName "overview"}} -
{{.ProfileReadme}}
+
{{.ProfileReadmeContent}}
{{else if eq .TabName "organizations"}} {{template "repo/user_cards" .}} {{else}} From 3078826d0111e818dc765d94c650f4cb5b3ecce7 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Sun, 5 Jan 2025 00:35:35 +0000 Subject: [PATCH 05/37] [skip ci] Updated translations via Crowdin --- options/locale/locale_pt-PT.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 0fb56f6763..cbb7f09c82 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -495,7 +495,7 @@ register_notify.text_3=Se esta conta foi criada para si,
defina a s reset_password=Recupere a sua conta reset_password.title=%s, você pediu para recuperar a sua conta -reset_password.text=Por favor clique na seguinte ligação para recuperar a sua conta em %s: +reset_password.text=Para recuperar a sua conta, clique na ligação seguinte (válida por %s): register_success=Inscrição bem sucedida @@ -1017,6 +1017,8 @@ owner=Proprietário(a) owner_helper=Algumas organizações podem não aparecer na lista suspensa devido a um limite máximo de contagem de repositórios. repo_name=Nome do repositório repo_name_profile_public_hint=.profile é um repositório especial que pode usar para adicionar README.md ao seu perfil público da organização, visível para qualquer pessoa. Certifique-se que é público e inicialize-o com um README na pasta do perfil para começar. +repo_name_profile_private_hint=.profile-private é um repositório especial que pode usar para adicionar um README.md ao seu perfil de membro da organização, visível apenas para membros da organização. Certifique-se que é privado e inicialize-o com um README na pasta de perfil para começar. +repo_name_helper=Bons nomes de repositórios usam palavras-chave curtas, memorizáveis e únicas. Um repositório chamado ".profile" ou ".profile-private" pode ser usado para adicionar um README.md ao perfil do utilizador ou da organização. repo_size=Tamanho do repositório template=Modelo template_select=Escolha um modelo. From 42377360296b7c810b284472ba6743bf684186fb Mon Sep 17 00:00:00 2001 From: ChristopherHX Date: Sun, 5 Jan 2025 14:47:18 +0100 Subject: [PATCH 06/37] workflow_dispatch use workflow from trigger branch (#33098) * htmx updates the input form on branch switch * add workflow warning to dispatch modal * use name if description of input is empty * show error if workflow_dispatch not available on branch Closes #33073 Closes #33099 --------- Co-authored-by: wxiaoguang --- options/locale/locale_en-US.ini | 1 + routers/web/repo/actions/actions.go | 226 +++++++++++------- routers/web/repo/actions/view.go | 9 +- routers/web/web.go | 3 +- templates/repo/actions/workflow_dispatch.tmpl | 27 +-- .../actions/workflow_dispatch_inputs.tmpl | 45 ++++ 6 files changed, 189 insertions(+), 122 deletions(-) create mode 100644 templates/repo/actions/workflow_dispatch_inputs.tmpl diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 07c9ffa9fc..96404a6143 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3765,6 +3765,7 @@ workflow.not_found = Workflow '%s' not found. workflow.run_success = Workflow '%s' run successfully. workflow.from_ref = Use workflow from workflow.has_workflow_dispatch = This workflow has a workflow_dispatch event trigger. +workflow.has_no_workflow_dispatch = Workflow '%s' has no workflow_dispatch event trigger. need_approval_desc = Need approval to run workflows for fork pull request. diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index f0d8d81fee..539c4b6ed0 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -32,8 +32,9 @@ import ( ) const ( - tplListActions templates.TplName = "repo/actions/list" - tplViewActions templates.TplName = "repo/actions/view" + tplListActions templates.TplName = "repo/actions/list" + tplDispatchInputsActions templates.TplName = "repo/actions/workflow_dispatch_inputs" + tplViewActions templates.TplName = "repo/actions/view" ) type Workflow struct { @@ -64,107 +65,143 @@ func MustEnableActions(ctx *context.Context) { func List(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("actions.actions") ctx.Data["PageIsActions"] = true - workflowID := ctx.FormString("workflow") - actorID := ctx.FormInt64("actor") - status := ctx.FormInt("status") - ctx.Data["CurWorkflow"] = workflowID - var workflows []Workflow - var curWorkflow *model.Workflow - if empty, err := ctx.Repo.GitRepo.IsEmpty(); err != nil { - ctx.ServerError("IsEmpty", err) + commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) + if err != nil { + ctx.ServerError("GetBranchCommit", err) return - } else if !empty { - commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) - if err != nil { - ctx.ServerError("GetBranchCommit", err) - return - } - entries, err := actions.ListWorkflows(commit) - if err != nil { - ctx.ServerError("ListWorkflows", err) - return - } + } - // Get all runner labels - runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{ - RepoID: ctx.Repo.Repository.ID, - IsOnline: optional.Some(true), - WithAvailable: true, - }) - if err != nil { - ctx.ServerError("FindRunners", err) - return - } - allRunnerLabels := make(container.Set[string]) - for _, r := range runners { - allRunnerLabels.AddMultiple(r.AgentLabels...) - } + workflows := prepareWorkflowDispatchTemplate(ctx, commit) + if ctx.Written() { + return + } - workflows = make([]Workflow, 0, len(entries)) - for _, entry := range entries { - workflow := Workflow{Entry: *entry} - content, err := actions.GetContentFromEntry(entry) - if err != nil { - ctx.ServerError("GetContentFromEntry", err) - return - } - wf, err := model.ReadWorkflow(bytes.NewReader(content)) - if err != nil { - workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error()) - workflows = append(workflows, workflow) + prepareWorkflowList(ctx, workflows) + if ctx.Written() { + return + } + + ctx.HTML(http.StatusOK, tplListActions) +} + +func WorkflowDispatchInputs(ctx *context.Context) { + ref := ctx.FormString("ref") + if ref == "" { + ctx.NotFound("WorkflowDispatchInputs: no ref", nil) + return + } + // get target commit of run from specified ref + refName := git.RefName(ref) + var commit *git.Commit + var err error + if refName.IsTag() { + commit, err = ctx.Repo.GitRepo.GetTagCommit(refName.TagName()) + } else if refName.IsBranch() { + commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName.BranchName()) + } else { + ctx.ServerError("UnsupportedRefType", nil) + return + } + if err != nil { + ctx.ServerError("GetTagCommit/GetBranchCommit", err) + return + } + prepareWorkflowDispatchTemplate(ctx, commit) + if ctx.Written() { + return + } + ctx.HTML(http.StatusOK, tplDispatchInputsActions) +} + +func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (workflows []Workflow) { + workflowID := ctx.FormString("workflow") + ctx.Data["CurWorkflow"] = workflowID + ctx.Data["CurWorkflowExists"] = false + + var curWorkflow *model.Workflow + + entries, err := actions.ListWorkflows(commit) + if err != nil { + ctx.ServerError("ListWorkflows", err) + return nil + } + + // Get all runner labels + runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{ + RepoID: ctx.Repo.Repository.ID, + IsOnline: optional.Some(true), + WithAvailable: true, + }) + if err != nil { + ctx.ServerError("FindRunners", err) + return nil + } + allRunnerLabels := make(container.Set[string]) + for _, r := range runners { + allRunnerLabels.AddMultiple(r.AgentLabels...) + } + + workflows = make([]Workflow, 0, len(entries)) + for _, entry := range entries { + workflow := Workflow{Entry: *entry} + content, err := actions.GetContentFromEntry(entry) + if err != nil { + ctx.ServerError("GetContentFromEntry", err) + return nil + } + wf, err := model.ReadWorkflow(bytes.NewReader(content)) + if err != nil { + workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error()) + workflows = append(workflows, workflow) + continue + } + // The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run. + hasJobWithoutNeeds := false + // Check whether you have matching runner and a job without "needs" + emptyJobsNumber := 0 + for _, j := range wf.Jobs { + if j == nil { + emptyJobsNumber++ continue } - // The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run. - hasJobWithoutNeeds := false - // Check whether have matching runner and a job without "needs" - emptyJobsNumber := 0 - for _, j := range wf.Jobs { - if j == nil { - emptyJobsNumber++ + if !hasJobWithoutNeeds && len(j.Needs()) == 0 { + hasJobWithoutNeeds = true + } + runsOnList := j.RunsOn() + for _, ro := range runsOnList { + if strings.Contains(ro, "${{") { + // Skip if it contains expressions. + // The expressions could be very complex and could not be evaluated here, + // so just skip it, it's OK since it's just a tooltip message. continue } - if !hasJobWithoutNeeds && len(j.Needs()) == 0 { - hasJobWithoutNeeds = true - } - runsOnList := j.RunsOn() - for _, ro := range runsOnList { - if strings.Contains(ro, "${{") { - // Skip if it contains expressions. - // The expressions could be very complex and could not be evaluated here, - // so just skip it, it's OK since it's just a tooltip message. - continue - } - if !allRunnerLabels.Contains(ro) { - workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", ro) - break - } - } - if workflow.ErrMsg != "" { + if !allRunnerLabels.Contains(ro) { + workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", ro) break } } - if !hasJobWithoutNeeds { - workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs") - } - if emptyJobsNumber == len(wf.Jobs) { - workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job") - } - workflows = append(workflows, workflow) - - if workflow.Entry.Name() == workflowID { - curWorkflow = wf + if workflow.ErrMsg != "" { + break } } + if !hasJobWithoutNeeds { + workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs") + } + if emptyJobsNumber == len(wf.Jobs) { + workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job") + } + workflows = append(workflows, workflow) + + if workflow.Entry.Name() == workflowID { + curWorkflow = wf + ctx.Data["CurWorkflowExists"] = true + } } + ctx.Data["workflows"] = workflows ctx.Data["RepoLink"] = ctx.Repo.Repository.Link() - page := ctx.FormInt("page") - if page <= 0 { - page = 1 - } - actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() ctx.Data["ActionsConfig"] = actionsConfig @@ -188,7 +225,7 @@ func List(ctx *context.Context) { branches, err := git_model.FindBranchNames(ctx, branchOpts) if err != nil { ctx.ServerError("FindBranchNames", err) - return + return nil } // always put default branch on the top if it exists if slices.Contains(branches, ctx.Repo.Repository.DefaultBranch) { @@ -200,12 +237,23 @@ func List(ctx *context.Context) { tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID) if err != nil { ctx.ServerError("GetTagNamesByRepoID", err) - return + return nil } ctx.Data["Tags"] = tags } } } + return workflows +} + +func prepareWorkflowList(ctx *context.Context, workflows []Workflow) { + actorID := ctx.FormInt64("actor") + status := ctx.FormInt("status") + workflowID := ctx.FormString("workflow") + page := ctx.FormInt("page") + if page <= 0 { + page = 1 + } // if status or actor query param is not given to frontend href, (href="//actions") // they will be 0 by default, which indicates get all status or actors @@ -264,8 +312,6 @@ func List(ctx *context.Context) { pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0 - - ctx.HTML(http.StatusOK, tplListActions) } // loadIsRefDeleted loads the IsRefDeleted field for each run in the list. diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index ba17fa427d..9a18ca5305 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -812,13 +812,8 @@ func Run(ctx *context_module.Context) { return } - // get workflow entry from default branch commit - defaultBranchCommit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) - if err != nil { - ctx.Error(http.StatusInternalServerError, err.Error()) - return - } - entries, err := actions.ListWorkflows(defaultBranchCommit) + // get workflow entry from runTargetCommit + entries, err := actions.ListWorkflows(runTargetCommit) if err != nil { ctx.Error(http.StatusInternalServerError, err.Error()) return diff --git a/routers/web/web.go b/routers/web/web.go index 5e0995545e..ff91bda3d2 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1412,6 +1412,7 @@ func registerRoutes(m *web.Router) { m.Post("/disable", reqRepoAdmin, actions.DisableWorkflowFile) m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile) m.Post("/run", reqRepoActionsWriter, actions.Run) + m.Get("/workflow-dispatch-inputs", reqRepoActionsWriter, actions.WorkflowDispatchInputs) m.Group("/runs/{run}", func() { m.Combo(""). @@ -1433,7 +1434,7 @@ func registerRoutes(m *web.Router) { m.Group("/workflows/{workflow_name}", func() { m.Get("/badge.svg", actions.GetWorkflowBadge) }) - }, optSignIn, context.RepoAssignment, reqRepoActionsReader, actions.MustEnableActions) + }, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions) // end "/{username}/{reponame}/actions" m.Group("/{username}/{reponame}/wiki", func() { diff --git a/templates/repo/actions/workflow_dispatch.tmpl b/templates/repo/actions/workflow_dispatch.tmpl index 21f3ef2077..55fe122419 100644 --- a/templates/repo/actions/workflow_dispatch.tmpl +++ b/templates/repo/actions/workflow_dispatch.tmpl @@ -11,7 +11,7 @@ diff --git a/templates/repo/actions/workflow_dispatch_inputs.tmpl b/templates/repo/actions/workflow_dispatch_inputs.tmpl new file mode 100644 index 0000000000..8b8292af1d --- /dev/null +++ b/templates/repo/actions/workflow_dispatch_inputs.tmpl @@ -0,0 +1,45 @@ +{{if not .WorkflowDispatchConfig}} +
{{/* using "ui message" in "ui form" needs to force to display */}} + {{if not .CurWorkflowExists}} + {{ctx.Locale.Tr "actions.workflow.not_found" $.CurWorkflow}} + {{else}} + {{ctx.Locale.Tr "actions.workflow.has_no_workflow_dispatch" $.CurWorkflow}} + {{end}} +
+{{else}} + {{range $item := .WorkflowDispatchConfig.Inputs}} +
+ {{if eq .Type "choice"}} + + {{/* htmx won't initialize the fomantic dropdown, so it is a standard "select" input */}} + + {{else if eq .Type "boolean"}} + {{/* htmx doesn't trigger our JS code to attach fomantic label to checkbox, so here we use standard checkbox */}} + + {{else if eq .Type "number"}} + + + {{else}} + + + {{end}} +
+ {{end}} +
+ +
+{{end}} +{{range .workflows}} + {{if and .ErrMsg (eq .Entry.Name $.CurWorkflow)}} +
+
{{svg "octicon-alert" 16 "text red"}} {{.ErrMsg}}
+
+ {{end}} +{{end}} From cf60734a4d05215095cb2089c6fb898793dd4e37 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 6 Jan 2025 05:20:22 +0800 Subject: [PATCH 07/37] Fix dropdown menu header and mobile view (#33108) ![image](https://github.com/user-attachments/assets/3f831c8c-ef87-4282-880a-c2738f3e1d17) ---- ![image](https://github.com/user-attachments/assets/c4c0519b-cfa6-42b4-bd28-205ee514eb34) ---- ![image](https://github.com/user-attachments/assets/8624a605-9f2b-4905-9cbc-0af073972874) --- templates/base/head_navbar.tmpl | 4 ++-- templates/repo/commit_page.tmpl | 2 +- templates/user/dashboard/navbar.tmpl | 4 ++-- web_src/css/modules/navbar.css | 3 ++- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index bf0e7e632b..baf37494b9 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -70,7 +70,7 @@ {{svg "octicon-triangle-down"}}