From f15d5f0c4a429970a8e4e63a89554994a50ca0b2 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Fri, 11 Oct 2024 21:13:09 +0800 Subject: [PATCH 01/49] Fix checkbox bug on private/archive filter (#32236) (#32240) Backport #32236 by cloudchamb3r fix #32235 Co-authored-by: cloudchamb3r Co-authored-by: wxiaoguang --- web_src/js/components/DashboardRepoList.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue index cb23de93dcd7d..79f942d345d9a 100644 --- a/web_src/js/components/DashboardRepoList.vue +++ b/web_src/js/components/DashboardRepoList.vue @@ -362,9 +362,9 @@ export default sfc; // activate the IDE's Vue plugin +
+ +

Selection

diff --git a/web_src/css/base.css b/web_src/css/base.css index 067772ef59293..8587e4bfbde05 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -1354,6 +1354,10 @@ table th[data-sortt-desc] .svg { min-width: 0; /* make ellipsis work */ } +.ui.multiple.selection.dropdown { + flex-wrap: wrap; +} + .ui.ui.dropdown.selection { min-width: 14em; /* match the default min width */ } From 24b65f122ae06f54fbed0f4ae30d9b1ea02b45bf Mon Sep 17 00:00:00 2001 From: Giteabot Date: Mon, 14 Oct 2024 03:27:37 +0800 Subject: [PATCH 03/49] Only rename a user when they should receive a different name (#32247) (#32249) Backport #32247 by @lunny Fix #31996 Co-authored-by: Lunny Xiao --- services/user/user.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/user/user.go b/services/user/user.go index 2287e36c716ac..9aded62a51af8 100644 --- a/services/user/user.go +++ b/services/user/user.go @@ -32,6 +32,10 @@ import ( // RenameUser renames a user func RenameUser(ctx context.Context, u *user_model.User, newUserName string) error { + if newUserName == u.Name { + return nil + } + // Non-local users are not allowed to change their username. if !u.IsOrganization() && !u.IsLocal() { return user_model.ErrUserIsNotLocal{ @@ -40,10 +44,6 @@ func RenameUser(ctx context.Context, u *user_model.User, newUserName string) err } } - if newUserName == u.Name { - return nil - } - if err := user_model.IsUsableUsername(newUserName); err != nil { return err } From 55562f9c79b32ca5c64b3b8588b36379199df1d3 Mon Sep 17 00:00:00 2001 From: Zettat123 Date: Mon, 14 Oct 2024 16:55:16 +0800 Subject: [PATCH 04/49] Update scheduled tasks even if changes are pushed by "ActionsUser" (#32246) (#32252) Backport #32246 Fix #32219 Co-authored-by: delvh --- services/actions/notifier_helper.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 0030ef9a9161d..a84159a5cc544 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -115,11 +115,20 @@ func (input *notifyInput) Notify(ctx context.Context) { } func notify(ctx context.Context, input *notifyInput) error { + shouldDetectSchedules := input.Event == webhook_module.HookEventPush && input.Ref.BranchName() == input.Repo.DefaultBranch if input.Doer.IsActions() { // avoiding triggering cyclically, for example: // a comment of an issue will trigger the runner to add a new comment as reply, // and the new comment will trigger the runner again. log.Debug("ignore executing %v for event %v whose doer is %v", getMethod(ctx), input.Event, input.Doer.Name) + + // we should update schedule tasks in this case, because + // 1. schedule tasks cannot be triggered by other events, so cyclic triggering will not occur + // 2. some schedule tasks may update the repo periodically, so the refs of schedule tasks need to be updated + if shouldDetectSchedules { + return DetectAndHandleSchedules(ctx, input.Repo) + } + return nil } if input.Repo.IsEmpty || input.Repo.IsArchived { @@ -173,7 +182,6 @@ func notify(ctx context.Context, input *notifyInput) error { var detectedWorkflows []*actions_module.DetectedWorkflow actionsConfig := input.Repo.MustGetUnit(ctx, unit_model.TypeActions).ActionsConfig() - shouldDetectSchedules := input.Event == webhook_module.HookEventPush && input.Ref.BranchName() == input.Repo.DefaultBranch workflows, schedules, err := actions_module.DetectWorkflows(gitRepo, commit, input.Event, input.Payload, From db7349bc0d07d88349629a42b157fa91ef594255 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 15 Oct 2024 22:32:54 +0800 Subject: [PATCH 05/49] Make `owner/repo/pulls` handlers use "PR reader" permission (#32254) (#32265) Backport #32254 (no conflict) --- routers/web/web.go | 55 ++++++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/routers/web/web.go b/routers/web/web.go index 5e19d1fd131c8..0391eb0d7fa36 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1454,6 +1454,35 @@ func registerRoutes(m *web.Route) { ) // end "/{username}/{reponame}/activity" + m.Group("/{username}/{reponame}", func() { + m.Group("/pulls/{index}", func() { + m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewIssue) + m.Get(".diff", repo.DownloadPullDiff) + m.Get(".patch", repo.DownloadPullPatch) + m.Group("/commits", func() { + m.Get("", context.RepoRef(), repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewPullCommits) + m.Get("/list", context.RepoRef(), repo.GetPullCommits) + m.Get("/{sha:[a-f0-9]{7,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit) + }) + m.Post("/merge", context.RepoMustNotBeArchived(), web.Bind(forms.MergePullRequestForm{}), repo.MergePullRequest) + m.Post("/cancel_auto_merge", context.RepoMustNotBeArchived(), repo.CancelAutoMergePullRequest) + m.Post("/update", repo.UpdatePullRequest) + m.Post("/set_allow_maintainer_edit", web.Bind(forms.UpdateAllowEditsForm{}), repo.SetAllowEdits) + m.Post("/cleanup", context.RepoMustNotBeArchived(), context.RepoRef(), repo.CleanUpPullRequest) + m.Group("/files", func() { + m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForAllCommitsOfPr) + m.Get("/{sha:[a-f0-9]{7,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesStartingFromCommit) + m.Get("/{shaFrom:[a-f0-9]{7,40}}..{shaTo:[a-f0-9]{7,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForRange) + m.Group("/reviews", func() { + m.Get("/new_comment", repo.RenderNewCodeCommentForm) + m.Post("/comments", web.Bind(forms.CodeCommentForm{}), repo.SetShowOutdatedComments, repo.CreateCodeComment) + m.Post("/submit", web.Bind(forms.SubmitReviewForm{}), repo.SubmitReview) + }, context.RepoMustNotBeArchived()) + }) + }) + }, ignSignIn, context.RepoAssignment, repo.MustAllowPulls, reqRepoPullsReader) + // end "/{username}/{reponame}/pulls/{index}": repo pull request + m.Group("/{username}/{reponame}", func() { m.Group("/activity_author_data", func() { m.Get("", repo.ActivityAuthors) @@ -1492,32 +1521,6 @@ func registerRoutes(m *web.Route) { return cancel }) - m.Group("/pulls/{index}", func() { - m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewIssue) - m.Get(".diff", repo.DownloadPullDiff) - m.Get(".patch", repo.DownloadPullPatch) - m.Group("/commits", func() { - m.Get("", context.RepoRef(), repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewPullCommits) - m.Get("/list", context.RepoRef(), repo.GetPullCommits) - m.Get("/{sha:[a-f0-9]{7,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit) - }) - m.Post("/merge", context.RepoMustNotBeArchived(), web.Bind(forms.MergePullRequestForm{}), repo.MergePullRequest) - m.Post("/cancel_auto_merge", context.RepoMustNotBeArchived(), repo.CancelAutoMergePullRequest) - m.Post("/update", repo.UpdatePullRequest) - m.Post("/set_allow_maintainer_edit", web.Bind(forms.UpdateAllowEditsForm{}), repo.SetAllowEdits) - m.Post("/cleanup", context.RepoMustNotBeArchived(), context.RepoRef(), repo.CleanUpPullRequest) - m.Group("/files", func() { - m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForAllCommitsOfPr) - m.Get("/{sha:[a-f0-9]{7,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesStartingFromCommit) - m.Get("/{shaFrom:[a-f0-9]{7,40}}..{shaTo:[a-f0-9]{7,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForRange) - m.Group("/reviews", func() { - m.Get("/new_comment", repo.RenderNewCodeCommentForm) - m.Post("/comments", web.Bind(forms.CodeCommentForm{}), repo.SetShowOutdatedComments, repo.CreateCodeComment) - m.Post("/submit", web.Bind(forms.SubmitReviewForm{}), repo.SubmitReview) - }, context.RepoMustNotBeArchived()) - }) - }, repo.MustAllowPulls) - m.Group("/media", func() { m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.SingleDownloadOrLFS) m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.SingleDownloadOrLFS) From 7e0fd4c20800dc0323e4eb3cfdcbdf81e4ecb333 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 17 Oct 2024 09:01:44 +0800 Subject: [PATCH 06/49] Warn users when they try to use a non-root-url to sign in/up (#32272) (#32273) --- web_src/js/features/common-global.js | 8 ++++++++ web_src/js/features/user-auth.js | 7 ++++++- web_src/js/index.js | 3 ++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js index 65eb237ddeee3..80916b049d7f7 100644 --- a/web_src/js/features/common-global.js +++ b/web_src/js/features/common-global.js @@ -453,3 +453,11 @@ export function checkAppUrl() { showGlobalErrorMessage(`Your ROOT_URL in app.ini is "${appUrl}", it's unlikely matching the site you are visiting. Mismatched ROOT_URL config causes wrong URL links for web UI/mail content/webhook notification/OAuth2 sign-in.`, 'warning'); } + +export function checkAppUrlScheme() { + const curUrl = window.location.href; + // some users visit "http://domain" while appUrl is "https://domain", COOKIE_SECURE makes it impossible to sign in + if (curUrl.startsWith('http:') && appUrl.startsWith('https:')) { + showGlobalErrorMessage(`This instance is configured to run under HTTPS (by ROOT_URL config), you are accessing by HTTP. Mismatched scheme might cause problems for sign-in/sign-up.`, 'warning'); + } +} diff --git a/web_src/js/features/user-auth.js b/web_src/js/features/user-auth.js index a871ac471c2ec..13147ca72797b 100644 --- a/web_src/js/features/user-auth.js +++ b/web_src/js/features/user-auth.js @@ -1,4 +1,9 @@ -import {checkAppUrl} from './common-global.js'; +import {checkAppUrl, checkAppUrlScheme} from './common-global.js'; + +export function initUserCheckAppUrl() { + if (!document.querySelector('.page-content.user.signin, .page-content.user.signup, .page-content.user.link-account')) return; + checkAppUrlScheme(); +} export function initUserAuthOauth2() { const outer = document.getElementById('oauth2-login-navigator'); diff --git a/web_src/js/index.js b/web_src/js/index.js index 4c3852b4065a0..8cbeeea5d72cb 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -23,7 +23,7 @@ import {initFindFileInRepo} from './features/repo-findfile.js'; import {initCommentContent, initMarkupContent} from './markup/content.js'; import {initPdfViewer} from './render/pdf.js'; -import {initUserAuthOauth2} from './features/user-auth.js'; +import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.js'; import { initRepoIssueDue, initRepoIssueReferenceRepositorySearch, @@ -184,6 +184,7 @@ onDomReady(() => { initCommitStatuses(); initCaptcha(); + initUserCheckAppUrl(); initUserAuthOauth2(); initUserAuthWebAuthn(); initUserAuthWebAuthnRegister(); From c1023b97aa217cda357953d3ceb42b8cf058d904 Mon Sep 17 00:00:00 2001 From: cloudchamb3r Date: Thu, 17 Oct 2024 14:34:39 +0900 Subject: [PATCH 07/49] [v1.22 backport] Fix null errors on conversation holder (#32258) (#32266) (#32282) Backport #32266 fix #32258 Errors in the issue was due to unhandled null check. so i fixed it. ### Detailed description for Issue & Fix To reproduce that issue, the comment must be deleted on Conversation tab. #### Before Delete image #### After Delete (AS-IS) image gitea already have remove logic for `timeline-item-group`, but because of null ref exception the later logic that removes `timeline-item-group` could be not be called correctly. --- web_src/js/features/repo-issue.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js index 519db34934b65..6cd254dd81739 100644 --- a/web_src/js/features/repo-issue.js +++ b/web_src/js/features/repo-issue.js @@ -188,14 +188,17 @@ export function initRepoIssueCommentDelete() { const path = conversationHolder.getAttribute('data-path'); const side = conversationHolder.getAttribute('data-side'); const idx = conversationHolder.getAttribute('data-idx'); - const lineType = conversationHolder.closest('tr').getAttribute('data-line-type'); - - if (lineType === 'same') { - document.querySelector(`[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`).classList.remove('tw-invisible'); - } else { - document.querySelector(`[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`).classList.remove('tw-invisible'); + const lineType = conversationHolder.closest('tr')?.getAttribute('data-line-type'); + + // the conversation holder could appear either on the "Conversation" page, or the "Files Changed" page + // on the Conversation page, there is no parent "tr", so no need to do anything for "add-code-comment" + if (lineType) { + if (lineType === 'same') { + document.querySelector(`[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`).classList.remove('tw-invisible'); + } else { + document.querySelector(`[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`).classList.remove('tw-invisible'); + } } - conversationHolder.remove(); } From 2a99607add16cf7459429bdc4d2674b2b4ffdf8a Mon Sep 17 00:00:00 2001 From: a1012112796 <1012112796@qq.com> Date: Thu, 17 Oct 2024 16:03:21 +0800 Subject: [PATCH 08/49] make `show stats` work when only one file changed (#32244) (#32268) Backport #32244 fix https://github.com/go-gitea/gitea/issues/32226 in https://github.com/go-gitea/gitea/pull/27775 , it do some changes to only show diff file tree when more than one file changed. But looks it also break the `diff-file-list` logic, which looks not expected change. so try fix it. /cc @silverwind example view: ![image](https://github.com/user-attachments/assets/281e9c4f-a269-4d36-94eb-a132058aea87) Signed-off-by: a1012112796 <1012112796@qq.com> --- web_src/js/features/repo-diff-filetree.js | 2 ++ web_src/js/features/repo-diff.js | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/web_src/js/features/repo-diff-filetree.js b/web_src/js/features/repo-diff-filetree.js index 5dd2c42e74e29..52d7cf030df10 100644 --- a/web_src/js/features/repo-diff-filetree.js +++ b/web_src/js/features/repo-diff-filetree.js @@ -8,7 +8,9 @@ export function initDiffFileTree() { const fileTreeView = createApp(DiffFileTree); fileTreeView.mount(el); +} +export function initDiffFileList() { const fileListElement = document.getElementById('diff-file-list'); if (!fileListElement) return; diff --git a/web_src/js/features/repo-diff.js b/web_src/js/features/repo-diff.js index 00f74515df6a8..e7324b21b8c05 100644 --- a/web_src/js/features/repo-diff.js +++ b/web_src/js/features/repo-diff.js @@ -1,7 +1,7 @@ import $ from 'jquery'; import {initCompReactionSelector} from './comp/ReactionSelector.js'; import {initRepoIssueContentHistory} from './repo-issue-content.js'; -import {initDiffFileTree} from './repo-diff-filetree.js'; +import {initDiffFileTree, initDiffFileList} from './repo-diff-filetree.js'; import {initDiffCommitSelect} from './repo-diff-commitselect.js'; import {validateTextareaNonEmpty} from './comp/ComboMarkdownEditor.js'; import {initViewedCheckboxListenerFor, countAndUpdateViewedFiles, initExpandAndCollapseFilesButton} from './pull-view-file.js'; @@ -220,6 +220,7 @@ export function initRepoDiffView() { initRepoDiffConversationForm(); if (!$('#diff-file-list').length) return; initDiffFileTree(); + initDiffFileList(); initDiffCommitSelect(); initRepoDiffShowMore(); initRepoDiffReviewButton(); From 99cac1f50c4bceb8b28979caeea4af9e5129e85e Mon Sep 17 00:00:00 2001 From: Zettat123 Date: Fri, 18 Oct 2024 10:36:23 +0800 Subject: [PATCH 09/49] Always update expiration time when creating an artifact (#32281) (#32285) Backport #32281 Fix #32256 --- models/actions/artifact.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/models/actions/artifact.go b/models/actions/artifact.go index 3d0a288e6287e..0bc66ba24e846 100644 --- a/models/actions/artifact.go +++ b/models/actions/artifact.go @@ -69,7 +69,7 @@ func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPa OwnerID: t.OwnerID, CommitSHA: t.CommitSHA, Status: int64(ArtifactStatusUploadPending), - ExpiredUnix: timeutil.TimeStamp(time.Now().Unix() + 3600*24*expiredDays), + ExpiredUnix: timeutil.TimeStamp(time.Now().Unix() + timeutil.Day*expiredDays), } if _, err := db.GetEngine(ctx).Insert(artifact); err != nil { return nil, err @@ -78,6 +78,13 @@ func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPa } else if err != nil { return nil, err } + + if _, err := db.GetEngine(ctx).ID(artifact.ID).Cols("expired_unix").Update(&ActionArtifact{ + ExpiredUnix: timeutil.TimeStamp(time.Now().Unix() + timeutil.Day*expiredDays), + }); err != nil { + return nil, err + } + return artifact, nil } From 0c12252c23ae2495209ea0340cf15e581cddfc93 Mon Sep 17 00:00:00 2001 From: YR Chen Date: Mon, 21 Oct 2024 02:12:51 +0800 Subject: [PATCH 10/49] Update github.com/go-enry/go-enry to v2.9.1 (#32295) (#32296) Backport #32295 `go-enry` v2.9.1 includes latest file patterns from Linguist, which can identify more generated file type, eg. `pdm.lock`. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b20cba2728cd6..73bbd453b432a 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/go-chi/chi/v5 v5.0.12 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.8.7 + github.com/go-enry/go-enry/v2 v2.9.1 github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e github.com/go-git/go-billy/v5 v5.5.0 github.com/go-git/go-git/v5 v5.11.0 diff --git a/go.sum b/go.sum index 2a819b451422a..b395a201f5fd3 100644 --- a/go.sum +++ b/go.sum @@ -288,8 +288,8 @@ 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= github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= -github.com/go-enry/go-enry/v2 v2.8.7 h1:vbab0pcf5Yo1cHQLzbWZ+QomUh3EfEU8EiR5n7W0lnQ= -github.com/go-enry/go-enry/v2 v2.8.7/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8= +github.com/go-enry/go-enry/v2 v2.9.1 h1:G9iDteJ/Mc0F4Di5NeQknf83R2OkRbwY9cAYmcqVG6U= +github.com/go-enry/go-enry/v2 v2.9.1/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8= github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo= github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4= github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= From b6f8372d7d96a42e56ba5e16618ccff2391eefd5 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Mon, 21 Oct 2024 02:32:34 +0200 Subject: [PATCH 11/49] API: enhance SearchIssues swagger docs (#32208) (#32298) Backport #32208 This will result in better api clients generated out of the openapi docs for SearchIssues --- *Sponsored by Kithara Software GmbH* --- routers/api/v1/repo/issue.go | 51 +++++++++++++++++++----------- templates/swagger/v1_json.tmpl | 58 ++++++++++++++++++++++++---------- 2 files changed, 73 insertions(+), 36 deletions(-) diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index e4d95e9aa035e..0afdd9e868044 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -41,80 +41,93 @@ func SearchIssues(ctx *context.APIContext) { // parameters: // - name: state // in: query - // description: whether issue is open or closed + // description: State of the issue // type: string + // enum: [open, closed, all] + // default: open // - name: labels // in: query - // description: comma separated list of labels. Fetch only issues that have any of this labels. Non existent labels are discarded + // description: Comma-separated list of label names. Fetch only issues that have any of these labels. Non existent labels are discarded. // type: string // - name: milestones // in: query - // description: comma separated list of milestone names. Fetch only issues that have any of this milestones. Non existent are discarded + // description: Comma-separated list of milestone names. Fetch only issues that have any of these milestones. Non existent milestones are discarded. // type: string // - name: q // in: query - // description: search string + // description: Search string // type: string // - name: priority_repo_id // in: query - // description: repository to prioritize in the results + // description: Repository ID to prioritize in the results // type: integer // format: int64 // - name: type // in: query - // description: filter by type (issues / pulls) if set + // description: Filter by issue type // type: string + // enum: [issues, pulls] // - name: since // in: query - // description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format + // description: Only show issues updated after the given time (RFC 3339 format) // type: string // format: date-time - // required: false // - name: before // in: query - // description: Only show notifications updated before the given time. This is a timestamp in RFC 3339 format + // description: Only show issues updated before the given time (RFC 3339 format) // type: string // format: date-time - // required: false // - name: assigned // in: query - // description: filter (issues / pulls) assigned to you, default is false + // description: Filter issues or pulls assigned to the authenticated user // type: boolean + // default: false // - name: created // in: query - // description: filter (issues / pulls) created by you, default is false + // description: Filter issues or pulls created by the authenticated user // type: boolean + // default: false // - name: mentioned // in: query - // description: filter (issues / pulls) mentioning you, default is false + // description: Filter issues or pulls mentioning the authenticated user // type: boolean + // default: false // - name: review_requested // in: query - // description: filter pulls requesting your review, default is false + // description: Filter pull requests where the authenticated user's review was requested // type: boolean + // default: false // - name: reviewed // in: query - // description: filter pulls reviewed by you, default is false + // description: Filter pull requests reviewed by the authenticated user // type: boolean + // default: false // - name: owner // in: query - // description: filter by owner + // description: Filter by repository owner // type: string // - name: team // in: query - // description: filter by team (requires organization owner parameter to be provided) + // description: Filter by team (requires organization owner parameter) // type: string // - name: page // in: query - // description: page number of results to return (1-based) + // description: Page number of results to return (1-based) // type: integer + // minimum: 1 + // default: 1 // - name: limit // in: query - // description: page size of results + // description: Number of items per page // type: integer + // minimum: 0 // responses: // "200": // "$ref": "#/responses/IssueList" + // "400": + // "$ref": "#/responses/error" + // "422": + // "$ref": "#/responses/validationError" before, since, err := context.GetQueryBeforeSince(ctx.Base) if err != nil { diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index d5c0097636114..42e622096aa83 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -3444,107 +3444,125 @@ "operationId": "issueSearchIssues", "parameters": [ { + "enum": [ + "open", + "closed", + "all" + ], "type": "string", - "description": "whether issue is open or closed", + "default": "open", + "description": "State of the issue", "name": "state", "in": "query" }, { "type": "string", - "description": "comma separated list of labels. Fetch only issues that have any of this labels. Non existent labels are discarded", + "description": "Comma-separated list of label names. Fetch only issues that have any of these labels. Non existent labels are discarded.", "name": "labels", "in": "query" }, { "type": "string", - "description": "comma separated list of milestone names. Fetch only issues that have any of this milestones. Non existent are discarded", + "description": "Comma-separated list of milestone names. Fetch only issues that have any of these milestones. Non existent milestones are discarded.", "name": "milestones", "in": "query" }, { "type": "string", - "description": "search string", + "description": "Search string", "name": "q", "in": "query" }, { "type": "integer", "format": "int64", - "description": "repository to prioritize in the results", + "description": "Repository ID to prioritize in the results", "name": "priority_repo_id", "in": "query" }, { + "enum": [ + "issues", + "pulls" + ], "type": "string", - "description": "filter by type (issues / pulls) if set", + "description": "Filter by issue type", "name": "type", "in": "query" }, { "type": "string", "format": "date-time", - "description": "Only show notifications updated after the given time. This is a timestamp in RFC 3339 format", + "description": "Only show issues updated after the given time (RFC 3339 format)", "name": "since", "in": "query" }, { "type": "string", "format": "date-time", - "description": "Only show notifications updated before the given time. This is a timestamp in RFC 3339 format", + "description": "Only show issues updated before the given time (RFC 3339 format)", "name": "before", "in": "query" }, { "type": "boolean", - "description": "filter (issues / pulls) assigned to you, default is false", + "default": false, + "description": "Filter issues or pulls assigned to the authenticated user", "name": "assigned", "in": "query" }, { "type": "boolean", - "description": "filter (issues / pulls) created by you, default is false", + "default": false, + "description": "Filter issues or pulls created by the authenticated user", "name": "created", "in": "query" }, { "type": "boolean", - "description": "filter (issues / pulls) mentioning you, default is false", + "default": false, + "description": "Filter issues or pulls mentioning the authenticated user", "name": "mentioned", "in": "query" }, { "type": "boolean", - "description": "filter pulls requesting your review, default is false", + "default": false, + "description": "Filter pull requests where the authenticated user's review was requested", "name": "review_requested", "in": "query" }, { "type": "boolean", - "description": "filter pulls reviewed by you, default is false", + "default": false, + "description": "Filter pull requests reviewed by the authenticated user", "name": "reviewed", "in": "query" }, { "type": "string", - "description": "filter by owner", + "description": "Filter by repository owner", "name": "owner", "in": "query" }, { "type": "string", - "description": "filter by team (requires organization owner parameter to be provided)", + "description": "Filter by team (requires organization owner parameter)", "name": "team", "in": "query" }, { + "minimum": 1, "type": "integer", - "description": "page number of results to return (1-based)", + "default": 1, + "description": "Page number of results to return (1-based)", "name": "page", "in": "query" }, { + "minimum": 0, "type": "integer", - "description": "page size of results", + "description": "Number of items per page", "name": "limit", "in": "query" } @@ -3552,6 +3570,12 @@ "responses": { "200": { "$ref": "#/responses/IssueList" + }, + "400": { + "$ref": "#/responses/error" + }, + "422": { + "$ref": "#/responses/validationError" } } } From b7d12347f3bd45b2be6f05c20ae835da0be8ace3 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 23 Oct 2024 10:48:42 +0800 Subject: [PATCH 12/49] Add warn log when deleting inactive users (#32318) (#32321) Backport #32318 Add log for the problem #31480 --- services/user/user.go | 1 + 1 file changed, 1 insertion(+) diff --git a/services/user/user.go b/services/user/user.go index 9aded62a51af8..7855dbb78b713 100644 --- a/services/user/user.go +++ b/services/user/user.go @@ -289,6 +289,7 @@ func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error { if err = DeleteUser(ctx, u, false); err != nil { // Ignore inactive users that were ever active but then were set inactive by admin if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) || models.IsErrUserOwnPackages(err) { + log.Warn("Inactive user %q has repositories, organizations or packages, skipping deletion: %v", u.Name, err) continue } select { From 0d11ba93dd544320300173fd47714ee5a711b4d2 Mon Sep 17 00:00:00 2001 From: Zettat123 Date: Wed, 23 Oct 2024 12:56:13 +0800 Subject: [PATCH 13/49] Fix the permission check for user search API and limit the number of returned users for `/user/search` (#32310) Partially backport #32288 --------- Co-authored-by: wxiaoguang --- routers/api/v1/api.go | 12 +++++++-- routers/web/user/search.go | 31 +++++++---------------- routers/web/web.go | 2 +- web_src/js/features/comp/SearchUserBox.js | 27 +++++++++----------- 4 files changed, 32 insertions(+), 40 deletions(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 19b643135665d..6e2d98c648ab4 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -356,12 +356,20 @@ func reqToken() func(ctx *context.APIContext) { func reqExploreSignIn() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { - if setting.Service.Explore.RequireSigninView && !ctx.IsSigned { + if (setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView) && !ctx.IsSigned { ctx.Error(http.StatusUnauthorized, "reqExploreSignIn", "you must be signed in to search for users") } } } +func reqUsersExploreEnabled() func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + if setting.Service.Explore.DisableUsersPage { + ctx.NotFound() + } + } +} + func reqBasicOrRevProxyAuth() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if ctx.IsSigned && setting.Service.EnableReverseProxyAuthAPI && ctx.Data["AuthedMethod"].(string) == auth.ReverseProxyMethodName { @@ -955,7 +963,7 @@ func Routes() *web.Route { // Users (requires user scope) m.Group("/users", func() { - m.Get("/search", reqExploreSignIn(), user.Search) + m.Get("/search", reqExploreSignIn(), reqUsersExploreEnabled(), user.Search) m.Group("/{username}", func() { m.Get("", reqExploreSignIn(), user.GetInfo) diff --git a/routers/web/user/search.go b/routers/web/user/search.go index fb7729bbe1419..be5eee90a971a 100644 --- a/routers/web/user/search.go +++ b/routers/web/user/search.go @@ -8,37 +8,24 @@ import ( "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" ) -// Search search users -func Search(ctx *context.Context) { - listOptions := db.ListOptions{ - Page: ctx.FormInt("page"), - PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), - } - - users, maxResults, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{ +// SearchCandidates searches candidate users for dropdown list +func SearchCandidates(ctx *context.Context) { + users, _, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{ Actor: ctx.Doer, Keyword: ctx.FormTrim("q"), - UID: ctx.FormInt64("uid"), Type: user_model.UserTypeIndividual, - IsActive: ctx.FormOptionalBool("active"), - ListOptions: listOptions, + IsActive: optional.Some(true), + ListOptions: db.ListOptions{PageSize: setting.UI.MembersPagingNum}, }) if err != nil { - ctx.JSON(http.StatusInternalServerError, map[string]any{ - "ok": false, - "error": err.Error(), - }) + ctx.ServerError("Unable to search users", err) return } - - ctx.SetTotalCountHeader(maxResults) - - ctx.JSON(http.StatusOK, map[string]any{ - "ok": true, - "data": convert.ToUsers(ctx, ctx.Doer, users), - }) + ctx.JSON(http.StatusOK, map[string]any{"data": convert.ToUsers(ctx, ctx.Doer, users)}) } diff --git a/routers/web/web.go b/routers/web/web.go index 0391eb0d7fa36..787c5f51be77d 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -668,7 +668,7 @@ func registerRoutes(m *web.Route) { m.Post("/forgot_password", auth.ForgotPasswdPost) m.Post("/logout", auth.SignOut) m.Get("/stopwatches", reqSignIn, user.GetStopwatches) - m.Get("/search", ignExploreSignIn, user.Search) + m.Get("/search_candidates", ignExploreSignIn, user.SearchCandidates) m.Group("/oauth2", func() { m.Get("/{provider}", auth.SignInOAuth) m.Get("/{provider}/callback", auth.SignInOAuthCallback) diff --git a/web_src/js/features/comp/SearchUserBox.js b/web_src/js/features/comp/SearchUserBox.js index 081c47425f8b8..b03cbad16fe22 100644 --- a/web_src/js/features/comp/SearchUserBox.js +++ b/web_src/js/features/comp/SearchUserBox.js @@ -8,41 +8,38 @@ export function initCompSearchUserBox() { const searchUserBox = document.getElementById('search-user-box'); if (!searchUserBox) return; - const $searchUserBox = $(searchUserBox); const allowEmailInput = searchUserBox.getAttribute('data-allow-email') === 'true'; const allowEmailDescription = searchUserBox.getAttribute('data-allow-email-description') ?? undefined; - $searchUserBox.search({ + $(searchUserBox).search({ minCharacters: 2, apiSettings: { - url: `${appSubUrl}/user/search?active=1&q={query}`, + url: `${appSubUrl}/user/search_candidates?q={query}`, onResponse(response) { - const items = []; - const searchQuery = $searchUserBox.find('input').val(); + const resultItems = []; + const searchQuery = searchUserBox.querySelector('input').value; const searchQueryUppercase = searchQuery.toUpperCase(); - $.each(response.data, (_i, item) => { + for (const item of response.data) { const resultItem = { title: item.login, image: item.avatar_url, + description: htmlEscape(item.full_name), }; - if (item.full_name) { - resultItem.description = htmlEscape(item.full_name); - } if (searchQueryUppercase === item.login.toUpperCase()) { - items.unshift(resultItem); + resultItems.unshift(resultItem); // add the exact match to the top } else { - items.push(resultItem); + resultItems.push(resultItem); } - }); + } - if (allowEmailInput && !items.length && looksLikeEmailAddressCheck.test(searchQuery)) { + if (allowEmailInput && !resultItems.length && looksLikeEmailAddressCheck.test(searchQuery)) { const resultItem = { title: searchQuery, description: allowEmailDescription, }; - items.push(resultItem); + resultItems.push(resultItem); } - return {results: items}; + return {results: resultItems}; }, }, searchFields: ['login', 'full_name'], From bf53ab26fa3f3b8822d9a0638d398239702c86ab Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 25 Oct 2024 17:54:56 +0800 Subject: [PATCH 14/49] Fix disable 2fa bug (#32320) (#32330) Backport #32320 --- routers/web/user/setting/security/2fa.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/routers/web/user/setting/security/2fa.go b/routers/web/user/setting/security/2fa.go index cd09102369868..4fd1546576021 100644 --- a/routers/web/user/setting/security/2fa.go +++ b/routers/web/user/setting/security/2fa.go @@ -33,8 +33,9 @@ func RegenerateScratchTwoFactor(ctx *context.Context) { if auth.IsErrTwoFactorNotEnrolled(err) { ctx.Flash.Error(ctx.Tr("settings.twofa_not_enrolled")) ctx.Redirect(setting.AppSubURL + "/user/settings/security") + } else { + ctx.ServerError("SettingsTwoFactor: Failed to GetTwoFactorByUID", err) } - ctx.ServerError("SettingsTwoFactor: Failed to GetTwoFactorByUID", err) return } @@ -63,8 +64,9 @@ func DisableTwoFactor(ctx *context.Context) { if auth.IsErrTwoFactorNotEnrolled(err) { ctx.Flash.Error(ctx.Tr("settings.twofa_not_enrolled")) ctx.Redirect(setting.AppSubURL + "/user/settings/security") + } else { + ctx.ServerError("SettingsTwoFactor: Failed to GetTwoFactorByUID", err) } - ctx.ServerError("SettingsTwoFactor: Failed to GetTwoFactorByUID", err) return } @@ -73,8 +75,9 @@ func DisableTwoFactor(ctx *context.Context) { // There is a potential DB race here - we must have been disabled by another request in the intervening period ctx.Flash.Success(ctx.Tr("settings.twofa_disabled")) ctx.Redirect(setting.AppSubURL + "/user/settings/security") + } else { + ctx.ServerError("SettingsTwoFactor: Failed to DeleteTwoFactorByID", err) } - ctx.ServerError("SettingsTwoFactor: Failed to DeleteTwoFactorByID", err) return } From 9d62d7a44364782568af17662d8147858f2526f3 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Thu, 31 Oct 2024 06:49:09 +0100 Subject: [PATCH 15/49] Respect UI.ExploreDefaultSort setting again (#32357) (#32385) Backport #32357 fix regression of https://github.com/go-gitea/gitea/pull/29430 --- *Sponsored by Kithara Software GmbH* --- routers/web/explore/org.go | 3 ++- routers/web/explore/user.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/routers/web/explore/org.go b/routers/web/explore/org.go index f8fd6ec38efb6..6724324899b3e 100644 --- a/routers/web/explore/org.go +++ b/routers/web/explore/org.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" ) @@ -33,7 +34,7 @@ func Organizations(ctx *context.Context) { ) sortOrder := ctx.FormString("sort") if sortOrder == "" { - sortOrder = "newest" + sortOrder = util.Iif(supportedSortOrders.Contains(setting.UI.ExploreDefaultSort), setting.UI.ExploreDefaultSort, "newest") ctx.SetFormString("sort", sortOrder) } diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go index b79a79fb2c86f..20a17bf3c389f 100644 --- a/routers/web/explore/user.go +++ b/routers/web/explore/user.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/sitemap" "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" ) @@ -147,7 +148,7 @@ func Users(ctx *context.Context) { ) sortOrder := ctx.FormString("sort") if sortOrder == "" { - sortOrder = "newest" + sortOrder = util.Iif(supportedSortOrders.Contains(setting.UI.ExploreDefaultSort), setting.UI.ExploreDefaultSort, "newest") ctx.SetFormString("sort", sortOrder) } From 898f852d03286db15ce0849eb6ab72d01082420c Mon Sep 17 00:00:00 2001 From: Zettat123 Date: Fri, 1 Nov 2024 11:53:59 +0800 Subject: [PATCH 16/49] Fix `missing signature key` error when pulling Docker images with `SERVE_DIRECT` enabled (#32365) (#32397) Backport #32365 Fix #28121 I did some tests and found that the `missing signature key` error is caused by an incorrect `Content-Type` header. Gitea correctly sets the `Content-Type` header when serving files. https://github.com/go-gitea/gitea/blob/348d1d0f322ca57c459acd902f54821d687ca804/routers/api/packages/container/container.go#L712-L717 However, when `SERVE_DIRECT` is enabled, the `Content-Type` header may be set to an incorrect value by the storage service. To fix this issue, we can use query parameters to override response header values. https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html In this PR, I introduced a new parameter to the `URL` method to support additional parameters. ``` URL(path, name string, reqParams url.Values) (*url.URL, error) ``` --- modules/packages/content_store.go | 4 ++-- modules/storage/helper.go | 2 +- modules/storage/helper_test.go | 2 +- modules/storage/local.go | 2 +- modules/storage/minio.go | 8 ++++++-- modules/storage/storage.go | 2 +- routers/api/actions/artifacts.go | 2 +- routers/api/actions/artifactsv4.go | 2 +- routers/api/packages/container/container.go | 4 +++- routers/api/packages/maven/maven.go | 2 +- routers/api/v1/repo/file.go | 4 ++-- routers/web/base.go | 2 +- routers/web/repo/actions/view.go | 2 +- routers/web/repo/attachment.go | 2 +- routers/web/repo/download.go | 2 +- routers/web/repo/repo.go | 2 +- services/lfs/server.go | 2 +- services/packages/packages.go | 6 +++--- 18 files changed, 29 insertions(+), 23 deletions(-) diff --git a/modules/packages/content_store.go b/modules/packages/content_store.go index da93e6cf6bcbb..6438fb174f6bb 100644 --- a/modules/packages/content_store.go +++ b/modules/packages/content_store.go @@ -37,8 +37,8 @@ func (s *ContentStore) ShouldServeDirect() bool { return setting.Packages.Storage.MinioConfig.ServeDirect } -func (s *ContentStore) GetServeDirectURL(key BlobHash256Key, filename string) (*url.URL, error) { - return s.store.URL(KeyToRelativePath(key), filename) +func (s *ContentStore) GetServeDirectURL(key BlobHash256Key, filename string, reqParams url.Values) (*url.URL, error) { + return s.store.URL(KeyToRelativePath(key), filename, reqParams) } // FIXME: Workaround to be removed in v1.20 diff --git a/modules/storage/helper.go b/modules/storage/helper.go index f8dff9e64d912..9e6cceb537da7 100644 --- a/modules/storage/helper.go +++ b/modules/storage/helper.go @@ -30,7 +30,7 @@ func (s discardStorage) Delete(_ string) error { return fmt.Errorf("%s", s) } -func (s discardStorage) URL(_, _ string) (*url.URL, error) { +func (s discardStorage) URL(_, _ string, _ url.Values) (*url.URL, error) { return nil, fmt.Errorf("%s", s) } diff --git a/modules/storage/helper_test.go b/modules/storage/helper_test.go index f4c2d0467f7e7..62ebd8753c89b 100644 --- a/modules/storage/helper_test.go +++ b/modules/storage/helper_test.go @@ -37,7 +37,7 @@ func Test_discardStorage(t *testing.T) { assert.Error(t, err, string(tt)) } { - got, err := tt.URL("path", "name") + got, err := tt.URL("path", "name", nil) assert.Nil(t, got) assert.Errorf(t, err, string(tt)) } diff --git a/modules/storage/local.go b/modules/storage/local.go index 9bb532f1df812..00c7f668aa2c3 100644 --- a/modules/storage/local.go +++ b/modules/storage/local.go @@ -114,7 +114,7 @@ func (l *LocalStorage) Delete(path string) error { } // URL gets the redirect URL to a file -func (l *LocalStorage) URL(path, name string) (*url.URL, error) { +func (l *LocalStorage) URL(path, name string, reqParams url.Values) (*url.URL, error) { return nil, ErrURLNotSupported } diff --git a/modules/storage/minio.go b/modules/storage/minio.go index b58ab67dc747a..becc013dc2c26 100644 --- a/modules/storage/minio.go +++ b/modules/storage/minio.go @@ -235,8 +235,12 @@ func (m *MinioStorage) Delete(path string) error { } // URL gets the redirect URL to a file. The presigned link is valid for 5 minutes. -func (m *MinioStorage) URL(path, name string) (*url.URL, error) { - reqParams := make(url.Values) +func (m *MinioStorage) URL(path, name string, serveDirectReqParams url.Values) (*url.URL, error) { + // copy serveDirectReqParams + reqParams, err := url.ParseQuery(serveDirectReqParams.Encode()) + if err != nil { + return nil, err + } // TODO it may be good to embed images with 'inline' like ServeData does, but we don't want to have to read the file, do we? reqParams.Set("response-content-disposition", "attachment; filename=\""+quoteEscaper.Replace(name)+"\"") u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(path), 5*time.Minute, reqParams) diff --git a/modules/storage/storage.go b/modules/storage/storage.go index 8f970b5dfccc9..52a250080cd4a 100644 --- a/modules/storage/storage.go +++ b/modules/storage/storage.go @@ -63,7 +63,7 @@ type ObjectStorage interface { Save(path string, r io.Reader, size int64) (int64, error) Stat(path string) (os.FileInfo, error) Delete(path string) error - URL(path, name string) (*url.URL, error) + URL(path, name string, reqParams url.Values) (*url.URL, error) IterateObjects(path string, iterator func(path string, obj Object) error) error } diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go index 35e3ee6906837..5b915f6cb211a 100644 --- a/routers/api/actions/artifacts.go +++ b/routers/api/actions/artifacts.go @@ -429,7 +429,7 @@ func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) { for _, artifact := range artifacts { var downloadURL string if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect { - u, err := ar.fs.URL(artifact.StoragePath, artifact.ArtifactName) + u, err := ar.fs.URL(artifact.StoragePath, artifact.ArtifactName, nil) if err != nil && !errors.Is(err, storage.ErrURLNotSupported) { log.Error("Error getting serve direct url: %v", err) } diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go index dde9caf4f26b2..bc33d0ecaa590 100644 --- a/routers/api/actions/artifactsv4.go +++ b/routers/api/actions/artifactsv4.go @@ -449,7 +449,7 @@ func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) { respData := GetSignedArtifactURLResponse{} if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect { - u, err := storage.ActionsArtifacts.URL(artifact.StoragePath, artifact.ArtifactPath) + u, err := storage.ActionsArtifacts.URL(artifact.StoragePath, artifact.ArtifactPath, nil) if u != nil && err == nil { respData.SignedUrl = u.String() } diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index acdc513468669..97ed031526a76 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -715,7 +715,9 @@ func DeleteManifest(ctx *context.Context) { } func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) { - s, u, _, err := packages_service.GetPackageBlobStream(ctx, pfd.File, pfd.Blob) + serveDirectReqParams := make(url.Values) + serveDirectReqParams.Set("response-content-type", pfd.Properties.GetByName(container_module.PropertyMediaType)) + s, u, _, err := packages_service.GetPackageBlobStream(ctx, pfd.File, pfd.Blob, serveDirectReqParams) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go index c3f1c4a87ee4b..7106394eeab92 100644 --- a/routers/api/packages/maven/maven.go +++ b/routers/api/packages/maven/maven.go @@ -215,7 +215,7 @@ func servePackageFile(ctx *context.Context, params parameters, serveContent bool return } - s, u, _, err := packages_service.GetPackageBlobStream(ctx, pf, pb) + s, u, _, err := packages_service.GetPackageBlobStream(ctx, pf, pb, nil) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 979f5f30b9ecd..59f15d0a66601 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -203,7 +203,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) { if setting.LFS.Storage.MinioConfig.ServeDirect { // If we have a signed url (S3, object storage), redirect to this directly. - u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name()) + u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name(), nil) if u != nil && err == nil { ctx.Redirect(u.String()) return @@ -328,7 +328,7 @@ func download(ctx *context.APIContext, archiveName string, archiver *repo_model. rPath := archiver.RelativePath() if setting.RepoArchive.Storage.MinioConfig.ServeDirect { // If we have a signed url (S3, object storage), redirect to this directly. - u, err := storage.RepoArchives.URL(rPath, downloadName) + u, err := storage.RepoArchives.URL(rPath, downloadName, nil) if u != nil && err == nil { ctx.Redirect(u.String()) return diff --git a/routers/web/base.go b/routers/web/base.go index 78dde57fa6f85..285d1ecddc342 100644 --- a/routers/web/base.go +++ b/routers/web/base.go @@ -39,7 +39,7 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/") rPath = util.PathJoinRelX(rPath) - u, err := objStore.URL(rPath, path.Base(rPath)) + u, err := objStore.URL(rPath, path.Base(rPath), nil) if err != nil { if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) { log.Warn("Unable to find %s %s", prefix, rPath) diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 12909bddd55bf..2d475d7051717 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -626,7 +626,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) { if len(artifacts) == 1 && artifacts[0].ArtifactName+".zip" == artifacts[0].ArtifactPath && artifacts[0].ContentEncoding == "application/zip" { art := artifacts[0] if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect { - u, err := storage.ActionsArtifacts.URL(art.StoragePath, art.ArtifactPath) + u, err := storage.ActionsArtifacts.URL(art.StoragePath, art.ArtifactPath, nil) if u != nil && err == nil { ctx.Redirect(u.String()) return diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go index f0c5622aec73d..870784db25915 100644 --- a/routers/web/repo/attachment.go +++ b/routers/web/repo/attachment.go @@ -129,7 +129,7 @@ func ServeAttachment(ctx *context.Context, uuid string) { if setting.Attachment.Storage.MinioConfig.ServeDirect { // If we have a signed url (S3, object storage), redirect to this directly. - u, err := storage.Attachments.URL(attach.RelativePath(), attach.Name) + u, err := storage.Attachments.URL(attach.RelativePath(), attach.Name, nil) if u != nil && err == nil { ctx.Redirect(u.String()) diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go index c4a8baecca352..a5dc9caa2afff 100644 --- a/routers/web/repo/download.go +++ b/routers/web/repo/download.go @@ -55,7 +55,7 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim if setting.LFS.Storage.MinioConfig.ServeDirect { // If we have a signed url (S3, object storage), redirect to this directly. - u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name()) + u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name(), nil) if u != nil && err == nil { ctx.Redirect(u.String()) return nil diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index c1eda8b674760..ac1ba1e27fca7 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -494,7 +494,7 @@ func download(ctx *context.Context, archiveName string, archiver *repo_model.Rep rPath := archiver.RelativePath() if setting.RepoArchive.Storage.MinioConfig.ServeDirect { // If we have a signed url (S3, object storage), redirect to this directly. - u, err := storage.RepoArchives.URL(rPath, downloadName) + u, err := storage.RepoArchives.URL(rPath, downloadName, nil) if u != nil && err == nil { ctx.Redirect(u.String()) return diff --git a/services/lfs/server.go b/services/lfs/server.go index ace501e15f0df..fb531c87290bd 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -455,7 +455,7 @@ func buildObjectResponse(rc *requestContext, pointer lfs_module.Pointer, downloa var link *lfs_module.Link if setting.LFS.Storage.MinioConfig.ServeDirect { // If we have a signed url (S3, object storage), redirect to this directly. - u, err := storage.LFS.URL(pointer.RelativePath(), pointer.Oid) + u, err := storage.LFS.URL(pointer.RelativePath(), pointer.Oid, nil) if u != nil && err == nil { // Presigned url does not need the Authorization header // https://github.com/go-gitea/gitea/issues/21525 diff --git a/services/packages/packages.go b/services/packages/packages.go index 64b1ddd869632..95579be34be5b 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -596,12 +596,12 @@ func GetPackageFileStream(ctx context.Context, pf *packages_model.PackageFile) ( return nil, nil, nil, err } - return GetPackageBlobStream(ctx, pf, pb) + return GetPackageBlobStream(ctx, pf, pb, nil) } // GetPackageBlobStream returns the content of the specific package blob // If the storage supports direct serving and it's enabled, only the direct serving url is returned. -func GetPackageBlobStream(ctx context.Context, pf *packages_model.PackageFile, pb *packages_model.PackageBlob) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) { +func GetPackageBlobStream(ctx context.Context, pf *packages_model.PackageFile, pb *packages_model.PackageBlob, serveDirectReqParams url.Values) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) { key := packages_module.BlobHash256Key(pb.HashSHA256) cs := packages_module.NewContentStore() @@ -611,7 +611,7 @@ func GetPackageBlobStream(ctx context.Context, pf *packages_model.PackageFile, p var err error if cs.ShouldServeDirect() { - u, err = cs.GetServeDirectURL(key, pf.Name) + u, err = cs.GetServeDirectURL(key, pf.Name, serveDirectReqParams) if err != nil && !errors.Is(err, storage.ErrURLNotSupported) { log.Error("Error getting serve direct url: %v", err) } From a3b7b98336262ce4bbcc31fb0ee5ed0cedb04c6a Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 1 Nov 2024 22:34:09 -0700 Subject: [PATCH 17/49] Fix broken image when editing comment with non-image attachments (#32319) (#32345) Backport #32319 Fix #32316 --------- Co-authored-by: yp05327 <576951401@qq.com> --- web_src/js/features/repo-issue-edit.js | 7 +++++-- web_src/js/utils/image.js | 4 ++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/web_src/js/features/repo-issue-edit.js b/web_src/js/features/repo-issue-edit.js index 919daa9c503d9..5396a51788dec 100644 --- a/web_src/js/features/repo-issue-edit.js +++ b/web_src/js/features/repo-issue-edit.js @@ -4,6 +4,7 @@ import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkd import {createDropzone} from './dropzone.js'; import {GET, POST} from '../modules/fetch.js'; import {hideElem, showElem} from '../utils/dom.js'; +import {isImageFile} from '../utils/image.js'; import {attachRefIssueContextPopup} from './contextpopup.js'; import {initCommentContent, initMarkupContent} from '../markup/content.js'; @@ -84,10 +85,12 @@ async function onEditContent(event) { for (const attachment of data) { const imgSrc = `${dropzone.getAttribute('data-link-url')}/${attachment.uuid}`; dz.emit('addedfile', attachment); - dz.emit('thumbnail', attachment, imgSrc); + if (isImageFile(attachment.name)) { + dz.emit('thumbnail', attachment, imgSrc); + dropzone.querySelector(`img[src='${imgSrc}']`).style.maxWidth = '100%'; + } dz.emit('complete', attachment); fileUuidDict[attachment.uuid] = {submitted: true}; - dropzone.querySelector(`img[src='${imgSrc}']`).style.maxWidth = '100%'; const input = document.createElement('input'); input.id = attachment.uuid; input.name = 'files'; diff --git a/web_src/js/utils/image.js b/web_src/js/utils/image.js index ed5d98e35ad0b..3333ad3e541a4 100644 --- a/web_src/js/utils/image.js +++ b/web_src/js/utils/image.js @@ -45,3 +45,7 @@ export async function imageInfo(blob) { return {width, dppx}; } + +export function isImageFile(name) { + return /\.(jpe?g|png|gif|webp|svg|heic)$/i.test(name); +} From 7430d069b31df06c23c659b8bc6cd81b651c8bed Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 4 Nov 2024 19:43:30 -0800 Subject: [PATCH 18/49] Fix created_unix for mirroring (#32342) (#32406) Fix #32233 Backport #32342 --- modules/repository/repo.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/repository/repo.go b/modules/repository/repo.go index 3d1899b2fe006..def2220b17d12 100644 --- a/modules/repository/repo.go +++ b/modules/repository/repo.go @@ -340,9 +340,10 @@ func pullMirrorReleaseSync(ctx context.Context, repo *repo_model.Repository, git for _, tag := range updates { if _, err := db.GetEngine(ctx).Where("repo_id = ? AND lower_tag_name = ?", repo.ID, strings.ToLower(tag.Name)). - Cols("sha1"). + Cols("sha1", "created_unix"). Update(&repo_model.Release{ - Sha1: tag.Object.String(), + Sha1: tag.Object.String(), + CreatedUnix: timeutil.TimeStamp(tag.Tagger.When.Unix()), }); err != nil { return fmt.Errorf("unable to update tag %s for pull-mirror Repo[%d:%s/%s]: %w", tag.Name, repo.ID, repo.OwnerName, repo.Name, err) } From 936847b3da22cdedf7bf8455adbdd0fa71e94a7a Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 5 Nov 2024 14:13:19 +0800 Subject: [PATCH 19/49] Quick fix milestone deadline 9999 for 1.22 (#32423) --- models/issues/milestone.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/issues/milestone.go b/models/issues/milestone.go index db0312adf0057..8affe14d71c02 100644 --- a/models/issues/milestone.go +++ b/models/issues/milestone.go @@ -84,7 +84,7 @@ func (m *Milestone) BeforeUpdate() { // this object. func (m *Milestone) AfterLoad() { m.NumOpenIssues = m.NumIssues - m.NumClosedIssues - if m.DeadlineUnix.Year() == 9999 { + if m.DeadlineUnix.Year() >= 9999 { return } From 16e51e91a1fdca1d42ea0d464c33bf7b2ca76266 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 5 Nov 2024 19:22:11 -0800 Subject: [PATCH 20/49] Only query team tables if repository is under org when getting assignees (#32414) (#32426) backport #32414 It's unnecessary to query the team table if the repository is not under organization when getting assignees. --- models/repo/user_repo.go | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go index c305603e02070..ecc9216950738 100644 --- a/models/repo/user_repo.go +++ b/models/repo/user_repo.go @@ -110,26 +110,28 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us return nil, err } - additionalUserIDs := make([]int64, 0, 10) - if err = e.Table("team_user"). - Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id"). - Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id"). - Where("`team_repo`.repo_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))", - repo.ID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests). - Distinct("`team_user`.uid"). - Select("`team_user`.uid"). - Find(&additionalUserIDs); err != nil { - return nil, err - } - uniqueUserIDs := make(container.Set[int64]) uniqueUserIDs.AddMultiple(userIDs...) - uniqueUserIDs.AddMultiple(additionalUserIDs...) + + if repo.Owner.IsOrganization() { + additionalUserIDs := make([]int64, 0, 10) + if err = e.Table("team_user"). + Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id"). + Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id"). + Where("`team_repo`.repo_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))", + repo.ID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests). + Distinct("`team_user`.uid"). + Select("`team_user`.uid"). + Find(&additionalUserIDs); err != nil { + return nil, err + } + uniqueUserIDs.AddMultiple(additionalUserIDs...) + } // Leave a seat for owner itself to append later, but if owner is an organization // and just waste 1 unit is cheaper than re-allocate memory once. users := make([]*user_model.User, 0, len(uniqueUserIDs)+1) - if len(userIDs) > 0 { + if len(uniqueUserIDs) > 0 { if err = e.In("id", uniqueUserIDs.Values()). Where(builder.Eq{"`user`.is_active": true}). OrderBy(user_model.GetOrderByName()). From 22a93c1cdc63340ec4fb4805ed140e57b55fd57b Mon Sep 17 00:00:00 2001 From: Giteabot Date: Fri, 8 Nov 2024 09:13:49 +0800 Subject: [PATCH 21/49] Only provide the commit summary for Discord webhook push events (#32432) (#32447) Backport #32432 by @kemzeb Resolves #32371. #31970 should have just showed the commit summary, but `strings.SplitN()` was misused such that we did not perform any splitting at all and just used the message. This was not caught in the unit test made in that PR since the test commit summary was > 50 (which truncated away the commit description). This snapshot resolves this and adds another unit test to ensure that we only show the commit summary. Co-authored-by: Kemal Zebari <60799661+kemzeb@users.noreply.github.com> --- services/webhook/discord.go | 2 +- services/webhook/discord_test.go | 16 +++++++++++++++- services/webhook/general_test.go | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/services/webhook/discord.go b/services/webhook/discord.go index f93337e53a48c..21757953423f4 100644 --- a/services/webhook/discord.go +++ b/services/webhook/discord.go @@ -151,7 +151,7 @@ func (d discordConvertor) Push(p *api.PushPayload) (DiscordPayload, error) { // for each commit, generate attachment text for i, commit := range p.Commits { // limit the commit message display to just the summary, otherwise it would be hard to read - message := strings.TrimRight(strings.SplitN(commit.Message, "\n", 1)[0], "\r") + message := strings.TrimRight(strings.SplitN(commit.Message, "\n", 2)[0], "\r") // a limit of 50 is set because GitHub does the same if utf8.RuneCountInString(message) > 50 { diff --git a/services/webhook/discord_test.go b/services/webhook/discord_test.go index fbb4b24ef12dd..36b99d452ea8e 100644 --- a/services/webhook/discord_test.go +++ b/services/webhook/discord_test.go @@ -80,12 +80,26 @@ func TestDiscordPayload(t *testing.T) { assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) }) - t.Run("PushWithLongCommitMessage", func(t *testing.T) { + t.Run("PushWithMultilineCommitMessage", func(t *testing.T) { p := pushTestMultilineCommitMessagePayload() pl, err := dc.Push(p) require.NoError(t, err) + assert.Len(t, pl.Embeds, 1) + assert.Equal(t, "[test/repo:test] 2 new commits", pl.Embeds[0].Title) + assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) chore: This is a commit summary - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) chore: This is a commit summary - user1", pl.Embeds[0].Description) + assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name) + assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL) + assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL) + }) + + t.Run("PushWithLongCommitSummary", func(t *testing.T) { + p := pushTestPayloadWithCommitMessage("This is a commit summary ⚠️⚠️⚠️⚠️ containing 你好 ⚠️⚠️️\n\nThis is the message body") + + pl, err := dc.Push(p) + require.NoError(t, err) + assert.Len(t, pl.Embeds, 1) assert.Equal(t, "[test/repo:test] 2 new commits", pl.Embeds[0].Title) assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) This is a commit summary ⚠️⚠️⚠️⚠️ containing 你好... - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) This is a commit summary ⚠️⚠️⚠️⚠️ containing 你好... - user1", pl.Embeds[0].Description) diff --git a/services/webhook/general_test.go b/services/webhook/general_test.go index 04c6b595a8aab..3feda2b09e8a5 100644 --- a/services/webhook/general_test.go +++ b/services/webhook/general_test.go @@ -68,7 +68,7 @@ func pushTestPayload() *api.PushPayload { } func pushTestMultilineCommitMessagePayload() *api.PushPayload { - return pushTestPayloadWithCommitMessage("This is a commit summary ⚠️⚠️⚠️⚠️ containing 你好 ⚠️⚠️️\n\nThis is the message body.") + return pushTestPayloadWithCommitMessage("chore: This is a commit summary\n\nThis is a commit description.") } func pushTestPayloadWithCommitMessage(message string) *api.PushPayload { From 62d84331947014cc658560ba07bd9f5fec3c8175 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Mon, 11 Nov 2024 04:05:42 +0800 Subject: [PATCH 22/49] Fix mermaid diagram height when initially hidden (#32457) (#32464) Backport #32457 by @silverwind In a hidden iframe, `document.body.clientHeight` is not reliable. Use `IntersectionObserver` to detect the visibility change and update the height there. Fixes: https://github.com/go-gitea/gitea/issues/32392 image Co-authored-by: silverwind --- web_src/js/markup/mermaid.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/web_src/js/markup/mermaid.js b/web_src/js/markup/mermaid.js index 0549fb3e315d4..e420ff12a2398 100644 --- a/web_src/js/markup/mermaid.js +++ b/web_src/js/markup/mermaid.js @@ -56,10 +56,21 @@ export async function renderMermaid() { btn.setAttribute('data-clipboard-text', source); mermaidBlock.append(btn); + const updateIframeHeight = () => { + iframe.style.height = `${iframe.contentWindow.document.body.clientHeight}px`; + }; + + // update height when element's visibility state changes, for example when the diagram is inside + // a
+ block and the
block becomes visible upon user interaction, it + // would initially set a incorrect height and the correct height is set during this callback. + (new IntersectionObserver(() => { + updateIframeHeight(); + }, {root: document.documentElement})).observe(iframe); + iframe.addEventListener('load', () => { pre.replaceWith(mermaidBlock); mermaidBlock.classList.remove('tw-hidden'); - iframe.style.height = `${iframe.contentWindow.document.body.clientHeight}px`; + updateIframeHeight(); setTimeout(() => { // avoid flash of iframe background mermaidBlock.classList.remove('is-loading'); iframe.classList.remove('tw-invisible'); From eb5733636b68643abe75d2ef085912145f5a263a Mon Sep 17 00:00:00 2001 From: Giteabot Date: Mon, 11 Nov 2024 07:49:59 +0800 Subject: [PATCH 23/49] Fix broken releases when re-pushing tags (#32435) (#32449) Backport #32435 by @Zettat123 Fix #32427 --------- Co-authored-by: Zettat123 Co-authored-by: Lunny Xiao --- services/repository/push.go | 19 +++++++----- tests/integration/repo_tag_test.go | 47 ++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/services/repository/push.go b/services/repository/push.go index 8b81588c07285..ec001a8510cd4 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -320,9 +320,10 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo } releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{ - RepoID: repo.ID, - TagNames: tags, - IncludeTags: true, + RepoID: repo.ID, + TagNames: tags, + IncludeDrafts: true, + IncludeTags: true, }) if err != nil { return fmt.Errorf("db.Find[repo_model.Release]: %w", err) @@ -409,13 +410,17 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo newReleases = append(newReleases, rel) } else { - rel.Title = parts[0] - rel.Note = note rel.Sha1 = commit.ID.String() rel.CreatedUnix = timeutil.TimeStamp(createdAt.Unix()) rel.NumCommits = commitsCount - if rel.IsTag && author != nil { - rel.PublisherID = author.ID + if rel.IsTag { + rel.Title = parts[0] + rel.Note = note + if author != nil { + rel.PublisherID = author.ID + } + } else { + rel.IsDraft = false } if err = repo_model.UpdateRelease(ctx, rel); err != nil { return fmt.Errorf("Update: %w", err) diff --git a/tests/integration/repo_tag_test.go b/tests/integration/repo_tag_test.go index d649f041ccf57..6e2b0db27e03f 100644 --- a/tests/integration/repo_tag_test.go +++ b/tests/integration/repo_tag_test.go @@ -4,17 +4,20 @@ package integration import ( + "fmt" "net/http" "net/url" "testing" "code.gitea.io/gitea/models" + auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" + api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/services/release" "code.gitea.io/gitea/tests" @@ -117,3 +120,47 @@ func TestCreateNewTagProtected(t *testing.T) { assert.NoError(t, err) } } + +func TestRepushTag(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + session := loginUser(t, owner.LowerName) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + httpContext := NewAPITestContext(t, owner.Name, repo.Name) + + dstPath := t.TempDir() + + u.Path = httpContext.GitPath() + u.User = url.UserPassword(owner.Name, userPassword) + + doGitClone(dstPath, u)(t) + + // create and push a tag + _, _, err := git.NewCommand(git.DefaultContext, "tag", "v2.0").RunStdString(&git.RunOpts{Dir: dstPath}) + assert.NoError(t, err) + _, _, err = git.NewCommand(git.DefaultContext, "push", "origin", "--tags", "v2.0").RunStdString(&git.RunOpts{Dir: dstPath}) + assert.NoError(t, err) + // create a release for the tag + createdRelease := createNewReleaseUsingAPI(t, session, token, owner, repo, "v2.0", "", "Release of v2.0", "desc") + assert.False(t, createdRelease.IsDraft) + // delete the tag + _, _, err = git.NewCommand(git.DefaultContext, "push", "origin", "--delete", "v2.0").RunStdString(&git.RunOpts{Dir: dstPath}) + assert.NoError(t, err) + // query the release by API and it should be a draft + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, "v2.0")) + resp := MakeRequest(t, req, http.StatusOK) + var respRelease *api.Release + DecodeJSON(t, resp, &respRelease) + assert.True(t, respRelease.IsDraft) + // re-push the tag + _, _, err = git.NewCommand(git.DefaultContext, "push", "origin", "--tags", "v2.0").RunStdString(&git.RunOpts{Dir: dstPath}) + assert.NoError(t, err) + // query the release by API and it should not be a draft + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, "v2.0")) + resp = MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &respRelease) + assert.False(t, respRelease.IsDraft) + }) +} From b48df1082eccda2a11e8891f50d8c6ce47fa7a85 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Tue, 12 Nov 2024 11:26:26 +0800 Subject: [PATCH 24/49] cargo registry - respect renamed dependencies (#32430) (#32478) Backport #32430 by usbalbin Co-authored-by: Albin Hedman Co-authored-by: wxiaoguang --- modules/packages/cargo/parser.go | 11 ++++- modules/packages/cargo/parser_test.go | 58 +++++++++++++++++++-------- 2 files changed, 52 insertions(+), 17 deletions(-) diff --git a/modules/packages/cargo/parser.go b/modules/packages/cargo/parser.go index 36cd44df847aa..d82e0e2f058d7 100644 --- a/modules/packages/cargo/parser.go +++ b/modules/packages/cargo/parser.go @@ -136,8 +136,16 @@ func parsePackage(r io.Reader) (*Package, error) { dependencies := make([]*Dependency, 0, len(meta.Deps)) for _, dep := range meta.Deps { + // https://doc.rust-lang.org/cargo/reference/registry-web-api.html#publish + // It is a string of the new package name if the dependency is renamed, otherwise empty + name := dep.ExplicitNameInToml + pkg := &dep.Name + if name == "" { + name = dep.Name + pkg = nil + } dependencies = append(dependencies, &Dependency{ - Name: dep.Name, + Name: name, Req: dep.VersionReq, Features: dep.Features, Optional: dep.Optional, @@ -145,6 +153,7 @@ func parsePackage(r io.Reader) (*Package, error) { Target: dep.Target, Kind: dep.Kind, Registry: dep.Registry, + Package: pkg, }) } diff --git a/modules/packages/cargo/parser_test.go b/modules/packages/cargo/parser_test.go index 2230a5b4999c9..0a120b8074cdf 100644 --- a/modules/packages/cargo/parser_test.go +++ b/modules/packages/cargo/parser_test.go @@ -13,16 +13,16 @@ import ( "github.com/stretchr/testify/assert" ) -const ( - description = "Package Description" - author = "KN4CK3R" - homepage = "https://gitea.io/" - license = "MIT" -) - func TestParsePackage(t *testing.T) { - createPackage := func(name, version string) io.Reader { - metadata := `{ + const ( + description = "Package Description" + author = "KN4CK3R" + homepage = "https://gitea.io/" + license = "MIT" + payload = "gitea test dummy payload" // a fake payload for test only + ) + makeDefaultPackageMeta := func(name, version string) string { + return `{ "name":"` + name + `", "vers":"` + version + `", "description":"` + description + `", @@ -36,18 +36,19 @@ func TestParsePackage(t *testing.T) { "homepage":"` + homepage + `", "license":"` + license + `" }` - + } + createPackage := func(metadata string) io.Reader { var buf bytes.Buffer binary.Write(&buf, binary.LittleEndian, uint32(len(metadata))) buf.WriteString(metadata) - binary.Write(&buf, binary.LittleEndian, uint32(4)) - buf.WriteString("test") + binary.Write(&buf, binary.LittleEndian, uint32(len(payload))) + buf.WriteString(payload) return &buf } t.Run("InvalidName", func(t *testing.T) { for _, name := range []string{"", "0test", "-test", "_test", strings.Repeat("a", 65)} { - data := createPackage(name, "1.0.0") + data := createPackage(makeDefaultPackageMeta(name, "1.0.0")) cp, err := ParsePackage(data) assert.Nil(t, cp) @@ -57,7 +58,7 @@ func TestParsePackage(t *testing.T) { t.Run("InvalidVersion", func(t *testing.T) { for _, version := range []string{"", "1.", "-1.0", "1.0.0/1"} { - data := createPackage("test", version) + data := createPackage(makeDefaultPackageMeta("test", version)) cp, err := ParsePackage(data) assert.Nil(t, cp) @@ -66,7 +67,7 @@ func TestParsePackage(t *testing.T) { }) t.Run("Valid", func(t *testing.T) { - data := createPackage("test", "1.0.0") + data := createPackage(makeDefaultPackageMeta("test", "1.0.0")) cp, err := ParsePackage(data) assert.NotNil(t, cp) @@ -78,9 +79,34 @@ func TestParsePackage(t *testing.T) { assert.Equal(t, []string{author}, cp.Metadata.Authors) assert.Len(t, cp.Metadata.Dependencies, 1) assert.Equal(t, "dep", cp.Metadata.Dependencies[0].Name) + assert.Nil(t, cp.Metadata.Dependencies[0].Package) assert.Equal(t, homepage, cp.Metadata.ProjectURL) assert.Equal(t, license, cp.Metadata.License) content, _ := io.ReadAll(cp.Content) - assert.Equal(t, "test", string(content)) + assert.Equal(t, payload, string(content)) + }) + + t.Run("Renamed", func(t *testing.T) { + data := createPackage(`{ + "name":"test-pkg", + "vers":"1.0", + "description":"test-desc", + "authors": ["test-author"], + "deps":[ + { + "name":"dep-renamed", + "explicit_name_in_toml":"dep-explicit", + "version_req":"1.0" + } + ], + "homepage":"https://gitea.io/", + "license":"MIT" +}`) + cp, err := ParsePackage(data) + assert.NoError(t, err) + assert.Equal(t, "test-pkg", cp.Name) + assert.Equal(t, "https://gitea.io/", cp.Metadata.ProjectURL) + assert.Equal(t, "dep-explicit", cp.Metadata.Dependencies[0].Name) + assert.Equal(t, "dep-renamed", *cp.Metadata.Dependencies[0].Package) }) } From 26437a03b0dc429e179ef0461e83ecb1b1474017 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 12 Nov 2024 14:09:47 +0800 Subject: [PATCH 25/49] Disable Oauth check if oauth disabled (#32368) (#32480) Partially backport Disable Oauth check if oauth disabled #32368 --- services/auth/oauth2.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go index 46d8510143675..861effd0b0b93 100644 --- a/services/auth/oauth2.go +++ b/services/auth/oauth2.go @@ -27,6 +27,9 @@ var ( // CheckOAuthAccessToken returns uid of user from oauth token func CheckOAuthAccessToken(ctx context.Context, accessToken string) int64 { + if !setting.OAuth2.Enabled { + return 0 + } // JWT tokens require a "." if !strings.Contains(accessToken, ".") { return 0 From ef339713c25253980f98d4c28b3fe5326538664b Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 13 Nov 2024 10:26:37 +0800 Subject: [PATCH 26/49] Refactor internal routers (partial backport, auth token const time comparing) (#32473) (#32479) Partially backport #32473. LFS related changes are not in 1.22, so skip them. 1. Ignore non-existing repos during migrations 2. Improve ReadBatchLine's comment 3. Use `X-Gitea-Internal-Auth` header for internal API calls and make the comparing constant time (it wasn't a serous problem because in a real world it's nearly impossible to timing-attack the token, but indeed security related and good to fix and backport) 4. Fix route mock nil check --- models/migrations/v1_21/v276.go | 5 ++++- modules/git/batch_reader.go | 5 ++--- modules/private/internal.go | 2 +- modules/web/route.go | 13 +++++++++++-- routers/private/internal.go | 18 ++++++++++-------- 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/models/migrations/v1_21/v276.go b/models/migrations/v1_21/v276.go index ed1bc3bda5241..15177bf0406df 100644 --- a/models/migrations/v1_21/v276.go +++ b/models/migrations/v1_21/v276.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/modules/git" giturl "code.gitea.io/gitea/modules/git/url" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "xorm.io/xorm" ) @@ -163,7 +164,9 @@ func migratePushMirrors(x *xorm.Engine) error { func getRemoteAddress(ownerName, repoName, remoteName string) (string, error) { repoPath := filepath.Join(setting.RepoRootPath, strings.ToLower(ownerName), strings.ToLower(repoName)+".git") - + if exist, _ := util.IsExist(repoPath); !exist { + return "", nil + } remoteURL, err := git.GetRemoteAddress(context.Background(), repoPath, remoteName) if err != nil { return "", fmt.Errorf("get remote %s's address of %s/%s failed: %v", remoteName, ownerName, repoName, err) diff --git a/modules/git/batch_reader.go b/modules/git/batch_reader.go index 3b1a466b2eaa9..7dfda721554dd 100644 --- a/modules/git/batch_reader.go +++ b/modules/git/batch_reader.go @@ -146,9 +146,8 @@ func catFileBatch(ctx context.Context, repoPath string) (WriteCloserError, *bufi } // ReadBatchLine reads the header line from cat-file --batch -// We expect: -// SP SP LF -// sha is a hex encoded here +// We expect: SP SP LF +// then leaving the rest of the stream " LF" to be read func ReadBatchLine(rd *bufio.Reader) (sha []byte, typ string, size int64, err error) { typ, err = rd.ReadString('\n') if err != nil { diff --git a/modules/private/internal.go b/modules/private/internal.go index 9c330a24a865b..c7e7773524d79 100644 --- a/modules/private/internal.go +++ b/modules/private/internal.go @@ -43,7 +43,7 @@ Ensure you are running in the correct environment or set the correct configurati req := httplib.NewRequest(url, method). SetContext(ctx). Header("X-Real-IP", getClientIP()). - Header("Authorization", fmt.Sprintf("Bearer %s", setting.InternalToken)). + Header("X-Gitea-Internal-Auth", fmt.Sprintf("Bearer %s", setting.InternalToken)). SetTLSClientConfig(&tls.Config{ InsecureSkipVerify: true, ServerName: setting.Domain, diff --git a/modules/web/route.go b/modules/web/route.go index 805fcb4411529..cda15aa0c77a8 100644 --- a/modules/web/route.go +++ b/modules/web/route.go @@ -5,6 +5,7 @@ package web import ( "net/http" + "reflect" "strings" "code.gitea.io/gitea/modules/web/middleware" @@ -80,15 +81,23 @@ func (r *Route) getPattern(pattern string) string { return strings.TrimSuffix(newPattern, "/") } +func isNilOrFuncNil(v any) bool { + if v == nil { + return true + } + r := reflect.ValueOf(v) + return r.Kind() == reflect.Func && r.IsNil() +} + func (r *Route) wrapMiddlewareAndHandler(h []any) ([]func(http.Handler) http.Handler, http.HandlerFunc) { handlerProviders := make([]func(http.Handler) http.Handler, 0, len(r.curMiddlewares)+len(h)+1) for _, m := range r.curMiddlewares { - if m != nil { + if !isNilOrFuncNil(m) { handlerProviders = append(handlerProviders, toHandlerProvider(m)) } } for _, m := range h { - if h != nil { + if !isNilOrFuncNil(m) { handlerProviders = append(handlerProviders, toHandlerProvider(m)) } } diff --git a/routers/private/internal.go b/routers/private/internal.go index ede310113ca45..439af74f16c1c 100644 --- a/routers/private/internal.go +++ b/routers/private/internal.go @@ -5,6 +5,7 @@ package private import ( + "crypto/subtle" "net/http" "strings" @@ -18,22 +19,23 @@ import ( chi_middleware "github.com/go-chi/chi/v5/middleware" ) -// CheckInternalToken check internal token is set -func CheckInternalToken(next http.Handler) http.Handler { +func authInternal(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - tokens := req.Header.Get("Authorization") - fields := strings.SplitN(tokens, " ", 2) if setting.InternalToken == "" { log.Warn(`The INTERNAL_TOKEN setting is missing from the configuration file: %q, internal API can't work.`, setting.CustomConf) http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) return } - if len(fields) != 2 || fields[0] != "Bearer" || fields[1] != setting.InternalToken { + + tokens := req.Header.Get("X-Gitea-Internal-Auth") // TODO: use something like JWT or HMAC to avoid passing the token in the clear + after, found := strings.CutPrefix(tokens, "Bearer ") + authSucceeded := found && subtle.ConstantTimeCompare([]byte(after), []byte(setting.InternalToken)) == 1 + if !authSucceeded { log.Debug("Forbidden attempt to access internal url: Authorization header: %s", tokens) http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) - } else { - next.ServeHTTP(w, req) + return } + next.ServeHTTP(w, req) }) } @@ -51,7 +53,7 @@ func bind[T any](_ T) any { func Routes() *web.Route { r := web.NewRoute() r.Use(context.PrivateContexter()) - r.Use(CheckInternalToken) + r.Use(authInternal) // Log the real ip address of the request from SSH is really helpful for diagnosing sometimes. // Since internal API will be sent only from Gitea sub commands and it's under control (checked by InternalToken), we can trust the headers. r.Use(chi_middleware.RealIP) From 52a66d78d444451a5f2a9219635511f8791a0a6d Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Wed, 13 Nov 2024 18:40:52 +0100 Subject: [PATCH 27/49] Update nix development environment vor v1.22.x (#32495) just bump: * golang: v1.22.2 -> v1.22.9 * nodejs: v20.12.2 -> v20.18.0 * python: v3.12.3 -> v3.12.7 --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 0b2278f080265..1890b82dcfa7b 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", "type": "github" }, "original": { @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1715534503, - "narHash": "sha256-5ZSVkFadZbFP1THataCaSf0JH2cAH3S29hU9rrxTEqk=", + "lastModified": 1731139594, + "narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=", "owner": "nixos", "repo": "nixpkgs", - "rev": "2057814051972fa1453ddfb0d98badbea9b83c06", + "rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2", "type": "github" }, "original": { From a4263d341c1d0cfb4fd17279d00e2b14f9ca35c1 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Thu, 14 Nov 2024 02:47:56 +0800 Subject: [PATCH 28/49] Add a doctor check to disable the "Actions" unit for mirrors (#32424) (#32497) Backport #32424 by @Zettat123 Resolve #32232 Users can disable the "Actions" unit for all mirror repos by running ``` gitea doctor check --run disable-mirror-actions-unit --fix ``` Co-authored-by: Zettat123 --- services/doctor/actions.go | 70 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 services/doctor/actions.go diff --git a/services/doctor/actions.go b/services/doctor/actions.go new file mode 100644 index 0000000000000..7c44fb83920c3 --- /dev/null +++ b/services/doctor/actions.go @@ -0,0 +1,70 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package doctor + +import ( + "context" + "fmt" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + unit_model "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" + repo_service "code.gitea.io/gitea/services/repository" +) + +func disableMirrorActionsUnit(ctx context.Context, logger log.Logger, autofix bool) error { + var reposToFix []*repo_model.Repository + + for page := 1; ; page++ { + repos, _, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ + ListOptions: db.ListOptions{ + PageSize: repo_model.RepositoryListDefaultPageSize, + Page: page, + }, + Mirror: optional.Some(true), + }) + if err != nil { + return fmt.Errorf("SearchRepository: %w", err) + } + if len(repos) == 0 { + break + } + + for _, repo := range repos { + if repo.UnitEnabled(ctx, unit_model.TypeActions) { + reposToFix = append(reposToFix, repo) + } + } + } + + if len(reposToFix) == 0 { + logger.Info("Found no mirror with actions unit enabled") + } else { + logger.Warn("Found %d mirrors with actions unit enabled", len(reposToFix)) + } + if !autofix || len(reposToFix) == 0 { + return nil + } + + for _, repo := range reposToFix { + if err := repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit_model.Type{unit_model.TypeActions}); err != nil { + return err + } + } + logger.Info("Fixed %d mirrors with actions unit enabled", len(reposToFix)) + + return nil +} + +func init() { + Register(&Check{ + Title: "Disable the actions unit for all mirrors", + Name: "disable-mirror-actions-unit", + IsDefault: false, + Run: disableMirrorActionsUnit, + Priority: 9, + }) +} From f79f8e13e37fc699800318a20cb2866a326c467e Mon Sep 17 00:00:00 2001 From: Giteabot Date: Thu, 14 Nov 2024 12:47:04 +0800 Subject: [PATCH 29/49] Fix nil panic if repo doesn't exist (#32501) (#32502) Backport #32501 by wxiaoguang fix #32496 Co-authored-by: wxiaoguang --- models/activities/action.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/models/activities/action.go b/models/activities/action.go index a99df558cbd98..95132f9ced683 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -248,6 +248,9 @@ func (a *Action) GetActDisplayNameTitle(ctx context.Context) string { // GetRepoUserName returns the name of the action repository owner. func (a *Action) GetRepoUserName(ctx context.Context) string { a.loadRepo(ctx) + if a.Repo == nil { + return "(non-existing-repo)" + } return a.Repo.OwnerName } @@ -260,6 +263,9 @@ func (a *Action) ShortRepoUserName(ctx context.Context) string { // GetRepoName returns the name of the action repository. func (a *Action) GetRepoName(ctx context.Context) string { a.loadRepo(ctx) + if a.Repo == nil { + return "(non-existing-repo)" + } return a.Repo.Name } From 781310df77b6785a4435ae9c85a59eba6e73852c Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 14 Nov 2024 18:06:31 -0800 Subject: [PATCH 30/49] Trim title before insert/update to database to match the size requirements of database (#32498) (#32507) --- models/actions/run.go | 3 +++ models/actions/runner.go | 2 ++ models/actions/schedule.go | 2 ++ models/issues/issue_update.go | 4 ++++ models/issues/pull.go | 1 + models/project/project.go | 4 ++++ models/repo/release.go | 1 + services/release/release.go | 1 + 8 files changed, 18 insertions(+) diff --git a/models/actions/run.go b/models/actions/run.go index 4f886999e9cd2..fdddeeabaa6e2 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -261,6 +261,7 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin } // InsertRun inserts a run +// The title will be cut off at 255 characters if it's longer than 255 characters. func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWorkflow) error { ctx, committer, err := db.TxContext(ctx) if err != nil { @@ -273,6 +274,7 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork return err } run.Index = index + run.Title, _ = util.SplitStringAtByteN(run.Title, 255) if err := db.Insert(ctx, run); err != nil { return err @@ -386,6 +388,7 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error { if len(cols) > 0 { sess.Cols(cols...) } + run.Title, _ = util.SplitStringAtByteN(run.Title, 255) affected, err := sess.Update(run) if err != nil { return err diff --git a/models/actions/runner.go b/models/actions/runner.go index 9192925d5a455..ac582a2c379bb 100644 --- a/models/actions/runner.go +++ b/models/actions/runner.go @@ -242,6 +242,7 @@ func GetRunnerByID(ctx context.Context, id int64) (*ActionRunner, error) { // UpdateRunner updates runner's information. func UpdateRunner(ctx context.Context, r *ActionRunner, cols ...string) error { e := db.GetEngine(ctx) + r.Name, _ = util.SplitStringAtByteN(r.Name, 255) var err error if len(cols) == 0 { _, err = e.ID(r.ID).AllCols().Update(r) @@ -263,6 +264,7 @@ func DeleteRunner(ctx context.Context, id int64) error { // CreateRunner creates new runner. func CreateRunner(ctx context.Context, t *ActionRunner) error { + t.Name, _ = util.SplitStringAtByteN(t.Name, 255) return db.Insert(ctx, t) } diff --git a/models/actions/schedule.go b/models/actions/schedule.go index 3646a046a0f35..cd9add089ca84 100644 --- a/models/actions/schedule.go +++ b/models/actions/schedule.go @@ -12,6 +12,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" "github.com/robfig/cron/v3" @@ -71,6 +72,7 @@ func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error { // Loop through each schedule row for _, row := range rows { + row.Title, _ = util.SplitStringAtByteN(row.Title, 255) // Create new schedule row if err = db.Insert(ctx, row); err != nil { return err diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go index 147b7eb3b91b0..a0d290a30ea57 100644 --- a/models/issues/issue_update.go +++ b/models/issues/issue_update.go @@ -21,6 +21,7 @@ import ( "code.gitea.io/gitea/modules/references" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) @@ -138,6 +139,7 @@ func ChangeIssueTitle(ctx context.Context, issue *Issue, doer *user_model.User, } defer committer.Close() + issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255) if err = UpdateIssueCols(ctx, issue, "name"); err != nil { return fmt.Errorf("updateIssueCols: %w", err) } @@ -381,6 +383,7 @@ func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssue } // NewIssue creates new issue with labels for repository. +// The title will be cut off at 255 characters if it's longer than 255 characters. func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) { ctx, committer, err := db.TxContext(ctx) if err != nil { @@ -394,6 +397,7 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, la } issue.Index = idx + issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255) if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ Repo: repo, diff --git a/models/issues/pull.go b/models/issues/pull.go index 6bed736847f51..7159b5fe09513 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -545,6 +545,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *Iss } issue.Index = idx + issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255) if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ Repo: repo, diff --git a/models/project/project.go b/models/project/project.go index 71b4987352f95..76890e2eee8eb 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -257,6 +257,7 @@ func GetSearchOrderByBySortType(sortType string) db.SearchOrderBy { } // NewProject creates a new Project +// The title will be cut off at 255 characters if it's longer than 255 characters. func NewProject(ctx context.Context, p *Project) error { if !IsBoardTypeValid(p.BoardType) { p.BoardType = BoardTypeNone @@ -276,6 +277,8 @@ func NewProject(ctx context.Context, p *Project) error { } defer committer.Close() + p.Title, _ = util.SplitStringAtByteN(p.Title, 255) + if err := db.Insert(ctx, p); err != nil { return err } @@ -331,6 +334,7 @@ func UpdateProject(ctx context.Context, p *Project) error { p.CardType = CardTypeTextOnly } + p.Title, _ = util.SplitStringAtByteN(p.Title, 255) _, err := db.GetEngine(ctx).ID(p.ID).Cols( "title", "description", diff --git a/models/repo/release.go b/models/repo/release.go index a9f65f6c3e886..cf0001575d20e 100644 --- a/models/repo/release.go +++ b/models/repo/release.go @@ -156,6 +156,7 @@ func IsReleaseExist(ctx context.Context, repoID int64, tagName string) (bool, er // UpdateRelease updates all columns of a release func UpdateRelease(ctx context.Context, rel *Release) error { + rel.Title, _ = util.SplitStringAtByteN(rel.Title, 255) _, err := db.GetEngine(ctx).ID(rel.ID).AllCols().Update(rel) return err } diff --git a/services/release/release.go b/services/release/release.go index 5c021404b8fdc..980a5e98e7fa1 100644 --- a/services/release/release.go +++ b/services/release/release.go @@ -142,6 +142,7 @@ func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, attachmentU return err } + rel.Title, _ = util.SplitStringAtByteN(rel.Title, 255) rel.LowerTagName = strings.ToLower(rel.TagName) if err = db.Insert(gitRepo.Ctx, rel); err != nil { return err From 257ce61023aa450e42c399a90f16a1d6f2f0fdd7 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Fri, 15 Nov 2024 11:27:04 +0800 Subject: [PATCH 31/49] Fix oauth2 error handle not return immediately (#32514) (#32516) Backport #32514 by lunny Co-authored-by: Lunny Xiao --- routers/web/auth/oauth.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index b337b6b156959..b2709942266c4 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -953,6 +953,8 @@ func SignInOAuthCallback(ctx *context.Context) { } if err, ok := err.(*go_oauth2.RetrieveError); ok { ctx.Flash.Error("OAuth2 RetrieveError: "+err.Error(), true) + ctx.Redirect(setting.AppSubURL + "/user/login") + return } ctx.ServerError("UserSignIn", err) return From d03dd04d65a7ab62c5e75a3a66eed819b484b539 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Fri, 15 Nov 2024 17:27:38 +0800 Subject: [PATCH 32/49] Remove transaction for archive download (#32186) (#32520) Backport #32186 by @lunny Since there is a status column in the database, the transaction is unnecessary when downloading an archive. The transaction is blocking database operations, especially with SQLite. Replace #27563 Co-authored-by: Lunny Xiao --- services/repository/archiver/archiver.go | 33 ++++++++----------- services/repository/archiver/archiver_test.go | 12 +++---- 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/services/repository/archiver/archiver.go b/services/repository/archiver/archiver.go index 01c58f0ce402a..c33369d047e8e 100644 --- a/services/repository/archiver/archiver.go +++ b/services/repository/archiver/archiver.go @@ -68,7 +68,7 @@ func (e RepoRefNotFoundError) Is(err error) bool { } // NewRequest creates an archival request, based on the URI. The -// resulting ArchiveRequest is suitable for being passed to ArchiveRepository() +// resulting ArchiveRequest is suitable for being passed to Await() // if it's determined that the request still needs to be satisfied. func NewRequest(repoID int64, repo *git.Repository, uri string) (*ArchiveRequest, error) { r := &ArchiveRequest{ @@ -151,13 +151,14 @@ func (aReq *ArchiveRequest) Await(ctx context.Context) (*repo_model.RepoArchiver } } +// doArchive satisfies the ArchiveRequest being passed in. Processing +// will occur in a separate goroutine, as this phase may take a while to +// complete. If the archive already exists, doArchive will not do +// anything. In all cases, the caller should be examining the *ArchiveRequest +// being returned for completion, as it may be different than the one they passed +// in. func doArchive(ctx context.Context, r *ArchiveRequest) (*repo_model.RepoArchiver, error) { - txCtx, committer, err := db.TxContext(ctx) - if err != nil { - return nil, err - } - defer committer.Close() - ctx, _, finished := process.GetManager().AddContext(txCtx, fmt.Sprintf("ArchiveRequest[%d]: %s", r.RepoID, r.GetArchiveName())) + ctx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("ArchiveRequest[%d]: %s", r.RepoID, r.GetArchiveName())) defer finished() archiver, err := repo_model.GetRepoArchiver(ctx, r.RepoID, r.Type, r.CommitID) @@ -192,7 +193,7 @@ func doArchive(ctx context.Context, r *ArchiveRequest) (*repo_model.RepoArchiver return nil, err } } - return archiver, committer.Commit() + return archiver, nil } if !errors.Is(err, os.ErrNotExist) { @@ -261,17 +262,7 @@ func doArchive(ctx context.Context, r *ArchiveRequest) (*repo_model.RepoArchiver } } - return archiver, committer.Commit() -} - -// ArchiveRepository satisfies the ArchiveRequest being passed in. Processing -// will occur in a separate goroutine, as this phase may take a while to -// complete. If the archive already exists, ArchiveRepository will not do -// anything. In all cases, the caller should be examining the *ArchiveRequest -// being returned for completion, as it may be different than the one they passed -// in. -func ArchiveRepository(ctx context.Context, request *ArchiveRequest) (*repo_model.RepoArchiver, error) { - return doArchive(ctx, request) + return archiver, nil } var archiverQueue *queue.WorkerPoolQueue[*ArchiveRequest] @@ -281,8 +272,10 @@ func Init(ctx context.Context) error { handler := func(items ...*ArchiveRequest) []*ArchiveRequest { for _, archiveReq := range items { log.Trace("ArchiverData Process: %#v", archiveReq) - if _, err := doArchive(ctx, archiveReq); err != nil { + if archiver, err := doArchive(ctx, archiveReq); err != nil { log.Error("Archive %v failed: %v", archiveReq, err) + } else { + log.Trace("ArchiverData Success: %#v", archiver) } } return nil diff --git a/services/repository/archiver/archiver_test.go b/services/repository/archiver/archiver_test.go index ec6e9dfac3c8a..b3f3ed7bf3e68 100644 --- a/services/repository/archiver/archiver_test.go +++ b/services/repository/archiver/archiver_test.go @@ -80,13 +80,13 @@ func TestArchive_Basic(t *testing.T) { inFlight[1] = tgzReq inFlight[2] = secondReq - ArchiveRepository(db.DefaultContext, zipReq) - ArchiveRepository(db.DefaultContext, tgzReq) - ArchiveRepository(db.DefaultContext, secondReq) + doArchive(db.DefaultContext, zipReq) + doArchive(db.DefaultContext, tgzReq) + doArchive(db.DefaultContext, secondReq) // Make sure sending an unprocessed request through doesn't affect the queue // count. - ArchiveRepository(db.DefaultContext, zipReq) + doArchive(db.DefaultContext, zipReq) // Sleep two seconds to make sure the queue doesn't change. time.Sleep(2 * time.Second) @@ -101,7 +101,7 @@ func TestArchive_Basic(t *testing.T) { // We still have the other three stalled at completion, waiting to remove // from archiveInProgress. Try to submit this new one before its // predecessor has cleared out of the queue. - ArchiveRepository(db.DefaultContext, zipReq2) + doArchive(db.DefaultContext, zipReq2) // Now we'll submit a request and TimedWaitForCompletion twice, before and // after we release it. We should trigger both the timeout and non-timeout @@ -109,7 +109,7 @@ func TestArchive_Basic(t *testing.T) { timedReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit+".tar.gz") assert.NoError(t, err) assert.NotNil(t, timedReq) - ArchiveRepository(db.DefaultContext, timedReq) + doArchive(db.DefaultContext, timedReq) zipReq2, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") assert.NoError(t, err) From b6eef3487499399f84c2aa78a6bb00c21e16c5c2 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Sun, 17 Nov 2024 01:15:33 +0800 Subject: [PATCH 33/49] Fix artifact v4 upload above 8MB (#31664) (#32523) --- routers/api/actions/artifacts_chunks.go | 50 +++++- routers/api/actions/artifactsv4.go | 146 +++++++++++++----- .../api_actions_artifact_v4_test.go | 130 ++++++++++++++++ 3 files changed, 286 insertions(+), 40 deletions(-) diff --git a/routers/api/actions/artifacts_chunks.go b/routers/api/actions/artifacts_chunks.go index b0c96585cb9c9..cdb56584b80ff 100644 --- a/routers/api/actions/artifacts_chunks.go +++ b/routers/api/actions/artifacts_chunks.go @@ -123,6 +123,54 @@ func listChunksByRunID(st storage.ObjectStorage, runID int64) (map[int64][]*chun return chunksMap, nil } +func listChunksByRunIDV4(st storage.ObjectStorage, runID, artifactID int64, blist *BlockList) ([]*chunkFileItem, error) { + storageDir := fmt.Sprintf("tmpv4%d", runID) + var chunks []*chunkFileItem + chunkMap := map[string]*chunkFileItem{} + dummy := &chunkFileItem{} + for _, name := range blist.Latest { + chunkMap[name] = dummy + } + if err := st.IterateObjects(storageDir, func(fpath string, obj storage.Object) error { + baseName := filepath.Base(fpath) + if !strings.HasPrefix(baseName, "block-") { + return nil + } + // when read chunks from storage, it only contains storage dir and basename, + // no matter the subdirectory setting in storage config + item := chunkFileItem{Path: storageDir + "/" + baseName, ArtifactID: artifactID} + var size int64 + var b64chunkName string + if _, err := fmt.Sscanf(baseName, "block-%d-%d-%s", &item.RunID, &size, &b64chunkName); err != nil { + return fmt.Errorf("parse content range error: %v", err) + } + rchunkName, err := base64.URLEncoding.DecodeString(b64chunkName) + if err != nil { + return fmt.Errorf("failed to parse chunkName: %v", err) + } + chunkName := string(rchunkName) + item.End = item.Start + size - 1 + if _, ok := chunkMap[chunkName]; ok { + chunkMap[chunkName] = &item + } + return nil + }); err != nil { + return nil, err + } + for i, name := range blist.Latest { + chunk, ok := chunkMap[name] + if !ok || chunk.Path == "" { + return nil, fmt.Errorf("missing Chunk (%d/%d): %s", i, len(blist.Latest), name) + } + chunks = append(chunks, chunk) + if i > 0 { + chunk.Start = chunkMap[blist.Latest[i-1]].End + 1 + chunk.End += chunk.Start + } + } + return chunks, nil +} + func mergeChunksForRun(ctx *ArtifactContext, st storage.ObjectStorage, runID int64, artifactName string) error { // read all db artifacts by name artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{ @@ -230,7 +278,7 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st rawChecksum := hash.Sum(nil) actualChecksum := hex.EncodeToString(rawChecksum) if !strings.HasSuffix(checksum, actualChecksum) { - return fmt.Errorf("update artifact error checksum is invalid") + return fmt.Errorf("update artifact error checksum is invalid %v vs %v", checksum, actualChecksum) } } diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go index bc33d0ecaa590..e39e5161d593a 100644 --- a/routers/api/actions/artifactsv4.go +++ b/routers/api/actions/artifactsv4.go @@ -24,8 +24,15 @@ package actions // PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=block // 1.3. Continue Upload Zip Content to Blobstorage (unauthenticated request), repeat until everything is uploaded // PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=appendBlock -// 1.4. Unknown xml payload to Blobstorage (unauthenticated request), ignored for now +// 1.4. BlockList xml payload to Blobstorage (unauthenticated request) +// Files of about 800MB are parallel in parallel and / or out of order, this file is needed to enshure the correct order // PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=blockList +// Request +// +// +// blockId1 +// blockId2 +// // 1.5. FinalizeArtifact // Post: /twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact // Request @@ -82,6 +89,7 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/base64" + "encoding/xml" "fmt" "io" "net/http" @@ -152,31 +160,34 @@ func ArtifactsV4Routes(prefix string) *web.Route { return m } -func (r artifactV4Routes) buildSignature(endp, expires, artifactName string, taskID int64) []byte { +func (r artifactV4Routes) buildSignature(endp, expires, artifactName string, taskID, artifactID int64) []byte { mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret()) mac.Write([]byte(endp)) mac.Write([]byte(expires)) mac.Write([]byte(artifactName)) mac.Write([]byte(fmt.Sprint(taskID))) + mac.Write([]byte(fmt.Sprint(artifactID))) return mac.Sum(nil) } -func (r artifactV4Routes) buildArtifactURL(ctx *ArtifactContext, endp, artifactName string, taskID int64) string { +func (r artifactV4Routes) buildArtifactURL(ctx *ArtifactContext, endp, artifactName string, taskID, artifactID int64) string { expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST") uploadURL := strings.TrimSuffix(httplib.GuessCurrentAppURL(ctx), "/") + strings.TrimSuffix(r.prefix, "/") + - "/" + endp + "?sig=" + base64.URLEncoding.EncodeToString(r.buildSignature(endp, expires, artifactName, taskID)) + "&expires=" + url.QueryEscape(expires) + "&artifactName=" + url.QueryEscape(artifactName) + "&taskID=" + fmt.Sprint(taskID) + "/" + endp + "?sig=" + base64.URLEncoding.EncodeToString(r.buildSignature(endp, expires, artifactName, taskID, artifactID)) + "&expires=" + url.QueryEscape(expires) + "&artifactName=" + url.QueryEscape(artifactName) + "&taskID=" + fmt.Sprint(taskID) + "&artifactID=" + fmt.Sprint(artifactID) return uploadURL } func (r artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (*actions.ActionTask, string, bool) { rawTaskID := ctx.Req.URL.Query().Get("taskID") + rawArtifactID := ctx.Req.URL.Query().Get("artifactID") sig := ctx.Req.URL.Query().Get("sig") expires := ctx.Req.URL.Query().Get("expires") artifactName := ctx.Req.URL.Query().Get("artifactName") dsig, _ := base64.URLEncoding.DecodeString(sig) taskID, _ := strconv.ParseInt(rawTaskID, 10, 64) + artifactID, _ := strconv.ParseInt(rawArtifactID, 10, 64) - expecedsig := r.buildSignature(endp, expires, artifactName, taskID) + expecedsig := r.buildSignature(endp, expires, artifactName, taskID, artifactID) if !hmac.Equal(dsig, expecedsig) { log.Error("Error unauthorized") ctx.Error(http.StatusUnauthorized, "Error unauthorized") @@ -271,6 +282,8 @@ func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) { return } artifact.ContentEncoding = ArtifactV4ContentEncoding + artifact.FileSize = 0 + artifact.FileCompressedSize = 0 if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { log.Error("Error UpdateArtifactByID: %v", err) ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID") @@ -279,7 +292,7 @@ func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) { respData := CreateArtifactResponse{ Ok: true, - SignedUploadUrl: r.buildArtifactURL(ctx, "UploadArtifact", artifactName, ctx.ActionTask.ID), + SignedUploadUrl: r.buildArtifactURL(ctx, "UploadArtifact", artifactName, ctx.ActionTask.ID, artifact.ID), } r.sendProtbufBody(ctx, &respData) } @@ -293,38 +306,77 @@ func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) { comp := ctx.Req.URL.Query().Get("comp") switch comp { case "block", "appendBlock": - // get artifact by name - artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName) - if err != nil { - log.Error("Error artifact not found: %v", err) - ctx.Error(http.StatusNotFound, "Error artifact not found") - return + blockid := ctx.Req.URL.Query().Get("blockid") + if blockid == "" { + // get artifact by name + artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName) + if err != nil { + log.Error("Error artifact not found: %v", err) + ctx.Error(http.StatusNotFound, "Error artifact not found") + return + } + + _, err = appendUploadChunk(r.fs, ctx, artifact, artifact.FileSize, ctx.Req.ContentLength, artifact.RunID) + if err != nil { + log.Error("Error runner api getting task: task is not running") + ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") + return + } + artifact.FileCompressedSize += ctx.Req.ContentLength + artifact.FileSize += ctx.Req.ContentLength + if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { + log.Error("Error UpdateArtifactByID: %v", err) + ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID") + return + } + } else { + _, err := r.fs.Save(fmt.Sprintf("tmpv4%d/block-%d-%d-%s", task.Job.RunID, task.Job.RunID, ctx.Req.ContentLength, base64.URLEncoding.EncodeToString([]byte(blockid))), ctx.Req.Body, -1) + if err != nil { + log.Error("Error runner api getting task: task is not running") + ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") + return + } } - - if comp == "block" { - artifact.FileSize = 0 - artifact.FileCompressedSize = 0 - } - - _, err = appendUploadChunk(r.fs, ctx, artifact, artifact.FileSize, ctx.Req.ContentLength, artifact.RunID) + ctx.JSON(http.StatusCreated, "appended") + case "blocklist": + rawArtifactID := ctx.Req.URL.Query().Get("artifactID") + artifactID, _ := strconv.ParseInt(rawArtifactID, 10, 64) + _, err := r.fs.Save(fmt.Sprintf("tmpv4%d/%d-%d-blocklist", task.Job.RunID, task.Job.RunID, artifactID), ctx.Req.Body, -1) if err != nil { log.Error("Error runner api getting task: task is not running") ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") return } - artifact.FileCompressedSize += ctx.Req.ContentLength - artifact.FileSize += ctx.Req.ContentLength - if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { - log.Error("Error UpdateArtifactByID: %v", err) - ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID") - return - } - ctx.JSON(http.StatusCreated, "appended") - case "blocklist": ctx.JSON(http.StatusCreated, "created") } } +type BlockList struct { + Latest []string `xml:"Latest"` +} + +type Latest struct { + Value string `xml:",chardata"` +} + +func (r *artifactV4Routes) readBlockList(runID, artifactID int64) (*BlockList, error) { + blockListName := fmt.Sprintf("tmpv4%d/%d-%d-blocklist", runID, runID, artifactID) + s, err := r.fs.Open(blockListName) + if err != nil { + return nil, err + } + + xdec := xml.NewDecoder(s) + blockList := &BlockList{} + err = xdec.Decode(blockList) + + delerr := r.fs.Delete(blockListName) + if delerr != nil { + log.Warn("Failed to delete blockList %s: %v", blockListName, delerr) + } + return blockList, err +} + func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) { var req FinalizeArtifactRequest @@ -343,18 +395,34 @@ func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) { ctx.Error(http.StatusNotFound, "Error artifact not found") return } - chunkMap, err := listChunksByRunID(r.fs, runID) + + var chunks []*chunkFileItem + blockList, err := r.readBlockList(runID, artifact.ID) if err != nil { - log.Error("Error merge chunks: %v", err) - ctx.Error(http.StatusInternalServerError, "Error merge chunks") - return - } - chunks, ok := chunkMap[artifact.ID] - if !ok { - log.Error("Error merge chunks") - ctx.Error(http.StatusInternalServerError, "Error merge chunks") - return + log.Warn("Failed to read BlockList, fallback to old behavior: %v", err) + chunkMap, err := listChunksByRunID(r.fs, runID) + if err != nil { + log.Error("Error merge chunks: %v", err) + ctx.Error(http.StatusInternalServerError, "Error merge chunks") + return + } + chunks, ok = chunkMap[artifact.ID] + if !ok { + log.Error("Error merge chunks") + ctx.Error(http.StatusInternalServerError, "Error merge chunks") + return + } + } else { + chunks, err = listChunksByRunIDV4(r.fs, runID, artifact.ID, blockList) + if err != nil { + log.Error("Error merge chunks: %v", err) + ctx.Error(http.StatusInternalServerError, "Error merge chunks") + return + } + artifact.FileSize = chunks[len(chunks)-1].End + 1 + artifact.FileCompressedSize = chunks[len(chunks)-1].End + 1 } + checksum := "" if req.Hash != nil { checksum = req.Hash.Value @@ -455,7 +523,7 @@ func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) { } } if respData.SignedUrl == "" { - respData.SignedUrl = r.buildArtifactURL(ctx, "DownloadArtifact", artifactName, ctx.ActionTask.ID) + respData.SignedUrl = r.buildArtifactURL(ctx, "DownloadArtifact", artifactName, ctx.ActionTask.ID, artifact.ID) } r.sendProtbufBody(ctx, &respData) } diff --git a/tests/integration/api_actions_artifact_v4_test.go b/tests/integration/api_actions_artifact_v4_test.go index f58f876849bb0..ec0fbbfa60a1d 100644 --- a/tests/integration/api_actions_artifact_v4_test.go +++ b/tests/integration/api_actions_artifact_v4_test.go @@ -7,12 +7,14 @@ import ( "bytes" "crypto/sha256" "encoding/hex" + "encoding/xml" "io" "net/http" "strings" "testing" "time" + "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/routers/api/actions" actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/tests" @@ -170,6 +172,134 @@ func TestActionsArtifactV4UploadSingleFileWithRetentionDays(t *testing.T) { assert.True(t, finalizeResp.Ok) } +func TestActionsArtifactV4UploadSingleFileWithPotentialHarmfulBlockID(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + token, err := actions_service.CreateAuthorizationToken(48, 792, 193) + assert.NoError(t, err) + + // acquire artifact upload url + req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{ + Version: 4, + Name: "artifactWithPotentialHarmfulBlockID", + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var uploadResp actions.CreateArtifactResponse + protojson.Unmarshal(resp.Body.Bytes(), &uploadResp) + assert.True(t, uploadResp.Ok) + assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") + + // get upload urls + idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/") + url := uploadResp.SignedUploadUrl[idx:] + "&comp=block&blockid=%2f..%2fmyfile" + blockListURL := uploadResp.SignedUploadUrl[idx:] + "&comp=blocklist" + + // upload artifact chunk + body := strings.Repeat("A", 1024) + req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)) + MakeRequest(t, req, http.StatusCreated) + + // verify that the exploit didn't work + _, err = storage.Actions.Stat("myfile") + assert.Error(t, err) + + // upload artifact blockList + blockList := &actions.BlockList{ + Latest: []string{ + "/../myfile", + }, + } + rawBlockList, err := xml.Marshal(blockList) + assert.NoError(t, err) + req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList)) + MakeRequest(t, req, http.StatusCreated) + + t.Logf("Create artifact confirm") + + sha := sha256.Sum256([]byte(body)) + + // confirm artifact upload + req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{ + Name: "artifactWithPotentialHarmfulBlockID", + Size: 1024, + Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])), + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })). + AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + var finalizeResp actions.FinalizeArtifactResponse + protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp) + assert.True(t, finalizeResp.Ok) +} + +func TestActionsArtifactV4UploadSingleFileWithChunksOutOfOrder(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + token, err := actions_service.CreateAuthorizationToken(48, 792, 193) + assert.NoError(t, err) + + // acquire artifact upload url + req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{ + Version: 4, + Name: "artifactWithChunksOutOfOrder", + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var uploadResp actions.CreateArtifactResponse + protojson.Unmarshal(resp.Body.Bytes(), &uploadResp) + assert.True(t, uploadResp.Ok) + assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") + + // get upload urls + idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/") + block1URL := uploadResp.SignedUploadUrl[idx:] + "&comp=block&blockid=block1" + block2URL := uploadResp.SignedUploadUrl[idx:] + "&comp=block&blockid=block2" + blockListURL := uploadResp.SignedUploadUrl[idx:] + "&comp=blocklist" + + // upload artifact chunks + bodyb := strings.Repeat("B", 1024) + req = NewRequestWithBody(t, "PUT", block2URL, strings.NewReader(bodyb)) + MakeRequest(t, req, http.StatusCreated) + + bodya := strings.Repeat("A", 1024) + req = NewRequestWithBody(t, "PUT", block1URL, strings.NewReader(bodya)) + MakeRequest(t, req, http.StatusCreated) + + // upload artifact blockList + blockList := &actions.BlockList{ + Latest: []string{ + "block1", + "block2", + }, + } + rawBlockList, err := xml.Marshal(blockList) + assert.NoError(t, err) + req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList)) + MakeRequest(t, req, http.StatusCreated) + + t.Logf("Create artifact confirm") + + sha := sha256.Sum256([]byte(bodya + bodyb)) + + // confirm artifact upload + req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{ + Name: "artifactWithChunksOutOfOrder", + Size: 2048, + Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])), + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })). + AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + var finalizeResp actions.FinalizeArtifactResponse + protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp) + assert.True(t, finalizeResp.Ok) +} + func TestActionsArtifactV4DownloadSingle(t *testing.T) { defer tests.PrepareTestEnv(t)() From 6555cfcac3dc784c0c46c0a42e9cf6dbcbee2517 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Sun, 17 Nov 2024 02:21:00 +0800 Subject: [PATCH 34/49] Fix basic auth with webauthn (#32531) (#32536) Backport #32531 by @lunny WebAuthn should behave the same way as TOTP. When enabled, basic auth with username/password should need to WebAuthn auth, otherwise returned 401. Co-authored-by: Lunny Xiao --- services/auth/basic.go | 10 ++++++ tests/integration/api_twofa_test.go | 53 +++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/services/auth/basic.go b/services/auth/basic.go index 90bd64237091d..1f6c3a442d1d8 100644 --- a/services/auth/basic.go +++ b/services/auth/basic.go @@ -5,6 +5,7 @@ package auth import ( + "errors" "net/http" "strings" @@ -141,6 +142,15 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore } if skipper, ok := source.Cfg.(LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() { + // Check if the user has webAuthn registration + hasWebAuthn, err := auth_model.HasWebAuthnRegistrationsByUID(req.Context(), u.ID) + if err != nil { + return nil, err + } + if hasWebAuthn { + return nil, errors.New("Basic authorization is not allowed while webAuthn enrolled") + } + if err := validateTOTP(req, u); err != nil { return nil, err } diff --git a/tests/integration/api_twofa_test.go b/tests/integration/api_twofa_test.go index aad806b6dc4ff..18e6fa91b7e6c 100644 --- a/tests/integration/api_twofa_test.go +++ b/tests/integration/api_twofa_test.go @@ -53,3 +53,56 @@ func TestAPITwoFactor(t *testing.T) { req.Header.Set("X-Gitea-OTP", passcode) MakeRequest(t, req, http.StatusOK) } + +func TestBasicAuthWithWebAuthn(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + // user1 has no webauthn enrolled, he can request API with basic auth + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + unittest.AssertNotExistsBean(t, &auth_model.WebAuthnCredential{UserID: user1.ID}) + req := NewRequest(t, "GET", "/api/v1/user") + req.SetBasicAuth(user1.Name, "password") + MakeRequest(t, req, http.StatusOK) + + // user1 has no webauthn enrolled, he can request git protocol with basic auth + req = NewRequest(t, "GET", "/user2/repo1/info/refs") + req.SetBasicAuth(user1.Name, "password") + MakeRequest(t, req, http.StatusOK) + + // user1 has no webauthn enrolled, he can request container package with basic auth + req = NewRequest(t, "GET", "/v2/token") + req.SetBasicAuth(user1.Name, "password") + resp := MakeRequest(t, req, http.StatusOK) + + type tokenResponse struct { + Token string `json:"token"` + } + var tokenParsed tokenResponse + DecodeJSON(t, resp, &tokenParsed) + assert.NotEmpty(t, tokenParsed.Token) + + // user32 has webauthn enrolled, he can't request API with basic auth + user32 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 32}) + unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{UserID: user32.ID}) + + req = NewRequest(t, "GET", "/api/v1/user") + req.SetBasicAuth(user32.Name, "notpassword") + resp = MakeRequest(t, req, http.StatusUnauthorized) + + type userResponse struct { + Message string `json:"message"` + } + var userParsed userResponse + DecodeJSON(t, resp, &userParsed) + assert.EqualValues(t, "Basic authorization is not allowed while webAuthn enrolled", userParsed.Message) + + // user32 has webauthn enrolled, he can't request git protocol with basic auth + req = NewRequest(t, "GET", "/user2/repo1/info/refs") + req.SetBasicAuth(user32.Name, "notpassword") + MakeRequest(t, req, http.StatusUnauthorized) + + // user32 has webauthn enrolled, he can't request container package with basic auth + req = NewRequest(t, "GET", "/v2/token") + req.SetBasicAuth(user1.Name, "notpassword") + MakeRequest(t, req, http.StatusUnauthorized) +} From 578c02d6529a589dfa5470462e8ca9ab5fa4a5fc Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 18 Nov 2024 11:42:30 +0800 Subject: [PATCH 35/49] Improve some sanitizer rules (#32534) This is a backport-only fix for 1.22 1.23 has a proper fix #32533 --- modules/markup/asciicast/asciicast.go | 2 +- modules/markup/csv/csv.go | 6 +++--- modules/markup/sanitizer_default.go | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/markup/asciicast/asciicast.go b/modules/markup/asciicast/asciicast.go index 06780623403a4..873029c1bd3b4 100644 --- a/modules/markup/asciicast/asciicast.go +++ b/modules/markup/asciicast/asciicast.go @@ -39,7 +39,7 @@ const ( // SanitizerRules implements markup.Renderer func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { return []setting.MarkupSanitizerRule{ - {Element: "div", AllowAttr: "class", Regexp: regexp.MustCompile(playerClassName)}, + {Element: "div", AllowAttr: "class", Regexp: regexp.MustCompile("^" + playerClassName + "$")}, {Element: "div", AllowAttr: playerSrcAttr}, } } diff --git a/modules/markup/csv/csv.go b/modules/markup/csv/csv.go index 1dd26eb8acdba..c700fb8dfc139 100644 --- a/modules/markup/csv/csv.go +++ b/modules/markup/csv/csv.go @@ -37,9 +37,9 @@ func (Renderer) Extensions() []string { // SanitizerRules implements markup.Renderer func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { return []setting.MarkupSanitizerRule{ - {Element: "table", AllowAttr: "class", Regexp: regexp.MustCompile(`data-table`)}, - {Element: "th", AllowAttr: "class", Regexp: regexp.MustCompile(`line-num`)}, - {Element: "td", AllowAttr: "class", Regexp: regexp.MustCompile(`line-num`)}, + {Element: "table", AllowAttr: "class", Regexp: regexp.MustCompile(`^data-table$`)}, + {Element: "th", AllowAttr: "class", Regexp: regexp.MustCompile(`^line-num$`)}, + {Element: "td", AllowAttr: "class", Regexp: regexp.MustCompile(`^line-num$`)}, } } diff --git a/modules/markup/sanitizer_default.go b/modules/markup/sanitizer_default.go index 669dc24eae39c..1f989b54c1e59 100644 --- a/modules/markup/sanitizer_default.go +++ b/modules/markup/sanitizer_default.go @@ -67,10 +67,10 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy { } // Allow classes for anchors - policy.AllowAttrs("class").Matching(regexp.MustCompile(`ref-issue( ref-external-issue)?`)).OnElements("a") + policy.AllowAttrs("class").Matching(regexp.MustCompile(`^ref-issue( ref-external-issue)?$`)).OnElements("a") // Allow classes for task lists - policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list-item`)).OnElements("li") + policy.AllowAttrs("class").Matching(regexp.MustCompile(`^task-list-item$`)).OnElements("li") // Allow classes for org mode list item status. policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li") @@ -79,7 +79,7 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy { policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i") // Allow classes for emojis - policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img") + policy.AllowAttrs("class").Matching(regexp.MustCompile(`^emoji$`)).OnElements("img") // Allow icons, emojis, chroma syntax and keyword markup on span policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span") From 673fee427e6426d08d8bb48e583de45d4f70018a Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 18 Nov 2024 07:55:27 -0800 Subject: [PATCH 36/49] Refactor push mirror find and add check for updating push mirror (#32539) (#32549) backport #32539 --------- Co-authored-by: wxiaoguang --- models/db/collation.go | 3 +- models/repo/pushmirror.go | 50 ++++++---- routers/web/repo/setting/setting.go | 51 +++------- services/forms/repo_form.go | 2 +- services/mirror/mirror.go | 10 +- services/mirror/queue.go | 11 ++- tests/integration/db_collation_test.go | 5 +- tests/integration/mirror_push_test.go | 123 ++++++++++++++++--------- 8 files changed, 147 insertions(+), 108 deletions(-) diff --git a/models/db/collation.go b/models/db/collation.go index c128cf502955e..a7db9f54423b9 100644 --- a/models/db/collation.go +++ b/models/db/collation.go @@ -68,7 +68,8 @@ func CheckCollations(x *xorm.Engine) (*CheckCollationsResult, error) { var candidateCollations []string if x.Dialect().URI().DBType == schemas.MYSQL { - if _, err = x.SQL("SELECT @@collation_database").Get(&res.DatabaseCollation); err != nil { + _, err = x.SQL("SELECT DEFAULT_COLLATION_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?", setting.Database.Name).Get(&res.DatabaseCollation) + if err != nil { return nil, err } res.IsCollationCaseSensitive = func(s string) bool { diff --git a/models/repo/pushmirror.go b/models/repo/pushmirror.go index bf134abfb152a..55e8f3a068f28 100644 --- a/models/repo/pushmirror.go +++ b/models/repo/pushmirror.go @@ -9,15 +9,13 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) -// ErrPushMirrorNotExist mirror does not exist error -var ErrPushMirrorNotExist = util.NewNotExistErrorf("PushMirror does not exist") - // PushMirror represents mirror information of a repository. type PushMirror struct { ID int64 `xorm:"pk autoincr"` @@ -96,26 +94,46 @@ func DeletePushMirrors(ctx context.Context, opts PushMirrorOptions) error { return util.NewInvalidArgumentErrorf("repoID required and must be set") } +type findPushMirrorOptions struct { + db.ListOptions + RepoID int64 + SyncOnCommit optional.Option[bool] +} + +func (opts findPushMirrorOptions) ToConds() builder.Cond { + cond := builder.NewCond() + if opts.RepoID > 0 { + cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) + } + if opts.SyncOnCommit.Has() { + cond = cond.And(builder.Eq{"sync_on_commit": opts.SyncOnCommit.Value()}) + } + return cond +} + // GetPushMirrorsByRepoID returns push-mirror information of a repository. func GetPushMirrorsByRepoID(ctx context.Context, repoID int64, listOptions db.ListOptions) ([]*PushMirror, int64, error) { - sess := db.GetEngine(ctx).Where("repo_id = ?", repoID) - if listOptions.Page != 0 { - sess = db.SetSessionPagination(sess, &listOptions) - mirrors := make([]*PushMirror, 0, listOptions.PageSize) - count, err := sess.FindAndCount(&mirrors) - return mirrors, count, err + return db.FindAndCount[PushMirror](ctx, findPushMirrorOptions{ + ListOptions: listOptions, + RepoID: repoID, + }) +} + +func GetPushMirrorByIDAndRepoID(ctx context.Context, id, repoID int64) (*PushMirror, bool, error) { + var pushMirror PushMirror + has, err := db.GetEngine(ctx).Where("id = ?", id).And("repo_id = ?", repoID).Get(&pushMirror) + if !has || err != nil { + return nil, has, err } - mirrors := make([]*PushMirror, 0, 10) - count, err := sess.FindAndCount(&mirrors) - return mirrors, count, err + return &pushMirror, true, nil } // GetPushMirrorsSyncedOnCommit returns push-mirrors for this repo that should be updated by new commits func GetPushMirrorsSyncedOnCommit(ctx context.Context, repoID int64) ([]*PushMirror, error) { - mirrors := make([]*PushMirror, 0, 10) - return mirrors, db.GetEngine(ctx). - Where("repo_id = ? AND sync_on_commit = ?", repoID, true). - Find(&mirrors) + return db.Find[PushMirror](ctx, findPushMirrorOptions{ + RepoID: repoID, + SyncOnCommit: optional.Some(true), + }) } // PushMirrorsIterate iterates all push-mirror repositories. diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index a7fd9a3de4537..d0913ec3a403e 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "net/http" - "strconv" "strings" "time" @@ -298,8 +297,8 @@ func SettingsPost(ctx *context.Context) { return } - m, err := selectPushMirrorByForm(ctx, form, repo) - if err != nil { + m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID) + if m == nil { ctx.NotFound("", nil) return } @@ -325,15 +324,13 @@ func SettingsPost(ctx *context.Context) { return } - id, err := strconv.ParseInt(form.PushMirrorID, 10, 64) - if err != nil { - ctx.ServerError("UpdatePushMirrorIntervalPushMirrorID", err) + m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID) + if m == nil { + ctx.NotFound("", nil) return } - m := &repo_model.PushMirror{ - ID: id, - Interval: interval, - } + + m.Interval = interval if err := repo_model.UpdatePushMirrorInterval(ctx, m); err != nil { ctx.ServerError("UpdatePushMirrorInterval", err) return @@ -342,7 +339,10 @@ func SettingsPost(ctx *context.Context) { // If we observed its implementation in the context of `push-mirror-sync` where it // is evident that pushing to the queue is necessary for updates. // So, there are updates within the given interval, it is necessary to update the queue accordingly. - mirror_service.AddPushMirrorToQueue(m.ID) + if !ctx.FormBool("push_mirror_defer_sync") { + // push_mirror_defer_sync is mainly for testing purpose, we do not really want to sync the push mirror immediately + mirror_service.AddPushMirrorToQueue(m.ID) + } ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) ctx.Redirect(repo.Link() + "/settings") @@ -356,18 +356,18 @@ func SettingsPost(ctx *context.Context) { // as an error on the UI for this action ctx.Data["Err_RepoName"] = nil - m, err := selectPushMirrorByForm(ctx, form, repo) - if err != nil { + m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID) + if m == nil { ctx.NotFound("", nil) return } - if err = mirror_service.RemovePushMirrorRemote(ctx, m); err != nil { + if err := mirror_service.RemovePushMirrorRemote(ctx, m); err != nil { ctx.ServerError("RemovePushMirrorRemote", err) return } - if err = repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: m.ID, RepoID: m.RepoID}); err != nil { + if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: m.ID, RepoID: m.RepoID}); err != nil { ctx.ServerError("DeletePushMirrorByID", err) return } @@ -970,24 +970,3 @@ func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.R } ctx.RenderWithErr(ctx.Tr("repo.mirror_address_url_invalid"), tplSettingsOptions, form) } - -func selectPushMirrorByForm(ctx *context.Context, form *forms.RepoSettingForm, repo *repo_model.Repository) (*repo_model.PushMirror, error) { - id, err := strconv.ParseInt(form.PushMirrorID, 10, 64) - if err != nil { - return nil, err - } - - pushMirrors, _, err := repo_model.GetPushMirrorsByRepoID(ctx, repo.ID, db.ListOptions{}) - if err != nil { - return nil, err - } - - for _, m := range pushMirrors { - if m.ID == id { - m.Repo = repo - return m, nil - } - } - - return nil, fmt.Errorf("PushMirror[%v] not associated to repository %v", id, repo) -} diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index f49cc2e86bcb1..8426f2fe17f90 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -119,7 +119,7 @@ type RepoSettingForm struct { MirrorPassword string LFS bool `form:"mirror_lfs"` LFSEndpoint string `form:"mirror_lfs_endpoint"` - PushMirrorID string + PushMirrorID int64 PushMirrorAddress string PushMirrorUsername string PushMirrorPassword string diff --git a/services/mirror/mirror.go b/services/mirror/mirror.go index 0270f870390dc..c08a030d745a2 100644 --- a/services/mirror/mirror.go +++ b/services/mirror/mirror.go @@ -8,7 +8,6 @@ import ( "fmt" repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" @@ -119,14 +118,7 @@ func Update(ctx context.Context, pullLimit, pushLimit int) error { return nil } -func queueHandler(items ...*SyncRequest) []*SyncRequest { - for _, req := range items { - doMirrorSync(graceful.GetManager().ShutdownContext(), req) - } - return nil -} - // InitSyncMirrors initializes a go routine to sync the mirrors func InitSyncMirrors() { - StartSyncMirrors(queueHandler) + StartSyncMirrors() } diff --git a/services/mirror/queue.go b/services/mirror/queue.go index 0d9a624730daa..ca5e2c7272a50 100644 --- a/services/mirror/queue.go +++ b/services/mirror/queue.go @@ -28,12 +28,19 @@ type SyncRequest struct { ReferenceID int64 // RepoID for pull mirror, MirrorID for push mirror } +func queueHandler(items ...*SyncRequest) []*SyncRequest { + for _, req := range items { + doMirrorSync(graceful.GetManager().ShutdownContext(), req) + } + return nil +} + // StartSyncMirrors starts a go routine to sync the mirrors -func StartSyncMirrors(queueHandle func(data ...*SyncRequest) []*SyncRequest) { +func StartSyncMirrors() { if !setting.Mirror.Enabled { return } - mirrorQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "mirror", queueHandle) + mirrorQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "mirror", queueHandler) if mirrorQueue == nil { log.Fatal("Unable to create mirror queue") } diff --git a/tests/integration/db_collation_test.go b/tests/integration/db_collation_test.go index 75a4c1594fd82..acec4aa5d1e85 100644 --- a/tests/integration/db_collation_test.go +++ b/tests/integration/db_collation_test.go @@ -73,9 +73,12 @@ func TestDatabaseCollation(t *testing.T) { t.Run("Convert tables to utf8mb4_bin", func(t *testing.T) { defer test.MockVariableValue(&setting.Database.CharsetCollation, "utf8mb4_bin")() - assert.NoError(t, db.ConvertDatabaseTable()) r, err := db.CheckCollations(x) assert.NoError(t, err) + assert.EqualValues(t, "utf8mb4_bin", r.ExpectedCollation) + assert.NoError(t, db.ConvertDatabaseTable()) + r, err = db.CheckCollations(x) + assert.NoError(t, err) assert.Equal(t, "utf8mb4_bin", r.DatabaseCollation) assert.True(t, r.CollationEquals(r.ExpectedCollation, r.DatabaseCollation)) assert.Empty(t, r.InconsistentCollationColumns) diff --git a/tests/integration/mirror_push_test.go b/tests/integration/mirror_push_test.go index 1c262b334967b..2910aafd05d58 100644 --- a/tests/integration/mirror_push_test.go +++ b/tests/integration/mirror_push_test.go @@ -9,7 +9,9 @@ import ( "net/http" "net/url" "strconv" + "strings" "testing" + "time" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" @@ -32,11 +34,10 @@ func TestMirrorPush(t *testing.T) { } func testMirrorPush(t *testing.T, u *url.URL) { - defer tests.PrepareTestEnv(t)() - setting.Migrations.AllowLocalNetworks = true assert.NoError(t, migrations.Init()) + _ = db.TruncateBeans(db.DefaultContext, &repo_model.PushMirror{}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) srcRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) @@ -45,9 +46,10 @@ func testMirrorPush(t *testing.T, u *url.URL) { }) assert.NoError(t, err) - ctx := NewAPITestContext(t, user.LowerName, srcRepo.Name) + session := loginUser(t, user.Name) - doCreatePushMirror(ctx, fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(ctx.Username), url.PathEscape(mirrorRepo.Name)), user.LowerName, userPassword)(t) + pushMirrorURL := fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(user.Name), url.PathEscape(mirrorRepo.Name)) + testCreatePushMirror(t, session, user.Name, srcRepo.Name, pushMirrorURL, user.LowerName, userPassword, "0") mirrors, _, err := repo_model.GetPushMirrorsByRepoID(db.DefaultContext, srcRepo.ID, db.ListOptions{}) assert.NoError(t, err) @@ -73,49 +75,86 @@ func testMirrorPush(t *testing.T, u *url.URL) { assert.Equal(t, srcCommit.ID, mirrorCommit.ID) // Cleanup - doRemovePushMirror(ctx, fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(ctx.Username), url.PathEscape(mirrorRepo.Name)), user.LowerName, userPassword, int(mirrors[0].ID))(t) + assert.True(t, doRemovePushMirror(t, session, user.Name, srcRepo.Name, mirrors[0].ID)) mirrors, _, err = repo_model.GetPushMirrorsByRepoID(db.DefaultContext, srcRepo.ID, db.ListOptions{}) assert.NoError(t, err) assert.Len(t, mirrors, 0) } -func doCreatePushMirror(ctx APITestContext, address, username, password string) func(t *testing.T) { - return func(t *testing.T) { - csrf := GetCSRF(t, ctx.Session, fmt.Sprintf("/%s/%s/settings", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame))) - - req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), map[string]string{ - "_csrf": csrf, - "action": "push-mirror-add", - "push_mirror_address": address, - "push_mirror_username": username, - "push_mirror_password": password, - "push_mirror_interval": "0", - }) - ctx.Session.MakeRequest(t, req, http.StatusSeeOther) - - flashCookie := ctx.Session.GetCookie(gitea_context.CookieNameFlash) - assert.NotNil(t, flashCookie) - assert.Contains(t, flashCookie.Value, "success") - } +func testCreatePushMirror(t *testing.T, session *TestSession, owner, repo, address, username, password, interval string) { + csrf := GetCSRF(t, session, fmt.Sprintf("/%s/%s/settings", url.PathEscape(owner), url.PathEscape(repo))) + req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings", url.PathEscape(owner), url.PathEscape(repo)), map[string]string{ + "_csrf": csrf, + "action": "push-mirror-add", + "push_mirror_address": address, + "push_mirror_username": username, + "push_mirror_password": password, + "push_mirror_interval": interval, + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + flashCookie := session.GetCookie(gitea_context.CookieNameFlash) + assert.NotNil(t, flashCookie) + assert.Contains(t, flashCookie.Value, "success") +} + +func doRemovePushMirror(t *testing.T, session *TestSession, owner, repo string, pushMirrorID int64) bool { + csrf := GetCSRF(t, session, fmt.Sprintf("/%s/%s/settings", url.PathEscape(owner), url.PathEscape(repo))) + + req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings", url.PathEscape(owner), url.PathEscape(repo)), map[string]string{ + "_csrf": csrf, + "action": "push-mirror-remove", + "push_mirror_id": strconv.FormatInt(pushMirrorID, 10), + }) + resp := session.MakeRequest(t, req, NoExpectedStatus) + flashCookie := session.GetCookie(gitea_context.CookieNameFlash) + return resp.Code == http.StatusSeeOther && flashCookie != nil && strings.Contains(flashCookie.Value, "success") +} + +func doUpdatePushMirror(t *testing.T, session *TestSession, owner, repo string, pushMirrorID int64, interval string) bool { + csrf := GetCSRF(t, session, fmt.Sprintf("/%s/%s/settings", url.PathEscape(owner), url.PathEscape(repo))) + + req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings", owner, repo), map[string]string{ + "_csrf": csrf, + "action": "push-mirror-update", + "push_mirror_id": strconv.FormatInt(pushMirrorID, 10), + "push_mirror_interval": interval, + "push_mirror_defer_sync": "true", + }) + resp := session.MakeRequest(t, req, NoExpectedStatus) + return resp.Code == http.StatusSeeOther } -func doRemovePushMirror(ctx APITestContext, address, username, password string, pushMirrorID int) func(t *testing.T) { - return func(t *testing.T) { - csrf := GetCSRF(t, ctx.Session, fmt.Sprintf("/%s/%s/settings", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame))) - - req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), map[string]string{ - "_csrf": csrf, - "action": "push-mirror-remove", - "push_mirror_id": strconv.Itoa(pushMirrorID), - "push_mirror_address": address, - "push_mirror_username": username, - "push_mirror_password": password, - "push_mirror_interval": "0", - }) - ctx.Session.MakeRequest(t, req, http.StatusSeeOther) - - flashCookie := ctx.Session.GetCookie(gitea_context.CookieNameFlash) - assert.NotNil(t, flashCookie) - assert.Contains(t, flashCookie.Value, "success") - } +func TestRepoSettingPushMirrorUpdate(t *testing.T) { + defer tests.PrepareTestEnv(t)() + setting.Migrations.AllowLocalNetworks = true + assert.NoError(t, migrations.Init()) + + session := loginUser(t, "user2") + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) + testCreatePushMirror(t, session, "user2", "repo2", "https://127.0.0.1/user1/repo1.git", "", "", "24h") + + pushMirrors, cnt, err := repo_model.GetPushMirrorsByRepoID(db.DefaultContext, repo2.ID, db.ListOptions{}) + assert.NoError(t, err) + assert.EqualValues(t, 1, cnt) + assert.EqualValues(t, 24*time.Hour, pushMirrors[0].Interval) + repo2PushMirrorID := pushMirrors[0].ID + + // update repo2 push mirror + assert.True(t, doUpdatePushMirror(t, session, "user2", "repo2", repo2PushMirrorID, "10m0s")) + pushMirror := unittest.AssertExistsAndLoadBean(t, &repo_model.PushMirror{ID: repo2PushMirrorID}) + assert.EqualValues(t, 10*time.Minute, pushMirror.Interval) + + // avoid updating repo2 push mirror from repo1 + assert.False(t, doUpdatePushMirror(t, session, "user2", "repo1", repo2PushMirrorID, "20m0s")) + pushMirror = unittest.AssertExistsAndLoadBean(t, &repo_model.PushMirror{ID: repo2PushMirrorID}) + assert.EqualValues(t, 10*time.Minute, pushMirror.Interval) // not changed + + // avoid deleting repo2 push mirror from repo1 + assert.False(t, doRemovePushMirror(t, session, "user2", "repo1", repo2PushMirrorID)) + unittest.AssertExistsAndLoadBean(t, &repo_model.PushMirror{ID: repo2PushMirrorID}) + + // delete repo2 push mirror + assert.True(t, doRemovePushMirror(t, session, "user2", "repo2", repo2PushMirrorID)) + unittest.AssertNotExistsBean(t, &repo_model.PushMirror{ID: repo2PushMirrorID}) } From 1b7031c5c273ab4ff41530e10edbc333155852a5 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Tue, 19 Nov 2024 10:49:29 +0800 Subject: [PATCH 37/49] Fix some places which doesn't repsect org full name setting (#32243) (#32550) Backport #32243 by @lunny Partially fix #31345 Co-authored-by: Lunny Xiao --- templates/admin/org/list.tmpl | 2 +- templates/user/dashboard/repolist.tmpl | 2 +- web_src/js/components/DashboardRepoList.vue | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/admin/org/list.tmpl b/templates/admin/org/list.tmpl index 987ceab1e0f99..55207a6f978c8 100644 --- a/templates/admin/org/list.tmpl +++ b/templates/admin/org/list.tmpl @@ -52,7 +52,7 @@ {{.ID}} - {{.Name}} + {{if and DefaultShowFullName .FullName}}{{.FullName}} ({{.Name}}){{else}}{{.Name}}{{end}} {{if .Visibility.IsPrivate}} {{svg "octicon-lock"}} {{end}} diff --git a/templates/user/dashboard/repolist.tmpl b/templates/user/dashboard/repolist.tmpl index be710675d560a..a2764ba608442 100644 --- a/templates/user/dashboard/repolist.tmpl +++ b/templates/user/dashboard/repolist.tmpl @@ -45,7 +45,7 @@ data.teamId = {{.Team.ID}}; {{end}} {{if not .ContextUser.IsOrganization}} -data.organizations = [{{range .Orgs}}{'name': {{.Name}}, 'num_repos': {{.NumRepos}}, 'org_visibility': {{.Visibility}}},{{end}}]; +data.organizations = [{{range .Orgs}}{'name': {{.Name}}, 'full_name': {{.FullName}}, 'num_repos': {{.NumRepos}}, 'org_visibility': {{.Visibility}}},{{end}}]; data.isOrganization = false; data.organizationsTotalCount = {{.UserOrgsCount}}; data.canCreateOrganization = {{.SignedUser.CanCreateOrganization}}; diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue index 79f942d345d9a..10df000d66d77 100644 --- a/web_src/js/components/DashboardRepoList.vue +++ b/web_src/js/components/DashboardRepoList.vue @@ -471,7 +471,7 @@ export default sfc; // activate the IDE's Vue plugin
  • -
    {{ org.name }}
    +
    {{ org.full_name ? `${org.full_name} (${org.name})` : org.name }}
    {{ org.org_visibility === 'limited' ? textOrgVisibilityLimited: textOrgVisibilityPrivate }} From cf2d3324434847ce18d449c7fb93b02199ea9692 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 18 Nov 2024 20:08:32 -0800 Subject: [PATCH 38/49] Refactor find forks and fix possible bugs that weak permissions check (#32528) (#32547) Backport #32528 - Move models/GetForks to services/FindForks - Add doer as a parameter of FindForks to check permissions - Slight performance optimization for get forks API with batch loading of repository units - Add tests for forking repository to organizations --------- Co-authored-by: wxiaoguang --- models/repo/fork.go | 15 ------ models/repo/repo_list.go | 26 +++++++--- routers/api/v1/repo/fork.go | 15 ++++-- routers/web/repo/view.go | 24 ++++----- services/repository/fork.go | 24 +++++++++ templates/repo/forks.tmpl | 8 +-- tests/integration/api_fork_test.go | 80 +++++++++++++++++++++++++++++ tests/integration/repo_fork_test.go | 52 +++++++++++++++++++ 8 files changed, 203 insertions(+), 41 deletions(-) diff --git a/models/repo/fork.go b/models/repo/fork.go index 07cd31c2690a9..1c75e86458b2f 100644 --- a/models/repo/fork.go +++ b/models/repo/fork.go @@ -54,21 +54,6 @@ func GetUserFork(ctx context.Context, repoID, userID int64) (*Repository, error) return &forkedRepo, nil } -// GetForks returns all the forks of the repository -func GetForks(ctx context.Context, repo *Repository, listOptions db.ListOptions) ([]*Repository, error) { - sess := db.GetEngine(ctx) - - var forks []*Repository - if listOptions.Page == 0 { - forks = make([]*Repository, 0, repo.NumForks) - } else { - forks = make([]*Repository, 0, listOptions.PageSize) - sess = db.SetSessionPagination(sess, &listOptions) - } - - return forks, sess.Find(&forks, &Repository{ForkID: repo.ID}) -} - // IncrementRepoForkNum increment repository fork number func IncrementRepoForkNum(ctx context.Context, repoID int64) error { _, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET num_forks=num_forks+1 WHERE id=?", repoID) diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index eacc98e2225d1..0baf2edf5852b 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -98,8 +98,7 @@ func (repos RepositoryList) IDs() []int64 { return repoIDs } -// LoadAttributes loads the attributes for the given RepositoryList -func (repos RepositoryList) LoadAttributes(ctx context.Context) error { +func (repos RepositoryList) LoadOwners(ctx context.Context) error { if len(repos) == 0 { return nil } @@ -107,10 +106,6 @@ func (repos RepositoryList) LoadAttributes(ctx context.Context) error { userIDs := container.FilterSlice(repos, func(repo *Repository) (int64, bool) { return repo.OwnerID, true }) - repoIDs := make([]int64, len(repos)) - for i := range repos { - repoIDs[i] = repos[i].ID - } // Load owners. users := make(map[int64]*user_model.User, len(userIDs)) @@ -123,12 +118,19 @@ func (repos RepositoryList) LoadAttributes(ctx context.Context) error { for i := range repos { repos[i].Owner = users[repos[i].OwnerID] } + return nil +} + +func (repos RepositoryList) LoadLanguageStats(ctx context.Context) error { + if len(repos) == 0 { + return nil + } // Load primary language. stats := make(LanguageStatList, 0, len(repos)) if err := db.GetEngine(ctx). Where("`is_primary` = ? AND `language` != ?", true, "other"). - In("`repo_id`", repoIDs). + In("`repo_id`", repos.IDs()). Find(&stats); err != nil { return fmt.Errorf("find primary languages: %w", err) } @@ -141,10 +143,18 @@ func (repos RepositoryList) LoadAttributes(ctx context.Context) error { } } } - return nil } +// LoadAttributes loads the attributes for the given RepositoryList +func (repos RepositoryList) LoadAttributes(ctx context.Context) error { + if err := repos.LoadOwners(ctx); err != nil { + return err + } + + return repos.LoadLanguageStats(ctx) +} + // SearchRepoOptions holds the search options type SearchRepoOptions struct { db.ListOptions diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go index a1e3c9804ba39..14a1a8d1c4a3d 100644 --- a/routers/api/v1/repo/fork.go +++ b/routers/api/v1/repo/fork.go @@ -55,11 +55,20 @@ func ListForks(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - forks, err := repo_model.GetForks(ctx, ctx.Repo.Repository, utils.GetListOptions(ctx)) + forks, total, err := repo_service.FindForks(ctx, ctx.Repo.Repository, ctx.Doer, utils.GetListOptions(ctx)) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetForks", err) + ctx.Error(http.StatusInternalServerError, "FindForks", err) return } + if err := repo_model.RepositoryList(forks).LoadOwners(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, "LoadOwners", err) + return + } + if err := repo_model.RepositoryList(forks).LoadUnits(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, "LoadUnits", err) + return + } + apiForks := make([]*api.Repository, len(forks)) for i, fork := range forks { permission, err := access_model.GetUserRepoPermission(ctx, fork, ctx.Doer) @@ -70,7 +79,7 @@ func ListForks(ctx *context.APIContext) { apiForks[i] = convert.ToRepo(ctx, fork, permission) } - ctx.SetTotalCountHeader(int64(ctx.Repo.Repository.NumForks)) + ctx.SetTotalCountHeader(total) ctx.JSON(http.StatusOK, apiForks) } diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index da849d7b72731..ad361776750dc 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -50,6 +50,7 @@ import ( "code.gitea.io/gitea/routers/web/feed" "code.gitea.io/gitea/services/context" issue_service "code.gitea.io/gitea/services/issue" + repo_service "code.gitea.io/gitea/services/repository" files_service "code.gitea.io/gitea/services/repository/files" "github.com/nektos/act/pkg/model" @@ -1155,26 +1156,25 @@ func Forks(ctx *context.Context) { if page <= 0 { page = 1 } + pageSize := setting.ItemsPerPage - pager := context.NewPagination(ctx.Repo.Repository.NumForks, setting.ItemsPerPage, page, 5) - ctx.Data["Page"] = pager - - forks, err := repo_model.GetForks(ctx, ctx.Repo.Repository, db.ListOptions{ - Page: pager.Paginater.Current(), - PageSize: setting.ItemsPerPage, + forks, total, err := repo_service.FindForks(ctx, ctx.Repo.Repository, ctx.Doer, db.ListOptions{ + Page: page, + PageSize: pageSize, }) if err != nil { - ctx.ServerError("GetForks", err) + ctx.ServerError("FindForks", err) return } - for _, fork := range forks { - if err = fork.LoadOwner(ctx); err != nil { - ctx.ServerError("LoadOwner", err) - return - } + if err := repo_model.RepositoryList(forks).LoadOwners(ctx); err != nil { + ctx.ServerError("LoadAttributes", err) + return } + pager := context.NewPagination(int(total), pageSize, page, 5) + ctx.Data["Page"] = pager + ctx.Data["Forks"] = forks ctx.HTML(http.StatusOK, tplForks) diff --git a/services/repository/fork.go b/services/repository/fork.go index f074fd1082118..7adbffdb734f5 100644 --- a/services/repository/fork.go +++ b/services/repository/fork.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" 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/git" "code.gitea.io/gitea/modules/gitrepo" @@ -20,6 +21,8 @@ import ( "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" notify_service "code.gitea.io/gitea/services/notify" + + "xorm.io/builder" ) // ErrForkAlreadyExist represents a "ForkAlreadyExist" kind of error. @@ -244,3 +247,24 @@ func ConvertForkToNormalRepository(ctx context.Context, repo *repo_model.Reposit return err } + +type findForksOptions struct { + db.ListOptions + RepoID int64 + Doer *user_model.User +} + +func (opts findForksOptions) ToConds() builder.Cond { + return builder.Eq{"fork_id": opts.RepoID}.And( + repo_model.AccessibleRepositoryCondition(opts.Doer, unit.TypeInvalid), + ) +} + +// FindForks returns all the forks of the repository +func FindForks(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, listOptions db.ListOptions) ([]*repo_model.Repository, int64, error) { + return db.FindAndCount[repo_model.Repository](ctx, findForksOptions{ + ListOptions: listOptions, + RepoID: repo.ID, + Doer: doer, + }) +} diff --git a/templates/repo/forks.tmpl b/templates/repo/forks.tmpl index 412c59b60e84d..725b67c651cb1 100644 --- a/templates/repo/forks.tmpl +++ b/templates/repo/forks.tmpl @@ -5,12 +5,14 @@

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

    +
    {{template "base/paginate" .}} diff --git a/tests/integration/api_fork_test.go b/tests/integration/api_fork_test.go index 7c231415a318a..357dd27f86888 100644 --- a/tests/integration/api_fork_test.go +++ b/tests/integration/api_fork_test.go @@ -7,8 +7,16 @@ import ( "net/http" "testing" + "code.gitea.io/gitea/models" + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" + org_model "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" ) func TestCreateForkNoLogin(t *testing.T) { @@ -16,3 +24,75 @@ func TestCreateForkNoLogin(t *testing.T) { req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{}) MakeRequest(t, req, http.StatusUnauthorized) } + +func TestAPIForkListLimitedAndPrivateRepos(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user1Sess := loginUser(t, "user1") + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"}) + + // fork into a limited org + limitedOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 22}) + assert.EqualValues(t, api.VisibleTypeLimited, limitedOrg.Visibility) + + ownerTeam1, err := org_model.OrgFromUser(limitedOrg).GetOwnerTeam(db.DefaultContext) + assert.NoError(t, err) + assert.NoError(t, models.AddTeamMember(db.DefaultContext, ownerTeam1, user1)) + user1Token := getTokenForLoggedInUser(t, user1Sess, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteOrganization) + req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{ + Organization: &limitedOrg.Name, + }).AddTokenAuth(user1Token) + MakeRequest(t, req, http.StatusAccepted) + + // fork into a private org + user4Sess := loginUser(t, "user4") + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user4"}) + privateOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23}) + assert.EqualValues(t, api.VisibleTypePrivate, privateOrg.Visibility) + + ownerTeam2, err := org_model.OrgFromUser(privateOrg).GetOwnerTeam(db.DefaultContext) + assert.NoError(t, err) + assert.NoError(t, models.AddTeamMember(db.DefaultContext, ownerTeam2, user4)) + user4Token := getTokenForLoggedInUser(t, user4Sess, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteOrganization) + req = NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{ + Organization: &privateOrg.Name, + }).AddTokenAuth(user4Token) + MakeRequest(t, req, http.StatusAccepted) + + t.Run("Anonymous", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks") + resp := MakeRequest(t, req, http.StatusOK) + + var forks []*api.Repository + DecodeJSON(t, resp, &forks) + + assert.Empty(t, forks) + assert.EqualValues(t, "0", resp.Header().Get("X-Total-Count")) + }) + + t.Run("Logged in", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks").AddTokenAuth(user1Token) + resp := MakeRequest(t, req, http.StatusOK) + + var forks []*api.Repository + DecodeJSON(t, resp, &forks) + + assert.Len(t, forks, 1) + assert.EqualValues(t, "1", resp.Header().Get("X-Total-Count")) + + assert.NoError(t, models.AddTeamMember(db.DefaultContext, ownerTeam2, user1)) + + req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks").AddTokenAuth(user1Token) + resp = MakeRequest(t, req, http.StatusOK) + + forks = []*api.Repository{} + DecodeJSON(t, resp, &forks) + + assert.Len(t, forks, 2) + assert.EqualValues(t, "2", resp.Header().Get("X-Total-Count")) + }) +} diff --git a/tests/integration/repo_fork_test.go b/tests/integration/repo_fork_test.go index feebebf062081..52b55888b9dd9 100644 --- a/tests/integration/repo_fork_test.go +++ b/tests/integration/repo_fork_test.go @@ -9,8 +9,12 @@ import ( "net/http/httptest" "testing" + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" + org_model "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -74,3 +78,51 @@ func TestRepoForkToOrg(t *testing.T) { _, exists := htmlDoc.doc.Find(`a.ui.button[href*="/fork"]`).Attr("href") assert.False(t, exists, "Forking should not be allowed anymore") } + +func TestForkListLimitedAndPrivateRepos(t *testing.T) { + defer tests.PrepareTestEnv(t)() + forkItemSelector := ".repo-fork-item" + + user1Sess := loginUser(t, "user1") + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"}) + + // fork to a limited org + limitedOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 22}) + assert.EqualValues(t, structs.VisibleTypeLimited, limitedOrg.Visibility) + ownerTeam1, err := org_model.OrgFromUser(limitedOrg).GetOwnerTeam(db.DefaultContext) + assert.NoError(t, err) + assert.NoError(t, models.AddTeamMember(db.DefaultContext, ownerTeam1, user1)) + testRepoFork(t, user1Sess, "user2", "repo1", limitedOrg.Name, "repo1", "") + + // fork to a private org + user4Sess := loginUser(t, "user4") + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user4"}) + privateOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23}) + assert.EqualValues(t, structs.VisibleTypePrivate, privateOrg.Visibility) + ownerTeam2, err := org_model.OrgFromUser(privateOrg).GetOwnerTeam(db.DefaultContext) + assert.NoError(t, err) + assert.NoError(t, models.AddTeamMember(db.DefaultContext, ownerTeam2, user4)) + testRepoFork(t, user4Sess, "user2", "repo1", privateOrg.Name, "repo1", "") + + t.Run("Anonymous", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + req := NewRequest(t, "GET", "/user2/repo1/forks") + resp := MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + assert.EqualValues(t, 0, htmlDoc.Find(forkItemSelector).Length()) + }) + + t.Run("Logged in", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/user2/repo1/forks") + resp := user1Sess.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + assert.EqualValues(t, 1, htmlDoc.Find(forkItemSelector).Length()) + + assert.NoError(t, models.AddTeamMember(db.DefaultContext, ownerTeam2, user1)) + resp = user1Sess.MakeRequest(t, req, http.StatusOK) + htmlDoc = NewHTMLParser(t, resp.Body) + assert.EqualValues(t, 2, htmlDoc.Find(forkItemSelector).Length()) + }) +} From 3661b14d9703bba1fabf1444a374a7be9fd8926a Mon Sep 17 00:00:00 2001 From: Giteabot Date: Wed, 20 Nov 2024 02:55:59 +0800 Subject: [PATCH 39/49] Remove unnecessary code (#32560) (#32567) Backport #32560 by @lunny PushMirrors only be used in the repository setting page. So it should not be loaded on every repository page. Co-authored-by: Lunny Xiao --- services/context/repo.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/services/context/repo.go b/services/context/repo.go index c8005cfc72fe5..45bd588166d90 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -394,14 +394,7 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) { } } - pushMirrors, _, err := repo_model.GetPushMirrorsByRepoID(ctx, repo.ID, db.ListOptions{}) - if err != nil { - ctx.ServerError("GetPushMirrorsByRepoID", err) - return - } - ctx.Repo.Repository = repo - ctx.Data["PushMirrors"] = pushMirrors ctx.Data["RepoName"] = ctx.Repo.Repository.Name ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty } From 81ec66c257fb45aba1c3b5c61719ac83bd52a811 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 21 Nov 2024 10:32:19 +0800 Subject: [PATCH 40/49] Fix submodule parsing (#32571) (#32577) A quick fix for #32568 Partially backport from #32571 --- modules/git/commit.go | 36 +++++++++++++++++++++----------- modules/git/commit_test.go | 42 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 14 deletions(-) diff --git a/modules/git/commit.go b/modules/git/commit.go index 86adaa79a667c..5f52a9d1ab3f6 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -377,31 +377,43 @@ func (c *Commit) GetSubModules() (*ObjectCache, error) { } defer rd.Close() + return configParseSubModules(rd) +} + +func configParseSubModules(rd io.Reader) (*ObjectCache, error) { scanner := bufio.NewScanner(rd) - c.submoduleCache = newObjectCache() - var ismodule bool - var path string + submoduleCache := newObjectCache() + var subModule *SubModule for scanner.Scan() { - if strings.HasPrefix(scanner.Text(), "[submodule") { - ismodule = true + line := strings.TrimSpace(scanner.Text()) + if strings.HasPrefix(line, "[") { + if subModule != nil { + submoduleCache.Set(subModule.Name, subModule) + subModule = nil + } + if strings.HasPrefix(line, "[submodule") { + subModule = &SubModule{} + } continue } - if ismodule { - fields := strings.Split(scanner.Text(), "=") + if subModule != nil { + fields := strings.Split(line, "=") k := strings.TrimSpace(fields[0]) if k == "path" { - path = strings.TrimSpace(fields[1]) + subModule.Name = strings.TrimSpace(fields[1]) } else if k == "url" { - c.submoduleCache.Set(path, &SubModule{path, strings.TrimSpace(fields[1])}) - ismodule = false + subModule.URL = strings.TrimSpace(fields[1]) } } } - if err = scanner.Err(); err != nil { + if subModule != nil { + submoduleCache.Set(subModule.Name, subModule) + } + if err := scanner.Err(); err != nil { return nil, fmt.Errorf("GetSubModules scan: %w", err) } - return c.submoduleCache, nil + return submoduleCache, nil } // GetSubModule get the sub module according entryname diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go index 0ddeb182ef839..8ed30262f7094 100644 --- a/modules/git/commit_test.go +++ b/modules/git/commit_test.go @@ -135,7 +135,7 @@ author KN4CK3R 1711702962 +0100 committer KN4CK3R 1711702962 +0100 encoding ISO-8859-1 gpgsig -----BEGIN PGP SIGNATURE----- - + iQGzBAABCgAdFiEE9HRrbqvYxPT8PXbefPSEkrowAa8FAmYGg7IACgkQfPSEkrow Aa9olwv+P0HhtCM6CRvlUmPaqswRsDPNR4i66xyXGiSxdI9V5oJL7HLiQIM7KrFR gizKa2COiGtugv8fE+TKqXKaJx6uJUJEjaBd8E9Af9PrAzjWj+A84lU6/PgPS8hq @@ -150,7 +150,7 @@ gpgsig -----BEGIN PGP SIGNATURE----- -----END PGP SIGNATURE----- ISO-8859-1` - + commitString = strings.ReplaceAll(commitString, "", " ") sha := &Sha1Hash{0xfe, 0xaf, 0x4b, 0xa6, 0xbc, 0x63, 0x5f, 0xec, 0x44, 0x2f, 0x46, 0xdd, 0xd4, 0x51, 0x24, 0x16, 0xec, 0x43, 0xc2, 0xc2} gitRepo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo1_bare")) assert.NoError(t, err) @@ -362,3 +362,41 @@ func Test_GetCommitBranchStart(t *testing.T) { assert.NotEmpty(t, startCommitID) assert.EqualValues(t, "9c9aef8dd84e02bc7ec12641deb4c930a7c30185", startCommitID) } + +func TestConfigSubModule(t *testing.T) { + input := ` +[core] +path = test + +[submodule "submodule1"] + path = path1 + url = https://gitea.io/foo/foo + #branch = b1 + +[other1] +branch = master + +[submodule "submodule2"] + path = path2 + url = https://gitea.io/bar/bar + branch = b2 + +[other2] +branch = main + +[submodule "submodule3"] + path = path3 + url = https://gitea.io/xxx/xxx +` + + subModules, err := configParseSubModules(strings.NewReader(input)) + assert.NoError(t, err) + assert.Len(t, subModules.cache, 3) + + sm1, _ := subModules.Get("path1") + assert.Equal(t, &SubModule{Name: "path1", URL: "https://gitea.io/foo/foo"}, sm1) + sm2, _ := subModules.Get("path2") + assert.Equal(t, &SubModule{Name: "path2", URL: "https://gitea.io/bar/bar"}, sm2) + sm3, _ := subModules.Get("path3") + assert.Equal(t, &SubModule{Name: "path3", URL: "https://gitea.io/xxx/xxx"}, sm3) +} From 0b5da2757093fd41d5fb45b4f12c7049f5447716 Mon Sep 17 00:00:00 2001 From: Rowan Bohde Date: Wed, 20 Nov 2024 21:18:00 -0600 Subject: [PATCH 41/49] allow the actions user to login via the jwt token (#32527) (#32580) Backport #32527 We have some actions that leverage the Gitea API that began receiving 401 errors, with a message that the user was not found. These actions use the `ACTIONS_RUNTIME_TOKEN` env var in the actions job to authenticate with the Gitea API. The format of this env var in actions jobs changed with go-gitea/gitea/pull/28885 to be a JWT (with a corresponding update to `act_runner`) Since it was a JWT, the OAuth parsing logic attempted to parse it as an OAuth token, and would return user not found, instead of falling back to look up the running task and assigning it to the actions user. Make ACTIONS_RUNTIME_TOKEN in action runners could be used, attempting to parse Oauth JWTs. The code to parse potential old `ACTION_RUNTIME_TOKEN` was kept in case someone is running an older version of act_runner that doesn't support the Actions JWT. --- models/fixtures/action_task.yml | 19 ++++++++++++ services/actions/auth.go | 11 +++++-- services/auth/oauth2.go | 23 ++++++++++++++ services/auth/oauth2_test.go | 55 +++++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 services/auth/oauth2_test.go diff --git a/models/fixtures/action_task.yml b/models/fixtures/action_task.yml index 443effe08c92a..d88a8ed8a9189 100644 --- a/models/fixtures/action_task.yml +++ b/models/fixtures/action_task.yml @@ -1,3 +1,22 @@ +- + id: 46 + attempt: 3 + runner_id: 1 + status: 3 # 3 is the status code for "cancelled" + started: 1683636528 + stopped: 1683636626 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + token_hash: 6d8ef48297195edcc8e22c70b3020eaa06c52976db67d39b4260c64a69a2cc1508825121b7b8394e48e00b1bf8718b2aaaaa + token_salt: eeeeeeee + token_last_eight: eeeeeeee + log_filename: artifact-test2/2f/47.log + log_in_storage: 1 + log_length: 707 + log_size: 90179 + log_expired: 0 - id: 47 job_id: 192 diff --git a/services/actions/auth.go b/services/actions/auth.go index 8e934d89a84c8..1ef21f6e0eb09 100644 --- a/services/actions/auth.go +++ b/services/actions/auth.go @@ -83,7 +83,12 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) { return 0, fmt.Errorf("split token failed") } - token, err := jwt.ParseWithClaims(parts[1], &actionsClaims{}, func(t *jwt.Token) (any, error) { + return TokenToTaskID(parts[1]) +} + +// TokenToTaskID returns the TaskID associated with the provided JWT token +func TokenToTaskID(token string) (int64, error) { + parsedToken, err := jwt.ParseWithClaims(token, &actionsClaims{}, func(t *jwt.Token) (any, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) } @@ -93,8 +98,8 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) { return 0, err } - c, ok := token.Claims.(*actionsClaims) - if !token.Valid || !ok { + c, ok := parsedToken.Claims.(*actionsClaims) + if !parsedToken.Valid || !ok { return 0, fmt.Errorf("invalid token claim") } diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go index 861effd0b0b93..4a5deda1fd701 100644 --- a/services/auth/oauth2.go +++ b/services/auth/oauth2.go @@ -17,6 +17,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/web/middleware" + "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/auth/source/oauth2" ) @@ -52,6 +53,18 @@ func CheckOAuthAccessToken(ctx context.Context, accessToken string) int64 { return grant.UserID } +// CheckTaskIsRunning verifies that the TaskID corresponds to a running task +func CheckTaskIsRunning(ctx context.Context, taskID int64) bool { + // Verify the task exists + task, err := actions_model.GetTaskByID(ctx, taskID) + if err != nil { + return false + } + + // Verify that it's running + return task.Status == actions_model.StatusRunning +} + // OAuth2 implements the Auth interface and authenticates requests // (API requests only) by looking for an OAuth token in query parameters or the // "Authorization" header. @@ -95,6 +108,16 @@ func parseToken(req *http.Request) (string, bool) { func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store DataStore) int64 { // Let's see if token is valid. if strings.Contains(tokenSHA, ".") { + // First attempt to decode an actions JWT, returning the actions user + if taskID, err := actions.TokenToTaskID(tokenSHA); err == nil { + if CheckTaskIsRunning(ctx, taskID) { + store.GetData()["IsActionsToken"] = true + store.GetData()["ActionsTaskID"] = taskID + return user_model.ActionsUserID + } + } + + // Otherwise, check if this is an OAuth access token uid := CheckOAuthAccessToken(ctx, tokenSHA) if uid != 0 { store.GetData()["IsApiToken"] = true diff --git a/services/auth/oauth2_test.go b/services/auth/oauth2_test.go new file mode 100644 index 0000000000000..75c231ff7a4ae --- /dev/null +++ b/services/auth/oauth2_test.go @@ -0,0 +1,55 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package auth + +import ( + "context" + "testing" + + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/web/middleware" + "code.gitea.io/gitea/services/actions" + + "github.com/stretchr/testify/assert" +) + +func TestUserIDFromToken(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + t.Run("Actions JWT", func(t *testing.T) { + const RunningTaskID = 47 + token, err := actions.CreateAuthorizationToken(RunningTaskID, 1, 2) + assert.NoError(t, err) + + ds := make(middleware.ContextData) + + o := OAuth2{} + uid := o.userIDFromToken(context.Background(), token, ds) + assert.Equal(t, int64(user_model.ActionsUserID), uid) + assert.Equal(t, ds["IsActionsToken"], true) + assert.Equal(t, ds["ActionsTaskID"], int64(RunningTaskID)) + }) +} + +func TestCheckTaskIsRunning(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + cases := map[string]struct { + TaskID int64 + Expected bool + }{ + "Running": {TaskID: 47, Expected: true}, + "Missing": {TaskID: 1, Expected: false}, + "Cancelled": {TaskID: 46, Expected: false}, + } + + for name := range cases { + c := cases[name] + t.Run(name, func(t *testing.T) { + actual := CheckTaskIsRunning(context.Background(), c.TaskID) + assert.Equal(t, c.Expected, actual) + }) + } +} From 8f6cc9573444fc956e875fb09351f38be08c769a Mon Sep 17 00:00:00 2001 From: Giteabot Date: Thu, 21 Nov 2024 13:25:36 +0800 Subject: [PATCH 42/49] Fix GetInactiveUsers (#32540) (#32588) Backport #32540 by @lunny Fix #31480 Co-authored-by: Lunny Xiao --- models/fixtures/user.yml | 1 + models/user/user.go | 18 ++++++++++++------ models/user/user_test.go | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index 8504d88ce5995..ef2a80b0ed4fb 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -332,6 +332,7 @@ repo_admin_change_team_access: false theme: "" keep_activity_private: false + created_unix: 1730468968 - id: 10 diff --git a/models/user/user.go b/models/user/user.go index cff83702c745d..41c367e4d665b 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -46,19 +46,19 @@ const ( UserTypeIndividual UserType = iota // Historic reason to make it starts at 0. // UserTypeOrganization defines an organization - UserTypeOrganization + UserTypeOrganization // 1 // UserTypeUserReserved reserves a (non-existing) user, i.e. to prevent a spam user from re-registering after being deleted, or to reserve the name until the user is actually created later on - UserTypeUserReserved + UserTypeUserReserved // 2 // UserTypeOrganizationReserved reserves a (non-existing) organization, to be used in combination with UserTypeUserReserved - UserTypeOrganizationReserved + UserTypeOrganizationReserved // 3 // UserTypeBot defines a bot user - UserTypeBot + UserTypeBot // 4 // UserTypeRemoteUser defines a remote user for federated users - UserTypeRemoteUser + UserTypeRemoteUser // 5 ) const ( @@ -829,7 +829,13 @@ func UpdateUserCols(ctx context.Context, u *User, cols ...string) error { // GetInactiveUsers gets all inactive users func GetInactiveUsers(ctx context.Context, olderThan time.Duration) ([]*User, error) { - var cond builder.Cond = builder.Eq{"is_active": false} + cond := builder.And( + builder.Eq{"is_active": false}, + builder.Or( // only plain user + builder.Eq{"`type`": UserTypeIndividual}, + builder.Eq{"`type`": UserTypeUserReserved}, + ), + ) if olderThan > 0 { cond = cond.And(builder.Lt{"created_unix": time.Now().Add(-olderThan).Unix()}) diff --git a/models/user/user_test.go b/models/user/user_test.go index b4ffa1f3229ec..4facd5b55d51f 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -562,3 +562,17 @@ func TestDisabledUserFeatures(t *testing.T) { assert.True(t, user_model.IsFeatureDisabledWithLoginType(user, f)) } } + +func TestGetInactiveUsers(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // all inactive users + // user1's createdunix is 1730468968 + users, err := user_model.GetInactiveUsers(db.DefaultContext, 0) + assert.NoError(t, err) + assert.Len(t, users, 1) + interval := time.Now().Unix() - 1730468968 + 3600*24 + users, err = user_model.GetInactiveUsers(db.DefaultContext, time.Duration(interval*int64(time.Second))) + assert.NoError(t, err) + assert.Len(t, users, 0) +} From a290aab0e84e76ecc13b3992b2db246198858f59 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 21 Nov 2024 14:27:02 +0800 Subject: [PATCH 43/49] Fix debian package clean up (#32351) (#32590) Partially backport #32351 --- models/packages/debian/search.go | 31 ++++++++-------- services/packages/debian/repository.go | 9 ++--- tests/integration/api_packages_debian_test.go | 35 +++++++++++++++++++ 3 files changed, 56 insertions(+), 19 deletions(-) diff --git a/models/packages/debian/search.go b/models/packages/debian/search.go index 77c4a18462380..5333d0c6e48eb 100644 --- a/models/packages/debian/search.go +++ b/models/packages/debian/search.go @@ -75,26 +75,27 @@ func ExistPackages(ctx context.Context, opts *PackageSearchOptions) (bool, error } // SearchPackages gets the packages matching the search options -func SearchPackages(ctx context.Context, opts *PackageSearchOptions, iter func(*packages.PackageFileDescriptor)) error { - return db.GetEngine(ctx). +func SearchPackages(ctx context.Context, opts *PackageSearchOptions) ([]*packages.PackageFileDescriptor, error) { + var pkgFiles []*packages.PackageFile + err := db.GetEngine(ctx). Table("package_file"). Select("package_file.*"). Join("INNER", "package_version", "package_version.id = package_file.version_id"). Join("INNER", "package", "package.id = package_version.package_id"). Where(opts.toCond()). - Asc("package.lower_name", "package_version.created_unix"). - Iterate(new(packages.PackageFile), func(_ int, bean any) error { - pf := bean.(*packages.PackageFile) - - pfd, err := packages.GetPackageFileDescriptor(ctx, pf) - if err != nil { - return err - } - - iter(pfd) - - return nil - }) + Asc("package.lower_name", "package_version.created_unix").Find(&pkgFiles) + if err != nil { + return nil, err + } + pfds := make([]*packages.PackageFileDescriptor, 0, len(pkgFiles)) + for _, pf := range pkgFiles { + pfd, err := packages.GetPackageFileDescriptor(ctx, pf) + if err != nil { + return nil, err + } + pfds = append(pfds, pfd) + } + return pfds, nil } // GetDistributions gets all available distributions diff --git a/services/packages/debian/repository.go b/services/packages/debian/repository.go index 611faa6adea6a..13e98a820e50e 100644 --- a/services/packages/debian/repository.go +++ b/services/packages/debian/repository.go @@ -206,7 +206,11 @@ func buildPackagesIndices(ctx context.Context, ownerID int64, repoVersion *packa w := io.MultiWriter(packagesContent, gzw, xzw) addSeparator := false - if err := debian_model.SearchPackages(ctx, opts, func(pfd *packages_model.PackageFileDescriptor) { + pfds, err := debian_model.SearchPackages(ctx, opts) + if err != nil { + return err + } + for _, pfd := range pfds { if addSeparator { fmt.Fprintln(w) } @@ -220,10 +224,7 @@ func buildPackagesIndices(ctx context.Context, ownerID int64, repoVersion *packa fmt.Fprintf(w, "SHA1: %s\n", pfd.Blob.HashSHA1) fmt.Fprintf(w, "SHA256: %s\n", pfd.Blob.HashSHA256) fmt.Fprintf(w, "SHA512: %s\n", pfd.Blob.HashSHA512) - }); err != nil { - return err } - gzw.Close() xzw.Close() diff --git a/tests/integration/api_packages_debian_test.go b/tests/integration/api_packages_debian_test.go index 05979fccb546c..98027d774c08f 100644 --- a/tests/integration/api_packages_debian_test.go +++ b/tests/integration/api_packages_debian_test.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "net/http" + "strconv" "strings" "testing" @@ -19,6 +20,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" debian_module "code.gitea.io/gitea/modules/packages/debian" + packages_cleanup_service "code.gitea.io/gitea/services/packages/cleanup" "code.gitea.io/gitea/tests" "github.com/blakesmith/ar" @@ -263,4 +265,37 @@ func TestPackageDebian(t *testing.T) { assert.Contains(t, body, "Components: "+strings.Join(components, " ")+"\n") assert.Contains(t, body, "Architectures: "+architectures[1]+"\n") }) + + t.Run("Cleanup", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + rule := &packages.PackageCleanupRule{ + Enabled: true, + RemovePattern: `.*`, + MatchFullName: true, + OwnerID: user.ID, + Type: packages.TypeDebian, + } + + _, err := packages.InsertCleanupRule(db.DefaultContext, rule) + assert.NoError(t, err) + + // When there were a lot of packages (> 50 or 100) and the code used "Iterate" to get all packages, it ever caused bugs, + // because "Iterate" keeps a dangling SQL session but the callback function still uses the same session to execute statements. + // The "Iterate" problem has been checked by TestContextSafety now, so here we only need to check the cleanup logic with a small number + packagesCount := 2 + for i := 0; i < packagesCount; i++ { + uploadURL := fmt.Sprintf("%s/pool/%s/%s/upload", rootURL, "test", "main") + req := NewRequestWithBody(t, "PUT", uploadURL, createArchive(packageName, "1.0."+strconv.Itoa(i), "all")).AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusCreated) + } + req := NewRequest(t, "GET", fmt.Sprintf("%s/dists/%s/Release", rootURL, "test")) + MakeRequest(t, req, http.StatusOK) + + err = packages_cleanup_service.CleanupTask(db.DefaultContext, 0) + assert.NoError(t, err) + + req = NewRequest(t, "GET", fmt.Sprintf("%s/dists/%s/Release", rootURL, "test")) + MakeRequest(t, req, http.StatusNotFound) + }) } From c2598b4642944e474b7b50638c904be3d0b65652 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 21 Nov 2024 07:22:18 -0800 Subject: [PATCH 44/49] Support HTTP POST requests to `/userinfo`, aligning to OpenID Core specification (#32578) (#32594) --- routers/web/web.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/web/web.go b/routers/web/web.go index 787c5f51be77d..bd2da620e2587 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -551,7 +551,7 @@ func registerRoutes(m *web.Route) { m.Post("/authorize", web.Bind(forms.AuthorizationForm{}), auth.AuthorizeOAuth) }, ignSignInAndCsrf, reqSignIn) - m.Methods("GET, OPTIONS", "/login/oauth/userinfo", optionsCorsHandler(), ignSignInAndCsrf, auth.InfoOAuth) + m.Methods("GET, POST, OPTIONS", "/login/oauth/userinfo", optionsCorsHandler(), ignSignInAndCsrf, auth.InfoOAuth) m.Methods("POST, OPTIONS", "/login/oauth/access_token", optionsCorsHandler(), web.Bind(forms.AccessTokenForm{}), ignSignInAndCsrf, auth.AccessTokenOAuth) m.Methods("GET, OPTIONS", "/login/oauth/keys", optionsCorsHandler(), ignSignInAndCsrf, auth.OIDCKeys) m.Methods("POST, OPTIONS", "/login/oauth/introspect", optionsCorsHandler(), web.Bind(forms.IntrospectTokenForm{}), ignSignInAndCsrf, auth.IntrospectOAuth) From 87ceecfb3a1d3e6d66b2dfa178b7e08aba2df7c3 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 21 Nov 2024 17:50:34 -0800 Subject: [PATCH 45/49] Fix the missing menu in organization project view page (#32313) (#32592) Backport #32313 #29248 didn't modify the view page. The class name is not good enough, so this is a quick fix. Before: org: ![image](https://github.com/user-attachments/assets/3e26502d-66b4-4043-ab03-003ba7391487) user: ![image](https://github.com/user-attachments/assets/9b22b90c-d63c-4228-acad-4d9fb20590ac) After: org: ![image](https://github.com/user-attachments/assets/21bf98a7-8a5b-4dc6-950a-88f529e36450) user: (no change) ![image](https://github.com/user-attachments/assets/fea0dcae-3625-44e8-bb9e-4c3733da8764) Co-authored-by: yp05327 <576951401@qq.com> --- templates/org/projects/view.tmpl | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/templates/org/projects/view.tmpl b/templates/org/projects/view.tmpl index e1ab81c4cde5a..bd74114fe2e47 100644 --- a/templates/org/projects/view.tmpl +++ b/templates/org/projects/view.tmpl @@ -1,9 +1,13 @@ {{template "base/head" .}} -
    - {{template "shared/user/org_profile_avatar" .}} -
    - {{template "user/overview/header" .}} -
    +
    + {{if .ContextUser.IsOrganization}} + {{template "org/header" .}} + {{else}} + {{template "shared/user/org_profile_avatar" .}} +
    + {{template "user/overview/header" .}} +
    + {{end}}
    {{template "projects/view" .}}
    From 2b8b2772fd2859f6816a5641988da62598cadef4 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 22 Nov 2024 00:12:40 -0800 Subject: [PATCH 46/49] Fix PR creation on forked repositories (#31863) (#32591) Resolves #20475 Backport #31863 Co-authored-by: Job --- routers/api/v1/repo/pull.go | 17 ++++++++++++++--- tests/integration/pull_create_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 8917836eb3e84..0234a7e67805f 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -1103,9 +1103,20 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) // Check if current user has fork of repository or in the same repository. headRepo := repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID) if headRepo == nil && !isSameRepo { - log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID) - ctx.NotFound("GetForkedRepo") - return nil, nil, nil, nil, "", "" + err := baseRepo.GetBaseRepo(ctx) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetBaseRepo", err) + return nil, nil, nil, nil, "", "" + } + + // Check if baseRepo's base repository is the same as headUser's repository. + if baseRepo.BaseRepo == nil || baseRepo.BaseRepo.OwnerID != headUser.ID { + log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID) + ctx.NotFound("GetBaseRepo") + return nil, nil, nil, nil, "", "" + } + // Assign headRepo so it can be used below. + headRepo = baseRepo.BaseRepo } var headGitRepo *git.Repository diff --git a/tests/integration/pull_create_test.go b/tests/integration/pull_create_test.go index 5a06a7817f661..9812d2073d1e9 100644 --- a/tests/integration/pull_create_test.go +++ b/tests/integration/pull_create_test.go @@ -199,3 +199,30 @@ func TestPullBranchDelete(t *testing.T) { session.MakeRequest(t, req, http.StatusOK) }) } + +/* +Setup: +The base repository is: user2/repo1 +Fork repository to: user1/repo1 +Push extra commit to: user2/repo1, which changes README.md +Create a PR on user1/repo1 + +Test checks: +Check if pull request can be created from base to the fork repository. +*/ +func TestPullCreatePrFromBaseToFork(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + sessionFork := loginUser(t, "user1") + testRepoFork(t, sessionFork, "user2", "repo1", "user1", "repo1", "") + + // Edit base repository + sessionBase := loginUser(t, "user2") + testEditFile(t, sessionBase, "user2", "repo1", "master", "README.md", "Hello, World (Edited)\n") + + // Create a PR + resp := testPullCreateDirectly(t, sessionFork, "user1", "repo1", "master", "user2", "repo1", "master", "This is a pull title") + // check the redirected URL + url := test.RedirectURL(resp) + assert.Regexp(t, "^/user1/repo1/pulls/[0-9]*$", url) + }) +} From 073ba977fc7c2f58aa3c2faebcdadda49e6e2aac Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 22 Nov 2024 00:50:35 -0800 Subject: [PATCH 47/49] Fix clean tmp dir (#32360) (#32593) Backport #32360 Try to fix #31792 Credit to @jeroenlaylo Copied from https://github.com/go-gitea/gitea/issues/31792#issuecomment-2311920520 Co-authored-by: wxiaoguang --- modules/git/repo_index.go | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/modules/git/repo_index.go b/modules/git/repo_index.go index 839057009872e..f45b6e61919f6 100644 --- a/modules/git/repo_index.go +++ b/modules/git/repo_index.go @@ -50,25 +50,35 @@ func (repo *Repository) readTreeToIndex(id ObjectID, indexFilename ...string) er } // ReadTreeToTemporaryIndex reads a treeish to a temporary index file -func (repo *Repository) ReadTreeToTemporaryIndex(treeish string) (filename, tmpDir string, cancel context.CancelFunc, err error) { +func (repo *Repository) ReadTreeToTemporaryIndex(treeish string) (tmpIndexFilename, tmpDir string, cancel context.CancelFunc, err error) { + defer func() { + // if error happens and there is a cancel function, do clean up + if err != nil && cancel != nil { + cancel() + cancel = nil + } + }() + + removeDirFn := func(dir string) func() { // it can't use the return value "tmpDir" directly because it is empty when error occurs + return func() { + if err := util.RemoveAll(dir); err != nil { + log.Error("failed to remove tmp index dir: %v", err) + } + } + } + tmpDir, err = os.MkdirTemp("", "index") if err != nil { - return filename, tmpDir, cancel, err + return "", "", nil, err } - filename = filepath.Join(tmpDir, ".tmp-index") - cancel = func() { - err := util.RemoveAll(tmpDir) - if err != nil { - log.Error("failed to remove tmp index file: %v", err) - } - } - err = repo.ReadTreeToIndex(treeish, filename) + tmpIndexFilename = filepath.Join(tmpDir, ".tmp-index") + cancel = removeDirFn(tmpDir) + err = repo.ReadTreeToIndex(treeish, tmpIndexFilename) if err != nil { - defer cancel() - return "", "", func() {}, err + return "", "", cancel, err } - return filename, tmpDir, cancel, err + return tmpIndexFilename, tmpDir, cancel, err } // EmptyIndex empties the index From cf1a38b03df9d24641ea861f63feeba35c1285dc Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 22 Nov 2024 20:42:58 -0800 Subject: [PATCH 48/49] Fix get reviewers' bug (#32415) (#32616) This PR rewrites `GetReviewer` function and move it to service layer. Reviewers should not be watchers, so that this PR removed all watchers from reviewers. When the repository is under an organization, the pull request unit read permission will be checked to resolve the bug of Fix #32394 Backport #32415 --- models/organization/team_repo.go | 14 +++++ models/organization/team_repo_test.go | 31 ++++++++++ models/repo/user_repo.go | 52 ---------------- models/repo/user_repo_test.go | 43 ------------- routers/api/v1/repo/collaborators.go | 10 ++- routers/web/repo/issue.go | 7 +-- services/issue/assignee.go | 11 ++-- services/pull/reviewer.go | 89 +++++++++++++++++++++++++++ services/pull/reviewer_test.go | 72 ++++++++++++++++++++++ services/repository/review.go | 24 -------- services/repository/review_test.go | 28 --------- tests/integration/api_repo_test.go | 4 +- 12 files changed, 227 insertions(+), 158 deletions(-) create mode 100644 models/organization/team_repo_test.go create mode 100644 services/pull/reviewer.go create mode 100644 services/pull/reviewer_test.go delete mode 100644 services/repository/review.go delete mode 100644 services/repository/review_test.go diff --git a/models/organization/team_repo.go b/models/organization/team_repo.go index 1184e39263635..c90dfdeda04dc 100644 --- a/models/organization/team_repo.go +++ b/models/organization/team_repo.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" "xorm.io/builder" ) @@ -83,3 +84,16 @@ func GetTeamsWithAccessToRepo(ctx context.Context, orgID, repoID int64, mode per OrderBy("name"). Find(&teams) } + +// GetTeamsWithAccessToRepoUnit returns all teams in an organization that have given access level to the repository special unit. +func GetTeamsWithAccessToRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type) ([]*Team, error) { + teams := make([]*Team, 0, 5) + return teams, db.GetEngine(ctx).Where("team_unit.access_mode >= ?", mode). + Join("INNER", "team_repo", "team_repo.team_id = team.id"). + Join("INNER", "team_unit", "team_unit.team_id = team.id"). + And("team_repo.org_id = ?", orgID). + And("team_repo.repo_id = ?", repoID). + And("team_unit.type = ?", unitType). + OrderBy("name"). + Find(&teams) +} diff --git a/models/organization/team_repo_test.go b/models/organization/team_repo_test.go new file mode 100644 index 0000000000000..c0d6750df90cb --- /dev/null +++ b/models/organization/team_repo_test.go @@ -0,0 +1,31 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package organization_test + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestGetTeamsWithAccessToRepoUnit(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + org41 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 41}) + repo61 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 61}) + + teams, err := organization.GetTeamsWithAccessToRepoUnit(db.DefaultContext, org41.ID, repo61.ID, perm.AccessModeRead, unit.TypePullRequests) + assert.NoError(t, err) + if assert.Len(t, teams, 2) { + assert.EqualValues(t, 21, teams[0].ID) + assert.EqualValues(t, 22, teams[1].ID) + } +} diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go index ecc9216950738..a9b1360df1410 100644 --- a/models/repo/user_repo.go +++ b/models/repo/user_repo.go @@ -11,7 +11,6 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" - api "code.gitea.io/gitea/modules/structs" "xorm.io/builder" ) @@ -146,57 +145,6 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us return users, nil } -// GetReviewers get all users can be requested to review: -// * for private repositories this returns all users that have read access or higher to the repository. -// * for public repositories this returns all users that have read access or higher to the repository, -// all repo watchers and all organization members. -// TODO: may be we should have a busy choice for users to block review request to them. -func GetReviewers(ctx context.Context, repo *Repository, doerID, posterID int64) ([]*user_model.User, error) { - // Get the owner of the repository - this often already pre-cached and if so saves complexity for the following queries - if err := repo.LoadOwner(ctx); err != nil { - return nil, err - } - - cond := builder.And(builder.Neq{"`user`.id": posterID}). - And(builder.Eq{"`user`.is_active": true}) - - if repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate { - // This a private repository: - // Anyone who can read the repository is a requestable reviewer - - cond = cond.And(builder.In("`user`.id", - builder.Select("user_id").From("access").Where( - builder.Eq{"repo_id": repo.ID}. - And(builder.Gte{"mode": perm.AccessModeRead}), - ), - )) - - if repo.Owner.Type == user_model.UserTypeIndividual && repo.Owner.ID != posterID { - // as private *user* repos don't generate an entry in the `access` table, - // the owner of a private repo needs to be explicitly added. - cond = cond.Or(builder.Eq{"`user`.id": repo.Owner.ID}) - } - } else { - // This is a "public" repository: - // Any user that has read access, is a watcher or organization member can be requested to review - cond = cond.And(builder.And(builder.In("`user`.id", - builder.Select("user_id").From("access"). - Where(builder.Eq{"repo_id": repo.ID}. - And(builder.Gte{"mode": perm.AccessModeRead})), - ).Or(builder.In("`user`.id", - builder.Select("user_id").From("watch"). - Where(builder.Eq{"repo_id": repo.ID}. - And(builder.In("mode", WatchModeNormal, WatchModeAuto))), - ).Or(builder.In("`user`.id", - builder.Select("uid").From("org_user"). - Where(builder.Eq{"org_id": repo.OwnerID}), - ))))) - } - - users := make([]*user_model.User, 0, 8) - return users, db.GetEngine(ctx).Where(cond).OrderBy(user_model.GetOrderByName()).Find(&users) -} - // GetIssuePostersWithSearch returns users with limit of 30 whose username started with prefix that have authored an issue/pull request for the given repository // If isShowFullName is set to true, also include full name prefix search func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull bool, search string, isShowFullName bool) ([]*user_model.User, error) { diff --git a/models/repo/user_repo_test.go b/models/repo/user_repo_test.go index d2bf6dc9121c9..f2abc2ffa01b9 100644 --- a/models/repo/user_repo_test.go +++ b/models/repo/user_repo_test.go @@ -38,46 +38,3 @@ func TestRepoAssignees(t *testing.T) { assert.NotContains(t, []int64{users[0].ID, users[1].ID, users[2].ID}, 15) } } - -func TestRepoGetReviewers(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - // test public repo - repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - - ctx := db.DefaultContext - reviewers, err := repo_model.GetReviewers(ctx, repo1, 2, 2) - assert.NoError(t, err) - if assert.Len(t, reviewers, 3) { - assert.ElementsMatch(t, []int64{1, 4, 11}, []int64{reviewers[0].ID, reviewers[1].ID, reviewers[2].ID}) - } - - // should include doer if doer is not PR poster. - reviewers, err = repo_model.GetReviewers(ctx, repo1, 11, 2) - assert.NoError(t, err) - assert.Len(t, reviewers, 3) - - // should not include PR poster, if PR poster would be otherwise eligible - reviewers, err = repo_model.GetReviewers(ctx, repo1, 11, 4) - assert.NoError(t, err) - assert.Len(t, reviewers, 2) - - // test private user repo - repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) - - reviewers, err = repo_model.GetReviewers(ctx, repo2, 2, 4) - assert.NoError(t, err) - assert.Len(t, reviewers, 1) - assert.EqualValues(t, reviewers[0].ID, 2) - - // test private org repo - repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) - - reviewers, err = repo_model.GetReviewers(ctx, repo3, 2, 1) - assert.NoError(t, err) - assert.Len(t, reviewers, 2) - - reviewers, err = repo_model.GetReviewers(ctx, repo3, 2, 2) - assert.NoError(t, err) - assert.Len(t, reviewers, 1) -} diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go index 4ce14f7d01837..74be5688bab16 100644 --- a/routers/api/v1/repo/collaborators.go +++ b/routers/api/v1/repo/collaborators.go @@ -18,6 +18,8 @@ import ( "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" + issue_service "code.gitea.io/gitea/services/issue" + pull_service "code.gitea.io/gitea/services/pull" repo_service "code.gitea.io/gitea/services/repository" ) @@ -323,7 +325,13 @@ func GetReviewers(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - reviewers, err := repo_model.GetReviewers(ctx, ctx.Repo.Repository, ctx.Doer.ID, 0) + canChooseReviewer := issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, ctx.Repo.Repository, 0) + if !canChooseReviewer { + ctx.Error(http.StatusForbidden, "GetReviewers", errors.New("doer has no permission to get reviewers")) + return + } + + reviewers, err := pull_service.GetReviewers(ctx, ctx.Repo.Repository, ctx.Doer.ID, 0) if err != nil { ctx.Error(http.StatusInternalServerError, "ListCollaborators", err) return diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 47333396c7e6d..3fdf59404530b 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -56,7 +56,6 @@ import ( "code.gitea.io/gitea/services/forms" issue_service "code.gitea.io/gitea/services/issue" pull_service "code.gitea.io/gitea/services/pull" - repo_service "code.gitea.io/gitea/services/repository" user_service "code.gitea.io/gitea/services/user" ) @@ -693,13 +692,13 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is posterID = 0 } - reviewers, err = repo_model.GetReviewers(ctx, repo, ctx.Doer.ID, posterID) + reviewers, err = pull_service.GetReviewers(ctx, repo, ctx.Doer.ID, posterID) if err != nil { ctx.ServerError("GetReviewers", err) return } - teamReviewers, err = repo_service.GetReviewerTeams(ctx, repo) + teamReviewers, err = pull_service.GetReviewerTeams(ctx, repo) if err != nil { ctx.ServerError("GetReviewerTeams", err) return @@ -1536,7 +1535,7 @@ func ViewIssue(ctx *context.Context) { if issue.IsPull { canChooseReviewer := false if ctx.Doer != nil && ctx.IsSigned { - canChooseReviewer = issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, repo, issue) + canChooseReviewer = issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, repo, issue.PosterID) } RetrieveRepoReviewers(ctx, repo, issue, canChooseReviewer) diff --git a/services/issue/assignee.go b/services/issue/assignee.go index a0aa5a339b1d3..352ea6fe58949 100644 --- a/services/issue/assignee.go +++ b/services/issue/assignee.go @@ -114,7 +114,7 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User, return err } - canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue) + canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue.PosterID) if isAdd { if !permReviewer.CanAccessAny(perm.AccessModeRead, unit.TypePullRequests) { @@ -173,7 +173,7 @@ func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team, } } - canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue) + canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue.PosterID) if isAdd { if issue.Repo.IsPrivate { @@ -267,9 +267,12 @@ func teamReviewRequestNotify(ctx context.Context, issue *issues_model.Issue, doe } // CanDoerChangeReviewRequests returns if the doer can add/remove review requests of a PR -func CanDoerChangeReviewRequests(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue) bool { +func CanDoerChangeReviewRequests(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, posterID int64) bool { + if repo.IsArchived { + return false + } // The poster of the PR can change the reviewers - if doer.ID == issue.PosterID { + if doer.ID == posterID { return true } diff --git a/services/pull/reviewer.go b/services/pull/reviewer.go new file mode 100644 index 0000000000000..bf0d8cb298c8b --- /dev/null +++ b/services/pull/reviewer.go @@ -0,0 +1,89 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package pull + +import ( + "context" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/perm" + 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/container" + + "xorm.io/builder" +) + +// GetReviewers get all users can be requested to review: +// - Poster should not be listed +// - For collaborator, all users that have read access or higher to the repository. +// - For repository under organization, users under the teams which have read permission or higher of pull request unit +// - Owner will be listed if it's not an organization, not the poster and not in the list of reviewers +func GetReviewers(ctx context.Context, repo *repo_model.Repository, doerID, posterID int64) ([]*user_model.User, error) { + if err := repo.LoadOwner(ctx); err != nil { + return nil, err + } + + e := db.GetEngine(ctx) + uniqueUserIDs := make(container.Set[int64]) + + collaboratorIDs := make([]int64, 0, 10) + if err := e.Table("collaboration").Where("repo_id=?", repo.ID). + And("mode >= ?", perm.AccessModeRead). + Select("user_id"). + Find(&collaboratorIDs); err != nil { + return nil, err + } + uniqueUserIDs.AddMultiple(collaboratorIDs...) + + if repo.Owner.IsOrganization() { + additionalUserIDs := make([]int64, 0, 10) + if err := e.Table("team_user"). + Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id"). + Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id"). + Where("`team_repo`.repo_id = ? AND (`team_unit`.access_mode >= ? AND `team_unit`.`type` = ?)", + repo.ID, perm.AccessModeRead, unit.TypePullRequests). + Distinct("`team_user`.uid"). + Select("`team_user`.uid"). + Find(&additionalUserIDs); err != nil { + return nil, err + } + uniqueUserIDs.AddMultiple(additionalUserIDs...) + } + + uniqueUserIDs.Remove(posterID) // posterID should not be in the list of reviewers + + // Leave a seat for owner itself to append later, but if owner is an organization + // and just waste 1 unit is cheaper than re-allocate memory once. + users := make([]*user_model.User, 0, len(uniqueUserIDs)+1) + if len(uniqueUserIDs) > 0 { + if err := e.In("id", uniqueUserIDs.Values()). + Where(builder.Eq{"`user`.is_active": true}). + OrderBy(user_model.GetOrderByName()). + Find(&users); err != nil { + return nil, err + } + } + + // add owner after all users are loaded because we can avoid load owner twice + if repo.OwnerID != posterID && !repo.Owner.IsOrganization() && !uniqueUserIDs.Contains(repo.OwnerID) { + users = append(users, repo.Owner) + } + + return users, nil +} + +// GetReviewerTeams get all teams can be requested to review +func GetReviewerTeams(ctx context.Context, repo *repo_model.Repository) ([]*organization.Team, error) { + if err := repo.LoadOwner(ctx); err != nil { + return nil, err + } + if !repo.Owner.IsOrganization() { + return nil, nil + } + + return organization.GetTeamsWithAccessToRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypePullRequests) +} diff --git a/services/pull/reviewer_test.go b/services/pull/reviewer_test.go new file mode 100644 index 0000000000000..1ff373bafb721 --- /dev/null +++ b/services/pull/reviewer_test.go @@ -0,0 +1,72 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package pull_test + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + pull_service "code.gitea.io/gitea/services/pull" + + "github.com/stretchr/testify/assert" +) + +func TestRepoGetReviewers(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // test public repo + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + ctx := db.DefaultContext + reviewers, err := pull_service.GetReviewers(ctx, repo1, 2, 0) + assert.NoError(t, err) + if assert.Len(t, reviewers, 1) { + assert.ElementsMatch(t, []int64{2}, []int64{reviewers[0].ID}) + } + + // should not include doer and remove the poster + reviewers, err = pull_service.GetReviewers(ctx, repo1, 11, 2) + assert.NoError(t, err) + assert.Len(t, reviewers, 0) + + // should not include PR poster, if PR poster would be otherwise eligible + reviewers, err = pull_service.GetReviewers(ctx, repo1, 11, 4) + assert.NoError(t, err) + assert.Len(t, reviewers, 1) + + // test private user repo + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) + + reviewers, err = pull_service.GetReviewers(ctx, repo2, 2, 4) + assert.NoError(t, err) + assert.Len(t, reviewers, 1) + assert.EqualValues(t, reviewers[0].ID, 2) + + // test private org repo + repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) + + reviewers, err = pull_service.GetReviewers(ctx, repo3, 2, 1) + assert.NoError(t, err) + assert.Len(t, reviewers, 2) + + reviewers, err = pull_service.GetReviewers(ctx, repo3, 2, 2) + assert.NoError(t, err) + assert.Len(t, reviewers, 1) +} + +func TestRepoGetReviewerTeams(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) + teams, err := pull_service.GetReviewerTeams(db.DefaultContext, repo2) + assert.NoError(t, err) + assert.Empty(t, teams) + + repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) + teams, err = pull_service.GetReviewerTeams(db.DefaultContext, repo3) + assert.NoError(t, err) + assert.Len(t, teams, 2) +} diff --git a/services/repository/review.go b/services/repository/review.go deleted file mode 100644 index 40513e6bc67ba..0000000000000 --- a/services/repository/review.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package repository - -import ( - "context" - - "code.gitea.io/gitea/models/organization" - "code.gitea.io/gitea/models/perm" - repo_model "code.gitea.io/gitea/models/repo" -) - -// GetReviewerTeams get all teams can be requested to review -func GetReviewerTeams(ctx context.Context, repo *repo_model.Repository) ([]*organization.Team, error) { - if err := repo.LoadOwner(ctx); err != nil { - return nil, err - } - if !repo.Owner.IsOrganization() { - return nil, nil - } - - return organization.GetTeamsWithAccessToRepo(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead) -} diff --git a/services/repository/review_test.go b/services/repository/review_test.go deleted file mode 100644 index 2db56d4e8a9cc..0000000000000 --- a/services/repository/review_test.go +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package repository - -import ( - "testing" - - "code.gitea.io/gitea/models/db" - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unittest" - - "github.com/stretchr/testify/assert" -) - -func TestRepoGetReviewerTeams(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) - teams, err := GetReviewerTeams(db.DefaultContext, repo2) - assert.NoError(t, err) - assert.Empty(t, teams) - - repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) - teams, err = GetReviewerTeams(db.DefaultContext, repo3) - assert.NoError(t, err) - assert.Len(t, teams, 2) -} diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go index 716da762e542d..732d5fc094f26 100644 --- a/tests/integration/api_repo_test.go +++ b/tests/integration/api_repo_test.go @@ -718,8 +718,8 @@ func TestAPIRepoGetReviewers(t *testing.T) { resp := MakeRequest(t, req, http.StatusOK) var reviewers []*api.User DecodeJSON(t, resp, &reviewers) - if assert.Len(t, reviewers, 3) { - assert.ElementsMatch(t, []int64{1, 4, 11}, []int64{reviewers[0].ID, reviewers[1].ID, reviewers[2].ID}) + if assert.Len(t, reviewers, 1) { + assert.ElementsMatch(t, []int64{2}, []int64{reviewers[0].ID}) } } From 293355777f334f9c3f09c2a19932f52253951298 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 25 Nov 2024 11:01:54 -0800 Subject: [PATCH 49/49] Add release note for v1.22.4 (#32513) Add release note for v1.22.4 --------- Co-authored-by: Kyle D. --- CHANGELOG.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e0244f1cb0cd..f9a832b482167 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,63 @@ This changelog goes through the changes that have been made in each release without substantial changes to our git log; to see the highlights of what has been added to each release, please refer to the [blog](https://blog.gitea.com). +## [1.22.4](https://github.com/go-gitea/gitea/releases/tag/1.22.4) - 2024-11-14 + +* SECURITY + * Fix basic auth with webauthn (#32531) (#32536) + * Refactor internal routers (partial backport, auth token const time comparing) (#32473) (#32479) +* PERFORMANCE + * Remove transaction for archive download (#32186) (#32520) +* BUGFIXES + * Fix `missing signature key` error when pulling Docker images with `SERVE_DIRECT` enabled (#32365) (#32397) + * Fix get reviewers fails when selecting user without pull request permissions unit (#32415) (#32616) + * Fix adding index files to tmp directory (#32360) (#32593) + * Fix PR creation on forked repositories via API (#31863) (#32591) + * Fix missing menu tabs in organization project view page (#32313) (#32592) + * Support HTTP POST requests to `/userinfo`, aligning to OpenID Core specification (#32578) (#32594) + * Fix debian package clean up cron job (#32351) (#32590) + * Fix GetInactiveUsers (#32540) (#32588) + * Allow the actions user to login via the jwt token (#32527) (#32580) + * Fix submodule parsing (#32571) (#32577) + * Refactor find forks and fix possible bugs that weaken permissions check (#32528) (#32547) + * Fix some places that don't respect org full name setting (#32243) (#32550) + * Refactor push mirror find and add check for updating push mirror (#32539) (#32549) + * Fix basic auth with webauthn (#32531) (#32536) + * Fix artifact v4 upload above 8MB (#31664) (#32523) + * Fix oauth2 error handle not return immediately (#32514) (#32516) + * Fix action not triggered when commit message is too long (#32498) (#32507) + * Fix `GetRepoLink` nil pointer dereference on dashboard feed page when repo is deleted with actions enabled (#32501) (#32502) + * Fix `missing signature key` error when pulling Docker images with `SERVE_DIRECT` enabled (#32397) (#32397) + * Fix the permission check for user search API and limit the number of returned users for `/user/search` (#32310) + * Fix SearchIssues swagger docs (#32208) (#32298) + * Fix dropdown content overflow (#31610) (#32250) + * Disable Oauth check if oauth disabled (#32368) (#32480) + * Respect renamed dependencies of Cargo registry (#32430) (#32478) + * Fix mermaid diagram height when initially hidden (#32457) (#32464) + * Fix broken releases when re-pushing tags (#32435) (#32449) + * Only provide the commit summary for Discord webhook push events (#32432) (#32447) + * Only query team tables if repository is under org when getting assignees (#32414) (#32426) + * Fix created_unix for mirroring (#32342) (#32406) + * Respect UI.ExploreDefaultSort setting again (#32357) (#32385) + * Fix broken image when editing comment with non-image attachments (#32319) (#32345) + * Fix disable 2fa bug (#32320) (#32330) + * Always update expiration time when creating an artifact (#32281) (#32285) + * Fix null errors on conversation holder (#32258) (#32266) (#32282) + * Only rename a user when they should receive a different name (#32247) (#32249) + * Fix checkbox bug on private/archive filter (#32236) (#32240) + * Add a doctor check to disable the "Actions" unit for mirrors (#32424) (#32497) + * Quick fix milestone deadline 9999 (#32423) + * Make `show stats` work when only one file changed (#32244) (#32268) + * Make `owner/repo/pulls` handlers use "PR reader" permission (#32254) (#32265) + * Update scheduled tasks even if changes are pushed by "ActionsUser" (#32246) (#32252) +* MISC + * Remove unnecessary code: `GetPushMirrorsByRepoID` called on all repo pages (#32560) (#32567) + * Improve some sanitizer rules (#32534) + * Update nix development environment vor v1.22.x (#32495) + * Add warn log when deleting inactive users (#32318) (#32321) + * Update github.com/go-enry/go-enry to v2.9.1 (#32295) (#32296) + * Warn users when they try to use a non-root-url to sign in/up (#32272) (#32273) + ## [1.22.3](https://github.com/go-gitea/gitea/releases/tag/1.22.3) - 2024-10-08 * SECURITY