From 6365d4b76196b252d96115a93ce568ca1f360134 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Thu, 8 Jun 2023 13:50:38 +0200 Subject: [PATCH 01/86] [GITEA] silently ignore obsolete sudo scope Fixes: https://codeberg.org/forgejo/forgejo/issues/820 (cherry picked from commit 6a7022ebbb83bda162974028cff01ebcc7c574ec) (cherry picked from commit 764eac47b50688d76fe90aad4819a426444ddb4a) (cherry picked from commit 1141eb7b6f2deeeca0acf1714058823d32097cfd) (cherry picked from commit 826b6509b6405ac0a0731ee0e1477ad2cbac585a) (cherry picked from commit 9990d932b8b72f9a27b6529b350eb09d44b7ef88) (cherry picked from commit 7eca57074385f296427d06c059d331d3704ccf15) (cherry picked from commit 66e1d3f082a99bb0006daf0f337850f251c235dc) (cherry picked from commit 188226a8e6b2926f1f276462741f7cc4d7a050b0) (cherry picked from commit 4cd1bff25c6cafa33464594c99b39326a6dd5740) (cherry picked from commit fad6b6d2c49492297d9d8512afc0369e544a6e75) (cherry picked from commit 5b25c3d8512466fd5fceea86b550bdb35c3aa04b) (cherry picked from commit 4746ece4dd018af781181744fb8743e83b64c6df) (cherry picked from commit 2a6f85afb33a1a0b7424c30de3cdff030f483294) (cherry picked from commit c027d724ee0b694e48d2b7ee1915ba55222a03e0) (cherry picked from commit be2f1eeaeb92e552b5defcf8b374ceb4c3a6b1ee) (cherry picked from commit 3058a54fe99c7cf0a015166b8b3f56f9ef9e45d9) (cherry picked from commit 53936d38a0cb1649748f02cf86ec684fa76825b6) (cherry picked from commit 311983cc978cc0a3128cdd8a9c12ac9605be62b9) (cherry picked from commit 1651ae757b31c31023d5e780a4446da5be8951bf) (cherry picked from commit d3dd8ea24dfd6fcf737eb16dcd0871a835b90477) (cherry picked from commit 9a80326ff3a504d3d6b62e37532aa60ebfdb400b) (cherry picked from commit 66eb33235ecaa93fd9834077bc88c9d330dd0e87) (cherry picked from commit 769e24d5a839dd017e08cb6304f9ef7dc242a918) (cherry picked from commit 436cc2121746648dd6551c4e0a9e72bf588ba12e) (cherry picked from commit 817faca7f0833ce372c5ea573979a1381f5233e7) (cherry picked from commit 80ee08aef1bcccaa023463b39ea929ce01dd752a) (cherry picked from commit 15f8885d0ce307d79ba181de0e2676ad1653c002) (cherry picked from commit 0944a4442cd1a42fa4dd8e8d106d08eb40f49c92) (cherry picked from commit 91631d41b0c1ec52772f0d03728482420f0e1b24) (cherry picked from commit 0fbda3386fbffc3a4b44cbd82d84b77ecda7b5e3) (cherry picked from commit a464b0e2ba6df8dbb238d9876cfb4838e7ea346a) (cherry picked from commit 0b98d50c9295341333ece6687e514abe3453e816) --- models/auth/access_token_scope.go | 2 +- models/auth/access_token_scope_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/models/auth/access_token_scope.go b/models/auth/access_token_scope.go index fe57276700..003ca5c9ab 100644 --- a/models/auth/access_token_scope.go +++ b/models/auth/access_token_scope.go @@ -250,7 +250,7 @@ func (s AccessTokenScope) parse() (accessTokenScopeBitmap, error) { remainingScopes = remainingScopes[i+1:] } singleScope := AccessTokenScope(v) - if singleScope == "" { + if singleScope == "" || singleScope == "sudo" { continue } if singleScope == AccessTokenScopeAll { diff --git a/models/auth/access_token_scope_test.go b/models/auth/access_token_scope_test.go index a6097e45d7..d11c5e6a3d 100644 --- a/models/auth/access_token_scope_test.go +++ b/models/auth/access_token_scope_test.go @@ -20,7 +20,7 @@ func TestAccessTokenScope_Normalize(t *testing.T) { tests := []scopeTestNormalize{ {"", "", nil}, {"write:misc,write:notification,read:package,write:notification,public-only", "public-only,write:misc,write:notification,read:package", nil}, - {"all", "all", nil}, + {"all,sudo", "all", nil}, {"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user", "all", nil}, {"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,public-only", "public-only,all", nil}, } From 29eddd86ead3dc0cfcbf9eb7fc3998bb31162b2d Mon Sep 17 00:00:00 2001 From: "Panagiotis \"Ivory\" Vasilopoulos" Date: Mon, 12 Jun 2023 13:57:01 +0200 Subject: [PATCH 02/86] [GITEA] add option for banning dots in usernames Refs: https://codeberg.org/forgejo/forgejo/pulls/676 Author: Panagiotis "Ivory" Vasilopoulos Date: Mon Jun 12 13:57:01 2023 +0200 Co-authored-by: Gusted (cherry picked from commit fabdda5c6e84017bf75ab5f9ab6cc0e583b70d09) (cherry picked from commit d2c7f45621028d37944659db096bc92c031dd8e7) (cherry picked from commit dfdbaba3d6b7abf1c542b0ea41b7812b729cc217) (cherry picked from commit a3cda092b8897e4d669cfcf2cb8b16236e3c9b32) (cherry picked from commit f0fdb5905c3b22bec043530da15d2c52f6bc41c9) (cherry picked from commit 9697e48c1f8b23d3dd1da246b525b63c3756353d) (cherry picked from commit 46e31009a86db18a9b5bd8e2f535b198df90c437) (cherry picked from commit 5bb2c54b6f55499937396339bcacd3b4d8fb6b5e) (cherry picked from commit 682f9d24e13b83d89bd6b86324960f1b4fc72eeb) (cherry picked from commit 18634810057ef88fd01b54cec33bd4bd04c53221) (cherry picked from commit 4f1b7c4ddbc4099aa9b6fda1e0145d37f638e567) (cherry picked from commit 6afe70bbf1290e604fc476ee27901d1722ac1272) (cherry picked from commit 5cec1d9c2d2a731fa44f761e6c90f0d20ab3ccc4) Conflicts: templates/admin/config.tmpl https://codeberg.org/forgejo/forgejo/pulls/1512 (cherry picked from commit de2d172473217e3437238fd9c691edc8d8524e1a) (cherry picked from commit 37a3172dd9e2646157ec49ca46f94b9b0012b061) (cherry picked from commit 92dfca0c5a8a8d4fd8a93b5468ba593283fc9452) (cherry picked from commit a713d59b0cbeaf2fe023be1daa42165cd0df3b1d) (cherry picked from commit e7bd71a6188ed4abbabf8b64b439e588c1c1f5f7) (cherry picked from commit 69f3e952c495ecf8af5e7fc8cca6f3ba31fd3da2) (cherry picked from commit 83fbb7b566f68f84f56d371bcfbba89bba602e2f) (cherry picked from commit 3196605fa99679d28c51c7faccb8402155d31c49) (cherry picked from commit e37eb8de9c8e9975fd2f33e0ea92d45da4c3835c) (cherry picked from commit 8c99f59e48098b0058c5692f17aa66352ad3ad01) (cherry picked from commit 74aa1ac66f659478b9e6994967a6207d7843b9ae) (cherry picked from commit 622440b3bd32ce4db6305187c854e1f9a8820305) (cherry picked from commit 2c1ec90984a82f34b14c0f7db25f1941ec129261) (cherry picked from commit 24d57152e0ab7ab25d5e526785984a7e412ac4eb) (cherry picked from commit 071e9013f3a072978fc2d3452c4b34e94edd34b4) (cherry picked from commit 27fbb726fa395c83a76238fd2989c697eedebb3b) --- custom/conf/app.example.ini | 5 +++++ modules/setting/service.go | 2 ++ modules/validation/helpers.go | 13 ++++++++++--- modules/validation/helpers_test.go | 31 +++++++++++++++++++++++++++++- modules/web/middleware/binding.go | 7 ++++++- options/locale/locale_en-US.ini | 2 ++ templates/admin/config.tmpl | 2 ++ 7 files changed, 57 insertions(+), 5 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 5b84d30938..488c653133 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -817,6 +817,11 @@ LEVEL = Info ;; Every new user will have restricted permissions depending on this setting ;DEFAULT_USER_IS_RESTRICTED = false ;; +;; Users will be able to use dots when choosing their username. Disabling this is +;; helpful if your usersare having issues with e.g. RSS feeds or advanced third-party +;; extensions that use strange regex patterns. +; ALLOW_DOTS_IN_USERNAMES = true +;; ;; Either "public", "limited" or "private", default is "public" ;; Limited is for users visible only to signed users ;; Private is for users visible only to members of their organizations diff --git a/modules/setting/service.go b/modules/setting/service.go index befb94b61b..afaee18101 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -68,6 +68,7 @@ var Service = struct { DefaultKeepEmailPrivate bool DefaultAllowCreateOrganization bool DefaultUserIsRestricted bool + AllowDotsInUsernames bool EnableTimetracking bool DefaultEnableTimetracking bool DefaultEnableDependencies bool @@ -180,6 +181,7 @@ func loadServiceFrom(rootCfg ConfigProvider) { Service.DefaultKeepEmailPrivate = sec.Key("DEFAULT_KEEP_EMAIL_PRIVATE").MustBool() Service.DefaultAllowCreateOrganization = sec.Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").MustBool(true) Service.DefaultUserIsRestricted = sec.Key("DEFAULT_USER_IS_RESTRICTED").MustBool(false) + Service.AllowDotsInUsernames = sec.Key("ALLOW_DOTS_IN_USERNAMES").MustBool(true) Service.EnableTimetracking = sec.Key("ENABLE_TIMETRACKING").MustBool(true) if Service.EnableTimetracking { Service.DefaultEnableTimetracking = sec.Key("DEFAULT_ENABLE_TIMETRACKING").MustBool(true) diff --git a/modules/validation/helpers.go b/modules/validation/helpers.go index f6e00f3887..567ad867fe 100644 --- a/modules/validation/helpers.go +++ b/modules/validation/helpers.go @@ -117,13 +117,20 @@ func IsValidExternalTrackerURLFormat(uri string) bool { } var ( - validUsernamePattern = regexp.MustCompile(`^[\da-zA-Z][-.\w]*$`) - invalidUsernamePattern = regexp.MustCompile(`[-._]{2,}|[-._]$`) // No consecutive or trailing non-alphanumeric chars + validUsernamePatternWithDots = regexp.MustCompile(`^[\da-zA-Z][-.\w]*$`) + validUsernamePatternWithoutDots = regexp.MustCompile(`^[\da-zA-Z][-\w]*$`) + + // No consecutive or trailing non-alphanumeric chars, catches both cases + invalidUsernamePattern = regexp.MustCompile(`[-._]{2,}|[-._]$`) ) // IsValidUsername checks if username is valid func IsValidUsername(name string) bool { // It is difficult to find a single pattern that is both readable and effective, // but it's easier to use positive and negative checks. - return validUsernamePattern.MatchString(name) && !invalidUsernamePattern.MatchString(name) + if setting.Service.AllowDotsInUsernames { + return validUsernamePatternWithDots.MatchString(name) && !invalidUsernamePattern.MatchString(name) + } + + return validUsernamePatternWithoutDots.MatchString(name) && !invalidUsernamePattern.MatchString(name) } diff --git a/modules/validation/helpers_test.go b/modules/validation/helpers_test.go index 52f383f698..a1bdf2a29c 100644 --- a/modules/validation/helpers_test.go +++ b/modules/validation/helpers_test.go @@ -155,7 +155,8 @@ func Test_IsValidExternalTrackerURLFormat(t *testing.T) { } } -func TestIsValidUsername(t *testing.T) { +func TestIsValidUsernameAllowDots(t *testing.T) { + setting.Service.AllowDotsInUsernames = true tests := []struct { arg string want bool @@ -185,3 +186,31 @@ func TestIsValidUsername(t *testing.T) { }) } } + +func TestIsValidUsernameBanDots(t *testing.T) { + setting.Service.AllowDotsInUsernames = false + defer func() { + setting.Service.AllowDotsInUsernames = true + }() + + tests := []struct { + arg string + want bool + }{ + {arg: "a", want: true}, + {arg: "abc", want: true}, + {arg: "0.b-c", want: false}, + {arg: "a.b-c_d", want: false}, + {arg: ".abc", want: false}, + {arg: "abc.", want: false}, + {arg: "a..bc", want: false}, + {arg: "a...bc", want: false}, + {arg: "a.-bc", want: false}, + {arg: "a._bc", want: false}, + } + for _, tt := range tests { + t.Run(tt.arg, func(t *testing.T) { + assert.Equalf(t, tt.want, IsValidUsername(tt.arg), "IsValidUsername[AllowDotsInUsernames=false](%v)", tt.arg) + }) + } +} diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go index d9bcdf3b2a..4e7fca80e2 100644 --- a/modules/web/middleware/binding.go +++ b/modules/web/middleware/binding.go @@ -8,6 +8,7 @@ import ( "reflect" "strings" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/validation" @@ -135,7 +136,11 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo case validation.ErrRegexPattern: data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message) case validation.ErrUsername: - data["ErrorMsg"] = trName + l.Tr("form.username_error") + if setting.Service.AllowDotsInUsernames { + data["ErrorMsg"] = trName + l.Tr("form.username_error") + } else { + data["ErrorMsg"] = trName + l.Tr("form.username_error_no_dots") + } case validation.ErrInvalidGroupTeamMap: data["ErrorMsg"] = trName + l.Tr("form.invalid_group_team_map_error", errs[0].Message) default: diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 9a06bb0952..3c96f77fca 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -294,6 +294,7 @@ default_allow_create_organization = Allow Creation of Organizations by Default default_allow_create_organization_popup = Allow new user accounts to create organizations by default. default_enable_timetracking = Enable Time Tracking by Default default_enable_timetracking_popup = Enable time tracking for new repositories by default. +allow_dots_in_usernames = Allow users to use dots in their usernames. Doesn't affect existing accounts. no_reply_address = Hidden Email Domain no_reply_address_helper = Domain name for users with a hidden email address. For example, the username 'joe' will be logged in Git as 'joe@noreply.example.org' if the hidden email domain is set to 'noreply.example.org'. password_algorithm = Password Hash Algorithm @@ -534,6 +535,7 @@ include_error = ` must contain substring "%s".` glob_pattern_error = ` glob pattern is invalid: %s.` regex_pattern_error = ` regex pattern is invalid: %s.` username_error = ` can only contain alphanumeric chars ('0-9','a-z','A-Z'), dash ('-'), underscore ('_') and dot ('.'). It cannot begin or end with non-alphanumeric chars, and consecutive non-alphanumeric chars are also forbidden.` +username_error_no_dots = ` can only contain alphanumeric chars ('0-9','a-z','A-Z'), dash ('-') and underscore ('_'). It cannot begin or end with non-alphanumeric chars, and consecutive non-alphanumeric chars are also forbidden.` invalid_group_team_map_error = ` mapping is invalid: %s` unknown_error = Unknown error: captcha_incorrect = The CAPTCHA code is incorrect. diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 1cc4b7bb09..ce6edf8a97 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -157,6 +157,8 @@
{{if .Service.DefaultKeepEmailPrivate}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
{{ctx.Locale.Tr "admin.config.default_allow_create_organization"}}
{{if .Service.DefaultAllowCreateOrganization}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
+
{{ctx.Locale.Tr "admin.config.allow_dots_in_usernames"}}
+
{{if .Service.AllowDotsInUsernames}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
{{ctx.Locale.Tr "admin.config.enable_timetracking"}}
{{if .Service.EnableTimetracking}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
{{if .Service.EnableTimetracking}} From 8be95ef7f41c9e1d343a89cbfe67bdccc01df1f8 Mon Sep 17 00:00:00 2001 From: Gusted Date: Sat, 24 Jun 2023 13:08:52 +0200 Subject: [PATCH 03/86] [GITEA] Add password length check on install page - Resolves #271 - Ensure that the adminstrator password is at least `MIN_PASSWORD_LENGTH`. (cherry picked from commit 28cb04c3f5040980e716ce66cd5906f324257e02) (cherry picked from commit 95371ebd92cd005e2d50a4754e60525cf6135b86) (cherry picked from commit a134288ab6b0291082d913c4e22456b31af58af9) (cherry picked from commit 4202f052cb32aec71a61dd2afd814035a9d85eea) (cherry picked from commit 510b7467d3ee0bf346ad1843775affe1df0675ae) (cherry picked from commit f3a6e1f121e89aaf608fd9890eaf06ed939d1006) (cherry picked from commit f340508819866f355feec6d01b349fa7df29ace9) (cherry picked from commit b891bb176d48c3855cc5b6e4573e7a337af9d382) (cherry picked from commit 1a1bfc38cc7863f5cb3022560cacb2006d08f113) (cherry picked from commit 083d5aefed10e54814c4438eabcd01973d305502) (cherry picked from commit 4586096be9b6214058245da3227541866ea4312f) (cherry picked from commit 039fa20cc8a5b50d5cc37de4503e8a9a80042bcc) (cherry picked from commit 3ec9cb5f5915cd0bd46ca0d20d0ab798dc7bd135) (cherry picked from commit 00be0eee3727130966c34a3b95b10f2af06ea2ec) (cherry picked from commit a1566030025df8cc83d20cbe2b6fb0f87304a1a5) (cherry picked from commit 4d305e77742c181f68cd24724dfc685723a41b31) (cherry picked from commit 51e8f21202ea766d69a4b3c26f44c6db07f47844) (cherry picked from commit 58e354c98e6b361f6d651ffdca3d5cb459adbf2f) (cherry picked from commit 20405564f56775ba0f29a54c9a6eca8136d8ac99) (cherry picked from commit 1d7f49568319cfa49e9c8338f2375432f4917739) (cherry picked from commit d457b9c9111c04ffcd26ff859e2ad804697c2621) (cherry picked from commit 72b54bc4cce030540310e50acc41ea789a1e5221) (cherry picked from commit d7ce723e350d21ef42eba7b7013543e2ba6e0e17) (cherry picked from commit ce5f863d5d3eff77b9736db453f0f9a65241c9bb) (cherry picked from commit 324b9318acbf5e12be922ee7f8fc0f0fece1743a) (cherry picked from commit fff11fc535c1a1122914170363bfc23aeb52e02c) (cherry picked from commit d3fa04aa699883df9b227382190f57726c591cb8) (cherry picked from commit d3b24691f389d863be834ccc8b2c8910b1614f30) (cherry picked from commit 736dfab3ae943fb1b87a5468248c5d80887a5e7c) --- routers/install/install.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/routers/install/install.go b/routers/install/install.go index 1dbfafcd14..cb7818bd33 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -358,6 +358,12 @@ func SubmitInstall(ctx *context.Context) { ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplInstall, form) return } + if len(form.AdminPasswd) < setting.MinPasswordLength { + ctx.Data["Err_Admin"] = true + ctx.Data["Err_AdminPasswd"] = true + ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplInstall, form) + return + } } // Init the engine with migration From 28cb0b191212457f90b661261b9d56ebc9e6e6bc Mon Sep 17 00:00:00 2001 From: Gusted Date: Sat, 5 Aug 2023 14:47:09 +0200 Subject: [PATCH 04/86] [GITEA] Allow release creation on commit - The code and tests are already there to allow releases to be created on commits. - This patch modifies the web code to take into account that an commitID could've been passed as target. - Added unit test. - Resolves https://codeberg.org/forgejo/forgejo/issues/1196 (cherry picked from commit 90863e0ab51d1b243f67de266bbeeb7a9031c525) (cherry picked from commit c805aa23b5c6c9a8ab79e2e66786a4ef798e827a) (cherry picked from commit cf45567ca60b2a9411694c8e9b649fd77c64bdae) (cherry picked from commit 672a2b91e5612f438bd7951d173f42c223629fd1) (cherry picked from commit 82c930152cd693f8451e9553504365c724e1fced) (cherry picked from commit 95ac2508b3e8dd9fc2b0168600d989dbce0744ec) (cherry picked from commit b13a81ab98a02e30d1b508bb89cdd67a05eae782) (cherry picked from commit 9f463a7c1fa74ce17ab6ff8df49e2bcea3c1bc89) (cherry picked from commit 758ce84dc58e0c689e0fcc34386c7a8ed50f3df9) Conflicts: tests/integration/release_test.go https://codeberg.org/forgejo/forgejo/pulls/1550 (cherry picked from commit edf0531aeead2f68bbb283e437494ace33a8d3b8) (cherry picked from commit 44b29f3a1df81c072737b139cad34435313f086c) (cherry picked from commit b851b674195ecf3020aba55c5f46704fa3405289) (cherry picked from commit 37b408f5aac53bf72cd530722c774d7ace8356e1) (cherry picked from commit e81dbedb88a8601cf5a071176ecdbf29a0018cc1) (cherry picked from commit d5fa6be6ecc789448a45d4968ead4f958c33040b) (cherry picked from commit b8c4be25297401bc570dbff41bf312545ade4b54) (cherry picked from commit f23ce2843c59e442f63a240862d0d2e009a6eff2) (cherry picked from commit 8b7bcabae27bc5f66c72c44693e1d051231d2a79) (cherry picked from commit 2d6e52dda9b7f5fd29d7700f9a7835627aeada90) (cherry picked from commit 42e4f3ffdd211d3bb45e505a0cf632172bcbf6b2) (cherry picked from commit 39a1f689d8cb7a741cb10c35d4748fb54ecec34a) (cherry picked from commit 553d4872f883b8ac5cd6e9e585c599201b06067a) (cherry picked from commit df3743372576e708b03fe253eac0f37901a524be) (cherry picked from commit d67eac487b6d5120cf7d4976b9c426eb4d00013a) --- routers/web/repo/release.go | 6 +++++- tests/integration/release_test.go | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index fdb247d413..4d139f2b79 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -397,7 +397,11 @@ func NewReleasePost(ctx *context.Context) { return } - if !ctx.Repo.GitRepo.IsBranchExist(form.Target) { + objectFormat, _ := ctx.Repo.GitRepo.GetObjectFormat() + + // form.Target can be a branch name or a full commitID. + if !ctx.Repo.GitRepo.IsBranchExist(form.Target) && + len(form.Target) == objectFormat.FullLength() && !ctx.Repo.GitRepo.IsCommitExist(form.Target) { ctx.RenderWithErr(ctx.Tr("form.target_branch_not_exist"), tplReleaseNew, &form) return } diff --git a/tests/integration/release_test.go b/tests/integration/release_test.go index 42d0d00e78..439e315347 100644 --- a/tests/integration/release_test.go +++ b/tests/integration/release_test.go @@ -21,6 +21,10 @@ import ( ) func createNewRelease(t *testing.T, session *TestSession, repoURL, tag, title string, preRelease, draft bool) { + createNewReleaseTarget(t, session, repoURL, tag, title, "master", preRelease, draft) +} + +func createNewReleaseTarget(t *testing.T, session *TestSession, repoURL, tag, title, target string, preRelease, draft bool) { req := NewRequest(t, "GET", repoURL+"/releases/new") resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) @@ -31,7 +35,7 @@ func createNewRelease(t *testing.T, session *TestSession, repoURL, tag, title st postData := map[string]string{ "_csrf": htmlDoc.GetCSRF(), "tag_name": tag, - "tag_target": "master", + "tag_target": target, "title": title, "content": "", } @@ -217,6 +221,15 @@ func TestViewReleaseListLogin(t *testing.T) { }, links) } +func TestReleaseOnCommit(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user2") + createNewReleaseTarget(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", "65f1bf27bc3bf70f64657658635e66094edbcb4d", false, false) + + checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").Tr("repo.release.stable"), 4) +} + func TestViewTagsList(t *testing.T) { defer tests.PrepareTestEnv(t)() From 75212b3a59b853df59f6fafab2542f9a2dd82ca3 Mon Sep 17 00:00:00 2001 From: Gusted Date: Fri, 18 Aug 2023 11:21:24 +0200 Subject: [PATCH 05/86] [GITEA] Improve HTML title on repositories - The `` element that lives inside the `<head>` element is an important element that gives browsers and search engine crawlers the title of the webpage, hence the element name. It's therefor important that this title is accurate. - Currently there are three issues with titles on repositories. It doesn't use the `FullName` and instead only uses the repository name, this doesn't distinguish which user or organisation the repository is on. It doesn't show the full treepath in the title when visiting an file inside a directory and instead only uses the latest path in treepath. It can show the repository name twice if the `.Title` variable also included the repository name such as on the repository homepage. - Use the repository's fullname (which include which user the repository is on) instead of just their name. - Display the repository's fullname if it isn't already in `.Title`. - Use the full treepath in the repository code view instead of just the last path. - Adds integration tests. - Adds a new repository (`repo59`) that has 3 depths for folders, which wasn't in any other fixture repository yet, so the full treepath for could be properly tested. - Resolves https://codeberg.org/forgejo/forgejo/issues/1276 (cherry picked from commit ff9a6a2cda34cf2b2e392cc47125ed0f619b287b) (cherry picked from commit 76dffc862103eb23d51445ef9d611296308c8413) (cherry picked from commit ff0615b9d0f3ea4bd86a28c4ac5b0c4740230c81) (cherry picked from commit 8712eaa394053a8c8f1f4cb17307e094c65c7059) (cherry picked from commit 0c11587582b8837778ee85f4e3b04241e5d71760) (cherry picked from commit 3cbd9fb7922177106b309f010dd34a68751873dc) Conflicts: tests/integration/repo_test.go https://codeberg.org/forgejo/forgejo/pulls/1512 (cherry picked from commit fbfdba8ae9e7cb9811452b30d5424fca41231a1f) Conflicts: models/fixtures/release.yml https://codeberg.org/forgejo/forgejo/pulls/1550 (cherry picked from commit 8b2bf0534ca6a2241c2a10cbecd7c96fb96558a6) (cherry picked from commit d706d9e222469c689eb069ec609968296657dfdc) (cherry picked from commit 6d46261a3f81d3642b313e76ad93c5f72fbd6bf8) (cherry picked from commit f864d18ad30760bd1e2fb1925b87b19e3208ad97) (cherry picked from commit 80f8620d0d746c7ce5e88eeef3ec62431c399ec8) [GITEA] Improve HTML title on repositories (squash) do not double escape (cherry picked from commit 22882fe25cde57837a31738a10c71c9478e16662) (cherry picked from commit 63e99df3d1ecb50da3b723848ca85d56b831a8d7) (cherry picked from commit b65d777bc78fabf7e3d1bf8c50aff4eb5395d783) (cherry picked from commit 2961f4f6320b4b38c33f33e7133e7f3d3f86bd0f) (cherry picked from commit f7f723628c76c5c2a0678139fbc4264feea352ea) (cherry picked from commit 9ed79158268160f62dc1b32183c9a487cd521ef7) (cherry picked from commit 8b9ead46085b8a7f1a9c63f561bce4795ccca31d) (cherry picked from commit 50eeaf1fbcf01d8616d8ea792a3b3cd736137f89) (cherry picked from commit ee6f32820e5e0e4ea2ae61fc6a72c475e805b5ac) (cherry picked from commit bf337bed3507a6554bbdd738e6ca1aa80d00df20) (cherry picked from commit 6be9501ec0c6eceda8faa48a4d1dc875da702880) (cherry picked from commit b39860570df95a860c151122a259becb6a221c0e) (cherry picked from commit 3f30f486d516cac043dbdcd780b2277b6a3278d7) (cherry picked from commit 5680ecdbe9b668ce69e5a55b2dd7fb7c0eb7087b) (cherry picked from commit da6a19ad16bd9014ac37e02f10095880baeac65c) (cherry picked from commit 5462493a77dc6f2bf8a0e07e6fbfbe9cce157bcd) (cherry picked from commit 530fe57ddea58aab0d4bfb3b8373a8f4e1632514) (cherry picked from commit f174f35644b2405567a97f6720a55f6cc5fe4f61) Conflicts: models/fixtures/repository.yml https://codeberg.org/forgejo/forgejo/pulls/2214 --- models/fixtures/release.yml | 14 +++ models/fixtures/repo_unit.yml | 32 ++++++ models/fixtures/repository.yml | 14 +++ models/fixtures/user.yml | 2 +- models/repo/repo_list_test.go | 6 +- routers/web/repo/view.go | 4 +- templates/base/head.tmpl | 3 +- .../user2/repo59.git/HEAD | 1 + .../user2/repo59.git/config | 4 + .../user2/repo59.git/description | 1 + .../user2/repo59.git/info/exclude | 6 + .../40/8bbd3bd1f96950f8cf2f98c479557f6b18817a | Bin 0 -> 63 bytes .../5d/5c87a90af64cc67f22d60a942d5efaef8bc96b | Bin 0 -> 50 bytes .../88/3e2970ed6937cbb63311e941adb97df0ae3a52 | Bin 0 -> 49 bytes .../8c/ac7a8f434451410cc91ab9c04d07baff974ad8 | Bin 0 -> 120 bytes .../a0/ccafed39086ef520be6886d9395eb2100d317e | Bin 0 -> 102 bytes .../ab/e2a9ddfd7f542ff89bc13960a929dc8ca86c99 | Bin 0 -> 36 bytes .../cd/879fb6cf5b7bbe0fbc3a0ef44c8695fde89a56 | Bin 0 -> 108 bytes .../d8/f53dfb33f6ccf4169c34970b5e747511c18beb | Bin 0 -> 784 bytes .../f3/c1ec36c0e7605be54e71f24035caa675b7ba41 | Bin 0 -> 48 bytes .../user2/repo59.git/packed-refs | 3 + .../user2/repo59.git/refs/heads/cake-recipe | 1 + tests/integration/api_repo_test.go | 6 +- tests/integration/integration_test.go | 15 +++ tests/integration/repo_test.go | 104 ++++++++++++++++++ 25 files changed, 206 insertions(+), 10 deletions(-) create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/HEAD create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/config create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/description create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/info/exclude create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/40/8bbd3bd1f96950f8cf2f98c479557f6b18817a create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/5d/5c87a90af64cc67f22d60a942d5efaef8bc96b create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/88/3e2970ed6937cbb63311e941adb97df0ae3a52 create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/8c/ac7a8f434451410cc91ab9c04d07baff974ad8 create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/a0/ccafed39086ef520be6886d9395eb2100d317e create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/ab/e2a9ddfd7f542ff89bc13960a929dc8ca86c99 create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/cd/879fb6cf5b7bbe0fbc3a0ef44c8695fde89a56 create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/d8/f53dfb33f6ccf4169c34970b5e747511c18beb create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/f3/c1ec36c0e7605be54e71f24035caa675b7ba41 create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/packed-refs create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/refs/heads/cake-recipe diff --git a/models/fixtures/release.yml b/models/fixtures/release.yml index 372a79509f..01635064c5 100644 --- a/models/fixtures/release.yml +++ b/models/fixtures/release.yml @@ -150,3 +150,17 @@ is_prerelease: false is_tag: false created_unix: 946684803 + +- id: 12 + repo_id: 1059 + publisher_id: 2 + tag_name: "v1.0" + lower_tag_name: "v1.0" + target: "main" + title: "v1.0" + sha1: "d8f53dfb33f6ccf4169c34970b5e747511c18beb" + num_commits: 1 + is_draft: false + is_prerelease: false + is_tag: false + created_unix: 946684803 diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml index e6c59f527a..e3590b06f0 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -608,6 +608,38 @@ type: 1 created_unix: 946684810 +# BEGIN Forgejo [GITEA] Improve HTML title on repositories +- + id: 1093 + repo_id: 1059 + type: 1 + created_unix: 946684810 + +- + id: 1094 + repo_id: 1059 + type: 2 + created_unix: 946684810 + +- + id: 1095 + repo_id: 1059 + type: 3 + created_unix: 946684810 + +- + id: 1096 + repo_id: 1059 + type: 4 + created_unix: 946684810 + +- + id: 1097 + repo_id: 1059 + type: 5 + created_unix: 946684810 +# END Forgejo [GITEA] Improve HTML title on repositories + - id: 91 repo_id: 58 diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index f4e8376735..358215d48a 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -1467,6 +1467,7 @@ owner_name: user27 lower_name: repo49 name: repo49 + description: A wonderful repository with more than just a README.md default_branch: master num_watches: 0 num_stars: 0 @@ -1694,6 +1695,19 @@ is_fsck_enabled: true close_issues_via_commit_in_any_branch: false +- + id: 1059 + owner_id: 2 + owner_name: user2 + lower_name: repo59 + name: repo59 + default_branch: master + is_empty: false + is_archived: false + is_private: false + status: 0 + num_issues: 0 + - id: 59 owner_id: 2 diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index 79fbb981f6..a68e00453a 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -66,7 +66,7 @@ num_followers: 2 num_following: 1 num_stars: 2 - num_repos: 15 + num_repos: 16 num_teams: 0 num_members: 0 visibility: 0 diff --git a/models/repo/repo_list_test.go b/models/repo/repo_list_test.go index 8a1799aac0..a8b958109c 100644 --- a/models/repo/repo_list_test.go +++ b/models/repo/repo_list_test.go @@ -138,12 +138,12 @@ func getTestCases() []struct { { name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative", opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: util.OptionalBoolFalse}, - count: 31, + count: 32, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative", opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: util.OptionalBoolFalse}, - count: 36, + count: 37, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName", @@ -158,7 +158,7 @@ func getTestCases() []struct { { name: "AllPublic/PublicRepositoriesOfOrganization", opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse, Template: util.OptionalBoolFalse}, - count: 31, + count: 32, }, { name: "AllTemplates", diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index aa07d5939d..99d5461c0f 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -166,7 +166,7 @@ func renderDirectory(ctx *context.Context) { if ctx.Repo.TreePath != "" { ctx.Data["HideRepoInfo"] = true - ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName) + ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+ctx.Repo.TreePath, ctx.Repo.RefName) } subfolder, readmeFile, err := findReadmeFileInEntries(ctx, entries, true) @@ -381,7 +381,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) { } defer dataRc.Close() - ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName) + ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+ctx.Repo.TreePath, ctx.Repo.RefName) ctx.Data["FileIsSymlink"] = entry.IsLink() ctx.Data["FileName"] = blob.Name() ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl index 876b42d512..18964f374d 100644 --- a/templates/base/head.tmpl +++ b/templates/base/head.tmpl @@ -2,7 +2,8 @@ <html lang="{{ctx.Locale.Lang}}" data-theme="{{ThemeName .SignedUser}}"> <head> <meta name="viewport" content="width=device-width, initial-scale=1"> - <title>{{if .Title}}{{.Title | RenderEmojiPlain}} - {{end}}{{if .Repository.Name}}{{.Repository.Name}} - {{end}}{{AppName}} + {{/* Display `- .Repsository.FullName` only if `.Title` does not already start with that. */}} + {{if .Title}}{{.Title | RenderEmojiPlain}} - {{end}}{{if and (.Repository.Name) (not (StringUtils.HasPrefix .Title .Repository.FullName))}}{{.Repository.FullName}} - {{end}}{{AppName}} {{if .ManifestData}}{{end}} diff --git a/tests/gitea-repositories-meta/user2/repo59.git/HEAD b/tests/gitea-repositories-meta/user2/repo59.git/HEAD new file mode 100644 index 0000000000..cb089cd89a --- /dev/null +++ b/tests/gitea-repositories-meta/user2/repo59.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/tests/gitea-repositories-meta/user2/repo59.git/config b/tests/gitea-repositories-meta/user2/repo59.git/config new file mode 100644 index 0000000000..07d359d07c --- /dev/null +++ b/tests/gitea-repositories-meta/user2/repo59.git/config @@ -0,0 +1,4 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = true diff --git a/tests/gitea-repositories-meta/user2/repo59.git/description b/tests/gitea-repositories-meta/user2/repo59.git/description new file mode 100644 index 0000000000..498b267a8c --- /dev/null +++ b/tests/gitea-repositories-meta/user2/repo59.git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/tests/gitea-repositories-meta/user2/repo59.git/info/exclude b/tests/gitea-repositories-meta/user2/repo59.git/info/exclude new file mode 100644 index 0000000000..a5196d1be8 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/repo59.git/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/40/8bbd3bd1f96950f8cf2f98c479557f6b18817a b/tests/gitea-repositories-meta/user2/repo59.git/objects/40/8bbd3bd1f96950f8cf2f98c479557f6b18817a new file mode 100644 index 0000000000000000000000000000000000000000..567284ef1c21f472841f3d729cbfe024d78c448c GIT binary patch literal 63 zcmV-F0Korv0ZYosPf{>3XHZrMN-fAQ&Me6)6NNXyJgE!N}W0ssfX5``$?8bAO5 literal 0 HcmV?d00001 diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/5d/5c87a90af64cc67f22d60a942d5efaef8bc96b b/tests/gitea-repositories-meta/user2/repo59.git/objects/5d/5c87a90af64cc67f22d60a942d5efaef8bc96b new file mode 100644 index 0000000000000000000000000000000000000000..f23960f4cc8c09af29fc4765f683b697a4547d74 GIT binary patch literal 50 zcmV-20L}k+0V^p=O;s>9VK6ZO0)@QP;*!j~bcW9d-8WL<+jltv I07;Az9utceGXMYp literal 0 HcmV?d00001 diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/88/3e2970ed6937cbb63311e941adb97df0ae3a52 b/tests/gitea-repositories-meta/user2/repo59.git/objects/88/3e2970ed6937cbb63311e941adb97df0ae3a52 new file mode 100644 index 0000000000000000000000000000000000000000..46cc9e3e5ec14999b5424f35c1c548db9e9e33c1 GIT binary patch literal 49 zcmb7FlR6{FfcPQQ3!H%bn$i7%S~Z$=-z96@n>ehkMsI7j#P%$ zXG=6znHT_pLP~0C0Yhv|`%12FKF8{nu5nG#jr;Y!`(!rMjI`9mlG377y^@L&h7LQ; ag14FGr?(jkzI0r>v-ZO}s~`Z9QY~(zy*Y~j literal 0 HcmV?d00001 diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/a0/ccafed39086ef520be6886d9395eb2100d317e b/tests/gitea-repositories-meta/user2/repo59.git/objects/a0/ccafed39086ef520be6886d9395eb2100d317e new file mode 100644 index 0000000000000000000000000000000000000000..3b71228a7ec7b91f1066cda387ef6efa28c2ae68 GIT binary patch literal 102 zcmV-s0Ga=I0V^p=O;xb4U@$Z=Ff%bx2y%6F@paY9O<`F5Xyx6%^&$E{W*@XnSgCoZ zXGP9TsG{Q3M6>2AdT%A&7_dRgi_+j`HiK;pmmM?MU`7Mx>)%bH?6OHM zk5sBnouqA=Y63LcbH7YOmH|GAl4Hc@EW@%K&C)1I1Uia^1hFYP#!;RNM>a}%Dtb?4 zJAn6?4K(;YO4A`5NBYlfjhe2`eoNZs4?rJ;J;Mr|fWQvz5u(27_uQ2I-(JxbV^x4( z|B6Ty%>s_%fUBlh_~u>6-<$#zs9bFmF%~6^Q@J9f=}|(LabtH3q_NxV$YbR02+h-Wy>8SN zL-n>NnOHxD1_ktBKMTFMF~Oeo44c$Nx1zjPg-n1^s{~0y49o8@?{IqT_1l9FDBH+tz~X}{gyB7!qm$KrCW*^)AW)NzRtpG4}}%%=|SNs z@Nz?3mnX@zAyYKZV*tH#nR{bxS}d9i?M(85HRz`|j6_MUVShjg2W|Ppe8h&7*MdL7JHfOBCJr|RNl?>o$nk$Jg%Kk7Ay z>ilZ^!I!SZjq{9}32tH%t54`QEQTUns*)fL4hA0LId@WGsHUmmoZfJs-`Nc O!MB&yMEwL3??r>^yMtc< literal 0 HcmV?d00001 diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/f3/c1ec36c0e7605be54e71f24035caa675b7ba41 b/tests/gitea-repositories-meta/user2/repo59.git/objects/f3/c1ec36c0e7605be54e71f24035caa675b7ba41 new file mode 100644 index 0000000000000000000000000000000000000000..b6803eb5a271b9d33c981d1ba24fa4126ae409db GIT binary patch literal 48 zcmV-00MGw;0V^p=O;s>9W-u`T0)@2voRrieh6QKVzqRDZ`>L=nqwS_;+$I5D!#V&X Gkq<4~#T1zU literal 0 HcmV?d00001 diff --git a/tests/gitea-repositories-meta/user2/repo59.git/packed-refs b/tests/gitea-repositories-meta/user2/repo59.git/packed-refs new file mode 100644 index 0000000000..114c84d2aa --- /dev/null +++ b/tests/gitea-repositories-meta/user2/repo59.git/packed-refs @@ -0,0 +1,3 @@ +# pack-refs with: peeled fully-peeled sorted +d8f53dfb33f6ccf4169c34970b5e747511c18beb refs/heads/master +d8f53dfb33f6ccf4169c34970b5e747511c18beb refs/tags/v1.0 diff --git a/tests/gitea-repositories-meta/user2/repo59.git/refs/heads/cake-recipe b/tests/gitea-repositories-meta/user2/repo59.git/refs/heads/cake-recipe new file mode 100644 index 0000000000..63bbea6692 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/repo59.git/refs/heads/cake-recipe @@ -0,0 +1 @@ +d8f53dfb33f6ccf4169c34970b5e747511c18beb diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go index 90f84c794e..04c1f29649 100644 --- a/tests/integration/api_repo_test.go +++ b/tests/integration/api_repo_test.go @@ -93,9 +93,9 @@ func TestAPISearchRepo(t *testing.T) { }{ { name: "RepositoriesMax50", requestURL: "/api/v1/repos/search?limit=50&private=false", expectedResults: expectedResults{ - nil: {count: 33}, - user: {count: 33}, - user2: {count: 33}, + nil: {count: 34}, + user: {count: 34}, + user2: {count: 34}, }, }, { diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index b505c9d857..60a85ee5d4 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -563,3 +563,18 @@ func GetCSRF(t testing.TB, session *TestSession, urlStr string) string { doc := NewHTMLParser(t, resp.Body) return doc.GetCSRF() } + +func GetHTMLTitle(t testing.TB, session *TestSession, urlStr string) string { + t.Helper() + + req := NewRequest(t, "GET", urlStr) + var resp *httptest.ResponseRecorder + if session == nil { + resp = MakeRequest(t, req, http.StatusOK) + } else { + resp = session.MakeRequest(t, req, http.StatusOK) + } + + doc := NewHTMLParser(t, resp.Body) + return doc.Find("head title").Text() +} diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go index f141b6dcb1..157d2ba99d 100644 --- a/tests/integration/repo_test.go +++ b/tests/integration/repo_test.go @@ -201,6 +201,110 @@ func TestViewAsRepoAdmin(t *testing.T) { } } +func TestRepoHTMLTitle(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + t.Run("Repository homepage", func(t *testing.T) { + t.Run("Without description", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + htmlTitle := GetHTMLTitle(t, nil, "/user2/repo1") + assert.EqualValues(t, "user2/repo1 - Gitea: Git with a cup of tea", htmlTitle) + }) + t.Run("With description", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + htmlTitle := GetHTMLTitle(t, nil, "/user27/repo49") + assert.EqualValues(t, "user27/repo49: A wonderful repository with more than just a README.md - Gitea: Git with a cup of tea", htmlTitle) + }) + }) + + t.Run("Code view", func(t *testing.T) { + t.Run("Directory", func(t *testing.T) { + t.Run("Default branch", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + htmlTitle := GetHTMLTitle(t, nil, "/user2/repo59/src/branch/master/deep/nesting") + assert.EqualValues(t, "repo59/deep/nesting at master - user2/repo59 - Gitea: Git with a cup of tea", htmlTitle) + }) + t.Run("Non-default branch", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + htmlTitle := GetHTMLTitle(t, nil, "/user2/repo59/src/branch/cake-recipe/deep/nesting") + assert.EqualValues(t, "repo59/deep/nesting at cake-recipe - user2/repo59 - Gitea: Git with a cup of tea", htmlTitle) + }) + t.Run("Commit", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + htmlTitle := GetHTMLTitle(t, nil, "/user2/repo59/src/commit/d8f53dfb33f6ccf4169c34970b5e747511c18beb/deep/nesting/") + assert.EqualValues(t, "repo59/deep/nesting at d8f53dfb33f6ccf4169c34970b5e747511c18beb - user2/repo59 - Gitea: Git with a cup of tea", htmlTitle) + }) + t.Run("Tag", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + htmlTitle := GetHTMLTitle(t, nil, "/user2/repo59/src/tag/v1.0/deep/nesting/") + assert.EqualValues(t, "repo59/deep/nesting at v1.0 - user2/repo59 - Gitea: Git with a cup of tea", htmlTitle) + }) + }) + t.Run("File", func(t *testing.T) { + t.Run("Default branch", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + htmlTitle := GetHTMLTitle(t, nil, "/user2/repo59/src/branch/master/deep/nesting/folder/secret_sauce_recipe.txt") + assert.EqualValues(t, "repo59/deep/nesting/folder/secret_sauce_recipe.txt at master - user2/repo59 - Gitea: Git with a cup of tea", htmlTitle) + }) + t.Run("Non-default branch", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + htmlTitle := GetHTMLTitle(t, nil, "/user2/repo59/src/branch/cake-recipe/deep/nesting/folder/secret_sauce_recipe.txt") + assert.EqualValues(t, "repo59/deep/nesting/folder/secret_sauce_recipe.txt at cake-recipe - user2/repo59 - Gitea: Git with a cup of tea", htmlTitle) + }) + t.Run("Commit", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + htmlTitle := GetHTMLTitle(t, nil, "/user2/repo59/src/commit/d8f53dfb33f6ccf4169c34970b5e747511c18beb/deep/nesting/folder/secret_sauce_recipe.txt") + assert.EqualValues(t, "repo59/deep/nesting/folder/secret_sauce_recipe.txt at d8f53dfb33f6ccf4169c34970b5e747511c18beb - user2/repo59 - Gitea: Git with a cup of tea", htmlTitle) + }) + t.Run("Tag", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + htmlTitle := GetHTMLTitle(t, nil, "/user2/repo59/src/tag/v1.0/deep/nesting/folder/secret_sauce_recipe.txt") + assert.EqualValues(t, "repo59/deep/nesting/folder/secret_sauce_recipe.txt at v1.0 - user2/repo59 - Gitea: Git with a cup of tea", htmlTitle) + }) + }) + }) + + t.Run("Issues view", func(t *testing.T) { + t.Run("Overview page", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + htmlTitle := GetHTMLTitle(t, nil, "/user2/repo1/issues") + assert.EqualValues(t, "Issues - user2/repo1 - Gitea: Git with a cup of tea", htmlTitle) + }) + t.Run("View issue page", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + htmlTitle := GetHTMLTitle(t, nil, "/user2/repo1/issues/1") + assert.EqualValues(t, "#1 - issue1 - user2/repo1 - Gitea: Git with a cup of tea", htmlTitle) + }) + }) + + t.Run("Pull requests view", func(t *testing.T) { + t.Run("Overview page", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + htmlTitle := GetHTMLTitle(t, nil, "/user2/repo1/pulls") + assert.EqualValues(t, "Pull Requests - user2/repo1 - Gitea: Git with a cup of tea", htmlTitle) + }) + t.Run("View pull request", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + htmlTitle := GetHTMLTitle(t, nil, "/user2/repo1/pulls/2") + assert.EqualValues(t, "#2 - issue2 - user2/repo1 - Gitea: Git with a cup of tea", htmlTitle) + }) + }) +} + // TestViewFileInRepo repo description, topics and summary should not be displayed when viewing a file func TestViewFileInRepo(t *testing.T) { defer tests.PrepareTestEnv(t)() From e34a05bc7319072b70d387975342d617b8136655 Mon Sep 17 00:00:00 2001 From: Gusted Date: Fri, 18 Aug 2023 04:39:23 +0200 Subject: [PATCH 06/86] [GITEA] Add slow SQL query warning - Databases are one of the most important parts of Forgejo, every interaction with Forgejo uses the database in one way or another. Therefore, it is important to maintain the database and recognize when Forgejo is not doing well with the database. Forgejo already has the option to log *every* SQL query along with its execution time, but monitoring becomes impractical for larger instances and takes up unnecessary storage in the logs. - Add a QoL enhancement that allows instance administrators to specify a threshold value beyond which query execution time is logged as a warning in the xorm logger. The default value is a conservative five seconds to avoid this becoming a source of spam in the logs. - The use case for this patch is that with an instance the size of Codeberg, monitoring SQL logs is not very fruitful and most of them are uninteresting. Recently, in the context of persistent deadlock issues (https://codeberg.org/forgejo/forgejo/issues/220), I have noticed that certain queries hold locks on tables like comment and issue for several seconds. This patch helps to identify which queries these are and when they happen. - Added unit test. (cherry picked from commit 24bbe7886fb4cb9a38c8dab8c44f4c9cbfa25481) (cherry picked from commit 6e29145b3c1455498531593d38e6a914941a12cb) (cherry picked from commit 63731e30712872bd2395eb3cf36d9996e5793645) (cherry picked from commit 3ce1a097369c132654de70df707b867e47bd1c40) (cherry picked from commit a64426907de788cc0937a7a2b16af4d2f26f7fe6) (cherry picked from commit 4b1921569156445c58d9889602733da5934c7b95) (cherry picked from commit e6356744359fa947c049827d60c2ea0e277e03dc) (cherry picked from commit 9cf501f1af4cd870221cef6af489618785b71186) (cherry picked from commit 0d6b934eba1c0e9b27b364791113aae816b6b366) (cherry picked from commit 4b6c2738795002887844a106f2fed2ef1673eed1) (cherry picked from commit 89b1315338b0c7a726a36a84e9844013a13560b8) (cherry picked from commit edd8e66ce991c395bb0af7720631c3cd26caaa51) [GITEA] Add slow SQL query warning (squash) document the setting (cherry picked from commit ce38599c5141c7fc6bc054819f5ff1c1b45bda1f) (cherry picked from commit 794aa67c68c8e24ac7301eb7ef767c6e2499a78d) (cherry picked from commit a4c2c6b004c21488e90f637ca7920f49108ed75d) (cherry picked from commit 97912752bc802db79bb26a6591aec885aea30ee4) (cherry picked from commit 00b5327c9750215a290238516e7b6fb1e6601e14) (cherry picked from commit 1069c860e78c11225b4d74ff3044df7786562821) (cherry picked from commit 84241f42c83852918b57c8bd25364697037fe42f) (cherry picked from commit e4bda0e8457d00c01b83f153ed5a4a8ea4cf85c8) (cherry picked from commit 7357fb91bff87045b133c3a7ac9fc70eea781bc4) (cherry picked from commit a8dd7f6da278ae112200b5efa5bf27e3961f5996) (cherry picked from commit e636e9f4beca7273dd8622baedb2f0c01db30449) (cherry picked from commit bf04ae86037f5cb5a81d02750aead2742b040367) (cherry picked from commit 93b19e3568169bd1cf9b8b78c1751c3d2d65a1b6) (cherry picked from commit 83f91363ad071675c73a1f636271cc043bf69707) --- custom/conf/app.example.ini | 4 ++ .../config-cheat-sheet.en-us.md | 1 + models/db/engine.go | 28 ++++++++++++++ models/db/engine_test.go | 38 +++++++++++++++++++ modules/setting/database.go | 2 + 5 files changed, 73 insertions(+) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 488c653133..a57f1c0a62 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -412,6 +412,10 @@ USER = root ;; ;; Whether execute database models migrations automatically ;AUTO_MIGRATION = true +;; +;; Threshold value (in seconds) beyond which query execution time is logged as a warning in the xorm logger +;; +;SLOW_QUERY_TRESHOLD = 5s ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md index eb9b8d1ae9..57bbc78718 100644 --- a/docs/content/administration/config-cheat-sheet.en-us.md +++ b/docs/content/administration/config-cheat-sheet.en-us.md @@ -458,6 +458,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a - `MAX_IDLE_CONNS` **2**: Max idle database connections on connection pool, default is 2 - this will be capped to `MAX_OPEN_CONNS`. - `CONN_MAX_LIFETIME` **0 or 3s**: Sets the maximum amount of time a DB connection may be reused - default is 0, meaning there is no limit (except on MySQL where it is 3s - see #6804 & #7071). - `AUTO_MIGRATION` **true**: Whether execute database models migrations automatically. +- `SLOW_QUERY_TRESHOLD` **5s**: Threshold value in seconds beyond which query execution time is logged as a warning in the xorm logger. [^1]: It may be necessary to specify a hostport even when listening on a unix socket, as the port is part of the socket name. see [#24552](https://github.com/go-gitea/gitea/issues/24552#issuecomment-1681649367) for additional details. diff --git a/models/db/engine.go b/models/db/engine.go index b2fbdcfbf0..ad8ce7ecff 100755 --- a/models/db/engine.go +++ b/models/db/engine.go @@ -11,10 +11,13 @@ import ( "io" "reflect" "strings" + "time" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "xorm.io/xorm" + "xorm.io/xorm/contexts" "xorm.io/xorm/names" "xorm.io/xorm/schemas" @@ -144,6 +147,13 @@ func InitEngine(ctx context.Context) error { xormEngine.SetConnMaxLifetime(setting.Database.ConnMaxLifetime) xormEngine.SetDefaultContext(ctx) + if setting.Database.SlowQueryTreshold > 0 { + xormEngine.AddHook(&SlowQueryHook{ + Treshold: setting.Database.SlowQueryTreshold, + Logger: log.GetLogger("xorm"), + }) + } + SetDefaultEngine(ctx, xormEngine) return nil } @@ -299,3 +309,21 @@ func SetLogSQL(ctx context.Context, on bool) { sess.Engine().ShowSQL(on) } } + +type SlowQueryHook struct { + Treshold time.Duration + Logger log.Logger +} + +var _ contexts.Hook = &SlowQueryHook{} + +func (SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) { + return c.Ctx, nil +} + +func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error { + if c.ExecuteTime >= h.Treshold { + h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime) + } + return nil +} diff --git a/models/db/engine_test.go b/models/db/engine_test.go index c9ae5f1542..ba922821b0 100644 --- a/models/db/engine_test.go +++ b/models/db/engine_test.go @@ -6,15 +6,19 @@ package db_test import ( "path/filepath" "testing" + "time" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" _ "code.gitea.io/gitea/cmd" // for TestPrimaryKeys "github.com/stretchr/testify/assert" + "xorm.io/xorm" ) func TestDumpDatabase(t *testing.T) { @@ -85,3 +89,37 @@ func TestPrimaryKeys(t *testing.T) { } } } + +func TestSlowQuery(t *testing.T) { + lc, cleanup := test.NewLogChecker("slow-query") + lc.StopMark("[Slow SQL Query]") + defer cleanup() + + e := db.GetEngine(db.DefaultContext) + engine, ok := e.(*xorm.Engine) + assert.True(t, ok) + + // It's not possible to clean this up with XORM, but it's luckily not harmful + // to leave around. + engine.AddHook(&db.SlowQueryHook{ + Treshold: time.Second * 10, + Logger: log.GetLogger("slow-query"), + }) + + // NOOP query. + e.Exec("SELECT 1 WHERE false;") + + _, stopped := lc.Check(100 * time.Millisecond) + assert.False(t, stopped) + + engine.AddHook(&db.SlowQueryHook{ + Treshold: 0, // Every query should be logged. + Logger: log.GetLogger("slow-query"), + }) + + // NOOP query. + e.Exec("SELECT 1 WHERE false;") + + _, stopped = lc.Check(100 * time.Millisecond) + assert.True(t, stopped) +} diff --git a/modules/setting/database.go b/modules/setting/database.go index e200b15b2e..54551b44bb 100644 --- a/modules/setting/database.go +++ b/modules/setting/database.go @@ -45,6 +45,7 @@ var ( ConnMaxLifetime time.Duration IterateBufferSize int AutoMigration bool + SlowQueryTreshold time.Duration }{ Timeout: 500, IterateBufferSize: 50, @@ -87,6 +88,7 @@ func loadDBSetting(rootCfg ConfigProvider) { Database.DBConnectRetries = sec.Key("DB_RETRIES").MustInt(10) Database.DBConnectBackoff = sec.Key("DB_RETRY_BACKOFF").MustDuration(3 * time.Second) Database.AutoMigration = sec.Key("AUTO_MIGRATION").MustBool(true) + Database.SlowQueryTreshold = sec.Key("SLOW_QUERY_TRESHOLD").MustDuration(5 * time.Second) } // DBConnStr returns database connection string From 6b0dab3ba0aba46d114943f519fd318ec4cc4133 Mon Sep 17 00:00:00 2001 From: Gusted Date: Fri, 18 Aug 2023 15:18:23 +0200 Subject: [PATCH 07/86] [GITEA] Use vertical tabs on issue filters - This is actually https://github.com/go-gitea/gitea/pull/19978 & https://github.com/go-gitea/gitea/pull/19486 but was removed in one of the UI refactors of v1.20 - This is a very technical fix and is best explained in the CSS comments. But the short version: When there's an overflow being set, but you want an element to 'break out' of that overflow with `position: absolute`, it sometimes doesn't work! You need to set some CSS to let the browser know that the element needs to use an element outside of that overflow as 'clip parent'. - Resolves my internal frustration with the mobile UI constantly getting broken. (cherry picked from commit 879f842bed999190506e1d60508e7aede1a4be21) (cherry picked from commit 6099c9b41b9a135fb14b41304459056050abbbe2) (cherry picked from commit 0749d00b160033de0457e17baa1e000e68810d94) (cherry picked from commit ec6a5428a7f05d8e6d2e8a6c476b2b9d35656b0f) (cherry picked from commit 9d0bee784d387fac026d3dcc09f10e496a99a7c5) (cherry picked from commit 61d6ae48828cb83ca3668a28ba8ddcb7fcb471d5) (cherry picked from commit 8b3f3639b60ac6f3de8f9fcd83ac7a48bbd659f0) (cherry picked from commit 2c600ddb2c3e76598b9bdbd58026ab76d7590470) (cherry picked from commit 960a9786ef62eb8664bca129e9d4fae22e98f378) (cherry picked from commit b194354c3b489879b10774174cc91a6915b43abc) (cherry picked from commit 8e7915ee8c7061474b821f2275d11344b06bb9df) (cherry picked from commit ba82b0c6fe217c44140b75f0afb0c92186460b23) (cherry picked from commit b2dfb233a8f9c6d1ea82cd11891de20e02aed22b) (cherry picked from commit ff3ec7f612c5c1cc743862e03a59c5fadd369401) (cherry picked from commit ef01240cc7344d309a13d19a5e5d6a723c5c4557) (cherry picked from commit 7778b5bb1019619cd0ed25ef6a0aad37ae978185) (cherry picked from commit 5f949b1b07ff57a4fc17438ea44523e2f3a489f8) (cherry picked from commit b3872096907177ee0226011ff181b80003be9c96) (cherry picked from commit 5d7e3a542effdfe04ed906868eeebfbff8270ee4) (cherry picked from commit ffef2231fb4abbee684a4ff7a51988d4977c3fe3) (cherry picked from commit c74cf73ab4d98c0c2dd4b07c5e84696f897d60c9) (cherry picked from commit 4aa9e9fca4e92f4573daa9738fa04fcfe19de364) --- web_src/css/repo/issue-list.css | 35 +++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/web_src/css/repo/issue-list.css b/web_src/css/repo/issue-list.css index 1421577af2..6c3755fa49 100644 --- a/web_src/css/repo/issue-list.css +++ b/web_src/css/repo/issue-list.css @@ -17,10 +17,6 @@ } @media (max-width: 767.98px) { - .issue-list-toolbar-right .dropdown .menu { - left: auto !important; - right: auto !important; - } .issue-list-navbar { order: 0; } @@ -31,6 +27,37 @@ .issue-list-search { order: 2 !important; } + /* Don't use flex wrap on mobile as it takes too much vertical space. + * Only set overflow properties on mobile screens, because while the + * CSS trick to pop out from overflowing works on desktop screen, it + * has a massive flaw that it cannot inherited any max width from it's 'real' + * parent and therefor ends up taking more vertical space than is desired. + **/ + .issue-list-toolbar-right .filter.menu { + flex-wrap: nowrap; + overflow-x: auto; + overflow-y: hidden; + } + + /* The following few CSS was created with care and built with the information + * from CSS-Tricks: https://css-tricks.com/popping-hidden-overflow/ + */ + + /* It's important that every element up to .issue-list-toolbar-right doesn't + * have a position set, such that element that wants to pop out will use + * .issue-list-toolbar-right as 'clip parent' and thereby avoids the + * overflow-y: hidden. + */ + .issue-list-toolbar-right .filter.menu > .dropdown.item { + position: initial; + } + /* It's important that this element and not an child has `position` set. + * Set width so that overflow-x knows where to stop overflowing. + */ + .issue-list-toolbar-right { + position: relative; + width: 100%; + } } #issue-list .flex-item-title .labels-list { From ba02501caf1b32165fe2221e2706a9ceddc237db Mon Sep 17 00:00:00 2001 From: zareck Date: Sun, 20 Aug 2023 11:16:30 -0300 Subject: [PATCH 08/86] [GITEA] add GitHub repo migration test Signed-off-by: zareck (cherry picked from commit f48e3ff0db027c6420446c0bab3089d9a46194a8) Removing comments and make command (cherry picked from commit 7664a423a5abf051383374b4156857e83faee7c0) (cherry picked from commit b2fb43536424f90373fdc177bd2c79c374efd2be) (cherry picked from commit 0a24a819a9561c8355adb00b7b202438c5c1bc1a) (cherry picked from commit 155cc19f75662998fcb2a1a08e345e0724437a58) (cherry picked from commit 223537f71a05107d69eb5edb8d62d40e5fac5fee) (cherry picked from commit ffbe2970cc7a778bfbcd9a93cc03bbc4bce38897) (cherry picked from commit 836836bd73a5635ee13b032d1600e7da842db42c) (cherry picked from commit 6b66fe449d5ee409ae5590ad08cdf46b7dfe8aa9) (cherry picked from commit a3933d9c3abd14e74d4c8c41ad5824ba34c0424a) (cherry picked from commit f1a49065f241886a9edc101ae360bf8b691fa400) (cherry picked from commit 63f4935e7de1901082afec0bec0a7997fd158dbb) (cherry picked from commit a1acdd76e6c41825ceb18445baacee1e8e627b3e) (cherry picked from commit 7f902568043e54d7059e031b0d8ccdb504837891) (cherry picked from commit 73620b0e8e01e7c52c9dff1097932b7bf1426be9) (cherry picked from commit 587540c818e6a8190c0742e1906e35be94207143) (cherry picked from commit 434d5366aca58383a12b22ac49797d5a54042b64) (cherry picked from commit e80e193af4f30726278cad43a627ca268517d584) (cherry picked from commit eb9be4cee6f53352cb18536dde945e1fb922ef4d) (cherry picked from commit f81cfdc9357da67715ab369a3041fbb42028125f) (cherry picked from commit ba69a943cb36d10e99037fcf7c052449edd13d2f) (cherry picked from commit ea9bc8824889a8a873d029b9b17da2d1c4cf6425) --- tests/integration/repo_migrate_test.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/integration/repo_migrate_test.go b/tests/integration/repo_migrate_test.go index 91e2961d6d..9fb7a73379 100644 --- a/tests/integration/repo_migrate_test.go +++ b/tests/integration/repo_migrate_test.go @@ -15,8 +15,8 @@ import ( "github.com/stretchr/testify/assert" ) -func testRepoMigrate(t testing.TB, session *TestSession, cloneAddr, repoName string) *httptest.ResponseRecorder { - req := NewRequest(t, "GET", fmt.Sprintf("/repo/migrate?service_type=%d", structs.PlainGitService)) // render plain git migration page +func testRepoMigrate(t testing.TB, session *TestSession, cloneAddr, repoName string, service structs.GitServiceType) *httptest.ResponseRecorder { + req := NewRequest(t, "GET", fmt.Sprintf("/repo/migrate?service_type=%d", service)) // render plain git migration page resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) @@ -31,7 +31,7 @@ func testRepoMigrate(t testing.TB, session *TestSession, cloneAddr, repoName str "clone_addr": cloneAddr, "uid": uid, "repo_name": repoName, - "service": fmt.Sprintf("%d", structs.PlainGitService), + "service": fmt.Sprintf("%d", service), }) resp = session.MakeRequest(t, req, http.StatusSeeOther) @@ -41,5 +41,17 @@ func testRepoMigrate(t testing.TB, session *TestSession, cloneAddr, repoName str func TestRepoMigrate(t *testing.T) { defer tests.PrepareTestEnv(t)() session := loginUser(t, "user2") - testRepoMigrate(t, session, "https://github.com/go-gitea/test_repo.git", "git") + for _, s := range []struct { + testName string + cloneAddr string + repoName string + service structs.GitServiceType + }{ + {"TestMigrateGithub", "https://github.com/go-gitea/test_repo.git", "git", structs.PlainGitService}, + {"TestMigrateGithub", "https://github.com/go-gitea/test_repo.git", "github", structs.GithubService}, + } { + t.Run(s.testName, func(t *testing.T) { + testRepoMigrate(t, session, s.cloneAddr, s.repoName, s.service) + }) + } } From 0d85b878256af42d0c077ed186e3f374da3f837e Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Wed, 30 Aug 2023 15:21:18 +0200 Subject: [PATCH 09/86] [GITEA] [picture].*AVATAR_UPLOAD_PATH is legacy (cherry picked from commit cb4cc01825458752efe01628f705b4f8676e49a2) (cherry picked from commit bef11d61318a462e34202f78fad7f883b0756a88) (cherry picked from commit 077b1c52b6e330a66aa55c4e29562278e94026d1) (cherry picked from commit aff7aa08587855b71495fa52c301d653a42da38f) (cherry picked from commit d2f8f6eacbc669a3ae800304f0f8b3f5a11e1a11) (cherry picked from commit 476bd3c4910d15fe7e5f68abc304fd4e3166edaa) (cherry picked from commit 2b39e973be4e72b20f26a40aed894b67371f563f) (cherry picked from commit 822f25de53ed9afaba3d6d8a2200a717fde189db) (cherry picked from commit ed941b0e60a0eede499e71d9024e8240cc3a9cb8) (cherry picked from commit ac6c5ddb2ae39cece4f676ecc0ef3a39bc0866ba) (cherry picked from commit 52b8e33612304ebe704c991c787fc5be7439503b) (cherry picked from commit 1c7d1427d25fec5f8180ebf3b22a707985b040ee) (cherry picked from commit 1caa855c6de3895aad691e04979e0a78c9d08fcb) (cherry picked from commit 55a04f5a9a741c5dba5cb1300c95dc24fae19e9c) (cherry picked from commit 31124e8818acc1ca44ae1cf871356705afd27f19) (cherry picked from commit 9415f18e70dab101f41f53e736bfa26059da9c6f) (cherry picked from commit 358222a7d333693caa95f5ad2e064cab5f49e820) (cherry picked from commit b6a9826552aba33a0b854fa24e672ebba7ddcd84) (cherry picked from commit bc191689484c93b1bf2b58a997d3b2289209a579) (cherry picked from commit eb1378b843ebc6fa4af3b0d685478aa839ddd218) --- custom/conf/app.example.ini | 3 --- 1 file changed, 3 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index a57f1c0a62..473a094c4d 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1792,9 +1792,6 @@ LEVEL = Info ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; -;AVATAR_UPLOAD_PATH = data/avatars -;REPOSITORY_AVATAR_UPLOAD_PATH = data/repo-avatars -;; ;; How Gitea deals with missing repository avatars ;; none = no avatar will be displayed; random = random avatar will be displayed; image = default image will be used ;REPOSITORY_AVATAR_FALLBACK = none From 3cd48ef4d53c55a81c97f1b666b8d4ba16a967c4 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Thu, 7 Sep 2023 07:11:29 +0000 Subject: [PATCH 10/86] [GITEA] notifies admins on new user registration Sends email with information on the new user (time of creation and time of last sign-in) and a link to manage the new user from the admin panel closes: https://codeberg.org/forgejo/forgejo/issues/480 Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/1371 Co-authored-by: Aravinth Manivannan Co-committed-by: Aravinth Manivannan (cherry picked from commit c721aa828ba6aec5ef95459cfc632a0a1f7463e9) (cherry picked from commit 6487efcb9da61be1f802f1cd8007330153322770) Conflicts: modules/notification/base/notifier.go modules/notification/base/null.go modules/notification/notification.go https://codeberg.org/forgejo/forgejo/pulls/1422 (cherry picked from commit 7ea66ee1c5dd21d9e6a43f961e8adc71ec79b806) Conflicts: services/notify/notifier.go services/notify/notify.go services/notify/null.go https://codeberg.org/forgejo/forgejo/pulls/1469 (cherry picked from commit 7d2d9970115c94954dacb45684f9e3c16117ebfe) (cherry picked from commit 435a54f14039408b315c99063bdce28c7ef6fe2f) (cherry picked from commit 8ec7b3e4484383445fa2622a28bb4f5c990dd4f2) [GITEA] notifies admins on new user registration (squash) performance bottleneck Refs: https://codeberg.org/forgejo/forgejo/issues/1479 (cherry picked from commit 97ac9147ff3643cca0a059688c6b3c53479e28a7) (cherry picked from commit 19f295c16bd392aa438477fa3c42038d63d1a06a) (cherry picked from commit 3367dcb2cf5328e2afc89f7d5a008b64ede1c987) [GITEA] notifies admins on new user registration (squash) cosmetic changes Co-authored-by: delvh (cherry picked from commit 9f1670e040b469ed4346aa2689a75088e4e71c8b) (cherry picked from commit de5bb2a224ab2ae9be891de1ee88a7454a07f7e9) (cherry picked from commit 8f8e52f31a4da080465521747a2c5c0c51ed65e3) (cherry picked from commit e0d51303129fe8763d87ed5f859eeae8f0cc6188) (cherry picked from commit f1288d6d9bfc9150596cb2f7ddb7300cf7ab6952) (cherry picked from commit 1db4736fd7cd75027f3cdf805e0f86c3a5f69c9d) (cherry picked from commit e8dcbb6cd68064209cdbe054d5886710cbe2925d) (cherry picked from commit 09625d647629b85397270e14dfe22258df2bcc43) [GITEA] notifies admins on new user registration (squash) ctx.Locale (cherry picked from commit dab7212fad44a252a1acf8da71b254b1a6715121) (cherry picked from commit 9b7bbae8c4cd5dc4d36726f10870462c8985e543) (cherry picked from commit f750b71d3db9a24dc2722effb8bbc2dded657cbb) (cherry picked from commit f79af366796a8ab581bbfa1f5609dc721798ae68) (cherry picked from commit e76eee334e446a45d841caf19a7c18eab89ca457) [GITEA] notifies admins on new user registration (squash) fix locale (cherry picked from commit 54cd100d8da37ccb0a545e2545995066f92180f0) (cherry picked from commit 053dbd3d50d3c7d1afae8d31c25bda92ceb8f8c0) [GITEA] notifies admins on new user registration (squash) fix URL 1. Use absolute URL in the admin panel link sent on new registrations 2. Include absolute URL of the newly signed-up user's profile. New email looks like this:
Please click to expand ``` --153937b1864f158f4fd145c4b5d4a513568681dd489021dd466a8ad7b770 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=UTF-8 User Information: @realaravinth ( http://localhost:3000/realaravinth ) ---------------------------------------------------------------------- * Created: 2023-12-13 19:36:50 +05:30 Please click here ( http://localhost:3000/admin/users/9 ) to manage the use= r from the admin panel. --153937b1864f158f4fd145c4b5d4a513568681dd489021dd466a8ad7b770 Content-Transfer-Encoding: quoted-printable Content-Type: text/html; charset=UTF-8 New user realaravinth just signed up

Please click here to manage the user from the admin panel.

--153937b1864f158f4fd145c4b5d4a513568681dd489021dd466a8ad7b770-- ```
fixes: https://codeberg.org/forgejo/forgejo/issues/1927 Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/1940 Reviewed-by: Earl Warren Reviewed-by: Gusted Co-authored-by: Aravinth Manivannan Co-committed-by: Aravinth Manivannan (cherry picked from commit b8d764e36a0cd8e60627805f87b84bb04152e9c1) (cherry picked from commit d48b84f623e369222e5e15965f22e27d74ff4243) Conflicts: routers/web/auth/auth.go https://codeberg.org/forgejo/forgejo/pulls/2034 (cherry picked from commit 02d3c125ccc97638849af33c7df315cbcb368127) (cherry picked from commit 367374ecc3832bb47d29ff79370103f907d0ca99) Conflicts: models/user/user_test.go https://codeberg.org/forgejo/forgejo/pulls/2119 (cherry picked from commit 4124fa5aa41c36b3ab3cc1c65d0e3d5e05ec4086) (cherry picked from commit 7f12610ff63d4907631d8cddcd7a49ae6f6e1508) [GITEA] notifies admins on new user registration (squash) DeleteByID trivial conflict because of 778ad795fd4a19dc15723b59a846a250034c7c3a Refactor deletion (#28610) (cherry picked from commit 05682614e5ef2462cbb6a1635ca01e296fe03d23) (cherry picked from commit 64bd374803a76c97619fe1e28bfc74f99ec91677) (cherry picked from commit 63d086f666a880b48d034b129e535fcfc82acf7d) --- custom/conf/app.example.ini | 2 + .../config-cheat-sheet.en-us.md | 1 + models/user/user.go | 6 ++ models/user/user_test.go | 10 ++ modules/setting/admin.go | 5 +- options/locale/locale_en-US.ini | 4 + routers/web/auth/auth.go | 2 + services/mailer/mail_admin_new_user.go | 81 +++++++++++++++ services/mailer/mail_admin_new_user_test.go | 98 +++++++++++++++++++ services/mailer/notify.go | 4 + services/notify/notifier.go | 2 + services/notify/notify.go | 7 ++ services/notify/null.go | 3 + templates/mail/notify/admin_new_user.tmpl | 21 ++++ 14 files changed, 244 insertions(+), 2 deletions(-) create mode 100644 services/mailer/mail_admin_new_user.go create mode 100644 services/mailer/mail_admin_new_user_test.go create mode 100644 templates/mail/notify/admin_new_user.tmpl diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 473a094c4d..2a21bd64b1 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1479,6 +1479,8 @@ LEVEL = Info ;; ;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled ;DEFAULT_EMAIL_NOTIFICATIONS = enabled +;; Send an email to all admins when a new user signs up to inform the admins about this act. Options: true, false +;SEND_NOTIFICATION_EMAIL_ON_NEW_USER = false ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md index 57bbc78718..eae956798f 100644 --- a/docs/content/administration/config-cheat-sheet.en-us.md +++ b/docs/content/administration/config-cheat-sheet.en-us.md @@ -518,6 +518,7 @@ And the following unique queues: - `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled - `DISABLE_REGULAR_ORG_CREATION`: **false**: Disallow regular (non-admin) users from creating organizations. +- `SEND_NOTIFICATION_EMAIL_ON_NEW_USER`: **false**: Send an email to all admins when a new user signs up to inform the admins about this act. ## Security (`security`) diff --git a/models/user/user.go b/models/user/user.go index 581e4a2b7b..ff197db8b3 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -228,6 +228,12 @@ func GetAllUsers(ctx context.Context) ([]*User, error) { return users, db.GetEngine(ctx).OrderBy("id").Where("type = ?", UserTypeIndividual).Find(&users) } +// GetAllAdmins returns a slice of all adminusers found in DB. +func GetAllAdmins(ctx context.Context) ([]*User, error) { + users := make([]*User, 0) + return users, db.GetEngine(ctx).OrderBy("id").Where("type = ?", UserTypeIndividual).And("is_admin = ?", true).Find(&users) +} + // IsLocal returns true if user login type is LoginPlain. func (u *User) IsLocal() bool { return u.LoginType <= auth.Plain diff --git a/models/user/user_test.go b/models/user/user_test.go index 65aebea43a..0e08529156 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -527,6 +527,16 @@ func TestIsUserVisibleToViewer(t *testing.T) { test(user31, nil, false) } +func TestGetAllAdmins(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + admins, err := user_model.GetAllAdmins(db.DefaultContext) + assert.NoError(t, err) + + assert.Len(t, admins, 1) + assert.Equal(t, int64(1), admins[0].ID) +} + func Test_ValidateUser(t *testing.T) { oldSetting := setting.Service.AllowedUserVisibilityModesSlice defer func() { diff --git a/modules/setting/admin.go b/modules/setting/admin.go index 2d2dd26de9..d7f0ee827d 100644 --- a/modules/setting/admin.go +++ b/modules/setting/admin.go @@ -5,8 +5,9 @@ package setting // Admin settings var Admin struct { - DisableRegularOrgCreation bool - DefaultEmailNotification string + DisableRegularOrgCreation bool + DefaultEmailNotification string + SendNotificationEmailOnNewUser bool } func loadAdminFrom(rootCfg ConfigProvider) { diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 3c96f77fca..8f115c9de4 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -441,6 +441,10 @@ activate_email = Verify your email address activate_email.title = %s, please verify your email address activate_email.text = Please click the following link to verify your email address within %s: +admin.new_user.subject = New user %s just signed up +admin.new_user.user_info = User Information +admin.new_user.text = Please click here to manage the user from the admin panel. + register_notify = Welcome to Gitea register_notify.title = %[1]s, welcome to %[2]s register_notify.text_1 = this is your registration confirmation email for %s! diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 9bc7566432..e86c5da3eb 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -32,6 +32,7 @@ import ( "code.gitea.io/gitea/services/externalaccount" "code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/mailer" + notify_service "code.gitea.io/gitea/services/notify" "github.com/markbates/goth" ) @@ -600,6 +601,7 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth. } } + notify_service.NewUserSignUp(ctx, u) // update external user information if gothUser != nil { if err := externalaccount.UpdateExternalUser(ctx, u, *gothUser); err != nil { diff --git a/services/mailer/mail_admin_new_user.go b/services/mailer/mail_admin_new_user.go new file mode 100644 index 0000000000..e9610e626a --- /dev/null +++ b/services/mailer/mail_admin_new_user.go @@ -0,0 +1,81 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT +package mailer + +import ( + "bytes" + "context" + "strconv" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/translation" +) + +const ( + tplNewUserMail base.TplName = "notify/admin_new_user" +) + +var sa = SendAsync + +// MailNewUser sends notification emails on new user registrations to all admins +func MailNewUser(ctx context.Context, u *user_model.User) { + if !setting.Admin.SendNotificationEmailOnNewUser { + return + } + + if setting.MailService == nil { + // No mail service configured + return + } + + recipients, err := user_model.GetAllAdmins(ctx) + if err != nil { + log.Error("user_model.GetAllAdmins: %v", err) + return + } + + langMap := make(map[string][]string) + for _, r := range recipients { + langMap[r.Language] = append(langMap[r.Language], r.Email) + } + + for lang, tos := range langMap { + mailNewUser(ctx, u, lang, tos) + } +} + +func mailNewUser(ctx context.Context, u *user_model.User, lang string, tos []string) { + locale := translation.NewLocale(lang) + + manageUserURL := setting.AppURL + "admin/users/" + strconv.FormatInt(u.ID, 10) + subject := locale.Tr("mail.admin.new_user.subject", u.Name) + body := locale.Tr("mail.admin.new_user.text", manageUserURL) + mailMeta := map[string]any{ + "NewUser": u, + "NewUserUrl": u.HTMLURL(), + "Subject": subject, + "Body": body, + "Language": locale.Language(), + "Locale": locale, + "Str2html": templates.Str2html, + } + + var mailBody bytes.Buffer + + if err := bodyTemplates.ExecuteTemplate(&mailBody, string(tplNewUserMail), mailMeta); err != nil { + log.Error("ExecuteTemplate [%s]: %v", string(tplNewUserMail)+"/body", err) + return + } + + msgs := make([]*Message, 0, len(tos)) + for _, to := range tos { + msg := NewMessage(to, subject, mailBody.String()) + msg.Info = subject + msgs = append(msgs, msg) + } + sa(msgs...) +} diff --git a/services/mailer/mail_admin_new_user_test.go b/services/mailer/mail_admin_new_user_test.go new file mode 100644 index 0000000000..b89d888ee1 --- /dev/null +++ b/services/mailer/mail_admin_new_user_test.go @@ -0,0 +1,98 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package mailer + +import ( + "context" + "strconv" + "testing" + + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/translation" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func getTestUsers(t *testing.T) []*user_model.User { + t.Helper() + admin := new(user_model.User) + admin.Name = "testadmin" + admin.IsAdmin = true + admin.Language = "en_US" + admin.Email = "admin@example.com" + require.NoError(t, user_model.CreateUser(db.DefaultContext, admin)) + + newUser := new(user_model.User) + newUser.Name = "new_user" + newUser.Language = "en_US" + newUser.IsAdmin = false + newUser.Email = "new_user@example.com" + newUser.LastLoginUnix = 1693648327 + newUser.CreatedUnix = 1693648027 + require.NoError(t, user_model.CreateUser(db.DefaultContext, newUser)) + + return []*user_model.User{admin, newUser} +} + +func cleanUpUsers(ctx context.Context, users []*user_model.User) { + for _, u := range users { + db.DeleteByID[user_model.User](ctx, u.ID) + } +} + +func TestAdminNotificationMail_test(t *testing.T) { + translation.InitLocales(context.Background()) + locale := translation.NewLocale("") + key := "mail.admin.new_user.user_info" + translatedKey := locale.Tr(key) + require.NotEqualValues(t, key, translatedKey) + + mailService := setting.Mailer{ + From: "test@example.com", + Protocol: "dummy", + } + + setting.MailService = &mailService + + // test with SEND_NOTIFICATION_EMAIL_ON_NEW_USER enabled + setting.Admin.SendNotificationEmailOnNewUser = true + + ctx := context.Background() + NewContext(ctx) + + users := getTestUsers(t) + oldSendAsync := sa + defer func() { + sa = oldSendAsync + cleanUpUsers(ctx, users) + }() + + called := false + sa = func(msgs ...*Message) { + assert.Equal(t, len(msgs), 1, "Test provides only one admin user, so only one email must be sent") + assert.Equal(t, msgs[0].To, users[0].Email, "checks if the recipient is the admin of the instance") + manageUserURL := setting.AppURL + "admin/users/" + strconv.FormatInt(users[1].ID, 10) + assert.Contains(t, msgs[0].Body, manageUserURL) + assert.Contains(t, msgs[0].Body, users[1].HTMLURL()) + assert.Contains(t, msgs[0].Body, translatedKey, "the .Locale translates to nothing") + assert.Contains(t, msgs[0].Body, users[1].Name, "user name of the newly created user") + for _, untranslated := range []string{"mail.admin", "admin.users"} { + assert.NotContains(t, msgs[0].Body, untranslated, "this is an untranslated placeholder prefix") + } + called = true + } + MailNewUser(ctx, users[1]) + assert.True(t, called) + + // test with SEND_NOTIFICATION_EMAIL_ON_NEW_USER disabled; emails shouldn't be sent + setting.Admin.SendNotificationEmailOnNewUser = false + sa = func(msgs ...*Message) { + assert.Equal(t, 1, 0, "this shouldn't execute. MailNewUser must exit early since SEND_NOTIFICATION_EMAIL_ON_NEW_USER is disabled") + } + + MailNewUser(ctx, users[1]) +} diff --git a/services/mailer/notify.go b/services/mailer/notify.go index cc4e6baf0b..5c6e635b7a 100644 --- a/services/mailer/notify.go +++ b/services/mailer/notify.go @@ -202,3 +202,7 @@ func (m *mailNotifier) RepoPendingTransfer(ctx context.Context, doer, newOwner * log.Error("SendRepoTransferNotifyMail: %v", err) } } + +func (m *mailNotifier) NewUserSignUp(ctx context.Context, newUser *user_model.User) { + MailNewUser(ctx, newUser) +} diff --git a/services/notify/notifier.go b/services/notify/notifier.go index ed053a812a..3230a5e5f5 100644 --- a/services/notify/notifier.go +++ b/services/notify/notifier.go @@ -59,6 +59,8 @@ type Notifier interface { EditWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, page, comment string) DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, page string) + NewUserSignUp(ctx context.Context, newUser *user_model.User) + NewRelease(ctx context.Context, rel *repo_model.Release) UpdateRelease(ctx context.Context, doer *user_model.User, rel *repo_model.Release) DeleteRelease(ctx context.Context, doer *user_model.User, rel *repo_model.Release) diff --git a/services/notify/notify.go b/services/notify/notify.go index 16fbb6325d..9cb329d302 100644 --- a/services/notify/notify.go +++ b/services/notify/notify.go @@ -347,6 +347,13 @@ func RepoPendingTransfer(ctx context.Context, doer, newOwner *user_model.User, r } } +// NewUserSignUp notifies about a newly signed up user to notifiers +func NewUserSignUp(ctx context.Context, newUser *user_model.User) { + for _, notifier := range notifiers { + notifier.NewUserSignUp(ctx, newUser) + } +} + // PackageCreate notifies creation of a package to notifiers func PackageCreate(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) { for _, notifier := range notifiers { diff --git a/services/notify/null.go b/services/notify/null.go index dddd421bef..894d118eac 100644 --- a/services/notify/null.go +++ b/services/notify/null.go @@ -197,6 +197,9 @@ func (*NullNotifier) SyncDeleteRef(ctx context.Context, doer *user_model.User, r func (*NullNotifier) RepoPendingTransfer(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) { } +func (*NullNotifier) NewUserSignUp(ctx context.Context, newUser *user_model.User) { +} + // PackageCreate places a place holder function func (*NullNotifier) PackageCreate(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) { } diff --git a/templates/mail/notify/admin_new_user.tmpl b/templates/mail/notify/admin_new_user.tmpl new file mode 100644 index 0000000000..34d1584f60 --- /dev/null +++ b/templates/mail/notify/admin_new_user.tmpl @@ -0,0 +1,21 @@ + + + + + {{.Subject}} + + + + + + +
    +

    {{.Locale.Tr "mail.admin.new_user.user_info" | Str2html}}: @{{.NewUser.Name}}

    +
  • {{.Locale.Tr "admin.users.created" | Str2html}}: {{DateTime "full" .NewUser.CreatedUnix}}
  • +
+

{{.Body | Str2html}}

+ + From cb703ac2926e6f40da6fa68eadcdfc0ffe8cc414 Mon Sep 17 00:00:00 2001 From: Gusted Date: Wed, 13 Sep 2023 01:08:37 +0200 Subject: [PATCH 11/86] [GITEA] Tidy up archive modal - Make it consistent with the other modals of the dangerous actions. (cherry picked from commit 576d7ec759baefd2382d565212c3168e38bbdd75) (cherry picked from commit 8b1225f9742cc0d3942824895923cbc8e9d49d04) (cherry picked from commit c2c47972ee492686842b1623f9fe941a0e599f0a) (cherry picked from commit eec301806b925388585546edc6407e3f6d644f44) (cherry picked from commit 6b5e728f0aaa87e2711c1c2d2111446fc412e0ca) (cherry picked from commit 3681691e65a73ef59205b066320c9ce58d4d80e4) (cherry picked from commit e39dfa550d691ff73a05a507d4cb2fd073940088) (cherry picked from commit 0c78c8c5ac6970495cde3e2737ed05b200e02f5a) (cherry picked from commit 661cf72db07039abde51e03dc43a89e240cebae0) [GITEA] Tidy up archive modal (squash) ctx.Locale (cherry picked from commit 4bb6ee71f0f0a45d70fed048444f0b1ae0f4ded0) (cherry picked from commit ddafd8fbe3fca0469d846aa4b65ed53675f2304e) (cherry picked from commit 9467a6915fe362057958982e7fb381f1cf5d9c98) (cherry picked from commit e632b10380bb00ffddd6e917ab150515a5c6d251) (cherry picked from commit 6609d075914a5d897afa59924f9e16b7a9710501) (cherry picked from commit c130b8a09a6f5a1fcc71fc0bbbb7738ca70774ec) (cherry picked from commit 1080de57542b4685d849ebb6bc4d900a770a22a8) (cherry picked from commit a9813744d4e1e08a0229a4749e6fca7d9a4d1de6) (cherry picked from commit 93232f410a639a2aa7ecff6299f202cd07e2a920) (cherry picked from commit 1bf1c6b6c134b9a6b4f8a28a3dac0fdcdd2fe749) --- templates/repo/settings/options.tmpl | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index 55609560fa..b6ad3aacfa 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -976,20 +976,23 @@ {{end}}
-

+

{{if .Repository.IsArchived}} {{ctx.Locale.Tr "repo.settings.unarchive.text"}} {{else}} {{ctx.Locale.Tr "repo.settings.archive.text"}} {{end}} -

-
-
- {{.CsrfTokenHtml}} - - - {{template "base/modal_actions_confirm" .}} +
+ + {{.CsrfTokenHtml}} + + +
+ + +
+ {{end}} {{end}} From 21230d2d24dc9c96811891e55e2bb4974f8940ca Mon Sep 17 00:00:00 2001 From: Gusted Date: Sun, 17 Sep 2023 20:25:30 +0200 Subject: [PATCH 12/86] [GITEA] Skip unsupported code comment - If there's a code comment that's received during the migration that contains no diffhunk, skip it. This either means it was commenting on old diffhunk or it's just a general codecomment. Forgejo supports neither of such type of code comment. - Resolves https://codeberg.org/forgejo/forgejo/issues/1407 (cherry picked from commit ae463c7c559e02975ce5e758d8780def978eebee) (cherry picked from commit bf48f02a86d6a193417f13a77031b8207a173dca) (cherry picked from commit 10c3f102fa9135de37e9f73137ae5a9cf7072635) (cherry picked from commit 828b4cc10cd0fc7e2540fe75e88b6ebf978c5c84) (cherry picked from commit 6427fa65b641a32ead53779e3e7bda97704567df) (cherry picked from commit 5b7a43c43fed0eb39e84edd652a699461f14fbbb) (cherry picked from commit 4eef0fce72894fba2a8a138836421588f96f1087) (cherry picked from commit a46192a4a6cffa1122ffc0c4781ba93c3067b05d) (cherry picked from commit 107a9b8233731b3ac2d3a5474795a227c5bb8c0d) (cherry picked from commit 308251fc48b674e58d27acf1ccf1bc00b5fe2d54) (cherry picked from commit 017c4a53c5c8e3e2f1a1d8a06b1f975697584973) (cherry picked from commit 4534a3393b5a6beb500eb36d92ac87dda485b984) (cherry picked from commit 74e0c1663d27afc98b77c59db9f9a1593f7ea766) (cherry picked from commit 9b17353f85c4f6273aec64996e70594fc2b8f37e) (cherry picked from commit 09b6f58304f526c2fc8c9aeecf238b8bfa9ab1c5) (cherry picked from commit bc649733a121503bd2c8855a7bcac5bfce883363) (cherry picked from commit f1d4c783e272b10d3193e78e0bbbca1b2f7ef75c) (cherry picked from commit d6850bc3087ba40e61099700be97818501472cbe) --- services/migrations/gitea_uploader.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index 23d855d615..44782cbd20 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -859,6 +859,11 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error { } for _, comment := range review.Comments { + // Skip code comment if it doesn't have a diff it is commeting on. + if comment.DiffHunk == "" { + continue + } + line := comment.Line if line != 0 { comment.Position = 1 From e16241fd992c22203d113a4a11e7f57f9ed2ddb3 Mon Sep 17 00:00:00 2001 From: Gusted Date: Thu, 14 Sep 2023 21:53:57 +0200 Subject: [PATCH 13/86] [GITEA] Detect file rename and show in history - Add a indication to the file history if the file has been renamed, this indication contains a link to browse the history of the file further. - Added unit testing. - Added integration testing. - Resolves https://codeberg.org/forgejo/forgejo/issues/1279 (cherry picked from commit 72c297521b1830360aab4b50e37efcc7e67e0d5d) (cherry picked from commit 283f9648947f8dd2f315ecca19566ccca2b49c18) Conflicts: options/locale/locale_en-US.ini https://codeberg.org/forgejo/forgejo/pulls/1550 (cherry picked from commit 7c30af7fdee08efd02041c01abca47394a69bb8b) (cherry picked from commit f3be6eb269526a9f4ea7861189f07977f2d4a32f) (cherry picked from commit 78e1755b94c18c043e0c8f8c2849803cc8069feb) (cherry picked from commit 73799479e0fb68534dac10f809ee246dbc809b62) (cherry picked from commit 938359b94120b7ea7bcdfbfda265ada691620da1) (cherry picked from commit b168a9c081f93c10d40319333fc24d68a4f9763c) [GITEA] Detect file rename and show in history (squash) ctx.Locale (cherry picked from commit 40447752ff97aa306295685dcf4ddd3b13f48320) (cherry picked from commit ea23594cdbb12c32dc28638f65bf40e37d344e5f) (cherry picked from commit cdc473850c85abcbe38c799c2d2446966978f2b2) (cherry picked from commit 86e6641c29df213d7db1b85867dafebcafeee1dd) (cherry picked from commit 2757de586b80834513e61033692ac72d25381431) (cherry picked from commit def4ae32ddb4b0b83f6bb9c197e00fdcd784928e) (cherry picked from commit 6dada09329e28840f7ad890bed333ae122838fb2) (cherry picked from commit 5d6d5272513629b126917c30f7bfde421fdcbe27) Conflicts: tests/integration/repo_test.go https://codeberg.org/forgejo/forgejo/pulls/2119 (cherry picked from commit d3c1bce7db31b243a7142b71bf4af36506752e6e) (cherry picked from commit 04bcb22d5c00d1fa8b39e2a3cf2e73f0a8c1204f) --- modules/git/commit.go | 56 ++++++++++++++++++ modules/git/commit_test.go | 27 +++++++++ options/locale/locale_en-US.ini | 2 + routers/web/repo/commit.go | 16 +++++ templates/repo/commits.tmpl | 5 ++ .../40/8bbd3bd1f96950f8cf2f98c479557f6b18817a | Bin 63 -> 0 bytes .../5d/5c87a90af64cc67f22d60a942d5efaef8bc96b | Bin 50 -> 0 bytes .../88/3e2970ed6937cbb63311e941adb97df0ae3a52 | Bin 49 -> 0 bytes .../8c/ac7a8f434451410cc91ab9c04d07baff974ad8 | Bin 120 -> 0 bytes .../a0/ccafed39086ef520be6886d9395eb2100d317e | Bin 102 -> 0 bytes .../ab/e2a9ddfd7f542ff89bc13960a929dc8ca86c99 | Bin 36 -> 0 bytes .../cd/879fb6cf5b7bbe0fbc3a0ef44c8695fde89a56 | Bin 108 -> 0 bytes .../d8/f53dfb33f6ccf4169c34970b5e747511c18beb | Bin 784 -> 0 bytes .../f3/c1ec36c0e7605be54e71f24035caa675b7ba41 | Bin 48 -> 0 bytes .../repo59.git/objects/info/commit-graph | Bin 0 -> 1292 bytes .../user2/repo59.git/objects/info/packs | 2 + ...d3a6fe138f1d77e14c2e6b8e6c41e5ae242adf.idx | Bin 0 -> 1660 bytes ...3a6fe138f1d77e14c2e6b8e6c41e5ae242adf.pack | Bin 0 -> 6316 bytes ...d3a6fe138f1d77e14c2e6b8e6c41e5ae242adf.rev | Bin 0 -> 136 bytes .../user2/repo59.git/packed-refs | 3 +- .../user2/repo59.git/refs/heads/cake-recipe | 1 - tests/integration/repo_test.go | 30 ++++++++++ 22 files changed, 140 insertions(+), 2 deletions(-) delete mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/40/8bbd3bd1f96950f8cf2f98c479557f6b18817a delete mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/5d/5c87a90af64cc67f22d60a942d5efaef8bc96b delete mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/88/3e2970ed6937cbb63311e941adb97df0ae3a52 delete mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/8c/ac7a8f434451410cc91ab9c04d07baff974ad8 delete mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/a0/ccafed39086ef520be6886d9395eb2100d317e delete mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/ab/e2a9ddfd7f542ff89bc13960a929dc8ca86c99 delete mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/cd/879fb6cf5b7bbe0fbc3a0ef44c8695fde89a56 delete mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/d8/f53dfb33f6ccf4169c34970b5e747511c18beb delete mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/f3/c1ec36c0e7605be54e71f24035caa675b7ba41 create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/info/commit-graph create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/info/packs create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/pack/pack-6dd3a6fe138f1d77e14c2e6b8e6c41e5ae242adf.idx create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/pack/pack-6dd3a6fe138f1d77e14c2e6b8e6c41e5ae242adf.pack create mode 100644 tests/gitea-repositories-meta/user2/repo59.git/objects/pack/pack-6dd3a6fe138f1d77e14c2e6b8e6c41e5ae242adf.rev delete mode 100644 tests/gitea-repositories-meta/user2/repo59.git/refs/heads/cake-recipe diff --git a/modules/git/commit.go b/modules/git/commit.go index 5d960e92f3..012ba975e8 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -515,6 +515,62 @@ func GetCommitFileStatus(ctx context.Context, repoPath, commitID string) (*Commi return fileStatus, nil } +func parseCommitRenames(renames *[][2]string, stdout io.Reader) { + rd := bufio.NewReader(stdout) + for { + // Skip (R || three digits || NULL byte) + _, err := rd.Discard(5) + if err != nil { + if err != io.EOF { + log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err) + } + return + } + oldFileName, err := rd.ReadString('\x00') + if err != nil { + if err != io.EOF { + log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err) + } + return + } + newFileName, err := rd.ReadString('\x00') + if err != nil { + if err != io.EOF { + log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err) + } + return + } + oldFileName = strings.TrimSuffix(oldFileName, "\x00") + newFileName = strings.TrimSuffix(newFileName, "\x00") + *renames = append(*renames, [2]string{oldFileName, newFileName}) + } +} + +// GetCommitFileRenames returns the renames that the commit contains. +func GetCommitFileRenames(ctx context.Context, repoPath, commitID string) ([][2]string, error) { + renames := [][2]string{} + stdout, w := io.Pipe() + done := make(chan struct{}) + go func() { + parseCommitRenames(&renames, stdout) + close(done) + }() + + stderr := new(bytes.Buffer) + err := NewCommand(ctx, "show", "--name-status", "--pretty=format:", "-z", "--diff-filter=R").AddDynamicArguments(commitID).Run(&RunOpts{ + Dir: repoPath, + Stdout: w, + Stderr: stderr, + }) + w.Close() // Close writer to exit parsing goroutine + if err != nil { + return nil, ConcatenateError(err, stderr.String()) + } + + <-done + return renames, nil +} + // GetFullCommitID returns full length (40) of commit ID by given short SHA in a repository. func GetFullCommitID(ctx context.Context, repoPath, shortID string) (string, error) { commitID, _, err := NewCommand(ctx, "rev-parse").AddDynamicArguments(shortID).RunStdString(&RunOpts{Dir: repoPath}) diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go index e512eecc56..2de6feeb31 100644 --- a/modules/git/commit_test.go +++ b/modules/git/commit_test.go @@ -278,3 +278,30 @@ func TestGetCommitFileStatusMerges(t *testing.T) { assert.Equal(t, commitFileStatus.Removed, expected.Removed) assert.Equal(t, commitFileStatus.Modified, expected.Modified) } + +func TestParseCommitRenames(t *testing.T) { + testcases := []struct { + output string + renames [][2]string + }{ + { + output: "R090\x00renamed.txt\x00history.txt\x00", + renames: [][2]string{{"renamed.txt", "history.txt"}}, + }, + { + output: "R090\x00renamed.txt\x00history.txt\x00R000\x00corruptedstdouthere", + renames: [][2]string{{"renamed.txt", "history.txt"}}, + }, + { + output: "R100\x00renamed.txt\x00history.txt\x00R001\x00readme.md\x00README.md\x00", + renames: [][2]string{{"renamed.txt", "history.txt"}, {"readme.md", "README.md"}}, + }, + } + + for _, testcase := range testcases { + renames := [][2]string{} + parseCommitRenames(&renames, strings.NewReader(testcase.output)) + + assert.Equal(t, testcase.renames, renames) + } +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 8f115c9de4..1b4c9f5b72 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1289,6 +1289,8 @@ commits.find = Search commits.search_all = All Branches commits.author = Author commits.message = Message +commits.browse_further = Browse further +commits.renamed_from = Renamed from %s commits.date = Date commits.older = Older commits.newer = Newer diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index abb39caa57..fe11b7fa69 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -243,6 +243,22 @@ func FileHistory(ctx *context.Context) { ctx.ServerError("CommitsByFileAndRange", err) return } + oldestCommit := commits[len(commits)-1] + + renamedFiles, err := git.GetCommitFileRenames(ctx, ctx.Repo.GitRepo.Path, oldestCommit.ID.String()) + if err != nil { + ctx.ServerError("GetCommitFileRenames", err) + return + } + + for _, renames := range renamedFiles { + if renames[1] == fileName { + ctx.Data["OldFilename"] = renames[0] + ctx.Data["OldFilenameHistory"] = fmt.Sprintf("%s/commits/commit/%s/%s", ctx.Repo.RepoLink, oldestCommit.ID.String(), renames[0]) + break + } + } + ctx.Data["Commits"] = git_model.ConvertFromGitCommit(ctx, commits, ctx.Repo.Repository) ctx.Data["Username"] = ctx.Repo.Owner.Name diff --git a/templates/repo/commits.tmpl b/templates/repo/commits.tmpl index 42004c2610..7b3b27af1d 100644 --- a/templates/repo/commits.tmpl +++ b/templates/repo/commits.tmpl @@ -13,6 +13,11 @@ {{template "repo/commits_table" .}} + {{if .OldFilename}} +
+ {{ctx.Locale.Tr "repo.commits.renamed_from" .OldFilename}} ({{ctx.Locale.Tr "repo.commits.browse_further"}}) +
+ {{end}} {{template "base/footer" .}} diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/40/8bbd3bd1f96950f8cf2f98c479557f6b18817a b/tests/gitea-repositories-meta/user2/repo59.git/objects/40/8bbd3bd1f96950f8cf2f98c479557f6b18817a deleted file mode 100644 index 567284ef1c21f472841f3d729cbfe024d78c448c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 63 zcmV-F0Korv0ZYosPf{>3XHZrMN-fAQ&Me6)6NNXyJgE!N}W0ssfX5``$?8bAO5 diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/5d/5c87a90af64cc67f22d60a942d5efaef8bc96b b/tests/gitea-repositories-meta/user2/repo59.git/objects/5d/5c87a90af64cc67f22d60a942d5efaef8bc96b deleted file mode 100644 index f23960f4cc8c09af29fc4765f683b697a4547d74..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50 zcmV-20L}k+0V^p=O;s>9VK6ZO0)@QP;*!j~bcW9d-8WL<+jltv I07;Az9utceGXMYp diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/88/3e2970ed6937cbb63311e941adb97df0ae3a52 b/tests/gitea-repositories-meta/user2/repo59.git/objects/88/3e2970ed6937cbb63311e941adb97df0ae3a52 deleted file mode 100644 index 46cc9e3e5ec14999b5424f35c1c548db9e9e33c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49 zcmb7FlR6{FfcPQQ3!H%bn$i7%S~Z$=-z96@n>ehkMsI7j#P%$ zXG=6znHT_pLP~0C0Yhv|`%12FKF8{nu5nG#jr;Y!`(!rMjI`9mlG377y^@L&h7LQ; ag14FGr?(jkzI0r>v-ZO}s~`Z9QY~(zy*Y~j diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/a0/ccafed39086ef520be6886d9395eb2100d317e b/tests/gitea-repositories-meta/user2/repo59.git/objects/a0/ccafed39086ef520be6886d9395eb2100d317e deleted file mode 100644 index 3b71228a7ec7b91f1066cda387ef6efa28c2ae68..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 102 zcmV-s0Ga=I0V^p=O;xb4U@$Z=Ff%bx2y%6F@paY9O<`F5Xyx6%^&$E{W*@XnSgCoZ zXGP9TsG{Q3M6>2AdT%A&7_dRgi_+j`HiK;pmmM?MU`7Mx>)%bH?6OHM zk5sBnouqA=Y63LcbH7YOmH|GAl4Hc@EW@%K&C)1I1Uia^1hFYP#!;RNM>a}%Dtb?4 zJAn6?4K(;YO4A`5NBYlfjhe2`eoNZs4?rJ;J;Mr|fWQvz5u(27_uQ2I-(JxbV^x4( z|B6Ty%>s_%fUBlh_~u>6-<$#zs9bFmF%~6^Q@J9f=}|(LabtH3q_NxV$YbR02+h-Wy>8SN zL-n>NnOHxD1_ktBKMTFMF~Oeo44c$Nx1zjPg-n1^s{~0y49o8@?{IqT_1l9FDBH+tz~X}{gyB7!qm$KrCW*^)AW)NzRtpG4}}%%=|SNs z@Nz?3mnX@zAyYKZV*tH#nR{bxS}d9i?M(85HRz`|j6_MUVShjg2W|Ppe8h&7*MdL7JHfOBCJr|RNl?>o$nk$Jg%Kk7Ay z>ilZ^!I!SZjq{9}32tH%t54`QEQTUns*)fL4hA0LId@WGsHUmmoZfJs-`Nc O!MB&yMEwL3??r>^yMtc< diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/f3/c1ec36c0e7605be54e71f24035caa675b7ba41 b/tests/gitea-repositories-meta/user2/repo59.git/objects/f3/c1ec36c0e7605be54e71f24035caa675b7ba41 deleted file mode 100644 index b6803eb5a271b9d33c981d1ba24fa4126ae409db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48 zcmV-00MGw;0V^p=O;s>9W-u`T0)@2voRrieh6QKVzqRDZ`>L=nqwS_;+$I5D!#V&X Gkq<4~#T1zU diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/info/commit-graph b/tests/gitea-repositories-meta/user2/repo59.git/objects/info/commit-graph new file mode 100644 index 0000000000000000000000000000000000000000..d151dc87e63b6d2485029740163c1cd02c7d41d0 GIT binary patch literal 1292 zcmZ>E5Aa}QWMT04ba7*V02d(J2f}1=advSGfwCLiT^x;|>^Be^M6&!quu)iyK;9@G z8DYQ#jO$TMyD(q|hVP)otZOq~hxjx+n&edca;Jo0TTt~>qlO(eF-;1GS6Q+>X#B*q zXnC03iOL&aZGRhoJM%?sj>&ZHxRO%AgWaz~!hS7&%G9F9;IU;%)q2U6wRV$c763g9 z!a%^1%EG==%(L8v{Wo8~;ety&&fTp1sip^yfh3`7IDl#{^{lDtcXkPMdd1A3JP%SxF1gMWxEDF>^5@ubS z@jAq(;n5_g;+H!m4BLXLry7Yw@3bh^P7CGT=PJLCyJX2r^^lKZp5->|zxnzN7hLLb z?q=mrH9dIDp?k0O#h;l0KhEpVI8qr}pDoc?6%zJq@l&Q2H3p9@ORCmOwyd?AG&44) zeI?g7pJVk(*SMzW#{GKVeKNaYhfPeA!r@hxY!4bgF)dmiW_O~p!%nl{ZKnC@ZN`Ex z9oO!x{jkm|sPmShlc&wf&DW9@7c-SjT=qEAS+QqLRll=Kpd-&oshtOW*?0Y)?sa3z z){=*9A_nY|rc-k_tj+IZ`MzT3?9&@1I_wNg%64-KT|0ErZ=PcE;{|8dzqRDZ`>L=n zqwS_;+$I5D!@31$WhcDQ-+Jo{Z&GE)mY-AX4Z3O;t$QeOsNH3$wQk+RLfb;;fROw< zs~@er`?o$s|HtfumI*60@ARz5nR%4w?APQj#nq;J zp5Td}556%w@H`>Oj{w5~;Pn@?d+qGzQ z=#$(_u|4p16j!cPuI0Wu*(=7fk+Mr)dWTGX;HzJL)F&)0 zuZ%(S$D#N-VDW4Y%!)u-ES-Ttd>RnD0onb)^aISF5`I8j2^4DsCZ~(Qthp2@_Y}x4 z0v22DK>9qeNC*MaZ-Lq1Bv36-pV;o)%gg==_sf<)^wG=i%W-_VPDShf{Yw4SqES8S R@AzV-R-~x4va}Sh1OP;7B9#CD literal 0 HcmV?d00001 diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/pack/pack-6dd3a6fe138f1d77e14c2e6b8e6c41e5ae242adf.pack b/tests/gitea-repositories-meta/user2/repo59.git/objects/pack/pack-6dd3a6fe138f1d77e14c2e6b8e6c41e5ae242adf.pack new file mode 100644 index 0000000000000000000000000000000000000000..ddb8c16caf1f0600bf22af94b7f41c3799679617 GIT binary patch literal 6316 zcmZu$RZtvYlUzK&;vRwqcXtWy?yz`Zad!w7++iVPaa|mO2Z!LYkl;>m4-UaE|6ScZ z-NW}VU-eW?Pu0|P*Jw(~r~m)}#D9+{N7^qZ1EN${B~KV}_k?lFXAlW|LxZ~U02~_Md>gA>-Ih>#u8Yz9iy<`p*MwOlPzs2TJS4S}$`heU< zcWE^S!=GCVycD1Q&FJ#UV!9BWJ0Fq|pd81!(w**g!N6-!ljzzt#IcSXrq&Uq zf(El>-5gwa-6T}|l>v4Mlv}EbOQT>hbv2{5QCStD=&=^8SsY|gUkG1tyf5G*`IKxW z$DX$yl2de(b#cL1G-Y1@&Fr&06S-9hC*hvN8B;6cdFIfmlGX)Y*%d@kdd}bZG_Ba8{?TVbDcsYs3?X;#Opo|`K7?;xlRTrynb<0*;BCT zX=z{I+9bzYrW0uY=C7N}zBg?f9c?$1YHuaj8u!!L^C^*c#+`qDHMw1yHAeCxbwYYJ*%rN$?ku7X!1I-Zv)X-4SeBDL6UQf zbja_Mo@OL(30U8_I7Fz+%E%p55UqfY*J&dfz`V8ic1KiZ%Y0Bp=?u$WCQ0AP^_RWN zCDBPRgY~ucI|E^0_olk~v7AK9i2YV zMzi~N;-bwFOeOnrV|HA+X$o-Q0e;L38JfVuJX|vAeh1cF%~_dpu6TJ-%iTp@Tn|7& zAWGASR{Sk@Rf57PIeNk}kG#t3EqCcfp>MX7ejr4*(7qi(YB9U6%fo^PB?1%$g)A5T zumxsAcP)c61UoVl4)y8%Q+{QPs_}D2qwZpj{vRy(EG3;bZsYn-4XjH3l45jnw0%Bl8?;^aj5iUvwHJd8RoGJ!jNC%+9o-6CA$9j2!H#(SNg9hW<4vf79H4#EWm5BcO{=LC(S(rOfAN)o-`ja&*tNars8&a%btD?Fs)f+kzEMe2$bz0t??pUdya(Ck05p#e!RaW(zdPKw0 zb9*K^nqf-c?%t*4P|I4CvT0|VzY7Uq(;a2rry+)S%z%xM$g-Ymp`tjdCfPj(W=cfCbsL=g0#)`2@{~{@vsI2)_cFj z0Q!)@JD-G)nu{~rv}faLpcb%;e$@-eX)>Le0~2;iTr;-l&|wQ>ZWic_>_QC@OZdUH zK$>lsK-`=8NACAVdY`P_=*`mJnAqt+>pd2_V1s5@j3iiFBr?5TPtV4j~X5T7m%nxI)XmO(hl2#TUxF~;Ft{9?t>n@l&gZ)Fs-5$`*l z=zQTQ;MIc=PNWz|Op1WUq`oC|sV4fpcJTGpE{Pr`?eD%p;v6FD`FK7=SFJUySh#nF z-{ksp#{-E10I{GdKaEMbfoYGVymBo9ESu}on))L~r{Y^#o^5`dw#f^vPZ&%6XC{Nh zM^1&%WsE$X_$@=Moq*g;h1CNck(NOJ3)<81`jb1+pR9znr{pu-y3Cr8cfjFD?caX2 z(V^5{(bppFY?hsOZ;3$37kU~d5`isZMVt+IP$eAmtKTTF(*0b^@}IRC!}gDH02Ois z%DRMn&ZnWCJ@v>=NNz3B_>7baE=KwNVTSlbQdzAoNS8kO)B3_}<7Cw5s$nWP5`seA zdS+dmU$o)72J9=KHbj^sjwlD+zVuvf>?KBR^mJTi2A;)OX~C<#^g~vaE?^&5Q;qo{ zAQTA}X^DjP30zP#kZ)<>?scIiA3`iE`8I@Ib2(0QwpWjhqP!<lk8R^VV!y#7)qYXvJoHRx3u4#Jq zEc8EODZAy777*7+&l`Zc`V`lBTH|BqWZzTHd>=a9phw3{{m#B2uzz>6bgcT^UYEv_ z5OEJPd|fHg@za2DQ8Zo8@N3?0rd>S)W6LiIp3_`ig8YvhV*v|J$&J|Jh-N!ckOwS$ zmR*{V*PxaN1|g%ymOti__y~RWOMp2RiwzbZjnlu-Bq7iFVV6M zmk-i@ZB?XOQ8pm|#)crR7Y7POAC{< z-mro+rT(z~Rzxz?9p|0A3PYiGtA0i@aL~M_aqF5B3}9L63-EN)K{pKau`9_w*>soB zKTUxLPMRf8>9C9pc^CcsoRgTi{N#tqe7O1cOF9CQD>mLWRco!;q>1Ol^)Ml$gAPZ2 zkWOuKiPW^NG7j>t3;K;q`LXJ-`$7t8oGOj`N6-Xe7UtC$s z;g4UY=%_yE=XhBvjE~bB-_5b~ER)tvA(^YdKyb`&7Co}PWt>(0*~wQ6ue^UN3Dh-| zV-shS9OrBuIJMLlNqUC|j6>@P-ko3MwNNeo7~lYw9Wy9x7SccF3X3mY7VxLAWc?;{ z+~L)ZK78V!ufDAGCpLf)@3hX8jScv1lQ3pkPu%ZXs7f5^iW2N2GU%^aq5-Nr7M@kxEMa` z?-mlgNs(TNcDtBBVDW^la8occJC+Q^*z77808z3g*{rF-V`jtX!0fd*iQF z`}S2b387T{;u#Y>co>vTql{HUZQyO(^`V2f#g#a*GpG1USis|gM|x?zTWP4d>P&>0 zq)eK0r1jf)x=Ii)lo{qWm6Xmw13m1PWD}d^!_NDvQ_UN79v)F@|Cq=#g|CWr0QZdp zare&u+3SLf`Ka?Ab-0m5$5t(XmnN)@`TG-2@J-3aHxy0dz~+r!9LjWQMlCMQ3fM^R zfw$l`hxxZ^tKID&=ikAm-|UU zUgAtr17$zjR+c$45E{22zZTQ+?I=;S2=t(=^OaKt1YTu}2x@TGP01xgLP}9rzNQ^P zUrL?fwMwAkyt4dyG?<8C~W+V%89FXP@#*j?b*KN>oGg z4Qyp;bapH*TFY%6mNb9sgT57A93W^eY-WY-3^5jwq4?5L`u4t&$=Cd&c-uhDB885n z?cqBvqw`O$b013=bguP_ZfJle{RDWGFn@#5BRrG2G>mN_mqy3-f}_6oEWSFhbL^fNWWWL-T3<}`Bdl~DmF78 zvgv4rMvQpyki<$CjHSaNX*3>5TXIs0P;El8_7ZfOO(Jnd{8z#Unx&xPz=)Z zPhcgRUc4uNkVF`bC4|RRD~2LT;UVA31%786ME2t2l*0~A>hHWBHOLP(*fj~|hHDyi+y2-W%ApPX^HA?#^m*#<0>F`R+)1dx% zrMSHlTz%dF{inM0MWrAQiWve5+LiN<$<`lE-3r&-#Q10_Z`%t+4~yv(_S}HX)?l8% zj9Q-Agfeg3Ybd4n=H>(${2z4m00PiBN`J1+S?T4QW_7T zvLrLplLkxv8!}W-()K$zRth8M0sgF_L@GNJS>aZ<#@2fe^61oCa9ZckTNfDPMKCzD zxw9cWuHTP})V9E!!D3|OWZdFZ<$v*ZjoDFTOqow_PI3!R+%Nl@q5n?WEIB zoGU>CRRYy+&M0+ku^F7_XuE~lE(AjrgYExy(qD4;?pyUkQp-_+q|HT$(x!*&95diJ z{CPX!i1Kxc6ezKY2*D15qLZ9(V(Q33|KmXZiLU8Nd4T+#nA}-S2{}S!<*JGOH>iy! zUa$`kUQnP)dwG2i(e?3#xcXOiuL;k5V8(5vTVb^7^AzL!CKlhIY6a09!_6=LltmcX zv=0G&Ok2~Ie*c&dHCPsB2)DSXFZ{HF1k|(~BAG)Swl9Y*z9)`6{I}kL}lE?56 z5@KVJSMS;wuv}VOU#T1AB ziD&vm$D;&KZ`BNA`00nT|K000kQz%&L?WH<_}U}EFdmr*)iceiJL6=);G0KZ^cKZ3 z4*sn=kyr6W%e6%pBo;@ZQ@P?;W3=&w#|Un2j^tiw;-Jz0F;Ixk$J*>^$@A~B8JMsb zAxRA2HJAS@6f7%PiHzDAKWMVAcnXhErMRnKNvvS7tDYTq>S1JcDu0)%W1x5#p67i) zl^g(Ew|}mwrceu%65L;7_m;r=-=c<&_M(cu-u1$+GQSQ4kytydov=V_3_0 zwa}}GUDS>ii844L`>0@%V`uEPJK4gmI?lq$DO$nP{+*K6C2=55RI1&Q@E>0$f`Tf_ z@5!qTwiAHY3Q1z-+M`6NIf=yIL&lqy9zmkUPnSC!Ih(PCgB+iqW87$JE_L(89@?eb zHBM#@SIURq9bauP;S_psa7tV7gS=83btS7Dn|@!|;}Wc;pIQjxY@{+RJpU+j*C<1TYS6jsvFSW}C3hoG%0TWcpm7CzFG+g-{)3&xi*uAv zv^i1#Y;r&)1U1OOKx4w3Q&1Ye0Mjprmye5!ms{(Nhqu>-_uROerQ5-}$It$Xd4<>b+M z*T@>OOns$@0J;hlqGNoNUQ?xa^Q#70QcWYpJXZCN=?9N2^|8$`7^r|&CxpsZHXE^RGUSnmphtoC^0f6;| ze)S9?%=^FTjSCg(($)!O6#oAd``!Ry_Te=D3-#Ipq66^ykfLnD`Tz6xRh)(#fGUSb z+VhW^j{X~E_7ToWwo%SXU8X6vQB6f%#Zgri6?SDftJcWmgbK6%h=z{h_=FPs{Pq?i zAY&WXqzU1_6Gs*GnAFucl-aeI)Y;V3^>z>e(tD`jPE@}finwvCP-$0&HRn z3PZqFxhzV5ErSushMp=!1xvBqxe7MsH!@gRb6jnLS=j(5kT`BIg86L2kGA_0)XH=1A8GzrtsZV P<#0@LlG?4MW7+sGiQYRB literal 0 HcmV?d00001 diff --git a/tests/gitea-repositories-meta/user2/repo59.git/objects/pack/pack-6dd3a6fe138f1d77e14c2e6b8e6c41e5ae242adf.rev b/tests/gitea-repositories-meta/user2/repo59.git/objects/pack/pack-6dd3a6fe138f1d77e14c2e6b8e6c41e5ae242adf.rev new file mode 100644 index 0000000000000000000000000000000000000000..81554dba7405b86a5e8db6c52a81b9c6590b0f0e GIT binary patch literal 136 zcmWIYbctYKU|@t|HXscGd_XJ!#2|5QAQl2*E+A$CVpbp)0b(8?W&vVeAm#*OW+3JV zVnHBg2VxE&7S6rA?4NMIZ23bUz3jdm$EWL5wC;O}2sbdA%~jT1wJNl|W?lK0SNV4U DT$U8_ literal 0 HcmV?d00001 diff --git a/tests/gitea-repositories-meta/user2/repo59.git/packed-refs b/tests/gitea-repositories-meta/user2/repo59.git/packed-refs index 114c84d2aa..77fedbf67d 100644 --- a/tests/gitea-repositories-meta/user2/repo59.git/packed-refs +++ b/tests/gitea-repositories-meta/user2/repo59.git/packed-refs @@ -1,3 +1,4 @@ # pack-refs with: peeled fully-peeled sorted -d8f53dfb33f6ccf4169c34970b5e747511c18beb refs/heads/master +d8f53dfb33f6ccf4169c34970b5e747511c18beb refs/heads/cake-recipe +80b83c5c8220c3aa3906e081f202a2a7563ec879 refs/heads/master d8f53dfb33f6ccf4169c34970b5e747511c18beb refs/tags/v1.0 diff --git a/tests/gitea-repositories-meta/user2/repo59.git/refs/heads/cake-recipe b/tests/gitea-repositories-meta/user2/repo59.git/refs/heads/cake-recipe deleted file mode 100644 index 63bbea6692..0000000000 --- a/tests/gitea-repositories-meta/user2/repo59.git/refs/heads/cake-recipe +++ /dev/null @@ -1 +0,0 @@ -d8f53dfb33f6ccf4169c34970b5e747511c18beb diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go index 157d2ba99d..8176fea5e8 100644 --- a/tests/integration/repo_test.go +++ b/tests/integration/repo_test.go @@ -473,6 +473,36 @@ func TestViewRepoDirectoryReadme(t *testing.T) { missing("symlink-loop", "/user2/readme-test/src/branch/symlink-loop/") } +func TestRenamedFileHistory(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + t.Run("Renamed file", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/user2/repo59/commits/branch/master/license") + resp := MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + + renameNotice := htmlDoc.doc.Find(".ui.bottom.attached.header") + assert.Equal(t, 1, renameNotice.Length()) + assert.Contains(t, renameNotice.Text(), "Renamed from licnse (Browse further)") + + oldFileHistoryLink, ok := renameNotice.Find("a").Attr("href") + assert.True(t, ok) + assert.Equal(t, "/user2/repo59/commits/commit/80b83c5c8220c3aa3906e081f202a2a7563ec879/licnse", oldFileHistoryLink) + }) + + t.Run("Non renamed file", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/repo59/commits/branch/master/README.md") + resp := MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + + htmlDoc.AssertElement(t, ".ui.bottom.attached.header", false) + }) +} + func TestMarkDownReadmeImage(t *testing.T) { defer tests.PrepareTestEnv(t)() From e59f46728466a05fab38d7f8999be6798bacb723 Mon Sep 17 00:00:00 2001 From: rome-user Date: Sat, 30 Sep 2023 18:03:22 -0700 Subject: [PATCH 14/86] [GITEA] fix indentation in Maven package install instructions The installation instructions of a Maven package places the `url` child of the `repository` node in an extra indentation level. This indentation is unnecesary since both the `id` and `url` nodes are direct children of the `repository` node. This commit removes the unnecessary indentation. Refs: https://codeberg.org/forgejo/forgejo/pulls/1534 (cherry picked from commit 82f0ddad7bfcb40595d0f79220834377b04382d8) (cherry picked from commit 905e546549bc69460d93f6e30bbe93124e924e57) (cherry picked from commit 4e58ab82b77a8f4e6f994fc21b42fb70f0629778) (cherry picked from commit 2f207e7deb692e8b356881017f615cf03c27fc38) (cherry picked from commit 3b8cc8ad2c5c432510f479e6b366caa61603b05b) (cherry picked from commit ca8565450c319c073e093b22180ef6c29461e281) (cherry picked from commit df5ed97ed07815aa16df3b1960a16f542bcb05a8) (cherry picked from commit fc1e529894f175550f5f88ee39a58c139337c6ee) (cherry picked from commit ef8810c09db4d2c539655f1ac07da8149fda94c9) (cherry picked from commit a2d1459c4d1fc6e9995bbdedbcf6a053d7370bc2) (cherry picked from commit 30e0d7bff00be21eee6d233c0d50c443a3c466a0) (cherry picked from commit ccb9ed98b9b7cb8a9223b3d787c4d861c82ab0eb) (cherry picked from commit 3782794fb41e5c205800a558556e961f96ecc55b) (cherry picked from commit 9e7d5b5de9d381360cd3164649990ce4397fff02) (cherry picked from commit 50687eaebe34421557c737b29e370c9128bc557b) (cherry picked from commit 28ae93f18e5d3f13b8c6e5192ecb35123f0172e6) --- templates/package/content/maven.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/package/content/maven.tmpl b/templates/package/content/maven.tmpl index b2cd567e16..100c12e180 100644 --- a/templates/package/content/maven.tmpl +++ b/templates/package/content/maven.tmpl @@ -7,7 +7,7 @@
<repositories>
 	<repository>
 		<id>gitea</id>
-			<url></url>
+		<url></url>
 	</repository>
 </repositories>
 

From 01191dc2adf8c57ae448be37e73158005a8ff74d Mon Sep 17 00:00:00 2001
From: Gusted 
Date: Sat, 30 Sep 2023 00:45:31 +0200
Subject: [PATCH 15/86] [GITEA] Drop sha256-simd in favor of stdlib
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- In Go 1.21 the crypto/sha256 [got a massive
improvement](https://go.dev/doc/go1.21#crypto/sha256) by utilizing the
SHA instructions for AMD64 CPUs, which sha256-simd already was doing.
The performance is now on par and I think it's preferable to use the
standard library rather than a package when possible.

```
cpu: AMD Ryzen 5 3600X 6-Core Processor
                │  simd.txt   │               go.txt                │
                │   sec/op    │    sec/op     vs base               │
Hash/8Bytes-12    63.25n ± 1%    73.38n ± 1%  +16.02% (p=0.002 n=6)
Hash/64Bytes-12   98.73n ± 1%   105.30n ± 1%   +6.65% (p=0.002 n=6)
Hash/1K-12        567.2n ± 1%    572.8n ± 1%   +0.99% (p=0.002 n=6)
Hash/8K-12        4.062µ ± 1%    4.062µ ± 1%        ~ (p=0.396 n=6)
Hash/1M-12        512.1µ ± 0%    510.6µ ± 1%        ~ (p=0.485 n=6)
Hash/5M-12        2.556m ± 1%    2.564m ± 0%        ~ (p=0.093 n=6)
Hash/10M-12       5.112m ± 0%    5.127m ± 0%        ~ (p=0.093 n=6)
geomean           13.82µ         14.27µ        +3.28%

                │   simd.txt   │               go.txt                │
                │     B/s      │     B/s       vs base               │
Hash/8Bytes-12    120.6Mi ± 1%   104.0Mi ± 1%  -13.81% (p=0.002 n=6)
Hash/64Bytes-12   618.2Mi ± 1%   579.8Mi ± 1%   -6.22% (p=0.002 n=6)
Hash/1K-12        1.682Gi ± 1%   1.665Gi ± 1%   -0.98% (p=0.002 n=6)
Hash/8K-12        1.878Gi ± 1%   1.878Gi ± 1%        ~ (p=0.310 n=6)
Hash/1M-12        1.907Gi ± 0%   1.913Gi ± 1%        ~ (p=0.485 n=6)
Hash/5M-12        1.911Gi ± 1%   1.904Gi ± 0%        ~ (p=0.093 n=6)
Hash/10M-12       1.910Gi ± 0%   1.905Gi ± 0%        ~ (p=0.093 n=6)
geomean           1.066Gi        1.032Gi        -3.18%
```

(cherry picked from commit abd94ff5b59c86e793fd9bf12187ea6cfd1f3fa1)
(cherry picked from commit 15e81637abf70576a564cf9eecaa9640228afb5b)

Conflicts:
	go.mod
	https://codeberg.org/forgejo/forgejo/pulls/1581
(cherry picked from commit 325d92917f655c999b81b08832ee623d6b669f0f)

Conflicts:
	modules/context/context_cookie.go
	https://codeberg.org/forgejo/forgejo/pulls/1617
(cherry picked from commit 358819e8959886faa171ac16541097500d0a703e)
(cherry picked from commit 362fd7aae17832fa922fa017794bc564ca43060d)
(cherry picked from commit 4f64ee294ee05c93042b6ec68f0a179ec249dab9)
(cherry picked from commit 4bde77f7b13c5f961c141c01b6da1f9eda5ec387)
(cherry picked from commit 1311e30a811675eb623692349e4e808a85aabef6)
(cherry picked from commit 57b69e334c2973118488b9b5dbdc8a2c88135756)
(cherry picked from commit 52dc892fadecf39e89c3c351edc9efb42522257b)
(cherry picked from commit 77f54f4187869c6eabcc837742fd3f908093a76c)
(cherry picked from commit 0d0392f3a510ce3683bb649dee1e65b45dd91354)

Conflicts:
	go.mod
	https://codeberg.org/forgejo/forgejo/pulls/2034
(cherry picked from commit 92798364e8fe3188a2100b54f3adea943f8309e9)
(cherry picked from commit 43d218127752aa9251c4c3ef71b9c060f109dffc)
(cherry picked from commit 45c88b86a35729fc0b2dc6b72bc33caf9f69265f)
(cherry picked from commit a1cd6f4e3a7956773cbc0aef8abb80d17b62eb49)
---
 go.mod                                           | 2 +-
 models/auth/twofactor.go                         | 2 +-
 models/migrations/base/hash.go                   | 2 +-
 models/migrations/v1_14/v166.go                  | 2 +-
 modules/auth/password/hash/pbkdf2.go             | 2 +-
 modules/avatar/hash.go                           | 3 +--
 modules/avatar/identicon/identicon.go            | 3 +--
 modules/base/tool.go                             | 2 +-
 modules/git/last_commit_cache.go                 | 3 +--
 modules/lfs/content_store.go                     | 3 +--
 modules/lfs/pointer.go                           | 3 +--
 modules/secret/secret.go                         | 3 +--
 modules/util/keypair.go                          | 3 +--
 modules/util/keypair_test.go                     | 2 +-
 routers/api/packages/chef/auth.go                | 3 +--
 routers/api/packages/maven/maven.go              | 3 +--
 services/lfs/server.go                           | 2 +-
 services/mailer/token/token.go                   | 3 +--
 services/webhook/deliver.go                      | 2 +-
 tests/integration/api_packages_chef_test.go      | 2 +-
 tests/integration/api_packages_container_test.go | 2 +-
 tests/integration/api_packages_test.go           | 2 +-
 22 files changed, 22 insertions(+), 32 deletions(-)

diff --git a/go.mod b/go.mod
index ed6d991b5f..6626f9bbb9 100644
--- a/go.mod
+++ b/go.mod
@@ -77,7 +77,6 @@ require (
 	github.com/mholt/archiver/v3 v3.5.1
 	github.com/microcosm-cc/bluemonday v1.0.26
 	github.com/minio/minio-go/v7 v7.0.66
-	github.com/minio/sha256-simd v1.0.1
 	github.com/msteinert/pam v1.2.0
 	github.com/nektos/act v0.2.52
 	github.com/niklasfasching/go-org v1.7.0
@@ -232,6 +231,7 @@ require (
 	github.com/mholt/acmez v1.2.0 // indirect
 	github.com/miekg/dns v1.1.57 // indirect
 	github.com/minio/md5-simd v1.1.2 // indirect
+	github.com/minio/sha256-simd v1.0.1 // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
diff --git a/models/auth/twofactor.go b/models/auth/twofactor.go
index 51061e5205..d0c341a192 100644
--- a/models/auth/twofactor.go
+++ b/models/auth/twofactor.go
@@ -6,6 +6,7 @@ package auth
 import (
 	"context"
 	"crypto/md5"
+	"crypto/sha256"
 	"crypto/subtle"
 	"encoding/base32"
 	"encoding/base64"
@@ -18,7 +19,6 @@ import (
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 
-	"github.com/minio/sha256-simd"
 	"github.com/pquerna/otp/totp"
 	"golang.org/x/crypto/pbkdf2"
 )
diff --git a/models/migrations/base/hash.go b/models/migrations/base/hash.go
index 0debec272b..00fd1efd4a 100644
--- a/models/migrations/base/hash.go
+++ b/models/migrations/base/hash.go
@@ -4,9 +4,9 @@
 package base
 
 import (
+	"crypto/sha256"
 	"encoding/hex"
 
-	"github.com/minio/sha256-simd"
 	"golang.org/x/crypto/pbkdf2"
 )
 
diff --git a/models/migrations/v1_14/v166.go b/models/migrations/v1_14/v166.go
index 78f33e8f9b..e5731582fd 100644
--- a/models/migrations/v1_14/v166.go
+++ b/models/migrations/v1_14/v166.go
@@ -4,9 +4,9 @@
 package v1_14 //nolint
 
 import (
+	"crypto/sha256"
 	"encoding/hex"
 
-	"github.com/minio/sha256-simd"
 	"golang.org/x/crypto/argon2"
 	"golang.org/x/crypto/bcrypt"
 	"golang.org/x/crypto/pbkdf2"
diff --git a/modules/auth/password/hash/pbkdf2.go b/modules/auth/password/hash/pbkdf2.go
index 9ff6d162fc..27382fedb8 100644
--- a/modules/auth/password/hash/pbkdf2.go
+++ b/modules/auth/password/hash/pbkdf2.go
@@ -4,12 +4,12 @@
 package hash
 
 import (
+	"crypto/sha256"
 	"encoding/hex"
 	"strings"
 
 	"code.gitea.io/gitea/modules/log"
 
-	"github.com/minio/sha256-simd"
 	"golang.org/x/crypto/pbkdf2"
 )
 
diff --git a/modules/avatar/hash.go b/modules/avatar/hash.go
index 4fc28a7739..50db9c1943 100644
--- a/modules/avatar/hash.go
+++ b/modules/avatar/hash.go
@@ -4,10 +4,9 @@
 package avatar
 
 import (
+	"crypto/sha256"
 	"encoding/hex"
 	"strconv"
-
-	"github.com/minio/sha256-simd"
 )
 
 // HashAvatar will generate a unique string, which ensures that when there's a
diff --git a/modules/avatar/identicon/identicon.go b/modules/avatar/identicon/identicon.go
index 9b7a2faf05..63926d5f19 100644
--- a/modules/avatar/identicon/identicon.go
+++ b/modules/avatar/identicon/identicon.go
@@ -7,11 +7,10 @@
 package identicon
 
 import (
+	"crypto/sha256"
 	"fmt"
 	"image"
 	"image/color"
-
-	"github.com/minio/sha256-simd"
 )
 
 const minImageSize = 16
diff --git a/modules/base/tool.go b/modules/base/tool.go
index e9f4dfa279..b72f3a1930 100644
--- a/modules/base/tool.go
+++ b/modules/base/tool.go
@@ -5,6 +5,7 @@ package base
 
 import (
 	"crypto/sha1"
+	"crypto/sha256"
 	"encoding/base64"
 	"encoding/hex"
 	"errors"
@@ -22,7 +23,6 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 
 	"github.com/dustin/go-humanize"
-	"github.com/minio/sha256-simd"
 )
 
 // EncodeSha1 string to sha1 hex value.
diff --git a/modules/git/last_commit_cache.go b/modules/git/last_commit_cache.go
index 7c7baedd2f..5b62b90b27 100644
--- a/modules/git/last_commit_cache.go
+++ b/modules/git/last_commit_cache.go
@@ -4,12 +4,11 @@
 package git
 
 import (
+	"crypto/sha256"
 	"fmt"
 
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
-
-	"github.com/minio/sha256-simd"
 )
 
 // Cache represents a caching interface
diff --git a/modules/lfs/content_store.go b/modules/lfs/content_store.go
index daf8c6cfdd..0d9c0c98ac 100644
--- a/modules/lfs/content_store.go
+++ b/modules/lfs/content_store.go
@@ -4,6 +4,7 @@
 package lfs
 
 import (
+	"crypto/sha256"
 	"encoding/hex"
 	"errors"
 	"hash"
@@ -12,8 +13,6 @@ import (
 
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/storage"
-
-	"github.com/minio/sha256-simd"
 )
 
 var (
diff --git a/modules/lfs/pointer.go b/modules/lfs/pointer.go
index 3e5bb8f91d..ebde20f826 100644
--- a/modules/lfs/pointer.go
+++ b/modules/lfs/pointer.go
@@ -4,6 +4,7 @@
 package lfs
 
 import (
+	"crypto/sha256"
 	"encoding/hex"
 	"errors"
 	"fmt"
@@ -12,8 +13,6 @@ import (
 	"regexp"
 	"strconv"
 	"strings"
-
-	"github.com/minio/sha256-simd"
 )
 
 const (
diff --git a/modules/secret/secret.go b/modules/secret/secret.go
index 9c2ecd181d..e70ae1839c 100644
--- a/modules/secret/secret.go
+++ b/modules/secret/secret.go
@@ -7,13 +7,12 @@ import (
 	"crypto/aes"
 	"crypto/cipher"
 	"crypto/rand"
+	"crypto/sha256"
 	"encoding/base64"
 	"encoding/hex"
 	"errors"
 	"fmt"
 	"io"
-
-	"github.com/minio/sha256-simd"
 )
 
 // AesEncrypt encrypts text and given key with AES.
diff --git a/modules/util/keypair.go b/modules/util/keypair.go
index 97f2d9ebca..8b86c142af 100644
--- a/modules/util/keypair.go
+++ b/modules/util/keypair.go
@@ -7,10 +7,9 @@ import (
 	"crypto"
 	"crypto/rand"
 	"crypto/rsa"
+	"crypto/sha256"
 	"crypto/x509"
 	"encoding/pem"
-
-	"github.com/minio/sha256-simd"
 )
 
 // GenerateKeyPair generates a public and private keypair
diff --git a/modules/util/keypair_test.go b/modules/util/keypair_test.go
index c9925f7988..c6f68c845a 100644
--- a/modules/util/keypair_test.go
+++ b/modules/util/keypair_test.go
@@ -7,12 +7,12 @@ import (
 	"crypto"
 	"crypto/rand"
 	"crypto/rsa"
+	"crypto/sha256"
 	"crypto/x509"
 	"encoding/pem"
 	"regexp"
 	"testing"
 
-	"github.com/minio/sha256-simd"
 	"github.com/stretchr/testify/assert"
 )
 
diff --git a/routers/api/packages/chef/auth.go b/routers/api/packages/chef/auth.go
index 3aef8281a4..a790e9a363 100644
--- a/routers/api/packages/chef/auth.go
+++ b/routers/api/packages/chef/auth.go
@@ -8,6 +8,7 @@ import (
 	"crypto"
 	"crypto/rsa"
 	"crypto/sha1"
+	"crypto/sha256"
 	"crypto/x509"
 	"encoding/base64"
 	"encoding/pem"
@@ -26,8 +27,6 @@ import (
 	chef_module "code.gitea.io/gitea/modules/packages/chef"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/auth"
-
-	"github.com/minio/sha256-simd"
 )
 
 const (
diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go
index 0b93382b01..5106395eb1 100644
--- a/routers/api/packages/maven/maven.go
+++ b/routers/api/packages/maven/maven.go
@@ -6,6 +6,7 @@ package maven
 import (
 	"crypto/md5"
 	"crypto/sha1"
+	"crypto/sha256"
 	"crypto/sha512"
 	"encoding/hex"
 	"encoding/xml"
@@ -26,8 +27,6 @@ import (
 	maven_module "code.gitea.io/gitea/modules/packages/maven"
 	"code.gitea.io/gitea/routers/api/packages/helper"
 	packages_service "code.gitea.io/gitea/services/packages"
-
-	"github.com/minio/sha256-simd"
 )
 
 const (
diff --git a/services/lfs/server.go b/services/lfs/server.go
index 62134b7d60..56714120ad 100644
--- a/services/lfs/server.go
+++ b/services/lfs/server.go
@@ -5,6 +5,7 @@ package lfs
 
 import (
 	stdCtx "context"
+	"crypto/sha256"
 	"encoding/base64"
 	"encoding/hex"
 	"errors"
@@ -33,7 +34,6 @@ import (
 	"code.gitea.io/gitea/modules/storage"
 
 	"github.com/golang-jwt/jwt/v5"
-	"github.com/minio/sha256-simd"
 )
 
 // requestContext contain variables from the HTTP request.
diff --git a/services/mailer/token/token.go b/services/mailer/token/token.go
index aa7b567188..8a5a762d6b 100644
--- a/services/mailer/token/token.go
+++ b/services/mailer/token/token.go
@@ -6,14 +6,13 @@ package token
 import (
 	"context"
 	crypto_hmac "crypto/hmac"
+	"crypto/sha256"
 	"encoding/base32"
 	"fmt"
 	"time"
 
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/util"
-
-	"github.com/minio/sha256-simd"
 )
 
 // A token is a verifiable container describing an action.
diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go
index 8f728d3aa6..935981d29c 100644
--- a/services/webhook/deliver.go
+++ b/services/webhook/deliver.go
@@ -7,6 +7,7 @@ import (
 	"context"
 	"crypto/hmac"
 	"crypto/sha1"
+	"crypto/sha256"
 	"crypto/tls"
 	"encoding/hex"
 	"fmt"
@@ -29,7 +30,6 @@ import (
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 
 	"github.com/gobwas/glob"
-	"github.com/minio/sha256-simd"
 )
 
 // Deliver deliver hook task
diff --git a/tests/integration/api_packages_chef_test.go b/tests/integration/api_packages_chef_test.go
index 4123c7216c..05545f11a6 100644
--- a/tests/integration/api_packages_chef_test.go
+++ b/tests/integration/api_packages_chef_test.go
@@ -11,6 +11,7 @@ import (
 	"crypto/rand"
 	"crypto/rsa"
 	"crypto/sha1"
+	"crypto/sha256"
 	"crypto/x509"
 	"encoding/base64"
 	"encoding/pem"
@@ -33,7 +34,6 @@ import (
 	chef_router "code.gitea.io/gitea/routers/api/packages/chef"
 	"code.gitea.io/gitea/tests"
 
-	"github.com/minio/sha256-simd"
 	"github.com/stretchr/testify/assert"
 )
 
diff --git a/tests/integration/api_packages_container_test.go b/tests/integration/api_packages_container_test.go
index 509ad424e6..9ac6e5256b 100644
--- a/tests/integration/api_packages_container_test.go
+++ b/tests/integration/api_packages_container_test.go
@@ -5,6 +5,7 @@ package integration
 
 import (
 	"bytes"
+	"crypto/sha256"
 	"encoding/base64"
 	"fmt"
 	"net/http"
@@ -24,7 +25,6 @@ import (
 	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/tests"
 
-	"github.com/minio/sha256-simd"
 	oci "github.com/opencontainers/image-spec/specs-go/v1"
 	"github.com/stretchr/testify/assert"
 )
diff --git a/tests/integration/api_packages_test.go b/tests/integration/api_packages_test.go
index 8c981566b6..daf32e82f9 100644
--- a/tests/integration/api_packages_test.go
+++ b/tests/integration/api_packages_test.go
@@ -5,6 +5,7 @@ package integration
 
 import (
 	"bytes"
+	"crypto/sha256"
 	"fmt"
 	"net/http"
 	"strings"
@@ -24,7 +25,6 @@ import (
 	packages_cleanup_service "code.gitea.io/gitea/services/packages/cleanup"
 	"code.gitea.io/gitea/tests"
 
-	"github.com/minio/sha256-simd"
 	"github.com/stretchr/testify/assert"
 )
 

From 7babc6efe11d0950ac47d3d4724bb5d414bdd6aa Mon Sep 17 00:00:00 2001
From: Gusted 
Date: Sat, 30 Sep 2023 22:16:47 +0200
Subject: [PATCH 16/86] [GITEA] Make atomic ssh keys replacement robust

- After stumbling upon https://github.com/golang/go/issues/22397 and
reading the implementations I realized that Forgejo code doesn't have
`Sync()` and it doesn't properly error handle the `Close` function.
- (likely) Resolves https://codeberg.org/forgejo/forgejo/issues/1446

(cherry picked from commit 0efcb334c2f123d0869a30d684189eb31e8b983f)
(cherry picked from commit 04ef02c0dd98c7437acb39383d311c0901366508)
(cherry picked from commit 85f2065c9bc6ded9c21909ec76a9e8fc2d22f462)
(cherry picked from commit 8d36b5cce66864e190bad3c9b0973e37ca774a22)
(cherry picked from commit 378dc30fb5a88ffe185c54de7e69224289038bff)
(cherry picked from commit 2b28bf826e51b8ccb4a693001c03ffe6132f7842)
(cherry picked from commit d0625a001e5f8fe202865bec7aadcf0c551d556d)
(cherry picked from commit f161a4f60f1cde80a41bece4929836257b9e0423)
(cherry picked from commit 7430ca43e57683ca324fb20269a60e05cb393589)
(cherry picked from commit ab6d38daf7eeb1dc993bfc0ac1a326af65128168)
(cherry picked from commit 0f703fd02e69bdcf2f77e120ff8641f1b8089020)
(cherry picked from commit 6931a8f6bbfa0fe4f68b462f88c4a3db7ea06306)
(cherry picked from commit 5e2065c1c0ac66d90fae23f989077fa8cb0416ef)
(cherry picked from commit 38c812acfffe4c83099881a8b47489caba64b42a)
(cherry picked from commit 494874e23f2edb90beabe0827dadefa035e35a71)
(cherry picked from commit d396b7fd47c20d08844f8de1255a32fe0de25249)
---
 models/asymkey/ssh_key_authorized_keys.go       | 7 ++++++-
 models/asymkey/ssh_key_authorized_principals.go | 7 ++++++-
 2 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/models/asymkey/ssh_key_authorized_keys.go b/models/asymkey/ssh_key_authorized_keys.go
index 267ab252c8..2b15450c98 100644
--- a/models/asymkey/ssh_key_authorized_keys.go
+++ b/models/asymkey/ssh_key_authorized_keys.go
@@ -169,7 +169,12 @@ func RewriteAllPublicKeys(ctx context.Context) error {
 		return err
 	}
 
-	t.Close()
+	if err := t.Sync(); err != nil {
+		return err
+	}
+	if err := t.Close(); err != nil {
+		return err
+	}
 	return util.Rename(tmpPath, fPath)
 }
 
diff --git a/models/asymkey/ssh_key_authorized_principals.go b/models/asymkey/ssh_key_authorized_principals.go
index 107d70c766..f3017c3089 100644
--- a/models/asymkey/ssh_key_authorized_principals.go
+++ b/models/asymkey/ssh_key_authorized_principals.go
@@ -92,7 +92,12 @@ func RewriteAllPrincipalKeys(ctx context.Context) error {
 		return err
 	}
 
-	t.Close()
+	if err := t.Sync(); err != nil {
+		return err
+	}
+	if err := t.Close(); err != nil {
+		return err
+	}
 	return util.Rename(tmpPath, fPath)
 }
 

From 876d9d5c6f272120d31bdb18df39c3a9d25e2917 Mon Sep 17 00:00:00 2001
From: Gusted 
Date: Sun, 1 Oct 2023 12:58:58 +0200
Subject: [PATCH 17/86] [GITEA] Use existing jsonschema library

- Use the 'existing' jsonschema library for the nodeinfo integration test.

(cherry picked from commit 73864840f27274d4cdaef23d47a6a71fc60529c3)
(cherry picked from commit da36df306b7a75434c75ed5f63608e06266ca480)

Conflicts:
	go.mod
	https://codeberg.org/forgejo/forgejo/pulls/1581
(cherry picked from commit 2b4ab46d8eacd2e6b2318f26e327ec59b804ea23)

Conflicts:
	go.mod
	https://codeberg.org/forgejo/forgejo/pulls/1617
(cherry picked from commit 8064130344eb0d797838f8444a6d5c0e3d425716)
(cherry picked from commit 0ccefc633e5cd206bd51f642c9fe7be56dae51ec)
(cherry picked from commit 19e647b531c5cfaba5beabbd1de2fe65dfd44da0)
(cherry picked from commit 2bcc04889d4d765eba0ebdffe7ba788c91923d35)
(cherry picked from commit 2fd1932699572a666ef616b7fb191ab5f56d75c6)
(cherry picked from commit b9a3e1e5258e1896550cc3750b7f688fdab1bb22)
(cherry picked from commit 92d932d23f2fcab4b38b72bb6b39778026418ecd)
(cherry picked from commit c125217fea154d6eb87ae86735906c90575bbff3)
(cherry picked from commit f9801ba57b58fefe5a4cc8c9da91a3593129f656)

Conflicts:
	go.mod
	https://codeberg.org/forgejo/forgejo/pulls/2034
(cherry picked from commit 2558a8a764ff6d35c70beb7510626cdeac38771d)
(cherry picked from commit f53b2d31127ddf17b6593d1bb3ec536ee04c8937)
(cherry picked from commit c098055f0a3266a94651bc2783330cf7b12efff2)
(cherry picked from commit 0e1591554a275d14da09114db8572bf731c5f112)
---
 go.mod                                |  3 ---
 go.sum                                |  7 -------
 tests/integration/integration_test.go | 19 +++++++++----------
 3 files changed, 9 insertions(+), 20 deletions(-)

diff --git a/go.mod b/go.mod
index 6626f9bbb9..3d8e0276ef 100644
--- a/go.mod
+++ b/go.mod
@@ -99,7 +99,6 @@ require (
 	github.com/ulikunitz/xz v0.5.11
 	github.com/urfave/cli/v2 v2.26.0
 	github.com/xanzy/go-gitlab v0.95.2
-	github.com/xeipuuv/gojsonschema v1.2.0
 	github.com/yohcop/openid-go v1.0.1
 	github.com/yuin/goldmark v1.6.0
 	github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
@@ -277,8 +276,6 @@ require (
 	github.com/valyala/fastjson v1.6.4 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
 	github.com/xanzy/ssh-agent v0.3.3 // indirect
-	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
-	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
 	github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
 	github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
 	github.com/zeebo/blake3 v0.2.3 // indirect
diff --git a/go.sum b/go.sum
index ec43f6aa0f..bbf26265be 100644
--- a/go.sum
+++ b/go.sum
@@ -837,13 +837,6 @@ github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23n
 github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
 github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
 github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
-github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
-github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
-github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
-github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
-github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
-github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
-github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
 github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
 github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
 github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI=
diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go
index 60a85ee5d4..3a3fcc83c2 100644
--- a/tests/integration/integration_test.go
+++ b/tests/integration/integration_test.go
@@ -43,8 +43,8 @@ import (
 	"github.com/markbates/goth"
 	"github.com/markbates/goth/gothic"
 	goth_gitlab "github.com/markbates/goth/providers/gitlab"
+	"github.com/santhosh-tekuri/jsonschema/v5"
 	"github.com/stretchr/testify/assert"
-	"github.com/xeipuuv/gojsonschema"
 )
 
 var testWebRoutes *web.Route
@@ -544,16 +544,15 @@ func VerifyJSONSchema(t testing.TB, resp *httptest.ResponseRecorder, schemaFile
 	_, schemaFileErr := os.Stat(schemaFilePath)
 	assert.Nil(t, schemaFileErr)
 
-	schema, schemaFileReadErr := os.ReadFile(schemaFilePath)
-	assert.Nil(t, schemaFileReadErr)
-	assert.True(t, len(schema) > 0)
+	schema, err := jsonschema.Compile(schemaFilePath)
+	assert.NoError(t, err)
 
-	nodeinfoSchema := gojsonschema.NewStringLoader(string(schema))
-	nodeinfoString := gojsonschema.NewStringLoader(resp.Body.String())
-	result, schemaValidationErr := gojsonschema.Validate(nodeinfoSchema, nodeinfoString)
-	assert.Nil(t, schemaValidationErr)
-	assert.Empty(t, result.Errors())
-	assert.True(t, result.Valid())
+	var data interface{}
+	err = json.Unmarshal(resp.Body.Bytes(), &data)
+	assert.NoError(t, err)
+
+	schemaValidation := schema.Validate(data)
+	assert.Nil(t, schemaValidation)
 }
 
 func GetCSRF(t testing.TB, session *TestSession, urlStr string) string {

From 816fe558126d0ecce969fdf2a196fa6afdcca792 Mon Sep 17 00:00:00 2001
From: Gusted 
Date: Sun, 1 Oct 2023 00:09:25 +0200
Subject: [PATCH 18/86] [GITEA] Use maintained gziphandler

- https://github.com/NYTimes/gziphandler doesn't seems to be maintained
anymore and Forgejo already includes
https://github.com/klauspost/compress which provides a maintained and
faster gzip handler fork.
- Enables Jitter to prevent BREACH attacks, as this *seems* to be
possible in the context of Forgejo.

(cherry picked from commit cc2847241d82001babd8d40c87d03169f21c14cd)
(cherry picked from commit 99ba56a8761dd08e08d9499cab2ded1a6b7b970f)

Conflicts:
	go.sum
	https://codeberg.org/forgejo/forgejo/pulls/1581
(cherry picked from commit 711638193daa2311e2ead6249a47dcec47b4e335)
(cherry picked from commit 9c12a37fde6fa84414bf332ff4a066facdb92d38)
(cherry picked from commit d13065345431a499f9e0b7a3c2043d7487b8aa5b)
(cherry picked from commit 45a16f8c3c6f7d5e4aab8fdde6a621cf36e4801c)
(cherry picked from commit a497acb31f76d580c8b0567f9461274bd78080f4)
(cherry picked from commit fe87fd828973945192b98310c5c3b2001c6e0f86)
(cherry picked from commit 6ac12e6693cf45cb12109028dabd868957c4b74c)
(cherry picked from commit 981ec37e1e72ab19c20067ff4d2a7e20a60d3305)
(cherry picked from commit 5d6892ec10086f0ba63f26693faa82d0fd4e3f4a)
(cherry picked from commit 9df7968f4fc72de9788d84ca3f349e4c98ee630e)
(cherry picked from commit 7d588d183329cd760053663ea2e1e82e62958409)

Conflicts:
	routers/web/web.go
	https://codeberg.org/forgejo/forgejo/pulls/2075
(cherry picked from commit defb101281f5a6ba410abc763674bafa7b63dffd)
(cherry picked from commit 5830f204a17767fda3e45d16dbf3af8c32e7f387)
(cherry picked from commit 029f4e98636a7776f430684e9d7142d69a444f96)
---
 assets/go-licenses.json                 | 10 +++++-----
 go.mod                                  |  1 -
 go.sum                                  |  2 --
 modules/web/handler.go                  | 10 ++++++++++
 routers/web/web.go                      | 13 ++++---------
 tests/integration/lfs_getobject_test.go |  8 ++++----
 6 files changed, 23 insertions(+), 21 deletions(-)

diff --git a/assets/go-licenses.json b/assets/go-licenses.json
index 1886787e33..ce98f05632 100644
--- a/assets/go-licenses.json
+++ b/assets/go-licenses.json
@@ -89,11 +89,6 @@
     "path": "github.com/DataDog/zstd/LICENSE",
     "licenseText": "Simplified BSD License\n\nCopyright (c) 2016, Datadog \u003cinfo@datadoghq.com\u003e\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright notice,\n      this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above copyright notice,\n      this list of conditions and the following disclaimer in the documentation\n      and/or other materials provided with the distribution.\n    * Neither the name of the copyright holder nor the names of its contributors\n      may be used to endorse or promote products derived from this software\n      without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
   },
-  {
-    "name": "github.com/NYTimes/gziphandler",
-    "path": "github.com/NYTimes/gziphandler/LICENSE",
-    "licenseText": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2016-2017 The New York Times Company\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
-  },
   {
     "name": "github.com/ProtonMail/go-crypto",
     "path": "github.com/ProtonMail/go-crypto/LICENSE",
@@ -664,6 +659,11 @@
     "path": "github.com/klauspost/compress/LICENSE",
     "licenseText": "Copyright (c) 2012 The Go Authors. All rights reserved.\nCopyright (c) 2019 Klaus Post. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n------------------\n\nFiles: gzhttp/*\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2016-2017 The New York Times Company\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n------------------\n\nFiles: s2/cmd/internal/readahead/*\n\nThe MIT License (MIT)\n\nCopyright (c) 2015 Klaus Post\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n---------------------\nFiles: snappy/*\nFiles: internal/snapref/*\n\nCopyright (c) 2011 The Snappy-Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n-----------------\n\nFiles: s2/cmd/internal/filepathx/*\n\nCopyright 2016 The filepathx Authors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
   },
+  {
+    "name": "github.com/klauspost/compress/gzhttp",
+    "path": "github.com/klauspost/compress/gzhttp/LICENSE",
+    "licenseText": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2016-2017 The New York Times Company\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
+  },
   {
     "name": "github.com/klauspost/compress/internal/snapref",
     "path": "github.com/klauspost/compress/internal/snapref/LICENSE",
diff --git a/go.mod b/go.mod
index 3d8e0276ef..3404e0d34d 100644
--- a/go.mod
+++ b/go.mod
@@ -15,7 +15,6 @@ require (
 	gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
 	github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121
 	github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
-	github.com/NYTimes/gziphandler v1.1.1
 	github.com/PuerkitoBio/goquery v1.8.1
 	github.com/alecthomas/chroma/v2 v2.12.0
 	github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
diff --git a/go.sum b/go.sum
index bbf26265be..60601f0815 100644
--- a/go.sum
+++ b/go.sum
@@ -93,8 +93,6 @@ github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBa
 github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
 github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
 github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
-github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
-github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
 github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE=
 github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
 github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
diff --git a/modules/web/handler.go b/modules/web/handler.go
index 26b7428016..728cc5a160 100644
--- a/modules/web/handler.go
+++ b/modules/web/handler.go
@@ -147,6 +147,16 @@ func toHandlerProvider(handler any) func(next http.Handler) http.Handler {
 		}
 	}
 
+	if hp, ok := handler.(func(next http.Handler) http.HandlerFunc); ok {
+		return func(next http.Handler) http.Handler {
+			h := hp(next) // this handle could be dynamically generated, so we can't use it for debug info
+			return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
+				routing.UpdateFuncInfo(req.Context(), funcInfo)
+				h.ServeHTTP(resp, req)
+			})
+		}
+	}
+
 	provider := func(next http.Handler) http.Handler {
 		return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) {
 			// wrap the response writer to check whether the response has been written
diff --git a/routers/web/web.go b/routers/web/web.go
index faacbc611f..19c760e5bd 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -49,17 +49,12 @@ import (
 	_ "code.gitea.io/gitea/modules/session" // to registers all internal adapters
 
 	"gitea.com/go-chi/captcha"
-	"github.com/NYTimes/gziphandler"
 	chi_middleware "github.com/go-chi/chi/v5/middleware"
 	"github.com/go-chi/cors"
+	"github.com/klauspost/compress/gzhttp"
 	"github.com/prometheus/client_golang/prometheus"
 )
 
-const (
-	// GzipMinSize represents min size to compress for the body size of response
-	GzipMinSize = 1400
-)
-
 // optionsCorsHandler return a http handler which sets CORS options if enabled by config, it blocks non-CORS OPTIONS requests.
 func optionsCorsHandler() func(next http.Handler) http.Handler {
 	var corsHandler func(next http.Handler) http.Handler
@@ -245,11 +240,11 @@ func Routes() *web.Route {
 	var mid []any
 
 	if setting.EnableGzip {
-		h, err := gziphandler.GzipHandlerWithOpts(gziphandler.MinSize(GzipMinSize))
+		wrapper, err := gzhttp.NewWrapper(gzhttp.RandomJitter(32, 0, false))
 		if err != nil {
-			log.Fatal("GzipHandlerWithOpts failed: %v", err)
+			log.Fatal("gzhttp.NewWrapper failed: %v", err)
 		}
-		mid = append(mid, h)
+		mid = append(mid, wrapper)
 	}
 
 	if setting.Service.EnableCaptcha {
diff --git a/tests/integration/lfs_getobject_test.go b/tests/integration/lfs_getobject_test.go
index a87f38be8a..dad05890a9 100644
--- a/tests/integration/lfs_getobject_test.go
+++ b/tests/integration/lfs_getobject_test.go
@@ -18,9 +18,9 @@ import (
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/routers/web"
 	"code.gitea.io/gitea/tests"
 
+	"github.com/klauspost/compress/gzhttp"
 	gzipp "github.com/klauspost/compress/gzip"
 	"github.com/stretchr/testify/assert"
 )
@@ -132,7 +132,7 @@ func TestGetLFSSmallTokenFail(t *testing.T) {
 
 func TestGetLFSLarge(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
-	content := make([]byte, web.GzipMinSize*10)
+	content := make([]byte, gzhttp.DefaultMinSize*10)
 	for i := range content {
 		content[i] = byte(i % 256)
 	}
@@ -143,7 +143,7 @@ func TestGetLFSLarge(t *testing.T) {
 
 func TestGetLFSGzip(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
-	b := make([]byte, web.GzipMinSize*10)
+	b := make([]byte, gzhttp.DefaultMinSize*10)
 	for i := range b {
 		b[i] = byte(i % 256)
 	}
@@ -159,7 +159,7 @@ func TestGetLFSGzip(t *testing.T) {
 
 func TestGetLFSZip(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
-	b := make([]byte, web.GzipMinSize*10)
+	b := make([]byte, gzhttp.DefaultMinSize*10)
 	for i := range b {
 		b[i] = byte(i % 256)
 	}

From 3794698320fbe4424d0311d639e22edac1e54f1b Mon Sep 17 00:00:00 2001
From: Grigory Kirillov 
Date: Fri, 13 Oct 2023 14:40:41 +0300
Subject: [PATCH 19/86] [GITEA] convert feed items' titles to plain text

Refs: https://codeberg.org/forgejo/forgejo/pulls/1595

(cherry picked from commit 35b962e6313df748e8855b4dfbf748f095ea1003)
(cherry picked from commit 1004e35b84a4a0deae999cb8a4c2924b85b47c8b)
(cherry picked from commit af51dd594db229f7a986325a6070d33782d85d28)
(cherry picked from commit ef10fae29607533db3616a23043cc0f2fc2dc71a)
(cherry picked from commit ff8027ed1b0a1274b7b6e4840e31e2ad4d18b159)
(cherry picked from commit 2540ff52ef2229ad6e17578c30ae617b3771c696)
(cherry picked from commit 57b4d775e1734d2cb6dd78a4e890d3548e2324eb)
(cherry picked from commit c388aba9b50bbdd7eb13518d91f8e00c5d1bce18)
(cherry picked from commit 7a3b605c11d5a9033b7c3db882b606fb009afca3)
(cherry picked from commit cc02354d0a6872e761d8215b6630a1467c6f8e75)
(cherry picked from commit e11c5ce82aeaaa62ced4bead72aa3d37453b792a)
(cherry picked from commit d1e7798bb2b32eb3a8bd1be669191d3e3a9a2510)
(cherry picked from commit 1813af7391a47b79b4cd44d4feb64e3002032db6)
(cherry picked from commit 0d55a8894508aae4225d76235d4bd7a9f862a849)
(cherry picked from commit bd9ac9ac6f0c7374c8254f9fe65f53758d90e0d2)
---
 routers/web/feed/convert.go                   | 10 ++++-
 .../api_feed_plain_text_titles_test.go        | 40 +++++++++++++++++++
 2 files changed, 49 insertions(+), 1 deletion(-)
 create mode 100644 tests/integration/api_feed_plain_text_titles_test.go

diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go
index 6dbc2c2cbc..1176277195 100644
--- a/routers/web/feed/convert.go
+++ b/routers/web/feed/convert.go
@@ -21,6 +21,7 @@ import (
 	"code.gitea.io/gitea/modules/util"
 
 	"github.com/gorilla/feeds"
+	"github.com/jaytaylor/html2text"
 )
 
 func toBranchLink(ctx *context.Context, act *activities_model.Action) string {
@@ -240,8 +241,15 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
 			content = desc
 		}
 
+		// It's a common practice for feed generators to use plain text titles.
+		// See https://codeberg.org/forgejo/forgejo/pulls/1595
+		plainTitle, err := html2text.FromString(title, html2text.Options{OmitLinks: true})
+		if err != nil {
+			return nil, err
+		}
+
 		items = append(items, &feeds.Item{
-			Title:       title,
+			Title:       plainTitle,
 			Link:        link,
 			Description: desc,
 			IsPermaLink: "false",
diff --git a/tests/integration/api_feed_plain_text_titles_test.go b/tests/integration/api_feed_plain_text_titles_test.go
new file mode 100644
index 0000000000..a058b7321c
--- /dev/null
+++ b/tests/integration/api_feed_plain_text_titles_test.go
@@ -0,0 +1,40 @@
+// Copyright 2023 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"net/http"
+	"testing"
+
+	"code.gitea.io/gitea/tests"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestFeedPlainTextTitles(t *testing.T) {
+	// This test verifies that items' titles in feeds are generated as plain text.
+	// See https://codeberg.org/forgejo/forgejo/pulls/1595
+
+	t.Run("Feed plain text titles", func(t *testing.T) {
+		t.Run("Atom", func(t *testing.T) {
+			defer tests.PrepareTestEnv(t)()
+
+			req := NewRequest(t, "GET", "/user2/repo1.atom")
+			resp := MakeRequest(t, req, http.StatusOK)
+
+			data := resp.Body.String()
+			assert.Contains(t, data, "the_1-user.with.all.allowedChars closed issue user2/repo1#4")
+		})
+
+		t.Run("RSS", func(t *testing.T) {
+			defer tests.PrepareTestEnv(t)()
+
+			req := NewRequest(t, "GET", "/user2/repo1.rss")
+			resp := MakeRequest(t, req, http.StatusOK)
+
+			data := resp.Body.String()
+			assert.Contains(t, data, "the_1-user.with.all.allowedChars closed issue user2/repo1#4")
+		})
+	})
+}

From 679438b5d621fd58d0618c28cd08abe0a5625037 Mon Sep 17 00:00:00 2001
From: Gusted 
Date: Fri, 20 Oct 2023 11:40:32 +0200
Subject: [PATCH 20/86] [GITEA] Add repo empty check for branch feed

- If you attempted to get a branch feed on a empty repository, it would
result in a panic as the code expects that the branch exists.
- `context.RepoRefByType` would normally already 404 if the branch
doesn't exist, however if a repository is empty, it would not do this
check.
- Fix bug where `/atom/branch/*` would return a RSS feed.

(cherry picked from commit d27bcd98a41b69e313535e5e91e4272136a4bab1)
(cherry picked from commit c58566403df728c1f71b1dd554a573c011a59d7e)
(cherry picked from commit b8b3f6ab8b576a28ed06cc0e501b14950cf78282)
(cherry picked from commit 195520100b214d6bf7a2740507f0a7ae10e5a7d1)
(cherry picked from commit 6e417087ddf41e79a146366a5db157c7a76af615)
(cherry picked from commit ff91e5957ac728118cddb06bddd95d32cb4df815)
(cherry picked from commit 6626d5cc75681d3b16b4496a4e0e83a257a3f46a)
(cherry picked from commit 62f8ab793b12251e1793bc14ace95cda76121baa)
(cherry picked from commit e5bbf1a2d060b4ef1324afd8ed9b38e294b3dffb)
(cherry picked from commit f5b8c8edea5d17ba51327684a6e8127ac0f09503)
(cherry picked from commit 50948fa11b9c9ccac9e86dc9943bad71cf189370)
(cherry picked from commit 83a9f7f4429ac4e91d7a80a0aced32cd74bbfc4c)
---
 routers/web/feed/render.go              | 13 +++---
 routers/web/repo/view.go                | 15 +++++--
 routers/web/web.go                      |  4 +-
 tests/integration/api_feed_user_test.go | 55 ++++++++++++++++++++++++-
 4 files changed, 73 insertions(+), 14 deletions(-)

diff --git a/routers/web/feed/render.go b/routers/web/feed/render.go
index 8931dae8cc..41f9af1c8c 100644
--- a/routers/web/feed/render.go
+++ b/routers/web/feed/render.go
@@ -8,11 +8,12 @@ import (
 )
 
 // RenderBranchFeed render format for branch or file
-func RenderBranchFeed(ctx *context.Context) {
-	_, _, showFeedType := GetFeedType(ctx.Params(":reponame"), ctx.Req)
-	if ctx.Repo.TreePath == "" {
-		ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType)
-	} else {
-		ShowFileFeed(ctx, ctx.Repo.Repository, showFeedType)
+func RenderBranchFeed(feedType string) func(ctx *context.Context) {
+	return func(ctx *context.Context) {
+		if ctx.Repo.TreePath == "" {
+			ShowBranchFeed(ctx, ctx.Repo.Repository, feedType)
+		} else {
+			ShowFileFeed(ctx, ctx.Repo.Repository, feedType)
+		}
 	}
 }
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 99d5461c0f..167be7b559 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -779,12 +779,19 @@ func Home(ctx *context.Context) {
 	if setting.Other.EnableFeed {
 		isFeed, _, showFeedType := feed.GetFeedType(ctx.Params(":reponame"), ctx.Req)
 		if isFeed {
-			switch {
-			case ctx.Link == fmt.Sprintf("%s.%s", ctx.Repo.RepoLink, showFeedType):
+			if ctx.Link == fmt.Sprintf("%s.%s", ctx.Repo.RepoLink, showFeedType) {
 				feed.ShowRepoFeed(ctx, ctx.Repo.Repository, showFeedType)
-			case ctx.Repo.TreePath == "":
+				return
+			}
+
+			if ctx.Repo.Repository.IsEmpty {
+				ctx.NotFound("MustBeNotEmpty", nil)
+				return
+			}
+
+			if ctx.Repo.TreePath == "" {
 				feed.ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType)
-			case ctx.Repo.TreePath != "":
+			} else {
 				feed.ShowFileFeed(ctx, ctx.Repo.Repository, showFeedType)
 			}
 			return
diff --git a/routers/web/web.go b/routers/web/web.go
index 19c760e5bd..9b42eed306 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1499,8 +1499,8 @@ func registerRoutes(m *web.Route) {
 			m.Get("/cherry-pick/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, repo.CherryPick)
 		}, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader)
 
-		m.Get("/rss/branch/*", context.RepoRefByType(context.RepoRefBranch), feedEnabled, feed.RenderBranchFeed)
-		m.Get("/atom/branch/*", context.RepoRefByType(context.RepoRefBranch), feedEnabled, feed.RenderBranchFeed)
+		m.Get("/rss/branch/*", repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefBranch), feedEnabled, feed.RenderBranchFeed("rss"))
+		m.Get("/atom/branch/*", repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefBranch), feedEnabled, feed.RenderBranchFeed("atom"))
 
 		m.Group("/src", func() {
 			m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.Home)
diff --git a/tests/integration/api_feed_user_test.go b/tests/integration/api_feed_user_test.go
index c44f9a1951..608f7608ae 100644
--- a/tests/integration/api_feed_user_test.go
+++ b/tests/integration/api_feed_user_test.go
@@ -7,15 +7,19 @@ import (
 	"net/http"
 	"testing"
 
+	"code.gitea.io/gitea/models/db"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/tests"
 
 	"github.com/stretchr/testify/assert"
 )
 
 func TestFeed(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
 	t.Run("User", func(t *testing.T) {
 		t.Run("Atom", func(t *testing.T) {
-			defer tests.PrepareTestEnv(t)()
+			defer tests.PrintCurrentTest(t)()
 
 			req := NewRequest(t, "GET", "/user2.atom")
 			resp := MakeRequest(t, req, http.StatusOK)
@@ -25,7 +29,7 @@ func TestFeed(t *testing.T) {
 		})
 
 		t.Run("RSS", func(t *testing.T) {
-			defer tests.PrepareTestEnv(t)()
+			defer tests.PrintCurrentTest(t)()
 
 			req := NewRequest(t, "GET", "/user2.rss")
 			resp := MakeRequest(t, req, http.StatusOK)
@@ -34,4 +38,51 @@ func TestFeed(t *testing.T) {
 			assert.Contains(t, data, `
Date: Sat, 4 Nov 2023 22:09:19 +0100
Subject: [PATCH 21/86] [GITEA] Use existing error functionality

- There's no need to use `github.com/pkg/errors` when the standard
library already has the functionality to wrap and create errors.

(cherry picked from commit 40f603a538036ce7e150adf404374d794eb46993)
(cherry picked from commit aa68a2753f204dfe44a037ccc91dacc2a0a53803)
(cherry picked from commit 48e252d73961ae17b56544bae79bc812dac10c44)
(cherry picked from commit cc6f40ccd208a6fd1527ba08e3123e9b01b218ab)
(cherry picked from commit 03c4b9735824bfaed19df14550ef458d52667b40)
(cherry picked from commit f25eeb76958b78fbe00fc6556aec8a37aba0ecdf)
(cherry picked from commit 989d8fa1cb81e0ff017ac575fdd1cf12507eb42b)
(cherry picked from commit 10e890ed8e7205c140677ff74db393fc0052da14)
(cherry picked from commit 581519389d3a35e775f7d3fdc15c251a3e0828b6)
(cherry picked from commit 03d00b11ac4880fd897631180c40bc1dedf5d7e5)
---
 go.mod                         |  2 +-
 services/pull/commit_status.go | 19 ++++++++++---------
 2 files changed, 11 insertions(+), 10 deletions(-)

diff --git a/go.mod b/go.mod
index 3404e0d34d..83cbdbfe68 100644
--- a/go.mod
+++ b/go.mod
@@ -82,7 +82,6 @@ require (
 	github.com/olivere/elastic/v7 v7.0.32
 	github.com/opencontainers/go-digest v1.0.0
 	github.com/opencontainers/image-spec v1.1.0-rc5
-	github.com/pkg/errors v0.9.1
 	github.com/pquerna/otp v1.4.0
 	github.com/prometheus/client_golang v1.17.0
 	github.com/quasoft/websspi v1.1.2
@@ -245,6 +244,7 @@ require (
 	github.com/pelletier/go-toml/v2 v2.1.1 // indirect
 	github.com/pierrec/lz4/v4 v4.1.19 // indirect
 	github.com/pjbgf/sha1cd v0.3.0 // indirect
+	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/prometheus/client_model v0.5.0 // indirect
 	github.com/prometheus/common v0.45.0 // indirect
diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go
index 39d60380ff..94acd504ef 100644
--- a/services/pull/commit_status.go
+++ b/services/pull/commit_status.go
@@ -6,6 +6,8 @@ package pull
 
 import (
 	"context"
+	"errors"
+	"fmt"
 
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
@@ -15,7 +17,6 @@ import (
 	"code.gitea.io/gitea/modules/structs"
 
 	"github.com/gobwas/glob"
-	"github.com/pkg/errors"
 )
 
 // MergeRequiredContextsCommitStatus returns a commit status state for given required contexts
@@ -95,7 +96,7 @@ func IsCommitStatusContextSuccess(commitStatuses []*git_model.CommitStatus, requ
 func IsPullCommitStatusPass(ctx context.Context, pr *issues_model.PullRequest) (bool, error) {
 	pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
 	if err != nil {
-		return false, errors.Wrap(err, "GetLatestCommitStatus")
+		return false, fmt.Errorf("GetFirstMatchProtectedBranchRule: %w", err)
 	}
 	if pb == nil || !pb.EnableStatusCheck {
 		return true, nil
@@ -112,21 +113,21 @@ func IsPullCommitStatusPass(ctx context.Context, pr *issues_model.PullRequest) (
 func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullRequest) (structs.CommitStatusState, error) {
 	// Ensure HeadRepo is loaded
 	if err := pr.LoadHeadRepo(ctx); err != nil {
-		return "", errors.Wrap(err, "LoadHeadRepo")
+		return "", fmt.Errorf("LoadHeadRepo: %w", err)
 	}
 
 	// check if all required status checks are successful
 	headGitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, pr.HeadRepo.RepoPath())
 	if err != nil {
-		return "", errors.Wrap(err, "OpenRepository")
+		return "", fmt.Errorf("RepositoryFromContextOrOpen: %w", err)
 	}
 	defer closer.Close()
 
 	if pr.Flow == issues_model.PullRequestFlowGithub && !headGitRepo.IsBranchExist(pr.HeadBranch) {
-		return "", errors.New("Head branch does not exist, can not merge")
+		return "", errors.New("head branch does not exist, can not merge")
 	}
 	if pr.Flow == issues_model.PullRequestFlowAGit && !git.IsReferenceExist(ctx, headGitRepo.Path, pr.GetGitRefName()) {
-		return "", errors.New("Head branch does not exist, can not merge")
+		return "", errors.New("head branch does not exist, can not merge")
 	}
 
 	var sha string
@@ -140,17 +141,17 @@ func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullR
 	}
 
 	if err := pr.LoadBaseRepo(ctx); err != nil {
-		return "", errors.Wrap(err, "LoadBaseRepo")
+		return "", fmt.Errorf("LoadBaseRepo: %w", err)
 	}
 
 	commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptions{ListAll: true})
 	if err != nil {
-		return "", errors.Wrap(err, "GetLatestCommitStatus")
+		return "", fmt.Errorf("GetLatestCommitStatus: %w", err)
 	}
 
 	pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
 	if err != nil {
-		return "", errors.Wrap(err, "LoadProtectedBranch")
+		return "", fmt.Errorf("GetFirstMatchProtectedBranchRule: %w", err)
 	}
 	var requiredContexts []string
 	if pb != nil {

From a97de1d5bbc1da33ff04db42025bdccdcd085f68 Mon Sep 17 00:00:00 2001
From: Gusted 
Date: Sat, 11 Nov 2023 13:01:24 +0100
Subject: [PATCH 22/86] [GITEA] Add noreply email address as verified for SSH
 signed Git commits

- When someone really wants to avoid sharing their email, they could
configure git to use the noreply email for git commits. However if they
also wanted to use SSH signing, it would not show up as verified as the
noreply email address was technically not an activated email address for
the user.
- Add unit tests for the `ParseCommitWithSSHSignature` function.
- Resolves https://codeberg.org/Codeberg/Community/issues/946

(cherry picked from commit 1685de7eba9343043c1e2cb277482eafb578095a)
(cherry picked from commit b1e8858de9d4f1e2f6bee31d4e399b07a7a69a8e)
(cherry picked from commit 1a6bf24d28522d57a56dcbdcf23ef19508ac39c8)
(cherry picked from commit 01229433456aa0836adbaafa06bcbefcf4111451)
(cherry picked from commit cc836148534ecf6e007eeb913cd94264fda171d3)
(cherry picked from commit 429febe0dc2e49bae6ce747c318e68a99b1da9a8)
(cherry picked from commit 58a9c2ebe9518576a1b399cce9089fd4894a5c38)
(cherry picked from commit fef94aff1c8b35d9a10cf735d4c7ba05f82c95b4)
(cherry picked from commit 5c6ecd757992f117d3aafc3a2034807cfccbf5a6)
(cherry picked from commit ffa33a82bf012dd7918e5c89873bf0309f95f62c)
---
 models/asymkey/main_test.go                   |   1 +
 models/asymkey/ssh_key_commit_verification.go |   6 +
 .../ssh_key_commit_verification_test.go       | 146 ++++++++++++++++++
 .../public_key.yml                            |  13 ++
 4 files changed, 166 insertions(+)
 create mode 100644 models/asymkey/ssh_key_commit_verification_test.go
 create mode 100644 models/fixtures/TestParseCommitWithSSHSignature/public_key.yml

diff --git a/models/asymkey/main_test.go b/models/asymkey/main_test.go
index be71f848d9..87b5c22c4a 100644
--- a/models/asymkey/main_test.go
+++ b/models/asymkey/main_test.go
@@ -14,6 +14,7 @@ func TestMain(m *testing.M) {
 		FixtureFiles: []string{
 			"gpg_key.yml",
 			"public_key.yml",
+			"TestParseCommitWithSSHSignature/public_key.yml",
 			"deploy_key.yml",
 			"gpg_key_import.yml",
 			"user.yml",
diff --git a/models/asymkey/ssh_key_commit_verification.go b/models/asymkey/ssh_key_commit_verification.go
index 27c6df3578..2b802710a8 100644
--- a/models/asymkey/ssh_key_commit_verification.go
+++ b/models/asymkey/ssh_key_commit_verification.go
@@ -39,6 +39,12 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer *
 			log.Error("GetEmailAddresses: %v", err)
 		}
 
+		// Add the noreply email address as verified address.
+		committerEmailAddresses = append(committerEmailAddresses, &user_model.EmailAddress{
+			IsActivated: true,
+			Email:       committer.GetPlaceholderEmail(),
+		})
+
 		activated := false
 		for _, e := range committerEmailAddresses {
 			if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) {
diff --git a/models/asymkey/ssh_key_commit_verification_test.go b/models/asymkey/ssh_key_commit_verification_test.go
new file mode 100644
index 0000000000..e1ed409bd7
--- /dev/null
+++ b/models/asymkey/ssh_key_commit_verification_test.go
@@ -0,0 +1,146 @@
+// Copyright 2023 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package asymkey
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/test"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestParseCommitWithSSHSignature(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	sshKey := unittest.AssertExistsAndLoadBean(t, &PublicKey{ID: 1000, OwnerID: 2})
+
+	t.Run("No commiter", func(t *testing.T) {
+		commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, &git.Commit{}, &user_model.User{})
+		assert.False(t, commitVerification.Verified)
+		assert.Equal(t, NoKeyFound, commitVerification.Reason)
+	})
+
+	t.Run("Commiter without keys", func(t *testing.T) {
+		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+
+		commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, &git.Commit{Committer: &git.Signature{Email: user.Email}}, user)
+		assert.False(t, commitVerification.Verified)
+		assert.Equal(t, NoKeyFound, commitVerification.Reason)
+	})
+
+	t.Run("Correct signature with wrong email", func(t *testing.T) {
+		gitCommit := &git.Commit{
+			Committer: &git.Signature{
+				Email: "non-existent",
+			},
+			Signature: &git.CommitGPGSignature{
+				Payload: `tree 2d491b2985a7ff848d5c02748e7ea9f9f7619f9f
+parent 45b03601635a1f463b81963a4022c7f87ce96ef9
+author user2  1699710556 +0100
+committer user2  1699710556 +0100
+
+Using email that isn't known to Forgejo
+`,
+				Signature: `-----BEGIN SSH SIGNATURE-----
+U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgoGSe9Zy7Ez9bSJcaTNjh/Y7p95
+f5DujjqkpzFRtw6CEAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
+AAAAQIMufOuSjZeDUujrkVK4sl7ICa0WwEftas8UAYxx0Thdkiw2qWjR1U1PKfTLm16/w8
+/bS1LX1lZNuzm2LR2qEgw=
+-----END SSH SIGNATURE-----
+`,
+			},
+		}
+		commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2)
+		assert.False(t, commitVerification.Verified)
+		assert.Equal(t, NoKeyFound, commitVerification.Reason)
+	})
+
+	t.Run("Incorrect signature with correct email", func(t *testing.T) {
+		gitCommit := &git.Commit{
+			Committer: &git.Signature{
+				Email: "user2@example.com",
+			},
+			Signature: &git.CommitGPGSignature{
+				Payload: `tree 853694aae8816094a0d875fee7ea26278dbf5d0f
+parent c2780d5c313da2a947eae22efd7dacf4213f4e7f
+author user2  1699707877 +0100
+committer user2  1699707877 +0100
+
+Add content
+`,
+				Signature: `-----BEGIN SSH SIGNATURE-----`,
+			},
+		}
+
+		commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2)
+		assert.False(t, commitVerification.Verified)
+		assert.Equal(t, NoKeyFound, commitVerification.Reason)
+	})
+
+	t.Run("Valid signature with correct email", func(t *testing.T) {
+		gitCommit := &git.Commit{
+			Committer: &git.Signature{
+				Email: "user2@example.com",
+			},
+			Signature: &git.CommitGPGSignature{
+				Payload: `tree 853694aae8816094a0d875fee7ea26278dbf5d0f
+parent c2780d5c313da2a947eae22efd7dacf4213f4e7f
+author user2  1699707877 +0100
+committer user2  1699707877 +0100
+
+Add content
+`,
+				Signature: `-----BEGIN SSH SIGNATURE-----
+U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgoGSe9Zy7Ez9bSJcaTNjh/Y7p95
+f5DujjqkpzFRtw6CEAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
+AAAAQBe2Fwk/FKY3SBCnG6jSYcO6ucyahp2SpQ/0P+otslzIHpWNW8cQ0fGLdhhaFynJXQ
+fs9cMpZVM9BfIKNUSO8QY=
+-----END SSH SIGNATURE-----
+`,
+			},
+		}
+
+		commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2)
+		assert.True(t, commitVerification.Verified)
+		assert.Equal(t, "user2 / SHA256:TKfwbZMR7e9OnlV2l1prfah1TXH8CmqR0PvFEXVCXA4", commitVerification.Reason)
+		assert.Equal(t, sshKey, commitVerification.SigningSSHKey)
+	})
+
+	t.Run("Valid signature with noreply email", func(t *testing.T) {
+		defer test.MockVariableValue(&setting.Service.NoReplyAddress, "noreply.example.com")()
+
+		gitCommit := &git.Commit{
+			Committer: &git.Signature{
+				Email: "user2@noreply.example.com",
+			},
+			Signature: &git.CommitGPGSignature{
+				Payload: `tree 4836c7f639f37388bab4050ef5c97bbbd54272fc
+parent 795be1b0117ea5c65456050bb9fd84744d4fd9c6
+author user2  1699709594 +0100
+committer user2  1699709594 +0100
+
+Commit with noreply
+`,
+				Signature: `-----BEGIN SSH SIGNATURE-----
+U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgoGSe9Zy7Ez9bSJcaTNjh/Y7p95
+f5DujjqkpzFRtw6CEAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
+AAAAQJz83KKxD6Bz/ZvNpqkA3RPOSQ4LQ5FfEItbtoONkbwV9wAWMnmBqgggo/lnXCJ3oq
+muPLbvEduU+Ze/1Ol1pgk=
+-----END SSH SIGNATURE-----
+`,
+			},
+		}
+
+		commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2)
+		assert.True(t, commitVerification.Verified)
+		assert.Equal(t, "user2 / SHA256:TKfwbZMR7e9OnlV2l1prfah1TXH8CmqR0PvFEXVCXA4", commitVerification.Reason)
+		assert.Equal(t, sshKey, commitVerification.SigningSSHKey)
+	})
+}
diff --git a/models/fixtures/TestParseCommitWithSSHSignature/public_key.yml b/models/fixtures/TestParseCommitWithSSHSignature/public_key.yml
new file mode 100644
index 0000000000..f76dabb1c1
--- /dev/null
+++ b/models/fixtures/TestParseCommitWithSSHSignature/public_key.yml
@@ -0,0 +1,13 @@
+-
+  id: 1000
+  owner_id: 2
+  name: user2@localhost
+  fingerprint: "SHA256:TKfwbZMR7e9OnlV2l1prfah1TXH8CmqR0PvFEXVCXA4"
+  content: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKBknvWcuxM/W0iXGkzY4f2O6feX+Q7o46pKcxUbcOgh user2@localhost"
+  # private key (base64-ed) LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUFNd0FBQUF0emMyZ3RaVwpReU5UVXhPUUFBQUNDZ1pKNzFuTHNUUDF0SWx4cE0yT0g5anVuM2wva082T09xU25NVkczRG9JUUFBQUpocG43YTZhWisyCnVnQUFBQXR6YzJndFpXUXlOVFV4T1FBQUFDQ2daSjcxbkxzVFAxdElseHBNMk9IOWp1bjNsL2tPNk9PcVNuTVZHM0RvSVEKQUFBRUFxVm12bmo1LzZ5TW12ck9Ub29xa3F5MmUrc21aK0tBcEtKR0crRnY5MlA2QmtudldjdXhNL1cwaVhHa3pZNGYyTwo2ZmVYK1E3bzQ2cEtjeFViY09naEFBQUFFMmQxYzNSbFpFQm5kWE4wWldRdFltVmhjM1FCQWc9PQotLS0tLUVORCBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0=
+  mode: 2
+  type: 1
+  verified: true
+  created_unix: 1559593109
+  updated_unix: 1565224552
+  login_source_id: 0

From 768377cb02b180d49dd025eb373dd8ab6d787cf7 Mon Sep 17 00:00:00 2001
From: Gusted 
Date: Tue, 14 Nov 2023 19:24:20 +0100
Subject: [PATCH 23/86] [GITEA] Accept shorter commit IDs in web route

- Be more liberal in what Forgejo accepts, by reducing the minimum
amount of characters for SHA to 4 characters, which is the minimum
amount that  Git needs in order to figure out which commit was meant.
- It's safe to reduce this requirements, as commits are passed to Git
which will error if the given commit ID results in more than one Git
object. Forgejo will catch this error as that the Commit doesn't exist,
which is a error that's already being handled in most places gracefully.
- Added integration testing.
- Resolves https://codeberg.org/forgejo/forgejo/issues/1760

(cherry picked from commit 0d655c7384b081c36aa4c6b7167280f52c1c42d3)
(cherry picked from commit 9b9aca2a02b06f41f6db847a77ea29f6385b46d2)
(cherry picked from commit 0d0ab1af1fb05e26168c112523f1400fef67f9b0)
(cherry picked from commit d3b352c85482e59c9d1da24a8fe0eb68b0f5858e)
(cherry picked from commit d6af2094df4611d590d8c5062743f5e39f2a7bd8)
(cherry picked from commit f96e55a7a9f06ff987a5e9663da492720d162b76)
(cherry picked from commit bb6261f8479ee8925ddc7f0079b414ef85f04d73)
(cherry picked from commit f6a4146161fda22341c17dc74d42fd13ad181e1f)
(cherry picked from commit ed0292137991d08ee2e6518e74ec221f94f51415)

Conflicts:
	routers/web/web.go
	https://codeberg.org/forgejo/forgejo/pulls/2214
---
 routers/web/web.go             | 20 ++++++++---------
 tests/integration/repo_test.go | 40 ++++++++++++++++++++++++++++++++++
 2 files changed, 50 insertions(+), 10 deletions(-)

diff --git a/routers/web/web.go b/routers/web/web.go
index 9b42eed306..552146ccc6 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1231,7 +1231,7 @@ func registerRoutes(m *web.Route) {
 					Post(web.Bind(forms.UploadRepoFileForm{}), repo.UploadFilePost)
 				m.Combo("/_diffpatch/*").Get(repo.NewDiffPatch).
 					Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost)
-				m.Combo("/_cherrypick/{sha:([a-f0-9]{7,64})}/*").Get(repo.CherryPick).
+				m.Combo("/_cherrypick/{sha:([a-f0-9]{4,64})}/*").Get(repo.CherryPick).
 					Post(web.Bind(forms.CherryPickForm{}), repo.CherryPickPost)
 			}, repo.MustBeEditable)
 			m.Group("", func() {
@@ -1373,8 +1373,8 @@ func registerRoutes(m *web.Route) {
 			m.Combo("/*").
 				Get(repo.Wiki).
 				Post(context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost)
-			m.Get("/commit/{sha:[a-f0-9]{7,64}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
-			m.Get("/commit/{sha:[a-f0-9]{7,64}}.{ext:patch|diff}", repo.RawDiff)
+			m.Get("/commit/{sha:[a-f0-9]{4,64}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
+			m.Get("/commit/{sha:[a-f0-9]{4,64}}.{ext:patch|diff}", repo.RawDiff)
 		}, repo.MustEnableWiki, func(ctx *context.Context) {
 			ctx.Data["PageIsWiki"] = true
 			ctx.Data["CloneButtonOriginLink"] = ctx.Repo.Repository.WikiCloneLink()
@@ -1434,7 +1434,7 @@ func registerRoutes(m *web.Route) {
 			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.Get("/{sha:[a-f0-9]{4,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)
@@ -1443,8 +1443,8 @@ func registerRoutes(m *web.Route) {
 			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.Get("/{sha:[a-f0-9]{4,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesStartingFromCommit)
+				m.Get("/{shaFrom:[a-f0-9]{4,40}}..{shaTo:[a-f0-9]{4,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)
@@ -1494,9 +1494,9 @@ func registerRoutes(m *web.Route) {
 
 		m.Group("", func() {
 			m.Get("/graph", repo.Graph)
-			m.Get("/commit/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
-			m.Get("/commit/{sha:([a-f0-9]{7,64})$}/load-branches-and-tags", repo.LoadBranchesAndTags)
-			m.Get("/cherry-pick/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, repo.CherryPick)
+			m.Get("/commit/{sha:([a-f0-9]{4,64})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
+			m.Get("/commit/{sha:([a-f0-9]{4,64})$}/load-branches-and-tags", repo.LoadBranchesAndTags)
+			m.Get("/cherry-pick/{sha:([a-f0-9]{4,64})$}", repo.SetEditorconfigIfExists, repo.CherryPick)
 		}, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader)
 
 		m.Get("/rss/branch/*", repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefBranch), feedEnabled, feed.RenderBranchFeed("rss"))
@@ -1513,7 +1513,7 @@ func registerRoutes(m *web.Route) {
 		m.Group("", func() {
 			m.Get("/forks", repo.Forks)
 		}, context.RepoRef(), reqRepoCodeReader)
-		m.Get("/commit/{sha:([a-f0-9]{7,64})}.{ext:patch|diff}", repo.MustBeNotEmpty, reqRepoCodeReader, repo.RawDiff)
+		m.Get("/commit/{sha:([a-f0-9]{4,64})}.{ext:patch|diff}", repo.MustBeNotEmpty, reqRepoCodeReader, repo.RawDiff)
 	}, ignSignIn, context.RepoAssignment, context.UnitTypes())
 
 	m.Post("/{username}/{reponame}/lastcommit/*", ignSignInAndCsrf, context.RepoAssignment, context.UnitTypes(), context.RepoRefByType(context.RepoRefCommit), reqRepoCodeReader, repo.LastCommit)
diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go
index 8176fea5e8..ed5466bc63 100644
--- a/tests/integration/repo_test.go
+++ b/tests/integration/repo_test.go
@@ -592,3 +592,43 @@ func TestViewCommit(t *testing.T) {
 	resp := MakeRequest(t, req, http.StatusNotFound)
 	assert.True(t, test.IsNormalPageCompleted(resp.Body.String()), "non-existing commit should render 404 page")
 }
+
+func TestCommitView(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	t.Run("Non-existent commit", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		req := NewRequest(t, "GET", "/user2/repo1/commit/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
+		MakeRequest(t, req, http.StatusNotFound)
+	})
+
+	t.Run("Too short commit ID", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		req := NewRequest(t, "GET", "/user2/repo1/commit/65f")
+		MakeRequest(t, req, http.StatusNotFound)
+	})
+
+	t.Run("Short commit ID", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		req := NewRequest(t, "GET", "/user2/repo1/commit/65f1")
+		resp := MakeRequest(t, req, http.StatusOK)
+
+		doc := NewHTMLParser(t, resp.Body)
+		commitTitle := doc.Find(".commit-summary").Text()
+		assert.Contains(t, commitTitle, "Initial commit")
+	})
+
+	t.Run("Full commit ID", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		req := NewRequest(t, "GET", "/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d")
+		resp := MakeRequest(t, req, http.StatusOK)
+
+		doc := NewHTMLParser(t, resp.Body)
+		commitTitle := doc.Find(".commit-summary").Text()
+		assert.Contains(t, commitTitle, "Initial commit")
+	})
+}

From 8c0056500697937d27f64bdebd42ba8f05f83288 Mon Sep 17 00:00:00 2001
From: Antonin Delpeuch 
Date: Sun, 19 Nov 2023 11:50:05 +0000
Subject: [PATCH 24/86] [GITEA] oauth2: use link_account page when
 email/username is missing (#1757)

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/1757
Co-authored-by: Antonin Delpeuch 
Co-committed-by: Antonin Delpeuch 
(cherry picked from commit 0f6e0f90359b4b669d297a533de18b41e3293df2)
(cherry picked from commit 779168a572c521507a35ba624dbd032ec28f272e)
(cherry picked from commit 29a2457321e4587f55b333d0c5698925e403f026)
(cherry picked from commit a1edc2314d2687c9320d884f8a584d8b539eec96)
(cherry picked from commit cd015946109d39c6e30091de2fff47eba01eb937)
(cherry picked from commit 74db46b0f50a5b465269688ef83e170b7584e2be)
(cherry picked from commit fd98f55204f1cec66c3941d85b45dc84f8ab9ecd)
(cherry picked from commit 3099d0e2818d1de763a686b6a23dcf5d55ba75ef)
(cherry picked from commit 9fbbe613649331243b3777955cf2818862583f7e)
---
 routers/web/auth/oauth.go       | 20 ++++++++++++--------
 tests/integration/oauth_test.go | 27 +++++++++++++++++++++++++++
 2 files changed, 39 insertions(+), 8 deletions(-)

diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go
index 2dee93a11f..0a8b9f6a4b 100644
--- a/routers/web/auth/oauth.go
+++ b/routers/web/auth/oauth.go
@@ -951,10 +951,16 @@ func SignInOAuthCallback(ctx *context.Context) {
 			return
 		} else if !setting.Service.AllowOnlyInternalRegistration && setting.OAuth2Client.EnableAutoRegistration {
 			// create new user with details from oauth2 provider
-			var missingFields []string
 			if gothUser.UserID == "" {
-				missingFields = append(missingFields, "sub")
+				log.Error("OAuth2 Provider %s returned empty or missing field: UserID", authSource.Name)
+				if authSource.IsOAuth2() && authSource.Cfg.(*oauth2.Source).Provider == "openidConnect" {
+					log.Error("You may need to change the 'OPENID_CONNECT_SCOPES' setting to request all required fields")
+				}
+				err = fmt.Errorf("OAuth2 Provider %s returned empty or missing field: UserID", authSource.Name)
+				ctx.ServerError("CreateUser", err)
+				return
 			}
+			var missingFields []string
 			if gothUser.Email == "" {
 				missingFields = append(missingFields, "email")
 			}
@@ -962,12 +968,10 @@ func SignInOAuthCallback(ctx *context.Context) {
 				missingFields = append(missingFields, "nickname")
 			}
 			if len(missingFields) > 0 {
-				log.Error("OAuth2 Provider %s returned empty or missing fields: %s", authSource.Name, missingFields)
-				if authSource.IsOAuth2() && authSource.Cfg.(*oauth2.Source).Provider == "openidConnect" {
-					log.Error("You may need to change the 'OPENID_CONNECT_SCOPES' setting to request all required fields")
-				}
-				err = fmt.Errorf("OAuth2 Provider %s returned empty or missing fields: %s", authSource.Name, missingFields)
-				ctx.ServerError("CreateUser", err)
+				// we don't have enough information to create an account automatically,
+				// so we prompt the user for the remaining bits
+				log.Trace("OAuth2 Provider %s returned empty or missing fields: %s, prompting the user for them", authSource.Name, missingFields)
+				showLinkingLogin(ctx, gothUser)
 				return
 			}
 			uname, err := getUserName(&gothUser)
diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go
index 4a00d73a02..fed73696e3 100644
--- a/tests/integration/oauth_test.go
+++ b/tests/integration/oauth_test.go
@@ -469,3 +469,30 @@ func TestSignInOAuthCallbackSignIn(t *testing.T) {
 	userAfterLogin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userGitLab.ID})
 	assert.Greater(t, userAfterLogin.LastLoginUnix, userGitLab.LastLoginUnix)
 }
+
+func TestSignUpViaOAuthWithMissingFields(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+	// enable auto-creation of accounts via OAuth2
+	enableAutoRegistration := setting.OAuth2Client.EnableAutoRegistration
+	setting.OAuth2Client.EnableAutoRegistration = true
+	defer func() {
+		setting.OAuth2Client.EnableAutoRegistration = enableAutoRegistration
+	}()
+
+	// OAuth2 authentication source GitLab
+	gitlabName := "gitlab"
+	addAuthSource(t, authSourcePayloadGitLabCustom(gitlabName))
+	userGitLabUserID := "5678"
+
+	// The Goth User returned by the oauth2 integration is missing
+	// an email address, so we won't be able to automatically create a local account for it.
+	defer mockCompleteUserAuth(func(res http.ResponseWriter, req *http.Request) (goth.User, error) {
+		return goth.User{
+			Provider: gitlabName,
+			UserID:   userGitLabUserID,
+		}, nil
+	})()
+	req := NewRequest(t, "GET", fmt.Sprintf("/user/oauth2/%s/callback?code=XYZ&state=XYZ", gitlabName))
+	resp := MakeRequest(t, req, http.StatusSeeOther)
+	assert.Equal(t, test.RedirectURL(resp), "/user/link_account")
+}

From c7e595f903925d268b017bad86398e3733b45ad4 Mon Sep 17 00:00:00 2001
From: Gusted 
Date: Mon, 20 Nov 2023 09:43:52 +0100
Subject: [PATCH 25/86] [GITEA] Add cancel button to wiki

- Add a cancel button to the Edit and New wiki pages.
- Resolves https://codeberg.org/forgejo/forgejo/issues/705

(cherry picked from commit 3284f690ea6a9a41da05f8229aadfd55e222df1c)
(cherry picked from commit 9f8bf83b0e7845a44ca91bc8497356dbab059374)
(cherry picked from commit bfd03a9f309fbb9413601daa304cea8f16b627ad)
(cherry picked from commit 6b5d5e0cf7547acf38d28e5ec26be82e78357e49)
(cherry picked from commit 3ef3ec0d8205bf168b02e8a91d20a6df685e242a)
(cherry picked from commit 5ae55325ef8213242f3a946d2347499bd5b960bd)
(cherry picked from commit f0894ae00319e0dff37392af752447f849bbe783)
(cherry picked from commit 18564b26f667b8837c5bf4a0165dfa75633afe5f)
(cherry picked from commit 06c130fd1f267a296e43b706ec40bf3191ab0489)
---
 options/locale/locale_en-US.ini | 1 +
 templates/repo/wiki/new.tmpl    | 1 +
 2 files changed, 2 insertions(+)

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 1b4c9f5b72..79d0a5fe00 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1901,6 +1901,7 @@ wiki.page_title = Page title
 wiki.page_content = Page content
 wiki.default_commit_message = Write a note about this page update (optional).
 wiki.save_page = Save Page
+wiki.cancel = Cancel
 wiki.last_commit_info = %s edited this page %s
 wiki.edit_page_button = Edit
 wiki.new_page_button = New Page
diff --git a/templates/repo/wiki/new.tmpl b/templates/repo/wiki/new.tmpl
index ff31df0c32..0d0e78c7ad 100644
--- a/templates/repo/wiki/new.tmpl
+++ b/templates/repo/wiki/new.tmpl
@@ -39,6 +39,7 @@
 				
+				{{ctx.Locale.Tr "repo.wiki.cancel"}}
 			
From b21cf2567ae7395ba6815309dc192911b396d91c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Dachary?= Date: Sun, 12 Nov 2023 20:01:24 +0100 Subject: [PATCH 26/86] [GITEA] test POST /{username}/{reponame}/{tags,release}/delete Refs: https://forgejo.org/2023-11-release-v1-20-5-1/#api-and-web-endpoint-vulnerable-to-manually-crafted-identifiers (cherry picked from commit 78dcbb62fe87abe044034d880c9e8c22b44c2c98) (cherry picked from commit 6707c08c1791926060a7735529f1945650030257) (cherry picked from commit 68da5a9cd82415caedac15a07e38206f7bd6fbde) (cherry picked from commit c27fb08cb00f130870d6059a0ebb67b505a3c252) (cherry picked from commit f15a2c558a74aaf954550c71974593bf012004db) (cherry picked from commit 8eb3ae693922fdb65a185ee63703b866d10fa60a) (cherry picked from commit d54d5952f25828e84f7024aae6e62d1a18b788ae) (cherry picked from commit ce22d57485ed718612cd0e40979cc591a5e18126) (cherry picked from commit bfc110ba3303b4d2664638712435cc8d47906da0) (cherry picked from commit 1fb3d555d972ad72d257c7d00e71148c88926e5d) (cherry picked from commit 859c2275db185df4b38be6d50a8b4886622ecfde) --- tests/integration/release_test.go | 38 +++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/integration/release_test.go b/tests/integration/release_test.go index 439e315347..96fcff0963 100644 --- a/tests/integration/release_test.go +++ b/tests/integration/release_test.go @@ -93,6 +93,44 @@ func TestCreateRelease(t *testing.T) { checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").Tr("repo.release.stable"), 4) } +func TestDeleteRelease(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 57, OwnerName: "user2", LowerName: "repo-release"}) + release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{TagName: "v2.0"}) + assert.False(t, release.IsTag) + + // Using the ID of a comment that does not belong to the repository must fail + session5 := loginUser(t, "user5") + otherRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user5", LowerName: "repo4"}) + + req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/releases/delete?id=%d", otherRepo.Link(), release.ID), map[string]string{ + "_csrf": GetCSRF(t, session5, otherRepo.Link()), + }) + session5.MakeRequest(t, req, http.StatusNotFound) + + session := loginUser(t, "user2") + req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/releases/delete?id=%d", repo.Link(), release.ID), map[string]string{ + "_csrf": GetCSRF(t, session, repo.Link()), + }) + session.MakeRequest(t, req, http.StatusOK) + release = unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: release.ID}) + + if assert.True(t, release.IsTag) { + req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/tags/delete?id=%d", otherRepo.Link(), release.ID), map[string]string{ + "_csrf": GetCSRF(t, session5, otherRepo.Link()), + }) + session5.MakeRequest(t, req, http.StatusNotFound) + + req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/tags/delete?id=%d", repo.Link(), release.ID), map[string]string{ + "_csrf": GetCSRF(t, session, repo.Link()), + }) + session.MakeRequest(t, req, http.StatusOK) + + unittest.AssertNotExistsBean(t, &repo_model.Release{ID: release.ID}) + } +} + func TestCreateReleasePreRelease(t *testing.T) { defer tests.PrepareTestEnv(t)() From be3f9a28a12c22c35fd6f95260902704a2e5b7bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Dachary?= Date: Mon, 20 Nov 2023 16:34:04 +0100 Subject: [PATCH 27/86] [GITEA] test POST /{username}/{reponame}/{type:issues|pulls}/move_pin Refs: https://forgejo.org/2023-11-release-v1-20-5-1/#api-and-web-endpoint-vulnerable-to-manually-crafted-identifiers (cherry picked from commit 52f50792606a22cbf1e144e1bd480984abf6f53f) (cherry picked from commit 65b942fa1ee50f9098bebc8948d7924a5a4668fa) (cherry picked from commit e140c5c983e3413dcfab45f5e522dfdc3feda26e) (cherry picked from commit 4d108fa1cf07d2ecc7a482010e75f36140657dd4) (cherry picked from commit 9430badc5c8245b287c6ec2ba9432324c2e95417) (cherry picked from commit 1e67f4665d6be336c09446c8698830459aad3975) (cherry picked from commit 992e0d3218bca7f4b0f1471e3d7d64b69c33bad8) (cherry picked from commit 0e25ca17f39aac88fc20147e54d40ee76bc70cdd) (cherry picked from commit 3c7d9769faf4287bfe9d7366fea3bfbce48d911c) Conflicts: tests/integration/issue_test.go https://codeberg.org/forgejo/forgejo/pulls/2119 (cherry picked from commit f6bdf76a1d45f1f100323d8e1a3749b24cf6d2d4) (cherry picked from commit a5e527f87262722542097b69de72d96ca68cd2e6) --- tests/integration/issue_test.go | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go index 4b3f581c2b..c9f8288410 100644 --- a/tests/integration/issue_test.go +++ b/tests/integration/issue_test.go @@ -579,6 +579,48 @@ func TestGetIssueInfo(t *testing.T) { assert.EqualValues(t, issue.ID, apiIssue.ID) } +func TestIssuePinMove(t *testing.T) { + defer tests.PrepareTestEnv(t)() + session := loginUser(t, "user2") + issueURL, issue := testIssueWithBean(t, "user2", 1, "Title", "Content") + assert.EqualValues(t, 0, issue.PinOrder) + + req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/pin", issueURL), map[string]string{ + "_csrf": GetCSRF(t, session, issueURL), + }) + session.MakeRequest(t, req, http.StatusOK) + issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID}) + + position := 1 + assert.EqualValues(t, position, issue.PinOrder) + + newPosition := 2 + + // Using the ID of an issue that does not belong to the repository must fail + { + session5 := loginUser(t, "user5") + movePinURL := "/user5/repo4/issues/move_pin?_csrf=" + GetCSRF(t, session5, issueURL) + req = NewRequestWithJSON(t, "POST", movePinURL, map[string]any{ + "id": issue.ID, + "position": newPosition, + }) + session5.MakeRequest(t, req, http.StatusNotFound) + + issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID}) + assert.EqualValues(t, position, issue.PinOrder) + } + + movePinURL := issueURL[:strings.LastIndexByte(issueURL, '/')] + "/move_pin?_csrf=" + GetCSRF(t, session, issueURL) + req = NewRequestWithJSON(t, "POST", movePinURL, map[string]any{ + "id": issue.ID, + "position": newPosition, + }) + session.MakeRequest(t, req, http.StatusNoContent) + + issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID}) + assert.EqualValues(t, newPosition, issue.PinOrder) +} + func TestUpdateIssueDeadline(t *testing.T) { defer tests.PrepareTestEnv(t)() From 0f436a0d229164ce59cb563cf0d5a0607eaae3ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Dachary?= Date: Sun, 12 Nov 2023 13:52:48 +0100 Subject: [PATCH 28/86] [GITEA] test GET /{owner}/{repo}/comments/{id}/attachments Refs: https://forgejo.org/2023-11-release-v1-20-5-1/#api-and-web-endpoint-vulnerable-to-manually-crafted-identifiers (cherry picked from commit 888dda12cf9bc95f9ef85ba5a518cf40152e07ea) (cherry picked from commit aceeca55da0c2e94f3e495c4a60148411a27c4ac) (cherry picked from commit ab7e649668dfcabfb03e2a87c3c4641f8d2fa6ff) (cherry picked from commit 7fb8598c7df683d701ca04d01d9ff52db1e39298) (cherry picked from commit fb4961e2a5d6b249b0761cbabb80d68106764835) (cherry picked from commit 9fe856a29a1e233d8cd15edcfd03847ca9b4c7d8) (cherry picked from commit 6db21c013dd4003937532587471c2411622dd384) (cherry picked from commit 72c84eb19c0ee6f7eaf13162991b79249e0d6ed7) (cherry picked from commit 07ebc9761dc2633dbb3fea0b85abd047dcbec9e8) (cherry picked from commit 0c8f4840022fcf521dc2264b8a2e8f9a9833b212) (cherry picked from commit 25df7d89bc7d726b6d18d135f8cef281702b6267) --- tests/integration/issue_test.go | 50 +++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go index c9f8288410..de4113a37b 100644 --- a/tests/integration/issue_test.go +++ b/tests/integration/issue_test.go @@ -227,6 +227,56 @@ func TestIssueCommentDelete(t *testing.T) { unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: commentID}) } +func TestIssueCommentAttachment(t *testing.T) { + defer tests.PrepareTestEnv(t)() + const repoURL = "user2/repo1" + const content = "Test comment 4" + const status = "" + session := loginUser(t, "user2") + issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description") + + req := NewRequest(t, "GET", issueURL) + resp := session.MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + link, exists := htmlDoc.doc.Find("#comment-form").Attr("action") + assert.True(t, exists, "The template has changed") + + uuid := createAttachment(t, session, repoURL, "image.png", generateImg(), http.StatusOK) + + commentCount := htmlDoc.doc.Find(".comment-list .comment .render-content").Length() + + req = NewRequestWithValues(t, "POST", link, map[string]string{ + "_csrf": htmlDoc.GetCSRF(), + "content": content, + "status": status, + "files": uuid, + }) + resp = session.MakeRequest(t, req, http.StatusOK) + + req = NewRequest(t, "GET", test.RedirectURL(resp)) + resp = session.MakeRequest(t, req, http.StatusOK) + + htmlDoc = NewHTMLParser(t, resp.Body) + + val := htmlDoc.doc.Find(".comment-list .comment .render-content p").Eq(commentCount).Text() + assert.Equal(t, content, val) + + idAttr, has := htmlDoc.doc.Find(".comment-list .comment").Eq(commentCount).Attr("id") + idStr := idAttr[strings.LastIndexByte(idAttr, '-')+1:] + assert.True(t, has) + id, err := strconv.Atoi(idStr) + assert.NoError(t, err) + assert.NotEqual(t, 0, id) + + req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/comments/%d/attachments", "user2", "repo1", id)) + session.MakeRequest(t, req, http.StatusOK) + + // Using the ID of a comment that does not belong to the repository must fail + req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/comments/%d/attachments", "user5", "repo4", id)) + session.MakeRequest(t, req, http.StatusNotFound) +} + func TestIssueCommentUpdate(t *testing.T) { defer tests.PrepareTestEnv(t)() session := loginUser(t, "user2") From 2e0933edcfa102b578fb3c2500f9e6af9e5ba1c7 Mon Sep 17 00:00:00 2001 From: Antonin Delpeuch Date: Tue, 28 Nov 2023 09:32:26 +0100 Subject: [PATCH 29/86] [GITEA] Enable mocked HTTP responses for GitLab migration test Fix gitlab migration unit test Closes #1837. The differences in dates can be explained by commit e19b9653ea, which changed the order in which "created_date" and "updated_date" are considered. (cherry picked from commit b0bba20aa44e30ef0296b89f336d426224d73a16) Mock HTTP requests in GitLab migration test This introduces a new utility which can be added to other tests making HTTP calls to a live service, to cache the responses of this service in the repository. (cherry picked from commit 52053b138948bd74c7eb88c0796c2e18f4247f3c) Enable mocked HTTP responses for GitLab migration test (cherry picked from commit 19cefc4de24b935a6a5c92be8360301f196f3aa5) Simplify HTTP mocking utility in unit tests Follow-up to https://codeberg.org/forgejo/forgejo/pulls/1841 (cherry picked from commit ca517c8bb4bf97f061b8b19fd3303d734f46660c) (cherry picked from commit b227e0dd6bdf2dc3e8679443fc538fbce4b3bcf5) (cherry picked from commit 6cc9d06556cda6c952a0542284fbe504114971ce) (cherry picked from commit f0746e648dc30510d655b8a3b821199b2638800f) (cherry picked from commit 414193341b8493723c16694789cbc08dc80b9ce5) (cherry picked from commit 6e93df3bbb6c589502afc9dc74a7ae1a7c0f7da8) (cherry picked from commit db0dbab5527c9f1783fd0eddb057c2d91cbb67e4) (cherry picked from commit 8f9c9c63fbd3f266bb29d38791e83dc369cc1350) (cherry picked from commit e74e26203095b675ccedbc2e166faed59369d467) --- .deadcode-out | 2 + models/unittest/mock_http.go | 113 ++++++++++++++++++ services/migrations/gitlab_test.go | 42 ++++--- .../full_download/_api_v4_projects_15578026 | 22 ++++ ...ssues!page=1&per_page=2&sort=asc&state=all | 29 +++++ ...026_issues_1_award_emoji!page=1&per_page=2 | 29 +++++ ...026_issues_1_award_emoji!page=2&per_page=2 | 31 +++++ ...026_issues_2_award_emoji!page=1&per_page=2 | 29 +++++ ...026_issues_2_award_emoji!page=2&per_page=2 | 29 +++++ ...026_issues_2_award_emoji!page=3&per_page=2 | 29 +++++ ...026_issues_2_award_emoji!page=4&per_page=2 | 31 +++++ ...6_issues_2_discussions!page=1&per_page=100 | 29 +++++ ...ojects_15578026_labels!page=1&per_page=100 | 29 +++++ ...rge_requests!page=1&per_page=1&view=simple | 29 +++++ ...ojects_15578026_merge_requests_1_approvals | 22 ++++ ..._api_v4_projects_15578026_merge_requests_2 | 22 ++++ ...ojects_15578026_merge_requests_2_approvals | 22 ++++ ...e_requests_2_award_emoji!page=1&per_page=1 | 29 +++++ ...e_requests_2_award_emoji!page=2&per_page=1 | 29 +++++ ...e_requests_2_award_emoji!page=3&per_page=1 | 31 +++++ ...6_milestones!page=1&per_page=100&state=all | 29 +++++ ...ects_15578026_releases!page=1&per_page=100 | 29 +++++ .../_api_v4_projects_gitea%2Ftest_repo | 22 ++++ .../gitlab/full_download/_api_v4_version | 22 ++++ 24 files changed, 708 insertions(+), 22 deletions(-) create mode 100644 models/unittest/mock_http.go create mode 100644 services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026 create mode 100644 services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues!page=1&per_page=2&sort=asc&state=all create mode 100644 services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_1_award_emoji!page=1&per_page=2 create mode 100644 services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_1_award_emoji!page=2&per_page=2 create mode 100644 services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=1&per_page=2 create mode 100644 services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=2&per_page=2 create mode 100644 services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=3&per_page=2 create mode 100644 services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=4&per_page=2 create mode 100644 services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_discussions!page=1&per_page=100 create mode 100644 services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_labels!page=1&per_page=100 create mode 100644 services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests!page=1&per_page=1&view=simple create mode 100644 services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_1_approvals create mode 100644 services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2 create mode 100644 services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_approvals create mode 100644 services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_award_emoji!page=1&per_page=1 create mode 100644 services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_award_emoji!page=2&per_page=1 create mode 100644 services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_award_emoji!page=3&per_page=1 create mode 100644 services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_milestones!page=1&per_page=100&state=all create mode 100644 services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_releases!page=1&per_page=100 create mode 100644 services/migrations/testdata/gitlab/full_download/_api_v4_projects_gitea%2Ftest_repo create mode 100644 services/migrations/testdata/gitlab/full_download/_api_v4_version diff --git a/.deadcode-out b/.deadcode-out index 3f21d8ef36..a64c5b936f 100644 --- a/.deadcode-out +++ b/.deadcode-out @@ -100,6 +100,8 @@ package "code.gitea.io/gitea/models/unittest" func LoadFixtures func Copy func CopyDir + func NewMockWebServer + func NormalizedFullPath func FixturesDir func fatalTestError func InitSettings diff --git a/models/unittest/mock_http.go b/models/unittest/mock_http.go new file mode 100644 index 0000000000..afdc5bed21 --- /dev/null +++ b/models/unittest/mock_http.go @@ -0,0 +1,113 @@ +// Copyright 2017 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package unittest + +import ( + "bufio" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "slices" + "strings" + "testing" + + "code.gitea.io/gitea/modules/log" + + "github.com/stretchr/testify/assert" +) + +// Mocks HTTP responses of a third-party service (such as GitHub, GitLab…) +// This has two modes: +// - live mode: the requests made to the mock HTTP server are transmitted to the live +// service, and responses are saved as test data files +// - test mode: the responses to requests to the mock HTTP server are read from the +// test data files +func NewMockWebServer(t *testing.T, liveServerBaseURL, testDataDir string, liveMode bool) *httptest.Server { + mockServerBaseURL := "" + ignoredHeaders := []string{"cf-ray", "server", "date", "report-to", "nel", "x-request-id"} + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := NormalizedFullPath(r.URL) + log.Info("Mock HTTP Server: got request for path %s", r.URL.Path) + // TODO check request method (support POST?) + fixturePath := fmt.Sprintf("%s/%s", testDataDir, strings.NewReplacer("/", "_", "?", "!").Replace(path)) + if liveMode { + liveURL := fmt.Sprintf("%s%s", liveServerBaseURL, path) + + request, err := http.NewRequest(r.Method, liveURL, nil) + assert.NoError(t, err, "constructing an HTTP request to %s failed", liveURL) + for headerName, headerValues := range r.Header { + // do not pass on the encoding: let the Transport of the HTTP client handle that for us + if strings.ToLower(headerName) != "accept-encoding" { + for _, headerValue := range headerValues { + request.Header.Add(headerName, headerValue) + } + } + } + + response, err := http.DefaultClient.Do(request) + assert.NoError(t, err, "HTTP request to %s failed: %s", liveURL) + + fixture, err := os.Create(fixturePath) + assert.NoError(t, err, "failed to open the fixture file %s for writing", fixturePath) + defer fixture.Close() + fixtureWriter := bufio.NewWriter(fixture) + + for headerName, headerValues := range response.Header { + for _, headerValue := range headerValues { + if !slices.Contains(ignoredHeaders, strings.ToLower(headerName)) { + _, err := fixtureWriter.WriteString(fmt.Sprintf("%s: %s\n", headerName, headerValue)) + assert.NoError(t, err, "writing the header of the HTTP response to the fixture file failed") + } + } + } + _, err = fixtureWriter.WriteString("\n") + assert.NoError(t, err, "writing the header of the HTTP response to the fixture file failed") + fixtureWriter.Flush() + + log.Info("Mock HTTP Server: writing response to %s", fixturePath) + _, err = io.Copy(fixture, response.Body) + assert.NoError(t, err, "writing the body of the HTTP response to %s failed", liveURL) + + err = fixture.Sync() + assert.NoError(t, err, "writing the body of the HTTP response to the fixture file failed") + } + + fixture, err := os.ReadFile(fixturePath) + assert.NoError(t, err, "missing mock HTTP response: "+fixturePath) + + w.WriteHeader(http.StatusOK) + + // replace any mention of the live HTTP service by the mocked host + stringFixture := strings.ReplaceAll(string(fixture), liveServerBaseURL, mockServerBaseURL) + // parse back the fixture file into a series of HTTP headers followed by response body + lines := strings.Split(stringFixture, "\n") + for idx, line := range lines { + colonIndex := strings.Index(line, ": ") + if colonIndex != -1 { + w.Header().Set(line[0:colonIndex], line[colonIndex+2:]) + } else { + // we reached the end of the headers (empty line), so what follows is the body + responseBody := strings.Join(lines[idx+1:], "\n") + _, err := w.Write([]byte(responseBody)) + assert.NoError(t, err, "writing the body of the HTTP response failed") + break + } + } + })) + mockServerBaseURL = server.URL + return server +} + +func NormalizedFullPath(url *url.URL) string { + // TODO normalize path (remove trailing slash?) + // TODO normalize RawQuery (order query parameters?) + if len(url.Query()) == 0 { + return url.EscapedPath() + } + return fmt.Sprintf("%s?%s", url.EscapedPath(), url.RawQuery) +} diff --git a/services/migrations/gitlab_test.go b/services/migrations/gitlab_test.go index 1e0aa2b025..fe56c2f4e6 100644 --- a/services/migrations/gitlab_test.go +++ b/services/migrations/gitlab_test.go @@ -13,6 +13,7 @@ import ( "testing" "time" + "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/json" base "code.gitea.io/gitea/modules/migration" @@ -21,18 +22,15 @@ import ( ) func TestGitlabDownloadRepo(t *testing.T) { - // Skip tests if Gitlab token is not found + // If a GitLab access token is provided, this test will make HTTP requests to the live gitlab.com instance. + // When doing so, the responses from gitlab.com will be saved as test data files. + // If no access token is available, those cached responses will be used instead. gitlabPersonalAccessToken := os.Getenv("GITLAB_READ_TOKEN") - if gitlabPersonalAccessToken == "" { - t.Skip("skipped test because GITLAB_READ_TOKEN was not in the environment") - } + fixturePath := "./testdata/gitlab/full_download" + server := unittest.NewMockWebServer(t, "https://gitlab.com", fixturePath, gitlabPersonalAccessToken != "") + defer server.Close() - resp, err := http.Get("https://gitlab.com/gitea/test_repo") - if err != nil || resp.StatusCode != http.StatusOK { - t.Skipf("Can't access test repo, skipping %s", t.Name()) - } - - downloader, err := NewGitlabDownloader(context.Background(), "https://gitlab.com", "gitea/test_repo", "", "", gitlabPersonalAccessToken) + downloader, err := NewGitlabDownloader(context.Background(), server.URL, "gitea/test_repo", "", "", gitlabPersonalAccessToken) if err != nil { t.Fatalf("NewGitlabDownloader is nil: %v", err) } @@ -43,8 +41,8 @@ func TestGitlabDownloadRepo(t *testing.T) { Name: "test_repo", Owner: "", Description: "Test repository for testing migration from gitlab to gitea", - CloneURL: "https://gitlab.com/gitea/test_repo.git", - OriginalURL: "https://gitlab.com/gitea/test_repo", + CloneURL: server.URL + "/gitea/test_repo.git", + OriginalURL: server.URL + "/gitea/test_repo", DefaultBranch: "master", }, repo) @@ -281,17 +279,17 @@ func TestGitlabDownloadRepo(t *testing.T) { UserName: "real6543", Content: "tada", }}, - PatchURL: "https://gitlab.com/gitea/test_repo/-/merge_requests/2.patch", + PatchURL: server.URL + "/gitea/test_repo/-/merge_requests/2.patch", Head: base.PullRequestBranch{ Ref: "feat/test", - CloneURL: "https://gitlab.com/gitea/test_repo/-/merge_requests/2", + CloneURL: server.URL + "/gitea/test_repo/-/merge_requests/2", SHA: "9f733b96b98a4175276edf6a2e1231489c3bdd23", RepoName: "test_repo", OwnerName: "lafriks", }, Base: base.PullRequestBranch{ Ref: "master", - SHA: "", + SHA: "c59c9b451acca9d106cc19d61d87afe3fbbb8b83", OwnerName: "lafriks", RepoName: "test_repo", }, @@ -309,16 +307,16 @@ func TestGitlabDownloadRepo(t *testing.T) { assertReviewsEqual(t, []*base.Review{ { IssueIndex: 1, - ReviewerID: 4102996, - ReviewerName: "zeripath", - CreatedAt: time.Date(2019, 11, 28, 16, 2, 8, 377000000, time.UTC), + ReviewerID: 527793, + ReviewerName: "axifive", + CreatedAt: time.Date(2019, 11, 28, 8, 54, 41, 34000000, time.UTC), State: "APPROVED", }, { IssueIndex: 1, - ReviewerID: 527793, - ReviewerName: "axifive", - CreatedAt: time.Date(2019, 11, 28, 16, 2, 8, 377000000, time.UTC), + ReviewerID: 4102996, + ReviewerName: "zeripath", + CreatedAt: time.Date(2019, 11, 28, 8, 54, 41, 34000000, time.UTC), State: "APPROVED", }, }, rvs) @@ -330,7 +328,7 @@ func TestGitlabDownloadRepo(t *testing.T) { IssueIndex: 2, ReviewerID: 4575606, ReviewerName: "real6543", - CreatedAt: time.Date(2020, 4, 19, 19, 24, 21, 108000000, time.UTC), + CreatedAt: time.Date(2019, 11, 28, 15, 56, 54, 108000000, time.UTC), State: "APPROVED", }, }, rvs) diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026 new file mode 100644 index 0000000000..4ecfe77e65 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026 @@ -0,0 +1,22 @@ +X-Frame-Options: SAMEORIGIN +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:46 GMT +Cf-Cache-Status: MISS +Set-Cookie: _cfuvid=TkY5Br2q4C67LJ2jZWlgdQaosj3Z4aI81Qb27PNKXfo-1701333886606-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Etag: W/"3cacfe29f44a69e84a577337eac55d89" +X-Content-Type-Options: nosniff +Ratelimit-Remaining: 1996 +Gitlab-Lb: haproxy-main-29-lb-gprd +Cache-Control: max-age=0, private, must-revalidate +Referrer-Policy: strict-origin-when-cross-origin +X-Gitlab-Meta: {"correlation_id":"291f87cd975a51a7b756806ce7b53e2e","version":"1"} +Ratelimit-Observed: 4 +Vary: Origin, Accept-Encoding +Gitlab-Sv: localhost +Content-Security-Policy: default-src 'none' +Ratelimit-Reset: 1701333946 +Content-Type: application/json +X-Runtime: 0.101081 +Strict-Transport-Security: max-age=31536000 +Ratelimit-Limit: 2000 + +{"id":15578026,"description":"Test repository for testing migration from gitlab to gitea","name":"test_repo","name_with_namespace":"gitea / test_repo","path":"test_repo","path_with_namespace":"gitea/test_repo","created_at":"2019-11-28T08:20:33.019Z","default_branch":"master","tag_list":["migration","test"],"topics":["migration","test"],"ssh_url_to_repo":"git@gitlab.com:gitea/test_repo.git","http_url_to_repo":"https://gitlab.com/gitea/test_repo.git","web_url":"https://gitlab.com/gitea/test_repo","readme_url":"https://gitlab.com/gitea/test_repo/-/blob/master/README.md","forks_count":1,"avatar_url":null,"star_count":0,"last_activity_at":"2020-04-19T19:46:04.527Z","namespace":{"id":3181312,"name":"gitea","path":"gitea","kind":"group","full_path":"gitea","parent_id":null,"avatar_url":"/uploads/-/system/group/avatar/3181312/gitea.png","web_url":"https://gitlab.com/groups/gitea"},"container_registry_image_prefix":"registry.gitlab.com/gitea/test_repo","_links":{"self":"https://gitlab.com/api/v4/projects/15578026","issues":"https://gitlab.com/api/v4/projects/15578026/issues","merge_requests":"https://gitlab.com/api/v4/projects/15578026/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/15578026/repository/branches","labels":"https://gitlab.com/api/v4/projects/15578026/labels","events":"https://gitlab.com/api/v4/projects/15578026/events","members":"https://gitlab.com/api/v4/projects/15578026/members","cluster_agents":"https://gitlab.com/api/v4/projects/15578026/cluster_agents"},"packages_enabled":true,"empty_repo":false,"archived":false,"visibility":"public","resolve_outdated_diff_discussions":false,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"container_registry_enabled":true,"service_desk_enabled":true,"can_create_merge_request_in":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","forking_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","pages_access_level":"enabled","analytics_access_level":"enabled","container_registry_access_level":"enabled","security_and_compliance_access_level":"private","releases_access_level":"enabled","environments_access_level":"enabled","feature_flags_access_level":"enabled","infrastructure_access_level":"enabled","monitor_access_level":"enabled","model_experiments_access_level":"enabled","emails_disabled":false,"emails_enabled":true,"shared_runners_enabled":true,"lfs_enabled":true,"creator_id":1241334,"import_status":"none","open_issues_count":0,"description_html":"\u003cp data-sourcepos=\"1:1-1:58\" dir=\"auto\"\u003eTest repository for testing migration from gitlab to gitea\u003c/p\u003e","updated_at":"2022-08-26T19:41:46.691Z","ci_config_path":null,"public_jobs":true,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"allow_merge_on_skipped_pipeline":null,"request_access_enabled":true,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":true,"printing_merge_request_link_enabled":true,"merge_method":"ff","squash_option":"default_off","enforce_auth_checks_on_uploads":true,"suggestion_commit_message":null,"merge_commit_template":null,"squash_commit_template":null,"issue_branch_template":null,"autoclose_referenced_issues":true,"external_authorization_classification_label":"","requirements_enabled":false,"requirements_access_level":"enabled","security_and_compliance_enabled":false,"compliance_frameworks":[],"permissions":{"project_access":null,"group_access":null}} \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues!page=1&per_page=2&sort=asc&state=all b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues!page=1&per_page=2&sort=asc&state=all new file mode 100644 index 0000000000..331a82720c --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues!page=1&per_page=2&sort=asc&state=all @@ -0,0 +1,29 @@ +Cache-Control: max-age=0, private, must-revalidate +Vary: Origin, Accept-Encoding +Gitlab-Lb: haproxy-main-24-lb-gprd +Content-Type: application/json +X-Per-Page: 2 +X-Prev-Page: +Set-Cookie: _cfuvid=N.nfy5eIdFH5lXhsnMyEbeBkoxabcl1SVeyyP0_NrdE-1701333887790-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Content-Security-Policy: default-src 'none' +X-Content-Type-Options: nosniff +X-Next-Page: +X-Total-Pages: 1 +Etag: W/"4c0531a3595f741f229f5a105e013b95" +X-Gitlab-Meta: {"correlation_id":"b2eca136986f016d946685fb99287f1c","version":"1"} +Ratelimit-Observed: 8 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:47 GMT +X-Frame-Options: SAMEORIGIN +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Remaining: 1992 +Gitlab-Sv: localhost +Cf-Cache-Status: MISS +Strict-Transport-Security: max-age=31536000 +Ratelimit-Limit: 2000 +X-Total: 2 +X-Page: 1 +X-Runtime: 0.178587 +Ratelimit-Reset: 1701333947 +Link: ; rel="first", ; rel="last" + +[{"id":27687675,"iid":1,"project_id":15578026,"title":"Please add an animated gif icon to the merge button","description":"I just want the merge button to hurt my eyes a little. :stuck_out_tongue_closed_eyes:","state":"closed","created_at":"2019-11-28T08:43:35.459Z","updated_at":"2019-11-28T08:46:23.304Z","closed_at":"2019-11-28T08:46:23.275Z","closed_by":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"labels":["bug","discussion"],"milestone":{"id":1082926,"iid":1,"project_id":15578026,"title":"1.0.0","description":"","state":"closed","created_at":"2019-11-28T08:42:30.301Z","updated_at":"2019-11-28T15:57:52.401Z","due_date":null,"start_date":null,"expired":false,"web_url":"https://gitlab.com/gitea/test_repo/-/milestones/1"},"assignees":[],"author":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"type":"ISSUE","assignee":null,"user_notes_count":0,"merge_requests_count":0,"upvotes":1,"downvotes":0,"due_date":null,"confidential":false,"discussion_locked":null,"issue_type":"issue","web_url":"https://gitlab.com/gitea/test_repo/-/issues/1","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"task_completion_status":{"count":0,"completed_count":0},"blocking_issues_count":0,"has_tasks":true,"task_status":"0 of 0 checklist items completed","_links":{"self":"https://gitlab.com/api/v4/projects/15578026/issues/1","notes":"https://gitlab.com/api/v4/projects/15578026/issues/1/notes","award_emoji":"https://gitlab.com/api/v4/projects/15578026/issues/1/award_emoji","project":"https://gitlab.com/api/v4/projects/15578026","closed_as_duplicate_of":null},"references":{"short":"#1","relative":"#1","full":"gitea/test_repo#1"},"severity":"UNKNOWN","moved_to_id":null,"service_desk_reply_to":null},{"id":27687706,"iid":2,"project_id":15578026,"title":"Test issue","description":"This is test issue 2, do not touch!","state":"closed","created_at":"2019-11-28T08:44:46.277Z","updated_at":"2019-11-28T08:45:44.987Z","closed_at":"2019-11-28T08:45:44.959Z","closed_by":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"labels":["duplicate"],"milestone":{"id":1082927,"iid":2,"project_id":15578026,"title":"1.1.0","description":"","state":"active","created_at":"2019-11-28T08:42:44.575Z","updated_at":"2019-11-28T08:42:44.575Z","due_date":null,"start_date":null,"expired":false,"web_url":"https://gitlab.com/gitea/test_repo/-/milestones/2"},"assignees":[],"author":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"type":"ISSUE","assignee":null,"user_notes_count":2,"merge_requests_count":0,"upvotes":1,"downvotes":1,"due_date":null,"confidential":false,"discussion_locked":null,"issue_type":"issue","web_url":"https://gitlab.com/gitea/test_repo/-/issues/2","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"task_completion_status":{"count":0,"completed_count":0},"blocking_issues_count":0,"has_tasks":true,"task_status":"0 of 0 checklist items completed","_links":{"self":"https://gitlab.com/api/v4/projects/15578026/issues/2","notes":"https://gitlab.com/api/v4/projects/15578026/issues/2/notes","award_emoji":"https://gitlab.com/api/v4/projects/15578026/issues/2/award_emoji","project":"https://gitlab.com/api/v4/projects/15578026","closed_as_duplicate_of":null},"references":{"short":"#2","relative":"#2","full":"gitea/test_repo#2"},"severity":"UNKNOWN","moved_to_id":null,"service_desk_reply_to":null}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_1_award_emoji!page=1&per_page=2 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_1_award_emoji!page=1&per_page=2 new file mode 100644 index 0000000000..d8ab294ee1 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_1_award_emoji!page=1&per_page=2 @@ -0,0 +1,29 @@ +X-Total-Pages: 1 +Referrer-Policy: strict-origin-when-cross-origin +Cache-Control: max-age=0, private, must-revalidate +X-Frame-Options: SAMEORIGIN +X-Runtime: 0.052748 +X-Total: 2 +Gitlab-Sv: localhost +Link: ; rel="first", ; rel="last" +X-Content-Type-Options: nosniff +X-Gitlab-Meta: {"correlation_id":"22fc215ac386644c9cb8736b652cf702","version":"1"} +X-Per-Page: 2 +Strict-Transport-Security: max-age=31536000 +Ratelimit-Reset: 1701333947 +Content-Security-Policy: default-src 'none' +Ratelimit-Remaining: 1991 +Ratelimit-Observed: 9 +X-Next-Page: +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:47 GMT +Ratelimit-Limit: 2000 +Gitlab-Lb: haproxy-main-43-lb-gprd +Cf-Cache-Status: MISS +Etag: W/"69c922434ed11248c864d157eb8eabfc" +Content-Type: application/json +Vary: Origin, Accept-Encoding +X-Page: 1 +X-Prev-Page: +Set-Cookie: _cfuvid=POpffkskz4lvLcv2Fhjp7lF3MsmIOugWDzFGtb3ZUig-1701333887995-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None + +[{"id":3009580,"name":"thumbsup","user":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:43:40.322Z","updated_at":"2019-11-28T08:43:40.322Z","awardable_id":27687675,"awardable_type":"Issue","url":null},{"id":3009585,"name":"open_mouth","user":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:44:01.902Z","updated_at":"2019-11-28T08:44:01.902Z","awardable_id":27687675,"awardable_type":"Issue","url":null}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_1_award_emoji!page=2&per_page=2 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_1_award_emoji!page=2&per_page=2 new file mode 100644 index 0000000000..248afeff8f --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_1_award_emoji!page=2&per_page=2 @@ -0,0 +1,31 @@ +Content-Type: application/json +Content-Length: 2 +X-Next-Page: +X-Gitlab-Meta: {"correlation_id":"6b9bc368e2cdc69a1b8e7d2dff7546c5","version":"1"} +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Reset: 1701333948 +Strict-Transport-Security: max-age=31536000 +X-Per-Page: 2 +X-Total: 2 +Ratelimit-Limit: 2000 +Etag: W/"4f53cda18c2baa0c0354bb5f9a3ecbe5" +Link: ; rel="first", ; rel="last" +X-Runtime: 0.069944 +Ratelimit-Remaining: 1990 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:48 GMT +Gitlab-Sv: localhost +X-Prev-Page: +X-Total-Pages: 1 +Ratelimit-Observed: 10 +Gitlab-Lb: haproxy-main-17-lb-gprd +Content-Security-Policy: default-src 'none' +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +Accept-Ranges: bytes +Cache-Control: max-age=0, private, must-revalidate +Vary: Origin, Accept-Encoding +X-Page: 2 +Cf-Cache-Status: MISS +Set-Cookie: _cfuvid=vcfsxezcg_2Kdh8xD5coOU_uxQIH1in.6BsRttrSIYg-1701333888217-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None + +[] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=1&per_page=2 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=1&per_page=2 new file mode 100644 index 0000000000..70c50c8fed --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=1&per_page=2 @@ -0,0 +1,29 @@ +Cf-Cache-Status: MISS +Etag: W/"5fdbcbf64f34ba0e74ce9dd8d6e0efe3" +X-Content-Type-Options: nosniff +X-Per-Page: 2 +Ratelimit-Limit: 2000 +Set-Cookie: _cfuvid=NLtUZdQlWvWiXr4L8Zfc555FowZOCxJlA0pAOAEkNvg-1701333888445-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +X-Runtime: 0.075612 +Ratelimit-Reset: 1701333948 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:48 GMT +X-Page: 1 +X-Prev-Page: +X-Total-Pages: 3 +X-Frame-Options: SAMEORIGIN +Referrer-Policy: strict-origin-when-cross-origin +Content-Security-Policy: default-src 'none' +Ratelimit-Observed: 11 +Gitlab-Lb: haproxy-main-09-lb-gprd +Gitlab-Sv: localhost +Ratelimit-Remaining: 1989 +Cache-Control: max-age=0, private, must-revalidate +Link: ; rel="next", ; rel="first", ; rel="last" +X-Gitlab-Meta: {"correlation_id":"a69709cf552479bc5f5f3e4e1f808790","version":"1"} +X-Next-Page: 2 +Strict-Transport-Security: max-age=31536000 +Content-Type: application/json +Vary: Origin, Accept-Encoding +X-Total: 6 + +[{"id":3009627,"name":"thumbsup","user":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:46:42.657Z","updated_at":"2019-11-28T08:46:42.657Z","awardable_id":27687706,"awardable_type":"Issue","url":null},{"id":3009628,"name":"thumbsdown","user":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:46:43.471Z","updated_at":"2019-11-28T08:46:43.471Z","awardable_id":27687706,"awardable_type":"Issue","url":null}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=2&per_page=2 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=2&per_page=2 new file mode 100644 index 0000000000..1014d9dbc5 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=2&per_page=2 @@ -0,0 +1,29 @@ +Ratelimit-Limit: 2000 +X-Content-Type-Options: nosniff +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:48 GMT +X-Total: 6 +Set-Cookie: _cfuvid=E5GyZy0rB2zrRH0ewMyrJd1wBrt7A2sGNmOHTiWwbYk-1701333888703-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +X-Next-Page: 3 +X-Page: 2 +Link: ; rel="prev", ; rel="next", ; rel="first", ; rel="last" +X-Gitlab-Meta: {"correlation_id":"41e59fb2e78f5e68518b81bd4ff3bb1b","version":"1"} +X-Prev-Page: 1 +Cf-Cache-Status: MISS +Referrer-Policy: strict-origin-when-cross-origin +Gitlab-Lb: haproxy-main-26-lb-gprd +Cache-Control: max-age=0, private, must-revalidate +Etag: W/"d16c513b32212d9286fce6f53340c1cf" +Vary: Origin, Accept-Encoding +X-Per-Page: 2 +Strict-Transport-Security: max-age=31536000 +Ratelimit-Reset: 1701333948 +Content-Type: application/json +X-Runtime: 0.105758 +Ratelimit-Remaining: 1988 +Content-Security-Policy: default-src 'none' +X-Frame-Options: SAMEORIGIN +Gitlab-Sv: localhost +X-Total-Pages: 3 +Ratelimit-Observed: 12 + +[{"id":3009632,"name":"laughing","user":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:47:14.381Z","updated_at":"2019-11-28T08:47:14.381Z","awardable_id":27687706,"awardable_type":"Issue","url":null},{"id":3009634,"name":"tada","user":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:47:18.254Z","updated_at":"2019-11-28T08:47:18.254Z","awardable_id":27687706,"awardable_type":"Issue","url":null}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=3&per_page=2 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=3&per_page=2 new file mode 100644 index 0000000000..9c42bfb9bf --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=3&per_page=2 @@ -0,0 +1,29 @@ +Content-Type: application/json +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +Referrer-Policy: strict-origin-when-cross-origin +Link: ; rel="prev", ; rel="first", ; rel="last" +Cf-Cache-Status: MISS +Etag: W/"165d37bf09a54bb31f4619cca8722cb4" +Ratelimit-Observed: 13 +Ratelimit-Limit: 2000 +Set-Cookie: _cfuvid=W6F2uJSFkB3Iyl27_xtklVvP4Z_nSsjPqytClHPW9H8-1701333888913-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +X-Page: 3 +X-Total-Pages: 3 +Strict-Transport-Security: max-age=31536000 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:48 GMT +Cache-Control: max-age=0, private, must-revalidate +Vary: Origin, Accept-Encoding +X-Gitlab-Meta: {"correlation_id":"b805751aeb6f562dff684cec34dfdc0b","version":"1"} +X-Per-Page: 2 +X-Prev-Page: 2 +X-Runtime: 0.061943 +Gitlab-Lb: haproxy-main-11-lb-gprd +Gitlab-Sv: localhost +X-Total: 6 +Ratelimit-Remaining: 1987 +Content-Security-Policy: default-src 'none' +X-Next-Page: +Ratelimit-Reset: 1701333948 + +[{"id":3009636,"name":"confused","user":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:47:27.248Z","updated_at":"2019-11-28T08:47:27.248Z","awardable_id":27687706,"awardable_type":"Issue","url":null},{"id":3009640,"name":"hearts","user":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:47:33.059Z","updated_at":"2019-11-28T08:47:33.059Z","awardable_id":27687706,"awardable_type":"Issue","url":null}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=4&per_page=2 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=4&per_page=2 new file mode 100644 index 0000000000..e550e4ad19 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_award_emoji!page=4&per_page=2 @@ -0,0 +1,31 @@ +Ratelimit-Remaining: 1986 +Accept-Ranges: bytes +Set-Cookie: _cfuvid=DZQIMINjFohqKKjkWnojkq2xuUaqb42YEQg3BZXe68w-1701333889148-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Strict-Transport-Security: max-age=31536000 +X-Prev-Page: +Ratelimit-Reset: 1701333949 +Ratelimit-Limit: 2000 +Etag: W/"4f53cda18c2baa0c0354bb5f9a3ecbe5" +Cache-Control: max-age=0, private, must-revalidate +X-Next-Page: +X-Page: 4 +Referrer-Policy: strict-origin-when-cross-origin +Gitlab-Sv: localhost +Content-Type: application/json +X-Content-Type-Options: nosniff +Link: ; rel="first", ; rel="last" +Vary: Origin, Accept-Encoding +Content-Security-Policy: default-src 'none' +X-Runtime: 0.081728 +X-Total: 6 +Content-Length: 2 +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"588184d2d9ad2c4a7e7c00a51575091c","version":"1"} +Cf-Cache-Status: MISS +X-Total-Pages: 3 +Ratelimit-Observed: 14 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:49 GMT +Gitlab-Lb: haproxy-main-34-lb-gprd +X-Per-Page: 2 + +[] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_discussions!page=1&per_page=100 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_discussions!page=1&per_page=100 new file mode 100644 index 0000000000..d74fc325a3 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_issues_2_discussions!page=1&per_page=100 @@ -0,0 +1,29 @@ +X-Runtime: 0.173849 +Content-Security-Policy: default-src 'none' +X-Per-Page: 100 +X-Prev-Page: +X-Total: 4 +Ratelimit-Observed: 15 +X-Page: 1 +Ratelimit-Remaining: 1985 +Cache-Control: max-age=0, private, must-revalidate +X-Gitlab-Meta: {"correlation_id":"1d2b49b1b17e0c2d58c800a1b6c7eca3","version":"1"} +X-Next-Page: +Referrer-Policy: strict-origin-when-cross-origin +Cf-Cache-Status: MISS +Content-Type: application/json +Etag: W/"bcc91e8a7b2eac98b4d96ae791e0649d" +Vary: Origin, Accept-Encoding +X-Frame-Options: SAMEORIGIN +Strict-Transport-Security: max-age=31536000 +Ratelimit-Reset: 1701333949 +Gitlab-Lb: haproxy-main-08-lb-gprd +Gitlab-Sv: localhost +Link: ; rel="first", ; rel="last" +X-Content-Type-Options: nosniff +X-Total-Pages: 1 +Ratelimit-Limit: 2000 +Set-Cookie: _cfuvid=yj.PGr9ftsz1kNpgtsmQhAcGpdMnklLE.NQ9h71hm5Q-1701333889475-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:49 GMT + +[{"id":"617967369d98d8b73b6105a40318fe839f931a24","individual_note":true,"notes":[{"id":251637434,"type":null,"body":"This is a comment","attachment":null,"author":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:44:52.501Z","updated_at":"2019-11-28T08:44:52.501Z","system":false,"noteable_id":27687706,"noteable_type":"Issue","project_id":15578026,"resolvable":false,"confidential":false,"internal":false,"noteable_iid":2,"commands_changes":{}}]},{"id":"b92d74daee411a17d844041bcd3c267ade58f680","individual_note":true,"notes":[{"id":251637528,"type":null,"body":"changed milestone to %2","attachment":null,"author":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:45:02.329Z","updated_at":"2019-11-28T08:45:02.335Z","system":true,"noteable_id":27687706,"noteable_type":"Issue","project_id":15578026,"resolvable":false,"confidential":false,"internal":false,"noteable_iid":2,"commands_changes":{}}]},{"id":"6010f567d2b58758ef618070372c97891ac75349","individual_note":true,"notes":[{"id":251637892,"type":null,"body":"closed","attachment":null,"author":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:45:45.007Z","updated_at":"2019-11-28T08:45:45.010Z","system":true,"noteable_id":27687706,"noteable_type":"Issue","project_id":15578026,"resolvable":false,"confidential":false,"internal":false,"noteable_iid":2,"commands_changes":{}}]},{"id":"632d0cbfd6a1a08f38aaf9ef7715116f4b188ebb","individual_note":true,"notes":[{"id":251637999,"type":null,"body":"A second comment","attachment":null,"author":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:45:53.501Z","updated_at":"2019-11-28T08:45:53.501Z","system":false,"noteable_id":27687706,"noteable_type":"Issue","project_id":15578026,"resolvable":false,"confidential":false,"internal":false,"noteable_iid":2,"commands_changes":{}}]}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_labels!page=1&per_page=100 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_labels!page=1&per_page=100 new file mode 100644 index 0000000000..c79889fbf4 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_labels!page=1&per_page=100 @@ -0,0 +1,29 @@ +Cache-Control: max-age=0, private, must-revalidate +Strict-Transport-Security: max-age=31536000 +Gitlab-Lb: haproxy-main-28-lb-gprd +Link: ; rel="first", ; rel="last" +X-Per-Page: 100 +X-Runtime: 0.149464 +X-Total: 9 +Ratelimit-Observed: 6 +Gitlab-Sv: localhost +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"2ccddf20767704a98ed6b582db3b103e","version":"1"} +X-Next-Page: +X-Prev-Page: +X-Total-Pages: 1 +Ratelimit-Remaining: 1994 +Etag: W/"5a3fb9bc7b1018070943f4aa1353f8b6" +Ratelimit-Limit: 2000 +X-Page: 1 +Content-Type: application/json +Vary: Origin, Accept-Encoding +Set-Cookie: _cfuvid=geNpLvH8Cv5XeYfUVwtpaazw43v9lCcqHE.vyXGk3kU-1701333887126-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +X-Content-Type-Options: nosniff +Ratelimit-Reset: 1701333947 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:47 GMT +Content-Security-Policy: default-src 'none' +Referrer-Policy: strict-origin-when-cross-origin +Cf-Cache-Status: MISS + +[{"id":12959095,"name":"bug","description":null,"description_html":"","text_color":"#FFFFFF","color":"#d9534f","subscribed":false,"priority":null,"is_project_label":true},{"id":12959097,"name":"confirmed","description":null,"description_html":"","text_color":"#FFFFFF","color":"#d9534f","subscribed":false,"priority":null,"is_project_label":true},{"id":12959096,"name":"critical","description":null,"description_html":"","text_color":"#FFFFFF","color":"#d9534f","subscribed":false,"priority":null,"is_project_label":true},{"id":12959100,"name":"discussion","description":null,"description_html":"","text_color":"#FFFFFF","color":"#428bca","subscribed":false,"priority":null,"is_project_label":true},{"id":12959098,"name":"documentation","description":null,"description_html":"","text_color":"#1F1E24","color":"#f0ad4e","subscribed":false,"priority":null,"is_project_label":true},{"id":12959554,"name":"duplicate","description":null,"description_html":"","text_color":"#FFFFFF","color":"#7F8C8D","subscribed":false,"priority":null,"is_project_label":true},{"id":12959102,"name":"enhancement","description":null,"description_html":"","text_color":"#FFFFFF","color":"#5cb85c","subscribed":false,"priority":null,"is_project_label":true},{"id":12959101,"name":"suggestion","description":null,"description_html":"","text_color":"#FFFFFF","color":"#428bca","subscribed":false,"priority":null,"is_project_label":true},{"id":12959099,"name":"support","description":null,"description_html":"","text_color":"#1F1E24","color":"#f0ad4e","subscribed":false,"priority":null,"is_project_label":true}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests!page=1&per_page=1&view=simple b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests!page=1&per_page=1&view=simple new file mode 100644 index 0000000000..7eec1ca917 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests!page=1&per_page=1&view=simple @@ -0,0 +1,29 @@ +Content-Type: application/json +X-Page: 1 +Link: ; rel="next", ; rel="first", ; rel="last" +Vary: Origin, Accept-Encoding +X-Runtime: 0.139912 +X-Gitlab-Meta: {"correlation_id":"002c20b78ace441f5585931fed7093ed","version":"1"} +Gitlab-Sv: localhost +Cache-Control: max-age=0, private, must-revalidate +X-Total: 2 +X-Total-Pages: 2 +Ratelimit-Observed: 16 +Ratelimit-Limit: 2000 +X-Content-Type-Options: nosniff +Ratelimit-Remaining: 1984 +Set-Cookie: _cfuvid=6nsOEFMJm7NgrvYZAMGwiJBdm5A5CU71S33zOdN8Kyo-1701333889768-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +X-Per-Page: 1 +X-Prev-Page: +Strict-Transport-Security: max-age=31536000 +Ratelimit-Reset: 1701333949 +Gitlab-Lb: haproxy-main-09-lb-gprd +X-Next-Page: 2 +Cf-Cache-Status: MISS +Content-Security-Policy: default-src 'none' +Etag: W/"14f72c1f555b0e6348d338190e9e4839" +X-Frame-Options: SAMEORIGIN +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:49 GMT + +[{"id":43524600,"iid":2,"project_id":15578026,"title":"Test branch","description":"do not merge this PR","state":"opened","created_at":"2019-11-28T15:56:54.104Z","updated_at":"2020-04-19T19:24:21.108Z","web_url":"https://gitlab.com/gitea/test_repo/-/merge_requests/2"}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_1_approvals b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_1_approvals new file mode 100644 index 0000000000..80df508bc3 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_1_approvals @@ -0,0 +1,22 @@ +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:51 GMT +Ratelimit-Limit: 2000 +Content-Security-Policy: default-src 'none' +X-Gitlab-Meta: {"correlation_id":"0a1a7339f3527e175a537afe058a6ac7","version":"1"} +Ratelimit-Observed: 21 +Cache-Control: max-age=0, private, must-revalidate +Gitlab-Lb: haproxy-main-38-lb-gprd +X-Frame-Options: SAMEORIGIN +Strict-Transport-Security: max-age=31536000 +Cf-Cache-Status: MISS +Content-Type: application/json +Etag: W/"19aa54b7d4531bd5ab98282e0b772f20" +X-Content-Type-Options: nosniff +Set-Cookie: _cfuvid=J2edW64FLja6v0MZCh5tVbLYO42.VvIsTqQ.uj1Gr_k-1701333891171-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Ratelimit-Reset: 1701333951 +Gitlab-Sv: localhost +Vary: Origin, Accept-Encoding +Ratelimit-Remaining: 1979 +X-Runtime: 0.155712 +Referrer-Policy: strict-origin-when-cross-origin + +{"id":43486906,"iid":1,"project_id":15578026,"title":"Update README.md","description":"add warning to readme","state":"merged","created_at":"2019-11-28T08:54:41.034Z","updated_at":"2019-11-28T16:02:08.377Z","merge_status":"can_be_merged","approved":true,"approvals_required":0,"approvals_left":0,"require_password_to_approve":false,"approved_by":[{"user":{"id":527793,"username":"axifive","name":"Alexey Terentyev","state":"active","locked":false,"avatar_url":"https://secure.gravatar.com/avatar/06683cd6b2e2c2ce0ab00fb80cc0729f?s=80\u0026d=identicon","web_url":"https://gitlab.com/axifive"}},{"user":{"id":4102996,"username":"zeripath","name":"zeripath","state":"active","locked":false,"avatar_url":"https://secure.gravatar.com/avatar/1ae18535c2b1aed798da090448997248?s=80\u0026d=identicon","web_url":"https://gitlab.com/zeripath"}}],"suggested_approvers":[],"approvers":[],"approver_groups":[],"user_has_approved":false,"user_can_approve":false,"approval_rules_left":[],"has_approval_rules":true,"merge_request_approvers_available":false,"multiple_approval_rules_available":false,"invalid_approvers_rules":[]} \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2 new file mode 100644 index 0000000000..7119342f6c --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2 @@ -0,0 +1,22 @@ +Etag: W/"914149155d75f8d8f7ed2e5351f0fadb" +X-Runtime: 0.234286 +Ratelimit-Remaining: 1983 +Strict-Transport-Security: max-age=31536000 +Referrer-Policy: strict-origin-when-cross-origin +Gitlab-Sv: localhost +Set-Cookie: _cfuvid=aYLZ68TRL8gsnraUk.zZIxRvuv981nIhZNIO9vVpgbU-1701333890175-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +Ratelimit-Observed: 17 +Content-Type: application/json +Cache-Control: max-age=0, private, must-revalidate +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:50 GMT +Ratelimit-Limit: 2000 +Vary: Origin, Accept-Encoding +Content-Security-Policy: default-src 'none' +Gitlab-Lb: haproxy-main-33-lb-gprd +Cf-Cache-Status: MISS +Ratelimit-Reset: 1701333950 +X-Gitlab-Meta: {"correlation_id":"10d576ab8e82b745b7202a6daec3c5e1","version":"1"} + +{"id":43524600,"iid":2,"project_id":15578026,"title":"Test branch","description":"do not merge this PR","state":"opened","created_at":"2019-11-28T15:56:54.104Z","updated_at":"2020-04-19T19:24:21.108Z","merged_by":null,"merge_user":null,"merged_at":null,"closed_by":null,"closed_at":null,"target_branch":"master","source_branch":"feat/test","user_notes_count":0,"upvotes":1,"downvotes":0,"author":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"assignees":[],"assignee":null,"reviewers":[],"source_project_id":15578026,"target_project_id":15578026,"labels":["bug"],"draft":false,"work_in_progress":false,"milestone":{"id":1082926,"iid":1,"project_id":15578026,"title":"1.0.0","description":"","state":"closed","created_at":"2019-11-28T08:42:30.301Z","updated_at":"2019-11-28T15:57:52.401Z","due_date":null,"start_date":null,"expired":false,"web_url":"https://gitlab.com/gitea/test_repo/-/milestones/1"},"merge_when_pipeline_succeeds":false,"merge_status":"can_be_merged","detailed_merge_status":"mergeable","sha":"9f733b96b98a4175276edf6a2e1231489c3bdd23","merge_commit_sha":null,"squash_commit_sha":null,"discussion_locked":null,"should_remove_source_branch":null,"force_remove_source_branch":true,"prepared_at":"2019-11-28T15:56:54.104Z","reference":"!2","references":{"short":"!2","relative":"!2","full":"gitea/test_repo!2"},"web_url":"https://gitlab.com/gitea/test_repo/-/merge_requests/2","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"squash":true,"squash_on_merge":true,"task_completion_status":{"count":0,"completed_count":0},"has_conflicts":false,"blocking_discussions_resolved":true,"approvals_before_merge":null,"subscribed":false,"changes_count":"1","latest_build_started_at":null,"latest_build_finished_at":null,"first_deployed_to_production_at":null,"pipeline":null,"head_pipeline":null,"diff_refs":{"base_sha":"c59c9b451acca9d106cc19d61d87afe3fbbb8b83","head_sha":"9f733b96b98a4175276edf6a2e1231489c3bdd23","start_sha":"c59c9b451acca9d106cc19d61d87afe3fbbb8b83"},"merge_error":null,"first_contribution":false,"user":{"can_merge":false}} \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_approvals b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_approvals new file mode 100644 index 0000000000..02a1deae29 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_approvals @@ -0,0 +1,22 @@ +Vary: Origin, Accept-Encoding +Ratelimit-Remaining: 1978 +Gitlab-Lb: haproxy-main-04-lb-gprd +Set-Cookie: _cfuvid=cKUtcpJODQwk9SDRn91k1DY8CY5Tg238DXGgT0a2go0-1701333891493-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Content-Type: application/json +Cf-Cache-Status: MISS +Etag: W/"ce2f774c1b05c5c8a14ec9274444cba3" +Ratelimit-Limit: 2000 +X-Content-Type-Options: nosniff +Cache-Control: max-age=0, private, must-revalidate +X-Gitlab-Meta: {"correlation_id":"98ee1da556bdb3d764679ebab9ab5312","version":"1"} +Referrer-Policy: strict-origin-when-cross-origin +X-Frame-Options: SAMEORIGIN +Gitlab-Sv: localhost +Content-Security-Policy: default-src 'none' +X-Runtime: 0.165471 +Strict-Transport-Security: max-age=31536000 +Ratelimit-Observed: 22 +Ratelimit-Reset: 1701333951 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:51 GMT + +{"id":43524600,"iid":2,"project_id":15578026,"title":"Test branch","description":"do not merge this PR","state":"opened","created_at":"2019-11-28T15:56:54.104Z","updated_at":"2020-04-19T19:24:21.108Z","merge_status":"can_be_merged","approved":true,"approvals_required":0,"approvals_left":0,"require_password_to_approve":false,"approved_by":[{"user":{"id":4575606,"username":"real6543","name":"6543","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/4575606/avatar.png","web_url":"https://gitlab.com/real6543"}}],"suggested_approvers":[],"approvers":[],"approver_groups":[{"group":{"id":3181312,"web_url":"https://gitlab.com/groups/gitea","name":"gitea","path":"gitea","description":"Mirror of Gitea source code repositories","visibility":"public","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"maintainer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":false,"emails_enabled":true,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"default_branch_protection_defaults":{"allowed_to_push":[{"access_level":30}],"allow_force_push":true,"allowed_to_merge":[{"access_level":30}]},"avatar_url":"https://gitlab.com/uploads/-/system/group/avatar/3181312/gitea.png","request_access_enabled":true,"full_name":"gitea","full_path":"gitea","created_at":"2018-07-04T16:32:10.176Z","parent_id":null,"shared_runners_setting":"enabled","ldap_cn":null,"ldap_access":null,"wiki_access_level":"enabled"}}],"user_has_approved":false,"user_can_approve":false,"approval_rules_left":[],"has_approval_rules":true,"merge_request_approvers_available":false,"multiple_approval_rules_available":false,"invalid_approvers_rules":[]} \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_award_emoji!page=1&per_page=1 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_award_emoji!page=1&per_page=1 new file mode 100644 index 0000000000..aab7a74df8 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_award_emoji!page=1&per_page=1 @@ -0,0 +1,29 @@ +X-Prev-Page: +X-Runtime: 0.066211 +Strict-Transport-Security: max-age=31536000 +Content-Security-Policy: default-src 'none' +X-Per-Page: 1 +X-Total: 2 +Ratelimit-Reset: 1701333950 +Ratelimit-Limit: 2000 +Cf-Cache-Status: MISS +X-Content-Type-Options: nosniff +X-Gitlab-Meta: {"correlation_id":"d0f8c843558e938161d7307b686f1cd9","version":"1"} +Gitlab-Sv: localhost +Set-Cookie: _cfuvid=WFMplweUX3zWl6uoteYRHeDcpElbTNYhWrIBbNEVC3A-1701333890399-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Content-Type: application/json +X-Total-Pages: 2 +Etag: W/"798718b23a2ec66b16cce20cb7155116" +X-Next-Page: 2 +Link: ; rel="next", ; rel="first", ; rel="last" +Vary: Origin, Accept-Encoding +Ratelimit-Observed: 18 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:50 GMT +X-Frame-Options: SAMEORIGIN +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Remaining: 1982 +Gitlab-Lb: haproxy-main-31-lb-gprd +Cache-Control: max-age=0, private, must-revalidate +X-Page: 1 + +[{"id":5541414,"name":"thumbsup","user":{"id":4575606,"username":"real6543","name":"6543","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/4575606/avatar.png","web_url":"https://gitlab.com/real6543"},"created_at":"2020-09-02T23:42:34.310Z","updated_at":"2020-09-02T23:42:34.310Z","awardable_id":43524600,"awardable_type":"MergeRequest","url":null}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_award_emoji!page=2&per_page=1 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_award_emoji!page=2&per_page=1 new file mode 100644 index 0000000000..5d85a567e8 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_award_emoji!page=2&per_page=1 @@ -0,0 +1,29 @@ +Cf-Cache-Status: MISS +Etag: W/"e6776aaa57e6a81bf8a2d8823272cc70" +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"db01aae11b0cf7febcf051706159faae","version":"1"} +X-Total-Pages: 2 +Strict-Transport-Security: max-age=31536000 +Gitlab-Lb: haproxy-main-51-lb-gprd +X-Total: 2 +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Limit: 2000 +Cache-Control: max-age=0, private, must-revalidate +X-Next-Page: +Set-Cookie: _cfuvid=X0n26HdiufQTXsshb4pvCyuf0jDSstPZ8GnIiyx57YU-1701333890628-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Link: ; rel="prev", ; rel="first", ; rel="last" +Ratelimit-Observed: 19 +Ratelimit-Reset: 1701333950 +Content-Type: application/json +Vary: Origin, Accept-Encoding +X-Per-Page: 1 +X-Runtime: 0.067511 +Gitlab-Sv: localhost +X-Prev-Page: 1 +Ratelimit-Remaining: 1981 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:50 GMT +Content-Security-Policy: default-src 'none' +X-Content-Type-Options: nosniff +X-Page: 2 + +[{"id":5541415,"name":"tada","user":{"id":4575606,"username":"real6543","name":"6543","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/4575606/avatar.png","web_url":"https://gitlab.com/real6543"},"created_at":"2020-09-02T23:42:59.060Z","updated_at":"2020-09-02T23:42:59.060Z","awardable_id":43524600,"awardable_type":"MergeRequest","url":null}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_award_emoji!page=3&per_page=1 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_award_emoji!page=3&per_page=1 new file mode 100644 index 0000000000..aa5a50e45b --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_merge_requests_2_award_emoji!page=3&per_page=1 @@ -0,0 +1,31 @@ +X-Page: 3 +Strict-Transport-Security: max-age=31536000 +Accept-Ranges: bytes +Set-Cookie: _cfuvid=CBSpRhUuajZbJ9Mc_r7SkVmZawoSi5ofuts2TGyHgRk-1701333890842-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Cache-Control: max-age=0, private, must-revalidate +X-Prev-Page: +X-Total-Pages: 2 +Ratelimit-Reset: 1701333950 +Gitlab-Sv: localhost +X-Content-Type-Options: nosniff +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:50 GMT +Gitlab-Lb: haproxy-main-05-lb-gprd +Etag: W/"4f53cda18c2baa0c0354bb5f9a3ecbe5" +Vary: Origin, Accept-Encoding +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"36817edd7cae21d5d6b875faf173ce80","version":"1"} +X-Per-Page: 1 +X-Total: 2 +Referrer-Policy: strict-origin-when-cross-origin +Content-Security-Policy: default-src 'none' +Link: ; rel="first", ; rel="last" +X-Next-Page: +X-Runtime: 0.061075 +Ratelimit-Limit: 2000 +Ratelimit-Observed: 20 +Cf-Cache-Status: MISS +Content-Type: application/json +Content-Length: 2 +Ratelimit-Remaining: 1980 + +[] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_milestones!page=1&per_page=100&state=all b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_milestones!page=1&per_page=100&state=all new file mode 100644 index 0000000000..78310e123d --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_milestones!page=1&per_page=100&state=all @@ -0,0 +1,29 @@ +X-Content-Type-Options: nosniff +X-Next-Page: +Ratelimit-Observed: 5 +X-Frame-Options: SAMEORIGIN +X-Per-Page: 100 +Ratelimit-Limit: 2000 +Cf-Cache-Status: MISS +Strict-Transport-Security: max-age=31536000 +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:46 GMT +Content-Security-Policy: default-src 'none' +Link: ; rel="first", ; rel="last" +X-Gitlab-Meta: {"correlation_id":"4f72373995f8681ce127c62d384745a3","version":"1"} +Gitlab-Sv: localhost +X-Runtime: 0.073691 +Ratelimit-Remaining: 1995 +Ratelimit-Reset: 1701333946 +Content-Type: application/json +Etag: W/"c8e2d3a5f05ee29c58b665c86684f9f9" +X-Page: 1 +Gitlab-Lb: haproxy-main-47-lb-gprd +Vary: Origin, Accept-Encoding +X-Prev-Page: +X-Total: 2 +Cache-Control: max-age=0, private, must-revalidate +X-Total-Pages: 1 +Set-Cookie: _cfuvid=ZfjvK5rh2nTUjEDt1Guwzd8zrl6uCDplfE8NBPbdJ7c-1701333886832-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None + +[{"id":1082927,"iid":2,"project_id":15578026,"title":"1.1.0","description":"","state":"active","created_at":"2019-11-28T08:42:44.575Z","updated_at":"2019-11-28T08:42:44.575Z","due_date":null,"start_date":null,"expired":false,"web_url":"https://gitlab.com/gitea/test_repo/-/milestones/2"},{"id":1082926,"iid":1,"project_id":15578026,"title":"1.0.0","description":"","state":"closed","created_at":"2019-11-28T08:42:30.301Z","updated_at":"2019-11-28T15:57:52.401Z","due_date":null,"start_date":null,"expired":false,"web_url":"https://gitlab.com/gitea/test_repo/-/milestones/1"}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_releases!page=1&per_page=100 b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_releases!page=1&per_page=100 new file mode 100644 index 0000000000..9be0b74729 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_15578026_releases!page=1&per_page=100 @@ -0,0 +1,29 @@ +Strict-Transport-Security: max-age=31536000 +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Reset: 1701333947 +X-Total: 1 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:47 GMT +Ratelimit-Limit: 2000 +X-Runtime: 0.178411 +Gitlab-Lb: haproxy-main-15-lb-gprd +Vary: Origin, Accept-Encoding +X-Page: 1 +X-Frame-Options: SAMEORIGIN +Ratelimit-Observed: 7 +Cf-Cache-Status: MISS +Etag: W/"dccc7159dc4b46989d13128a7d6ee859" +X-Content-Type-Options: nosniff +Ratelimit-Remaining: 1993 +X-Gitlab-Meta: {"correlation_id":"0044a3de3ede2f913cabe6e464dd73c2","version":"1"} +X-Total-Pages: 1 +X-Next-Page: +X-Per-Page: 100 +Content-Type: application/json +Content-Security-Policy: default-src 'none' +Set-Cookie: _cfuvid=h1ayMNs6W_kFPoFe28IpiaFUz1ZAPvY6npUWxARRx4I-1701333887452-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Link: ; rel="first", ; rel="last" +Gitlab-Sv: localhost +Cache-Control: max-age=0, private, must-revalidate +X-Prev-Page: + +[{"name":"First Release","tag_name":"v0.9.99","description":"A test release","created_at":"2019-11-28T09:09:48.840Z","released_at":"2019-11-28T09:09:48.836Z","upcoming_release":false,"author":{"id":1241334,"username":"lafriks","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"commit":{"id":"0720a3ec57c1f843568298117b874319e7deee75","short_id":"0720a3ec","created_at":"2019-11-28T08:49:16.000+00:00","parent_ids":["93ea21ce45d35690c35e80961d239645139e872c"],"title":"Add new file","message":"Add new file","author_name":"Lauris BH","author_email":"lauris@nix.lv","authored_date":"2019-11-28T08:49:16.000+00:00","committer_name":"Lauris BH","committer_email":"lauris@nix.lv","committed_date":"2019-11-28T08:49:16.000+00:00","trailers":{},"extended_trailers":{},"web_url":"https://gitlab.com/gitea/test_repo/-/commit/0720a3ec57c1f843568298117b874319e7deee75"},"commit_path":"/gitea/test_repo/-/commit/0720a3ec57c1f843568298117b874319e7deee75","tag_path":"/gitea/test_repo/-/tags/v0.9.99","assets":{"count":4,"sources":[{"format":"zip","url":"https://gitlab.com/gitea/test_repo/-/archive/v0.9.99/test_repo-v0.9.99.zip"},{"format":"tar.gz","url":"https://gitlab.com/gitea/test_repo/-/archive/v0.9.99/test_repo-v0.9.99.tar.gz"},{"format":"tar.bz2","url":"https://gitlab.com/gitea/test_repo/-/archive/v0.9.99/test_repo-v0.9.99.tar.bz2"},{"format":"tar","url":"https://gitlab.com/gitea/test_repo/-/archive/v0.9.99/test_repo-v0.9.99.tar"}],"links":[]},"evidences":[{"sha":"89f1223473ee01f192a83d0cb89f4d1eac1de74f01ad","filepath":"https://gitlab.com/gitea/test_repo/-/releases/v0.9.99/evidences/52147.json","collected_at":"2019-11-28T09:09:48.888Z"}],"_links":{"closed_issues_url":"https://gitlab.com/gitea/test_repo/-/issues?release_tag=v0.9.99\u0026scope=all\u0026state=closed","closed_merge_requests_url":"https://gitlab.com/gitea/test_repo/-/merge_requests?release_tag=v0.9.99\u0026scope=all\u0026state=closed","merged_merge_requests_url":"https://gitlab.com/gitea/test_repo/-/merge_requests?release_tag=v0.9.99\u0026scope=all\u0026state=merged","opened_issues_url":"https://gitlab.com/gitea/test_repo/-/issues?release_tag=v0.9.99\u0026scope=all\u0026state=opened","opened_merge_requests_url":"https://gitlab.com/gitea/test_repo/-/merge_requests?release_tag=v0.9.99\u0026scope=all\u0026state=opened","self":"https://gitlab.com/gitea/test_repo/-/releases/v0.9.99"}}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_projects_gitea%2Ftest_repo b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_gitea%2Ftest_repo new file mode 100644 index 0000000000..07fed52a81 --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_projects_gitea%2Ftest_repo @@ -0,0 +1,22 @@ +Content-Security-Policy: default-src 'none' +Vary: Origin, Accept-Encoding +Referrer-Policy: strict-origin-when-cross-origin +Gitlab-Sv: localhost +X-Content-Type-Options: nosniff +Ratelimit-Reset: 1701333946 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:46 GMT +X-Runtime: 0.144599 +Content-Type: application/json +X-Gitlab-Meta: {"correlation_id":"919887b868b11ed34c5917e98b4be40d","version":"1"} +Ratelimit-Observed: 2 +Gitlab-Lb: haproxy-main-42-lb-gprd +Strict-Transport-Security: max-age=31536000 +Cf-Cache-Status: MISS +Ratelimit-Limit: 2000 +Set-Cookie: _cfuvid=BENIrMVlxs_tt.JplEkDKbUrMpOF_kjRRLJOifNTLqY-1701333886061-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Etag: W/"3cacfe29f44a69e84a577337eac55d89" +X-Frame-Options: SAMEORIGIN +Cache-Control: max-age=0, private, must-revalidate +Ratelimit-Remaining: 1998 + +{"id":15578026,"description":"Test repository for testing migration from gitlab to gitea","name":"test_repo","name_with_namespace":"gitea / test_repo","path":"test_repo","path_with_namespace":"gitea/test_repo","created_at":"2019-11-28T08:20:33.019Z","default_branch":"master","tag_list":["migration","test"],"topics":["migration","test"],"ssh_url_to_repo":"git@gitlab.com:gitea/test_repo.git","http_url_to_repo":"https://gitlab.com/gitea/test_repo.git","web_url":"https://gitlab.com/gitea/test_repo","readme_url":"https://gitlab.com/gitea/test_repo/-/blob/master/README.md","forks_count":1,"avatar_url":null,"star_count":0,"last_activity_at":"2020-04-19T19:46:04.527Z","namespace":{"id":3181312,"name":"gitea","path":"gitea","kind":"group","full_path":"gitea","parent_id":null,"avatar_url":"/uploads/-/system/group/avatar/3181312/gitea.png","web_url":"https://gitlab.com/groups/gitea"},"container_registry_image_prefix":"registry.gitlab.com/gitea/test_repo","_links":{"self":"https://gitlab.com/api/v4/projects/15578026","issues":"https://gitlab.com/api/v4/projects/15578026/issues","merge_requests":"https://gitlab.com/api/v4/projects/15578026/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/15578026/repository/branches","labels":"https://gitlab.com/api/v4/projects/15578026/labels","events":"https://gitlab.com/api/v4/projects/15578026/events","members":"https://gitlab.com/api/v4/projects/15578026/members","cluster_agents":"https://gitlab.com/api/v4/projects/15578026/cluster_agents"},"packages_enabled":true,"empty_repo":false,"archived":false,"visibility":"public","resolve_outdated_diff_discussions":false,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"container_registry_enabled":true,"service_desk_enabled":true,"can_create_merge_request_in":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","forking_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","pages_access_level":"enabled","analytics_access_level":"enabled","container_registry_access_level":"enabled","security_and_compliance_access_level":"private","releases_access_level":"enabled","environments_access_level":"enabled","feature_flags_access_level":"enabled","infrastructure_access_level":"enabled","monitor_access_level":"enabled","model_experiments_access_level":"enabled","emails_disabled":false,"emails_enabled":true,"shared_runners_enabled":true,"lfs_enabled":true,"creator_id":1241334,"import_status":"none","open_issues_count":0,"description_html":"\u003cp data-sourcepos=\"1:1-1:58\" dir=\"auto\"\u003eTest repository for testing migration from gitlab to gitea\u003c/p\u003e","updated_at":"2022-08-26T19:41:46.691Z","ci_config_path":null,"public_jobs":true,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"allow_merge_on_skipped_pipeline":null,"request_access_enabled":true,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":true,"printing_merge_request_link_enabled":true,"merge_method":"ff","squash_option":"default_off","enforce_auth_checks_on_uploads":true,"suggestion_commit_message":null,"merge_commit_template":null,"squash_commit_template":null,"issue_branch_template":null,"autoclose_referenced_issues":true,"external_authorization_classification_label":"","requirements_enabled":false,"requirements_access_level":"enabled","security_and_compliance_enabled":false,"compliance_frameworks":[],"permissions":{"project_access":null,"group_access":null}} \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/full_download/_api_v4_version b/services/migrations/testdata/gitlab/full_download/_api_v4_version new file mode 100644 index 0000000000..7c2cd320ad --- /dev/null +++ b/services/migrations/testdata/gitlab/full_download/_api_v4_version @@ -0,0 +1,22 @@ +Etag: W/"4e5c0a031c3aacb6ba0a3c19e67d7592" +Ratelimit-Observed: 1 +Content-Type: application/json +Ratelimit-Remaining: 1999 +Set-Cookie: _cfuvid=qtYbzv8YeGg4q7XaV0aAE2.YqWIp_xrYPGilXrlecsk-1701333885742-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +X-Gitlab-Meta: {"correlation_id":"c7c75f0406e1b1b9705a6d7e9bdb06a5","version":"1"} +X-Runtime: 0.039264 +Cf-Cache-Status: MISS +Content-Security-Policy: default-src 'none' +Ratelimit-Reset: 1701333945 +Gitlab-Sv: localhost +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Content-Type-Options: nosniff +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Resettime: Thu, 30 Nov 2023 08:45:45 GMT +Gitlab-Lb: haproxy-main-23-lb-gprd +Cache-Control: max-age=0, private, must-revalidate +X-Frame-Options: SAMEORIGIN +Ratelimit-Limit: 2000 + +{"version":"16.7.0-pre","revision":"acd848a9228","kas":{"enabled":true,"externalUrl":"wss://kas.gitlab.com","version":"v16.7.0-rc2"},"enterprise":true} \ No newline at end of file From 97f02df163e2b4e23b82e23e5ef57a586b17f021 Mon Sep 17 00:00:00 2001 From: Antonin Delpeuch Date: Thu, 30 Nov 2023 13:56:47 +0000 Subject: [PATCH 30/86] [GITEA] Avoid conflicts of issue and PR numbers in GitLab migration (#1790) Closes #1789. The bug was due to the fact that GitLab does not guarantee that issue numbers are created sequentially: some identifiers can be skipped. Therefore, the new pull requests numbers should not be offset by the number of issues, but by the maximum issue number. See for instance https://gitlab.com/troyengel/archbuild/-/issues/?sort=created_date&state=all&first_page_size=20, where there is only a singe issue with number "2". Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/1790 Co-authored-by: Antonin Delpeuch Co-committed-by: Antonin Delpeuch (cherry picked from commit 2c185c39fe600041701d5f59cb1076a788815cb4) (cherry picked from commit 8f68dc4c9c2f0acab55d59a496b0f141befad969) (cherry picked from commit 7e932b7fca1b119e7cc646183c383ba51a5f1d14) (cherry picked from commit 6bbe75ecf8ac502bd42ff5765e6e7733f290a54e) (cherry picked from commit b18c2e8d658c3311e0a299696bd1b6612c52ef13) Conflicts: services/migrations/gitlab.go https://codeberg.org/forgejo/forgejo/pulls/2075 (cherry picked from commit abc129c762b3c1a992ad5c67adf62d8336eadbbe) (cherry picked from commit 28884fac10c455a9f40bebd961fca40afd4a749e) (cherry picked from commit 5f528dd85fa6705c60d15bce616a46f00df1b85b) (cherry picked from commit cb9b8a31b25b27fa5e386b20f02e532bbf3462a2) --- services/migrations/gitlab_test.go | 43 +++++++++++++++++++ .../_api_v4_projects_6590996 | 22 ++++++++++ ...sues!page=1&per_page=10&sort=asc&state=all | 29 +++++++++++++ ...96_issues_2_award_emoji!page=1&per_page=10 | 31 +++++++++++++ ...ge_requests!page=1&per_page=10&view=simple | 29 +++++++++++++ .../_api_v4_projects_6590996_merge_requests_1 | 22 ++++++++++ ..._requests_1_award_emoji!page=1&per_page=10 | 31 +++++++++++++ .../_api_v4_projects_troyengel%2Farchbuild | 22 ++++++++++ .../skipped_issue_number/_api_v4_version | 22 ++++++++++ 9 files changed, 251 insertions(+) create mode 100644 services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996 create mode 100644 services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_issues!page=1&per_page=10&sort=asc&state=all create mode 100644 services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_issues_2_award_emoji!page=1&per_page=10 create mode 100644 services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_merge_requests!page=1&per_page=10&view=simple create mode 100644 services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_merge_requests_1 create mode 100644 services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_merge_requests_1_award_emoji!page=1&per_page=10 create mode 100644 services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_troyengel%2Farchbuild create mode 100644 services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_version diff --git a/services/migrations/gitlab_test.go b/services/migrations/gitlab_test.go index fe56c2f4e6..25324a087f 100644 --- a/services/migrations/gitlab_test.go +++ b/services/migrations/gitlab_test.go @@ -334,6 +334,49 @@ func TestGitlabDownloadRepo(t *testing.T) { }, rvs) } +func TestGitlabSkippedIssueNumber(t *testing.T) { + // If a GitLab access token is provided, this test will make HTTP requests to the live gitlab.com instance. + // When doing so, the responses from gitlab.com will be saved as test data files. + // If no access token is available, those cached responses will be used instead. + gitlabPersonalAccessToken := os.Getenv("GITLAB_READ_TOKEN") + fixturePath := "./testdata/gitlab/skipped_issue_number" + server := unittest.NewMockWebServer(t, "https://gitlab.com", fixturePath, gitlabPersonalAccessToken != "") + defer server.Close() + + downloader, err := NewGitlabDownloader(context.Background(), server.URL, "troyengel/archbuild", "", "", gitlabPersonalAccessToken) + if err != nil { + t.Fatalf("NewGitlabDownloader is nil: %v", err) + } + repo, err := downloader.GetRepoInfo() + assert.NoError(t, err) + assertRepositoryEqual(t, &base.Repository{ + Name: "archbuild", + Owner: "troyengel", + Description: "Arch packaging and build files", + CloneURL: server.URL + "/troyengel/archbuild.git", + OriginalURL: server.URL + "/troyengel/archbuild", + DefaultBranch: "master", + }, repo) + + issues, isEnd, err := downloader.GetIssues(1, 10) + assert.NoError(t, err) + assert.True(t, isEnd) + + // the only issue in this repository has number 2 + assert.EqualValues(t, 1, len(issues)) + assert.EqualValues(t, 2, issues[0].Number) + assert.EqualValues(t, "vpn unlimited errors", issues[0].Title) + + prs, _, err := downloader.GetPullRequests(1, 10) + assert.NoError(t, err) + // the only merge request in this repository has number 1, + // but we offset it by the maximum issue number so it becomes + // pull request 3 in Forgejo + assert.EqualValues(t, 1, len(prs)) + assert.EqualValues(t, 3, prs[0].Number) + assert.EqualValues(t, "Review", prs[0].Title) +} + func gitlabClientMockSetup(t *testing.T) (*http.ServeMux, *httptest.Server, *gitlab.Client) { // mux is the HTTP request multiplexer used with the test server. mux := http.NewServeMux() diff --git a/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996 b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996 new file mode 100644 index 0000000000..db8d596173 --- /dev/null +++ b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996 @@ -0,0 +1,22 @@ +X-Runtime: 0.088022 +Strict-Transport-Security: max-age=31536000 +Ratelimit-Observed: 3 +Cache-Control: max-age=0, private, must-revalidate +Etag: W/"03ce4f6ce1c1e8c5a31df8a44cf2fbdd" +Gitlab-Lb: haproxy-main-11-lb-gprd +Content-Security-Policy: default-src 'none' +Ratelimit-Limit: 2000 +X-Gitlab-Meta: {"correlation_id":"b57b226f741f9140a1fea54f65cb5cfd","version":"1"} +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Remaining: 1997 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:24:53 GMT +Set-Cookie: _cfuvid=V0ToiOTUW0XbtWq7BirwVNfL1_YP1POMrLBnDSEWS0M-1701332633965-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +Gitlab-Sv: localhost +Content-Type: application/json +Vary: Origin, Accept-Encoding +Ratelimit-Reset: 1701332693 +Cf-Cache-Status: MISS + +{"id":6590996,"description":"Arch packaging and build files","name":"archbuild","name_with_namespace":"Troy Engel / archbuild","path":"archbuild","path_with_namespace":"troyengel/archbuild","created_at":"2018-06-03T22:53:17.388Z","default_branch":"master","tag_list":[],"topics":[],"ssh_url_to_repo":"git@gitlab.com:troyengel/archbuild.git","http_url_to_repo":"https://gitlab.com/troyengel/archbuild.git","web_url":"https://gitlab.com/troyengel/archbuild","readme_url":"https://gitlab.com/troyengel/archbuild/-/blob/master/README.md","forks_count":0,"avatar_url":null,"star_count":0,"last_activity_at":"2020-12-13T18:09:32.071Z","namespace":{"id":1452515,"name":"Troy Engel","path":"troyengel","kind":"user","full_path":"troyengel","parent_id":null,"avatar_url":"https://secure.gravatar.com/avatar/b226c267929f1bcfcc446e75a025591c?s=80\u0026d=identicon","web_url":"https://gitlab.com/troyengel"},"container_registry_image_prefix":"registry.gitlab.com/troyengel/archbuild","_links":{"self":"https://gitlab.com/api/v4/projects/6590996","issues":"https://gitlab.com/api/v4/projects/6590996/issues","merge_requests":"https://gitlab.com/api/v4/projects/6590996/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/6590996/repository/branches","labels":"https://gitlab.com/api/v4/projects/6590996/labels","events":"https://gitlab.com/api/v4/projects/6590996/events","members":"https://gitlab.com/api/v4/projects/6590996/members","cluster_agents":"https://gitlab.com/api/v4/projects/6590996/cluster_agents"},"packages_enabled":null,"empty_repo":false,"archived":true,"visibility":"public","owner":{"id":1215848,"username":"troyengel","name":"Troy Engel","state":"active","locked":false,"avatar_url":"https://secure.gravatar.com/avatar/b226c267929f1bcfcc446e75a025591c?s=80\u0026d=identicon","web_url":"https://gitlab.com/troyengel"},"resolve_outdated_diff_discussions":false,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"container_registry_enabled":true,"service_desk_enabled":true,"can_create_merge_request_in":false,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","forking_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","pages_access_level":"enabled","analytics_access_level":"enabled","container_registry_access_level":"enabled","security_and_compliance_access_level":"private","releases_access_level":"enabled","environments_access_level":"enabled","feature_flags_access_level":"enabled","infrastructure_access_level":"enabled","monitor_access_level":"enabled","model_experiments_access_level":"enabled","emails_disabled":false,"emails_enabled":true,"shared_runners_enabled":true,"lfs_enabled":false,"creator_id":1215848,"import_status":"finished","open_issues_count":0,"description_html":"\u003cp data-sourcepos=\"1:1-1:30\" dir=\"auto\"\u003eArch packaging and build files\u003c/p\u003e","updated_at":"2022-07-13T21:32:12.624Z","ci_config_path":null,"public_jobs":true,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"allow_merge_on_skipped_pipeline":null,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","squash_option":"default_off","enforce_auth_checks_on_uploads":true,"suggestion_commit_message":null,"merge_commit_template":null,"squash_commit_template":null,"issue_branch_template":null,"autoclose_referenced_issues":true,"external_authorization_classification_label":"","requirements_enabled":false,"requirements_access_level":"enabled","security_and_compliance_enabled":false,"compliance_frameworks":[],"permissions":{"project_access":null,"group_access":null}} \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_issues!page=1&per_page=10&sort=asc&state=all b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_issues!page=1&per_page=10&sort=asc&state=all new file mode 100644 index 0000000000..99133d5f8d --- /dev/null +++ b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_issues!page=1&per_page=10&sort=asc&state=all @@ -0,0 +1,29 @@ +Link: ; rel="first", ; rel="last" +Ratelimit-Observed: 4 +Ratelimit-Remaining: 1996 +Gitlab-Lb: haproxy-main-04-lb-gprd +Vary: Origin, Accept-Encoding +Content-Security-Policy: default-src 'none' +X-Next-Page: +Ratelimit-Reset: 1701332694 +Etag: W/"f50a70d0fc1465a289d231f80806ced7" +X-Gitlab-Meta: {"correlation_id":"47afd74254dd7946d2b2bded87448c60","version":"1"} +X-Page: 1 +X-Prev-Page: +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Resettime: Thu, 30 Nov 2023 08:24:54 GMT +Cf-Cache-Status: MISS +X-Total: 1 +X-Total-Pages: 1 +Strict-Transport-Security: max-age=31536000 +Content-Type: application/json +X-Frame-Options: SAMEORIGIN +Ratelimit-Limit: 2000 +Gitlab-Sv: localhost +Set-Cookie: _cfuvid=YDWTZ5VoSuLBDZgKsBnXMyYxz.0rHJ9TBYXv5zBj24Q-1701332634294-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Cache-Control: max-age=0, private, must-revalidate +X-Content-Type-Options: nosniff +X-Per-Page: 10 +X-Runtime: 0.179458 + +[{"id":11201348,"iid":2,"project_id":6590996,"title":"vpn unlimited errors","description":"updated version to 2.8.0, build and tried running `vpnu-arch`:\n\n```\nvpn-unlimited: /usr/lib/libcurl.so.3: no version information available (required by /usr/lib/libvpnu_rpc.so.1)\nvpn-unlimited: /usr/lib/libssl.so.1.0.0: no version information available (required by /usr/lib/libvpnu_enc.so.1)\nvpn-unlimited: symbol lookup error: /usr/lib/libvpnu_rpc.so.1: undefined symbol: _ZNK4Json5Value8asStringEv\n```\n","state":"closed","created_at":"2016-03-26T16:41:12.000Z","updated_at":"2016-03-27T12:19:27.000Z","closed_at":null,"closed_by":null,"labels":[],"milestone":null,"assignees":[],"author":{"id":10273,"username":"brauliobo","name":"Bráulio Bhavamitra","state":"active","locked":false,"avatar_url":"https://secure.gravatar.com/avatar/cd3fcb7a417c8acb989fc320b604a2a8?s=80\u0026d=identicon","web_url":"https://gitlab.com/brauliobo"},"type":"ISSUE","assignee":null,"user_notes_count":1,"merge_requests_count":0,"upvotes":0,"downvotes":0,"due_date":null,"confidential":false,"discussion_locked":null,"issue_type":"issue","web_url":"https://gitlab.com/troyengel/archbuild/-/issues/2","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"task_completion_status":{"count":0,"completed_count":0},"blocking_issues_count":0,"has_tasks":true,"task_status":"0 of 0 checklist items completed","_links":{"self":"https://gitlab.com/api/v4/projects/6590996/issues/2","notes":"https://gitlab.com/api/v4/projects/6590996/issues/2/notes","award_emoji":"https://gitlab.com/api/v4/projects/6590996/issues/2/award_emoji","project":"https://gitlab.com/api/v4/projects/6590996","closed_as_duplicate_of":null},"references":{"short":"#2","relative":"#2","full":"troyengel/archbuild#2"},"severity":"UNKNOWN","moved_to_id":null,"service_desk_reply_to":null}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_issues_2_award_emoji!page=1&per_page=10 b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_issues_2_award_emoji!page=1&per_page=10 new file mode 100644 index 0000000000..8f829d08f0 --- /dev/null +++ b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_issues_2_award_emoji!page=1&per_page=10 @@ -0,0 +1,31 @@ +Gitlab-Sv: localhost +X-Content-Type-Options: nosniff +Gitlab-Lb: haproxy-main-25-lb-gprd +X-Total-Pages: 1 +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Observed: 5 +Ratelimit-Remaining: 1995 +Content-Security-Policy: default-src 'none' +X-Gitlab-Meta: {"correlation_id":"eeab46d836341bd4cb18e3d2e82abf97","version":"1"} +Ratelimit-Limit: 2000 +Accept-Ranges: bytes +Content-Type: application/json +X-Page: 1 +X-Frame-Options: SAMEORIGIN +X-Prev-Page: +Cf-Cache-Status: MISS +X-Total: 0 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:24:54 GMT +Link: ; rel="first", ; rel="last" +X-Per-Page: 10 +Set-Cookie: _cfuvid=c5HuTPxOuSXdHSuVrXQALS.uV7WvAYfc5Mc_143EAB8-1701332634513-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Content-Length: 2 +Vary: Origin, Accept-Encoding +Cache-Control: max-age=0, private, must-revalidate +Etag: W/"4f53cda18c2baa0c0354bb5f9a3ecbe5" +X-Runtime: 0.069269 +Strict-Transport-Security: max-age=31536000 +Ratelimit-Reset: 1701332694 +X-Next-Page: + +[] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_merge_requests!page=1&per_page=10&view=simple b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_merge_requests!page=1&per_page=10&view=simple new file mode 100644 index 0000000000..5339392008 --- /dev/null +++ b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_merge_requests!page=1&per_page=10&view=simple @@ -0,0 +1,29 @@ +Vary: Origin, Accept-Encoding +Strict-Transport-Security: max-age=31536000 +Gitlab-Sv: localhost +X-Content-Type-Options: nosniff +X-Prev-Page: +Ratelimit-Reset: 1701332694 +Cache-Control: max-age=0, private, must-revalidate +Ratelimit-Limit: 2000 +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Observed: 6 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:24:54 GMT +Cf-Cache-Status: MISS +Content-Type: application/json +Content-Security-Policy: default-src 'none' +Etag: W/"1a50811aa3cccb2e6a404a976422a83a" +X-Total: 1 +Ratelimit-Remaining: 1994 +Set-Cookie: _cfuvid=u.zumTkG1ayCnh_OwrT9Q1Fl3MXV9Gh98W.ma4WN2Xs-1701332634745-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Link: ; rel="first", ; rel="last" +X-Frame-Options: SAMEORIGIN +X-Page: 1 +X-Total-Pages: 1 +Gitlab-Lb: haproxy-main-05-lb-gprd +X-Gitlab-Meta: {"correlation_id":"907f9e1f94131ea7a6d1405100a8cc4b","version":"1"} +X-Next-Page: +X-Per-Page: 10 +X-Runtime: 0.078413 + +[{"id":10518914,"iid":1,"project_id":6590996,"title":"Review","description":"*Created by: cgtx*\n\n### remove patch from makedepends\n- patch is in base-devel\n- The group base-devel is assumed to be already installed when building with makepkg. Members of \"base-devel\" should not be included in makedepends arrays.\n- https://wiki.archlinux.org/index.php/Pkgbuild#makedepends\n### remove python2 from makedepends\n- python2 is a dependency of python2-setuptools. It is redundant to list it again.\n- You do not need to list packages that your software depends on if other packages your software depends on already have those packages listed in their dependency.\n- https://wiki.archlinux.org/index.php/Pkgbuild#depends\n### more simple find/delete command\n- just because\n","state":"merged","created_at":"2014-12-12T15:01:32.000Z","updated_at":"2014-12-12T15:28:38.000Z","web_url":"https://gitlab.com/troyengel/archbuild/-/merge_requests/1"}] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_merge_requests_1 b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_merge_requests_1 new file mode 100644 index 0000000000..18e8a8583f --- /dev/null +++ b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_merge_requests_1 @@ -0,0 +1,22 @@ +Ratelimit-Observed: 7 +Set-Cookie: _cfuvid=_b9GQEo3CBPMs9QmGE89dBdOmbSTfnYjZlzValULQPs-1701332635000-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Strict-Transport-Security: max-age=31536000 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:24:54 GMT +Gitlab-Lb: haproxy-main-50-lb-gprd +Gitlab-Sv: localhost +X-Gitlab-Meta: {"correlation_id":"da44cd0303a4e62cc52ed8de3b2adf14","version":"1"} +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Remaining: 1993 +Etag: W/"f6299e7e884cb8df8109256c086eb4e7" +X-Runtime: 0.107573 +Content-Type: application/json +Ratelimit-Reset: 1701332694 +X-Frame-Options: SAMEORIGIN +Cache-Control: max-age=0, private, must-revalidate +X-Content-Type-Options: nosniff +Ratelimit-Limit: 2000 +Cf-Cache-Status: MISS +Content-Security-Policy: default-src 'none' +Vary: Origin, Accept-Encoding + +{"id":10518914,"iid":1,"project_id":6590996,"title":"Review","description":"*Created by: cgtx*\n\n### remove patch from makedepends\n- patch is in base-devel\n- The group base-devel is assumed to be already installed when building with makepkg. Members of \"base-devel\" should not be included in makedepends arrays.\n- https://wiki.archlinux.org/index.php/Pkgbuild#makedepends\n### remove python2 from makedepends\n- python2 is a dependency of python2-setuptools. It is redundant to list it again.\n- You do not need to list packages that your software depends on if other packages your software depends on already have those packages listed in their dependency.\n- https://wiki.archlinux.org/index.php/Pkgbuild#depends\n### more simple find/delete command\n- just because\n","state":"merged","created_at":"2014-12-12T15:01:32.000Z","updated_at":"2014-12-12T15:28:38.000Z","merged_by":null,"merge_user":null,"merged_at":null,"closed_by":null,"closed_at":null,"target_branch":"master","source_branch":"cgtx:review","user_notes_count":1,"upvotes":0,"downvotes":0,"author":{"id":1215848,"username":"troyengel","name":"Troy Engel","state":"active","locked":false,"avatar_url":"https://secure.gravatar.com/avatar/b226c267929f1bcfcc446e75a025591c?s=80\u0026d=identicon","web_url":"https://gitlab.com/troyengel"},"assignees":[],"assignee":null,"reviewers":[],"source_project_id":6590996,"target_project_id":6590996,"labels":[],"draft":false,"work_in_progress":false,"milestone":null,"merge_when_pipeline_succeeds":false,"merge_status":"cannot_be_merged","detailed_merge_status":"not_open","sha":"9006fee398299beed8f5d5086f8e6008ffc02280","merge_commit_sha":null,"squash_commit_sha":null,"discussion_locked":null,"should_remove_source_branch":null,"force_remove_source_branch":null,"prepared_at":"2014-12-12T15:01:32.000Z","reference":"!1","references":{"short":"!1","relative":"!1","full":"troyengel/archbuild!1"},"web_url":"https://gitlab.com/troyengel/archbuild/-/merge_requests/1","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"squash":false,"squash_on_merge":false,"task_completion_status":{"count":0,"completed_count":0},"has_conflicts":true,"blocking_discussions_resolved":true,"approvals_before_merge":null,"subscribed":false,"changes_count":"1","latest_build_started_at":null,"latest_build_finished_at":null,"first_deployed_to_production_at":null,"pipeline":null,"head_pipeline":null,"diff_refs":{"base_sha":"6edcf8fc09f6c44213c892f5108d34a5255a47e1","head_sha":"9006fee398299beed8f5d5086f8e6008ffc02280","start_sha":"6edcf8fc09f6c44213c892f5108d34a5255a47e1"},"merge_error":null,"first_contribution":false,"user":{"can_merge":false}} \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_merge_requests_1_award_emoji!page=1&per_page=10 b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_merge_requests_1_award_emoji!page=1&per_page=10 new file mode 100644 index 0000000000..d6f8dd4941 --- /dev/null +++ b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_6590996_merge_requests_1_award_emoji!page=1&per_page=10 @@ -0,0 +1,31 @@ +Link: ; rel="first", ; rel="last" +Set-Cookie: _cfuvid=qK29tijoyp0AdVoHf9Lqjc8Y28h4jplJDW9hOFLfq28-1701332635229-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Cache-Control: max-age=0, private, must-revalidate +Etag: W/"4f53cda18c2baa0c0354bb5f9a3ecbe5" +Ratelimit-Observed: 8 +Gitlab-Sv: localhost +Content-Length: 2 +Gitlab-Lb: haproxy-main-16-lb-gprd +X-Total: 0 +Ratelimit-Remaining: 1992 +Ratelimit-Reset: 1701332695 +Ratelimit-Limit: 2000 +Vary: Origin, Accept-Encoding +X-Frame-Options: SAMEORIGIN +Content-Type: application/json +X-Content-Type-Options: nosniff +X-Next-Page: +X-Page: 1 +Strict-Transport-Security: max-age=31536000 +Accept-Ranges: bytes +Content-Security-Policy: default-src 'none' +X-Per-Page: 10 +X-Total-Pages: 1 +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Resettime: Thu, 30 Nov 2023 08:24:55 GMT +Cf-Cache-Status: MISS +X-Gitlab-Meta: {"correlation_id":"eb59d63fed23cdbec69308570cc49c3e","version":"1"} +X-Runtime: 0.065972 +X-Prev-Page: + +[] \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_troyengel%2Farchbuild b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_troyengel%2Farchbuild new file mode 100644 index 0000000000..a8c2882c26 --- /dev/null +++ b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_projects_troyengel%2Farchbuild @@ -0,0 +1,22 @@ +Ratelimit-Resettime: Thu, 30 Nov 2023 08:24:53 GMT +Gitlab-Lb: haproxy-main-41-lb-gprd +Cache-Control: max-age=0, private, must-revalidate +Referrer-Policy: strict-origin-when-cross-origin +Cf-Cache-Status: MISS +X-Content-Type-Options: nosniff +Set-Cookie: _cfuvid=r78xThY2IPR6QvHnea1t_L7DbvuQp4.HWOiG1cKTWUg-1701332633720-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Ratelimit-Limit: 2000 +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Gitlab-Meta: {"correlation_id":"4c3e0f8b5858454b6e138ecae9902a8d","version":"1"} +X-Runtime: 0.097047 +Ratelimit-Observed: 2 +Ratelimit-Remaining: 1998 +X-Frame-Options: SAMEORIGIN +Content-Security-Policy: default-src 'none' +Etag: W/"03ce4f6ce1c1e8c5a31df8a44cf2fbdd" +Content-Type: application/json +Gitlab-Sv: localhost +Ratelimit-Reset: 1701332693 + +{"id":6590996,"description":"Arch packaging and build files","name":"archbuild","name_with_namespace":"Troy Engel / archbuild","path":"archbuild","path_with_namespace":"troyengel/archbuild","created_at":"2018-06-03T22:53:17.388Z","default_branch":"master","tag_list":[],"topics":[],"ssh_url_to_repo":"git@gitlab.com:troyengel/archbuild.git","http_url_to_repo":"https://gitlab.com/troyengel/archbuild.git","web_url":"https://gitlab.com/troyengel/archbuild","readme_url":"https://gitlab.com/troyengel/archbuild/-/blob/master/README.md","forks_count":0,"avatar_url":null,"star_count":0,"last_activity_at":"2020-12-13T18:09:32.071Z","namespace":{"id":1452515,"name":"Troy Engel","path":"troyengel","kind":"user","full_path":"troyengel","parent_id":null,"avatar_url":"https://secure.gravatar.com/avatar/b226c267929f1bcfcc446e75a025591c?s=80\u0026d=identicon","web_url":"https://gitlab.com/troyengel"},"container_registry_image_prefix":"registry.gitlab.com/troyengel/archbuild","_links":{"self":"https://gitlab.com/api/v4/projects/6590996","issues":"https://gitlab.com/api/v4/projects/6590996/issues","merge_requests":"https://gitlab.com/api/v4/projects/6590996/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/6590996/repository/branches","labels":"https://gitlab.com/api/v4/projects/6590996/labels","events":"https://gitlab.com/api/v4/projects/6590996/events","members":"https://gitlab.com/api/v4/projects/6590996/members","cluster_agents":"https://gitlab.com/api/v4/projects/6590996/cluster_agents"},"packages_enabled":null,"empty_repo":false,"archived":true,"visibility":"public","owner":{"id":1215848,"username":"troyengel","name":"Troy Engel","state":"active","locked":false,"avatar_url":"https://secure.gravatar.com/avatar/b226c267929f1bcfcc446e75a025591c?s=80\u0026d=identicon","web_url":"https://gitlab.com/troyengel"},"resolve_outdated_diff_discussions":false,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"container_registry_enabled":true,"service_desk_enabled":true,"can_create_merge_request_in":false,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","forking_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","pages_access_level":"enabled","analytics_access_level":"enabled","container_registry_access_level":"enabled","security_and_compliance_access_level":"private","releases_access_level":"enabled","environments_access_level":"enabled","feature_flags_access_level":"enabled","infrastructure_access_level":"enabled","monitor_access_level":"enabled","model_experiments_access_level":"enabled","emails_disabled":false,"emails_enabled":true,"shared_runners_enabled":true,"lfs_enabled":false,"creator_id":1215848,"import_status":"finished","open_issues_count":0,"description_html":"\u003cp data-sourcepos=\"1:1-1:30\" dir=\"auto\"\u003eArch packaging and build files\u003c/p\u003e","updated_at":"2022-07-13T21:32:12.624Z","ci_config_path":null,"public_jobs":true,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"allow_merge_on_skipped_pipeline":null,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","squash_option":"default_off","enforce_auth_checks_on_uploads":true,"suggestion_commit_message":null,"merge_commit_template":null,"squash_commit_template":null,"issue_branch_template":null,"autoclose_referenced_issues":true,"external_authorization_classification_label":"","requirements_enabled":false,"requirements_access_level":"enabled","security_and_compliance_enabled":false,"compliance_frameworks":[],"permissions":{"project_access":null,"group_access":null}} \ No newline at end of file diff --git a/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_version b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_version new file mode 100644 index 0000000000..eb6df2ffb1 --- /dev/null +++ b/services/migrations/testdata/gitlab/skipped_issue_number/_api_v4_version @@ -0,0 +1,22 @@ +Ratelimit-Observed: 1 +X-Gitlab-Meta: {"correlation_id":"aa75720bd9c597c7f2f886a4042d1f80","version":"1"} +Etag: W/"4e5c0a031c3aacb6ba0a3c19e67d7592" +X-Content-Type-Options: nosniff +Ratelimit-Limit: 2000 +Ratelimit-Resettime: Thu, 30 Nov 2023 08:24:53 GMT +X-Runtime: 0.039899 +Ratelimit-Remaining: 1999 +Set-Cookie: _cfuvid=7OAEitQ3J0BOxrXk2pMBApFg1KFnz5aBVqOY7mHwLRk-1701332633452-0-604800000; path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None +Content-Security-Policy: default-src 'none' +Gitlab-Sv: localhost +Cf-Cache-Status: MISS +Vary: Origin, Accept-Encoding +X-Frame-Options: SAMEORIGIN +Cache-Control: max-age=0, private, must-revalidate +Strict-Transport-Security: max-age=31536000 +Referrer-Policy: strict-origin-when-cross-origin +Ratelimit-Reset: 1701332693 +Gitlab-Lb: haproxy-main-39-lb-gprd +Content-Type: application/json + +{"version":"16.7.0-pre","revision":"acd848a9228","kas":{"enabled":true,"externalUrl":"wss://kas.gitlab.com","version":"v16.7.0-rc2"},"enterprise":true} \ No newline at end of file From 49c39f0ed5a011b26f2e33f35811bb31fab3cf64 Mon Sep 17 00:00:00 2001 From: Gusted Date: Fri, 17 Nov 2023 00:17:40 +0100 Subject: [PATCH 31/86] [GITEA] Allow user to select email for file operations in Web UI - Add a dropdown to the web interface for changing files to select which Email should be used for the commit. It only shows (and verifies) that a activated mail can be used, while this isn't necessary, it's better to have this already in place. - Added integration testing. - Resolves https://codeberg.org/forgejo/forgejo/issues/281 (cherry picked from commit 564e701f407c0e110f3c7a4102bf7ed7902b815f) (cherry picked from commit de8f2e03cc7d274049dd6a849b3d226968782644) (cherry picked from commit 0182cff12ed4b68bd49ebc2b9951d9a29f7a36ca) (cherry picked from commit 9c74254d4606febd702315c670db4fb6b14040a1) (cherry picked from commit 2f0b68f821ae53dd12b496cc660353d5bf7cd143) (cherry picked from commit 079b995d49ba7a625035fe9ec53741f6b0112007) (cherry picked from commit 6952ea6ee3de8157d056c4381de7529de6eaef7b) (cherry picked from commit 6c7d5a5d140152be80ec38a979a2a7b704ce653a) --- models/user/email_address.go | 19 ++ models/user/email_address_test.go | 35 ++++ options/locale/locale_en-US.ini | 1 + routers/web/repo/editor.go | 66 ++++++- services/forms/repo_form.go | 1 + services/repository/files/file.go | 4 + templates/repo/editor/commit_form.tmpl | 8 + tests/integration/api_repo_languages_test.go | 11 +- tests/integration/editor_test.go | 174 +++++++++++++++++-- tests/integration/empty_repo_test.go | 9 +- 10 files changed, 303 insertions(+), 25 deletions(-) diff --git a/models/user/email_address.go b/models/user/email_address.go index 2af2621f5f..b795a7e94b 100644 --- a/models/user/email_address.go +++ b/models/user/email_address.go @@ -189,6 +189,25 @@ func GetEmailAddresses(ctx context.Context, uid int64) ([]*EmailAddress, error) return emails, nil } +type ActivatedEmailAddress struct { + ID int64 + Email string +} + +func GetActivatedEmailAddresses(ctx context.Context, uid int64) ([]*ActivatedEmailAddress, error) { + emails := make([]*ActivatedEmailAddress, 0, 8) + if err := db.GetEngine(ctx). + Table("email_address"). + Select("id, email"). + Where("uid=?", uid). + And("is_activated=?", true). + Asc("id"). + Find(&emails); err != nil { + return nil, err + } + return emails, nil +} + // GetEmailAddressByID gets a user's email address by ID func GetEmailAddressByID(ctx context.Context, uid, id int64) (*EmailAddress, error) { // User ID is required for security reasons diff --git a/models/user/email_address_test.go b/models/user/email_address_test.go index 7f3ca75cfd..b20797d700 100644 --- a/models/user/email_address_test.go +++ b/models/user/email_address_test.go @@ -4,6 +4,7 @@ package user_test import ( + "fmt" "testing" "code.gitea.io/gitea/models/db" @@ -309,3 +310,37 @@ func TestEmailAddressValidate(t *testing.T) { }) } } + +func TestGetActivatedEmailAddresses(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + testCases := []struct { + UID int64 + expected []*user_model.ActivatedEmailAddress + }{ + { + UID: 1, + expected: []*user_model.ActivatedEmailAddress{{ID: 9, Email: "user1@example.com"}, {ID: 33, Email: "user1-2@example.com"}, {ID: 34, Email: "user1-3@example.com"}}, + }, + { + UID: 2, + expected: []*user_model.ActivatedEmailAddress{{ID: 3, Email: "user2@example.com"}}, + }, + { + UID: 4, + expected: []*user_model.ActivatedEmailAddress{{ID: 11, Email: "user4@example.com"}}, + }, + { + UID: 11, + expected: []*user_model.ActivatedEmailAddress{}, + }, + } + + for _, testCase := range testCases { + t.Run(fmt.Sprintf("User %d", testCase.UID), func(t *testing.T) { + emails, err := user_model.GetActivatedEmailAddresses(db.DefaultContext, testCase.UID) + assert.NoError(t, err) + assert.Equal(t, testCase.expected, emails) + }) + } +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 79d0a5fe00..a63cf3e1b2 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1251,6 +1251,7 @@ editor.new_branch_name_desc = New branch name… editor.cancel = Cancel editor.filename_cannot_be_empty = The filename cannot be empty. editor.filename_is_invalid = The filename is invalid: "%s". +editor.invalid_commit_mail = Invalid mail for creating a commit. editor.branch_does_not_exist = Branch "%s" does not exist in this repository. editor.branch_already_exists = Branch "%s" already exists in this repository. editor.directory_is_a_file = Directory name "%s" is already used as a filename in this repository. diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 5e7cd1caa3..c19e0aa32d 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -14,6 +14,7 @@ import ( 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/base" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/context" @@ -99,6 +100,27 @@ func getParentTreeFields(treePath string) (treeNames, treePaths []string) { return treeNames, treePaths } +// getSelectableEmailAddresses returns which emails can be used by the user as +// email for a Git commiter. +func getSelectableEmailAddresses(ctx *context.Context) ([]*user_model.ActivatedEmailAddress, error) { + // Retrieve emails that the user could use for commiter identity. + commitEmails, err := user_model.GetActivatedEmailAddresses(ctx, ctx.Doer.ID) + if err != nil { + return nil, fmt.Errorf("GetActivatedEmailAddresses: %w", err) + } + + // Allow for the placeholder mail to be used. Use -1 as ID to identify + // this entry to be the placerholder mail of the user. + placeholderMail := &user_model.ActivatedEmailAddress{ID: -1, Email: ctx.Doer.GetPlaceholderEmail()} + if ctx.Doer.KeepEmailPrivate { + commitEmails = append([]*user_model.ActivatedEmailAddress{placeholderMail}, commitEmails...) + } else { + commitEmails = append(commitEmails, placeholderMail) + } + + return commitEmails, nil +} + func editFile(ctx *context.Context, isNewFile bool) { ctx.Data["PageIsEdit"] = true ctx.Data["IsNewFile"] = isNewFile @@ -177,6 +199,12 @@ func editFile(ctx *context.Context, isNewFile bool) { treeNames = append(treeNames, fileName) } + commitEmails, err := getSelectableEmailAddresses(ctx) + if err != nil { + ctx.ServerError("getSelectableEmailAddresses", err) + return + } + ctx.Data["TreeNames"] = treeNames ctx.Data["TreePaths"] = treePaths ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() @@ -192,6 +220,8 @@ func editFile(ctx *context.Context, isNewFile bool) { ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",") ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath) + ctx.Data["CommitMails"] = commitEmails + ctx.Data["DefaultCommitMail"] = ctx.Doer.GetEmail() ctx.HTML(http.StatusOK, tplEditFile) } @@ -227,6 +257,12 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b branchName = form.NewBranchName } + commitEmails, err := getSelectableEmailAddresses(ctx) + if err != nil { + ctx.ServerError("getSelectableEmailAddresses", err) + return + } + ctx.Data["PageIsEdit"] = true ctx.Data["PageHasPosted"] = true ctx.Data["IsNewFile"] = isNewFile @@ -243,6 +279,8 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",") ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, form.TreePath) + ctx.Data["CommitMails"] = commitEmails + ctx.Data["DefaultCommitMail"] = ctx.Doer.GetEmail() if ctx.HasError() { ctx.HTML(http.StatusOK, tplEditFile) @@ -277,6 +315,30 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b operation = "create" } + gitIdentity := &files_service.IdentityOptions{ + Name: ctx.Doer.Name, + } + + // -1 is defined as placeholder email. + if form.CommitMailID == -1 { + gitIdentity.Email = ctx.Doer.GetPlaceholderEmail() + } else { + // Check if the given email is activated. + email, err := user_model.GetEmailAddressByID(ctx, ctx.Doer.ID, form.CommitMailID) + if err != nil { + ctx.ServerError("GetEmailAddressByID", err) + return + } + + if email == nil || !email.IsActivated { + ctx.Data["Err_CommitMailID"] = true + ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_mail"), tplEditFile, &form) + return + } + + gitIdentity.Email = email.Email + } + if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ LastCommitID: form.LastCommit, OldBranch: ctx.Repo.BranchName, @@ -290,7 +352,9 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b ContentReader: strings.NewReader(strings.ReplaceAll(form.Content, "\r", "")), }, }, - Signoff: form.Signoff, + Signoff: form.Signoff, + Author: gitIdentity, + Committer: gitIdentity, }); err != nil { // This is where we handle all the errors thrown by files_service.ChangeRepoFiles if git.IsErrNotExist(err) { diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 845eccf817..4d3d705330 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -760,6 +760,7 @@ type EditRepoFileForm struct { CommitChoice string `binding:"Required;MaxSize(50)"` NewBranchName string `binding:"GitRefName;MaxSize(100)"` LastCommit string + CommitMailID int64 `binding:"Required"` Signoff bool } diff --git a/services/repository/files/file.go b/services/repository/files/file.go index 16783f5b5f..852cca0371 100644 --- a/services/repository/files/file.go +++ b/services/repository/files/file.go @@ -123,6 +123,8 @@ func GetAuthorAndCommitterUsers(author, committer *IdentityOptions, doer *user_m if committer.Name != "" { committerUser.FullName = committer.Name } + // Use the provided email and not revert to placeholder mail. + committerUser.KeepEmailPrivate = false } else { committerUser = &user_model.User{ FullName: committer.Name, @@ -136,6 +138,8 @@ func GetAuthorAndCommitterUsers(author, committer *IdentityOptions, doer *user_m if authorUser.Name != "" { authorUser.FullName = author.Name } + // Use the provided email and not revert to placeholder mail. + authorUser.KeepEmailPrivate = false } else { authorUser = &user_model.User{ FullName: author.Name, diff --git a/templates/repo/editor/commit_form.tmpl b/templates/repo/editor/commit_form.tmpl index 34dde576a1..6fd240da62 100644 --- a/templates/repo/editor/commit_form.tmpl +++ b/templates/repo/editor/commit_form.tmpl @@ -66,6 +66,14 @@ {{end}} +
+ + +
diff --git a/tests/integration/signup_test.go b/tests/integration/signup_test.go index f983f98ad8..8b0013419f 100644 --- a/tests/integration/signup_test.go +++ b/tests/integration/signup_test.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/tests" @@ -91,3 +92,78 @@ func TestSignupEmail(t *testing.T) { } } } + +func TestSignupEmailChangeForInactiveUser(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + // Disable the captcha & enable email confirmation for registrations + defer test.MockVariableValue(&setting.Service.EnableCaptcha, false)() + defer test.MockVariableValue(&setting.Service.RegisterEmailConfirm, true)() + + // Create user + req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{ + "user_name": "exampleUserX", + "email": "wrong-email@example.com", + "password": "examplePassword!1", + "retype": "examplePassword!1", + }) + MakeRequest(t, req, http.StatusOK) + + session := loginUserWithPassword(t, "exampleUserX", "examplePassword!1") + + // Verify that the initial e-mail is the wrong one. + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "exampleUserX"}) + assert.Equal(t, "wrong-email@example.com", user.Email) + + // Change the email address + req = NewRequestWithValues(t, "POST", "/user/activate", map[string]string{ + "email": "fine-email@example.com", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + // Verify that the email was updated + user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "exampleUserX"}) + assert.Equal(t, "fine-email@example.com", user.Email) + + // Try to change the email again + req = NewRequestWithValues(t, "POST", "/user/activate", map[string]string{ + "email": "wrong-again@example.com", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + // Verify that the email was NOT updated + user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "exampleUserX"}) + assert.Equal(t, "fine-email@example.com", user.Email) +} + +func TestSignupEmailChangeForActiveUser(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + // Disable the captcha & enable email confirmation for registrations + defer test.MockVariableValue(&setting.Service.EnableCaptcha, false)() + defer test.MockVariableValue(&setting.Service.RegisterEmailConfirm, false)() + + // Create user + req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{ + "user_name": "exampleUserY", + "email": "wrong-email-2@example.com", + "password": "examplePassword!1", + "retype": "examplePassword!1", + }) + MakeRequest(t, req, http.StatusSeeOther) + + session := loginUserWithPassword(t, "exampleUserY", "examplePassword!1") + + // Verify that the initial e-mail is the wrong one. + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "exampleUserY"}) + assert.Equal(t, "wrong-email-2@example.com", user.Email) + + // Changing the email for a validated address is not available + req = NewRequestWithValues(t, "POST", "/user/activate", map[string]string{ + "email": "fine-email-2@example.com", + }) + session.MakeRequest(t, req, http.StatusNotFound) + + // Verify that the email remained unchanged + user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "exampleUserY"}) + assert.Equal(t, "wrong-email-2@example.com", user.Email) +} From f39f108934ae964c0efe79a1ccb5080d02e6dc72 Mon Sep 17 00:00:00 2001 From: Gusted Date: Mon, 18 Dec 2023 12:40:05 +0100 Subject: [PATCH 43/86] [GITEA] Add footnote testing - This adds coverage to the most common and the edge cases of what the footnote implementation should be capable of. This was partly done to ensure no hidden surprises when changing the implementation, as markdown rendering is one of the more important features of Forgejo. (cherry picked from commit 16ecdb41705332843921af8d58c1c9a242add95b) (cherry picked from commit 19dc5ef5e5808abe8a5f85d3eaca3317865595ad) (cherry picked from commit d5955efc0a463164c0b3a75b6621974af22ea47f) (cherry picked from commit 2cdaf1083617acbeec558deeb657a1375cbb3904) (cherry picked from commit 251b567794d3437aac614370e4fe2fdf7ad8b917) Conflicts: modules/markup/markdown/markdown_test.go https://codeberg.org/forgejo/forgejo/pulls/2153 (cherry picked from commit f863f4b0054c3310fc487091c353670c65c96f35) --- modules/markup/markdown/markdown_test.go | 198 +++++++++++++++++++++++ 1 file changed, 198 insertions(+) diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 11107f6be9..77afdcbfbd 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -546,6 +546,204 @@ func TestMathBlock(t *testing.T) { } } +func TestFootnote(t *testing.T) { + testcases := []struct { + testcase string + expected string + }{ + { + `Citation needed[^0]. +[^0]: Source`, + `

Citation needed1.

+
+
+
    +
  1. +

    Source ↩︎

    +
  2. +
+
+`, + }, + { + `Citation needed[^0]`, + `

Citation needed[^0]

+`, + }, + { + `Citation needed[^1], Citation needed twice[^3] +[^3]: Source`, + `

Citation needed[^1], Citation needed twice1

+
+
+
    +
  1. +

    Source ↩︎

    +
  2. +
+
+`, + }, + { + `Citation needed[^0] +[^1]: Source`, + `

Citation needed[^0]

+`, + }, + { + `Citation needed[^0] +[^0]: Source 1 +[^0]: Source 2`, + `

Citation needed1

+
+
+
    +
  1. +

    Source 1 ↩︎

    +
  2. +
+
+`, + }, + { + `Citation needed![^0] +[^0]: Source`, + `

Citation needed1

+
+
+
    +
  1. +

    Source ↩︎

    +
  2. +
+
+`, + }, + { + `Trigger [^`, + `

Trigger [^

+`, + }, + { + `Trigger 2 [^0`, + `

Trigger 2 [^0

+`, + }, + { + `Citation needed[^0] +[^0]: Source with citation needed[^1] +[^1]: Source`, + `

Citation needed1

+
+
+
    +
  1. +

    Source with citation needed2 ↩︎

    +
  2. +
  3. +

    Source ↩︎

    +
  4. +
+
+`, + }, + { + `Citation needed[^#] +[^#]: Source`, + `

Citation needed1

+
+
+
    +
  1. +

    Source ↩︎

    +
  2. +
+
+`, + }, + { + `Citation needed[^0] + [^0]: Source`, + `

Citation needed[^0]
+[^0]: Source

+`, + }, + { + `[^0]: Source + +Citation needed[^0].`, + `

Citation needed1.

+
+
+
    +
  1. +

    Source ↩︎

    +
  2. +
+
+`, + }, + { + `Citation needed[^] +[^]: Source`, + `

Citation needed[^]
+[^]: Source

+`, + }, + { + `Citation needed[^0] +[^0] Source`, + `

Citation needed[^0]
+[^0] Source

+`, + }, + { + `Citation needed[^0] +[^0 Source`, + `

Citation needed[^0]
+[^0 Source

+`, + }, + { + `Citation needed[^0] [^0]: Source`, + `

Citation needed[^0] [^0]: Source

+`, + }, + { + `Citation needed[^Source here 0 # 9-3] +[^Source here 0 # 9-3]: Source`, + `

Citation needed1

+
+
+
    +
  1. +

    Source ↩︎

    +
  2. +
+
+`, + }, + { + `Citation needed[^0] +[^0]:`, + `

Citation needed1

+
+
+
    +
  1. + ↩︎
  2. +
+
+`, + }, + } + for _, test := range testcases { + res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase) + assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase) + assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase) + } +} + func TestTaskList(t *testing.T) { testcases := []struct { testcase string From 5d1856717b16e22ec68a277e59094e6771a5db7d Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Wed, 13 Dec 2023 15:21:17 +0100 Subject: [PATCH 44/86] [GITEA] the ref of a scheduled action is always the default branch Since a scheduled action is only run from the default branch, it cannot be anything else. Refs: https://codeberg.org/forgejo/forgejo/issues/1926 (cherry picked from commit eff0822856fd727915f6e6493a80844cffd7b02a) (cherry picked from commit 2b1aa50bd14510d5eaf8db2c98ff4c604abe69e7) Conflicts: services/actions/notifier_helper.go https://codeberg.org/forgejo/forgejo/pulls/2075 (cherry picked from commit 4ff3474fc05529367d8d9e7de988166bcf924bd7) (cherry picked from commit 07b888703102762b5608a4232331febbd4fc6849) (cherry picked from commit cbecdd618d1bc03491bcaf4f07357a6ee04be449) --- services/actions/notifier_helper.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 2a3ffb76f3..6afc8aa9d4 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -461,7 +461,7 @@ func handleSchedules( OwnerID: input.Repo.OwnerID, WorkflowID: dwf.EntryName, TriggerUserID: input.Doer.ID, - Ref: ref, + Ref: input.Repo.DefaultBranch, CommitSHA: commit.ID.String(), Event: input.Event, EventPayload: string(p), From d31ce2f03d69cc784e53e921968c714986a7a4ef Mon Sep 17 00:00:00 2001 From: Gusted Date: Mon, 18 Dec 2023 18:14:04 +0100 Subject: [PATCH 45/86] [GITEA] Fix NPE in `UsernameSubRoute` - When the user is not found in `reloadparam`, early return when the user is not found to avoid calling `IsUserVisibleToViewer` which in turn avoids causing a NPE. - This fixes the case that a 500 error and 404 error is shown on the same page. - Add integration test for non-existant user RSS. - Regression by c6366089df8390bc1f017006caaf4d4c69825880 (cherry picked from commit f0e06962786ef8c417b0c6f07940c1909d3b91ba) (cherry picked from commit 75d806690875a4fc38eb1e3c904096be34657011) (cherry picked from commit 4d0a1e0637450865c7bbac69e42d92d63b95149c) (cherry picked from commit 5f40a485da1b2c5f129f32e2ddc2065e3ba9ccd0) (cherry picked from commit c4cb7812e39add6f7ff3d6f3f2d4e02c66435f0e) --- routers/web/user/home.go | 5 ++++- tests/integration/user_test.go | 29 +++++++++++++++++++---------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 44920817c9..79af74eb02 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -715,12 +715,15 @@ func UsernameSubRoute(ctx *context.Context) { reloadParam := func(suffix string) (success bool) { ctx.SetParams("username", strings.TrimSuffix(username, suffix)) context_service.UserAssignmentWeb()(ctx) + if ctx.Written() { + return false + } // check view permissions if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) { ctx.NotFound("user", fmt.Errorf(ctx.ContextUser.Name)) return false } - return !ctx.Written() + return true } switch { case strings.HasSuffix(username, ".png"): diff --git a/tests/integration/user_test.go b/tests/integration/user_test.go index d8e4c64e85..0defa109ae 100644 --- a/tests/integration/user_test.go +++ b/tests/integration/user_test.go @@ -243,16 +243,25 @@ func testExportUserGPGKeys(t *testing.T, user, expected string) { } func TestGetUserRss(t *testing.T) { - user34 := "the_34-user.with.all.allowedChars" - req := NewRequestf(t, "GET", "/%s.rss", user34) - resp := MakeRequest(t, req, http.StatusOK) - if assert.EqualValues(t, "application/rss+xml;charset=utf-8", resp.Header().Get("Content-Type")) { - rssDoc := NewHTMLParser(t, resp.Body).Find("channel") - title, _ := rssDoc.ChildrenFiltered("title").Html() - assert.EqualValues(t, "Feed of "the_1-user.with.all.allowedChars"", title) - description, _ := rssDoc.ChildrenFiltered("description").Html() - assert.EqualValues(t, "<p dir="auto">some <a href="https://commonmark.org/" rel="nofollow">commonmark</a>!</p>\n", description) - } + defer tests.PrepareTestEnv(t)() + + t.Run("Normal", func(t *testing.T) { + user34 := "the_34-user.with.all.allowedChars" + req := NewRequestf(t, "GET", "/%s.rss", user34) + resp := MakeRequest(t, req, http.StatusOK) + if assert.EqualValues(t, "application/rss+xml;charset=utf-8", resp.Header().Get("Content-Type")) { + rssDoc := NewHTMLParser(t, resp.Body).Find("channel") + title, _ := rssDoc.ChildrenFiltered("title").Html() + assert.EqualValues(t, "Feed of "the_1-user.with.all.allowedChars"", title) + description, _ := rssDoc.ChildrenFiltered("description").Html() + assert.EqualValues(t, "<p dir="auto">some <a href="https://commonmark.org/" rel="nofollow">commonmark</a>!</p>\n", description) + } + }) + t.Run("Non-existent user", func(t *testing.T) { + session := loginUser(t, "user2") + req := NewRequestf(t, "GET", "/non-existent-user.rss") + session.MakeRequest(t, req, http.StatusNotFound) + }) } func TestListStopWatches(t *testing.T) { From 9ed1487b73babe44d0b2855cc708184c55671ab0 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Sat, 23 Dec 2023 12:17:20 +0100 Subject: [PATCH 46/86] [ACTIONS] on.schedule: create a new payload do not reuse the payload of the event that triggered the creation of the scheduled event. Create a new one instead that contains no other information than the event name in the action field ("schedule"). (cherry picked from commit 0b40ca1ea5e6b704bcb6c0d370a21f633facc7d6) (cherry picked from commit f86487432b3b5f2fd4e2bb0a2d737674d9a105a6) (cherry picked from commit 4bd5d2e9d0c7987a9d7cce495509c8790dcdcd3a) (cherry picked from commit d10830e238f35bcd0100a4de68d68b15402ec05a) (cherry picked from commit 53f5a3aa911fb63689ef018fe583eeb03f248517) --- modules/structs/hook.go | 10 ++++++++++ services/actions/notifier_helper.go | 6 +++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/modules/structs/hook.go b/modules/structs/hook.go index 0babe84410..e8944c1130 100644 --- a/modules/structs/hook.go +++ b/modules/structs/hook.go @@ -402,6 +402,16 @@ func (p *PullRequestPayload) JSONPayload() ([]byte, error) { return json.MarshalIndent(p, "", " ") } +type HookScheduleAction string + +const ( + HookScheduleCreated HookScheduleAction = "schedule" +) + +type SchedulePayload struct { + Action HookScheduleAction `json:"action"` +} + // ReviewPayload FIXME type ReviewPayload struct { Type string `json:"type"` diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 6afc8aa9d4..2419301384 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -436,7 +436,11 @@ func handleSchedules( return nil } - p, err := json.Marshal(input.Payload) + payload := &api.SchedulePayload{ + Action: api.HookScheduleCreated, + } + + p, err := json.Marshal(payload) if err != nil { return fmt.Errorf("json.Marshal: %w", err) } From 7752ff8baa525918e00193606048e3c2dd5a4999 Mon Sep 17 00:00:00 2001 From: Gusted Date: Wed, 27 Dec 2023 23:22:06 +0100 Subject: [PATCH 47/86] [GITEA] Fix session generation for database - If the session doesn't exist, it shouldn't be expected that the variable is non-nil. Define the session variable instead and insert that. - Add unit tests to test the behavior of the database sessions code . - Regression caused by dd30d9d5c0f577cb6e084aae6de2752ad43474d8. - Resolves https://codeberg.org/forgejo/forgejo/issues/2042 (cherry picked from commit 90307ad2004a9a9ddda30af4038224fedf0e6ca3) (cherry picked from commit 874ef1978d7db5e8ba1482d4c8190b914fa110b3) (cherry picked from commit 27d5f035fc744d932d1e4c95c55d98479fccf368) (cherry picked from commit 65dbc4303ba8afdef70c573aaf782b76aaf0bbad) [GITEA] Fix session generation for database (squash) timeutil.Mock because of e743570f65 * Refactor timeutil package (#28623) (cherry picked from commit acc6b51be2b6d676129f653a8949b2c06aa2ad94) (cherry picked from commit 02b74317f2d8120a705599d6ae908634a1fa2b44) (cherry picked from commit 63b9b624bd203b7b5eff7439dbc09eeb9bc52ade) --- models/auth/session_test.go | 142 ++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 models/auth/session_test.go diff --git a/models/auth/session_test.go b/models/auth/session_test.go new file mode 100644 index 0000000000..3475fdd2cd --- /dev/null +++ b/models/auth/session_test.go @@ -0,0 +1,142 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package auth_test + +import ( + "testing" + "time" + + "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/timeutil" + + "github.com/stretchr/testify/assert" +) + +func TestAuthSession(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + defer timeutil.MockUnset() + + key := "I-Like-Free-Software" + + t.Run("Create Session", func(t *testing.T) { + // Ensure it doesn't exist. + ok, err := auth.ExistSession(db.DefaultContext, key) + assert.NoError(t, err) + assert.False(t, ok) + + preCount, err := auth.CountSessions(db.DefaultContext) + assert.NoError(t, err) + + now := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) + timeutil.MockSet(now) + + // New session is created. + sess, err := auth.ReadSession(db.DefaultContext, key) + assert.NoError(t, err) + assert.EqualValues(t, key, sess.Key) + assert.Empty(t, sess.Data) + assert.EqualValues(t, now.Unix(), sess.Expiry) + + // Ensure it exists. + ok, err = auth.ExistSession(db.DefaultContext, key) + assert.NoError(t, err) + assert.True(t, ok) + + // Ensure the session is taken into account for count.. + postCount, err := auth.CountSessions(db.DefaultContext) + assert.NoError(t, err) + assert.Greater(t, postCount, preCount) + }) + + t.Run("Update session", func(t *testing.T) { + data := []byte{0xba, 0xdd, 0xc0, 0xde} + now := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC) + timeutil.MockSet(now) + + // Update session. + err := auth.UpdateSession(db.DefaultContext, key, data) + assert.NoError(t, err) + + timeutil.MockSet(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)) + + // Read updated session. + // Ensure data is updated and expiry is set from the update session call. + sess, err := auth.ReadSession(db.DefaultContext, key) + assert.NoError(t, err) + assert.EqualValues(t, key, sess.Key) + assert.EqualValues(t, data, sess.Data) + assert.EqualValues(t, now.Unix(), sess.Expiry) + + timeutil.MockSet(now) + }) + + t.Run("Delete session", func(t *testing.T) { + // Ensure it't exist. + ok, err := auth.ExistSession(db.DefaultContext, key) + assert.NoError(t, err) + assert.True(t, ok) + + preCount, err := auth.CountSessions(db.DefaultContext) + assert.NoError(t, err) + + err = auth.DestroySession(db.DefaultContext, key) + assert.NoError(t, err) + + // Ensure it doens't exists. + ok, err = auth.ExistSession(db.DefaultContext, key) + assert.NoError(t, err) + assert.False(t, ok) + + // Ensure the session is taken into account for count.. + postCount, err := auth.CountSessions(db.DefaultContext) + assert.NoError(t, err) + assert.Less(t, postCount, preCount) + }) + + t.Run("Cleanup sessions", func(t *testing.T) { + timeutil.MockSet(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)) + + _, err := auth.ReadSession(db.DefaultContext, "sess-1") + assert.NoError(t, err) + + // One minute later. + timeutil.MockSet(time.Date(2023, 1, 1, 0, 1, 0, 0, time.UTC)) + _, err = auth.ReadSession(db.DefaultContext, "sess-2") + assert.NoError(t, err) + + // 5 minutes, shouldn't clean up anything. + err = auth.CleanupSessions(db.DefaultContext, 5*60) + assert.NoError(t, err) + + ok, err := auth.ExistSession(db.DefaultContext, "sess-1") + assert.NoError(t, err) + assert.True(t, ok) + + ok, err = auth.ExistSession(db.DefaultContext, "sess-2") + assert.NoError(t, err) + assert.True(t, ok) + + // 1 minute, should clean up sess-1. + err = auth.CleanupSessions(db.DefaultContext, 60) + assert.NoError(t, err) + + ok, err = auth.ExistSession(db.DefaultContext, "sess-1") + assert.NoError(t, err) + assert.False(t, ok) + + ok, err = auth.ExistSession(db.DefaultContext, "sess-2") + assert.NoError(t, err) + assert.True(t, ok) + + // Now, should clean up sess-2. + err = auth.CleanupSessions(db.DefaultContext, 0) + assert.NoError(t, err) + + ok, err = auth.ExistSession(db.DefaultContext, "sess-2") + assert.NoError(t, err) + assert.False(t, ok) + }) +} From 42c55e494e1018a210e54d405c15eec24a0b37c7 Mon Sep 17 00:00:00 2001 From: Gergely Nagy Date: Wed, 20 Dec 2023 21:44:55 +0100 Subject: [PATCH 48/86] [GITEA] Optionally allow anyone to edit Wikis This is largely based on gitea#6312 by @ashimokawa, with updates and fixes by myself, and incorporates the review feedback given in that pull request, and more. What this patch does is add a new "default_permissions" column to the `repo_units` table (defaulting to read permission), adjusts the permission checking code to take this into consideration, and then exposes a setting that lets a repo administrator enable any user on a Forgejo instance to edit the repo's wiki (effectively giving the wiki unit of the repo "write" permissions by default). By default, wikis will remain restricted to collaborators, but with the new setting exposed, they can be turned into globally editable wikis. Fixes Codeberg/Community#28. Signed-off-by: Gergely Nagy (cherry picked from commit 4b744399229f255eb124c22e3969715046043209) (cherry picked from commit 337cf62c1094273ab61fbaab8e7fb41eb6e2e979) (cherry picked from commit b6786fdb3246a3a455b59149943807c1f13a028a) (cherry picked from commit a5d2829a1027afd593fd855a8e2d7d7cd38234b8) [GITEA] Optionally allow anyone to edit Wikis (squash) AddTokenAuth (cherry picked from commit fed50cf72eaaa00ef1f4730f9b12a64a10b66113) --- models/forgejo_migrations/migrate.go | 3 ++ models/forgejo_migrations/v1_22/v4.go | 17 +++++++++ models/perm/access/repo_permission.go | 28 +++++++++++++-- models/repo/repo_unit.go | 41 ++++++++++++++++++--- models/repo/repo_unit_test.go | 9 +++++ options/locale/locale_en-US.ini | 1 + routers/web/repo/setting/setting.go | 13 +++++-- services/forms/repo_form.go | 1 + templates/repo/settings/options.tmpl | 10 ++++++ tests/integration/api_wiki_test.go | 52 +++++++++++++++++++++++++++ 10 files changed, 165 insertions(+), 10 deletions(-) create mode 100644 models/forgejo_migrations/v1_22/v4.go diff --git a/models/forgejo_migrations/migrate.go b/models/forgejo_migrations/migrate.go index 58f158bd17..8ac2fb63f2 100644 --- a/models/forgejo_migrations/migrate.go +++ b/models/forgejo_migrations/migrate.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models/forgejo/semver" forgejo_v1_20 "code.gitea.io/gitea/models/forgejo_migrations/v1_20" + forgejo_v1_22 "code.gitea.io/gitea/models/forgejo_migrations/v1_22" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -43,6 +44,8 @@ var migrations = []*Migration{ NewMigration("create the forgejo_sem_ver table", forgejo_v1_20.CreateSemVerTable), // v2 -> v3 NewMigration("create the forgejo_auth_token table", forgejo_v1_20.CreateAuthorizationTokenTable), + // v3 -> v4 + NewMigration("Add default_permissions to repo_unit", forgejo_v1_22.AddDefaultPermissionsToRepoUnit), } // GetCurrentDBVersion returns the current Forgejo database version. diff --git a/models/forgejo_migrations/v1_22/v4.go b/models/forgejo_migrations/v1_22/v4.go new file mode 100644 index 0000000000..f1195f5f66 --- /dev/null +++ b/models/forgejo_migrations/v1_22/v4.go @@ -0,0 +1,17 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "xorm.io/xorm" +) + +func AddDefaultPermissionsToRepoUnit(x *xorm.Engine) error { + type RepoUnit struct { + ID int64 + DefaultPermissions int `xorm:"NOT NULL DEFAULT 0"` + } + + return x.Sync(&RepoUnit{}) +} diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 395ecdf1a5..0b66e62d7d 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -33,6 +33,16 @@ func (p *Permission) IsAdmin() bool { return p.AccessMode >= perm_model.AccessModeAdmin } +// IsGloballyWriteable returns true if the unit is writeable by all users of the instance. +func (p *Permission) IsGloballyWriteable(unitType unit.Type) bool { + for _, u := range p.Units { + if u.Type == unitType { + return u.DefaultPermissions == repo_model.UnitAccessModeWrite + } + } + return false +} + // HasAccess returns true if the current user has at least read access to any unit of this repository func (p *Permission) HasAccess() bool { if p.UnitsMode == nil { @@ -198,7 +208,19 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use if err := repo.LoadOwner(ctx); err != nil { return perm, err } + if !repo.Owner.IsOrganization() { + // for a public repo, different repo units may have different default + // permissions for non-restricted users. + if !repo.IsPrivate && !user.IsRestricted && len(repo.Units) > 0 { + perm.UnitsMode = make(map[unit.Type]perm_model.AccessMode) + for _, u := range repo.Units { + if _, ok := perm.UnitsMode[u.Type]; !ok { + perm.UnitsMode[u.Type] = u.DefaultPermissions.ToAccessMode(perm.AccessMode) + } + } + } + return perm, nil } @@ -239,10 +261,12 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use } } - // for a public repo on an organization, a non-restricted user has read permission on non-team defined units. + // for a public repo on an organization, a non-restricted user should + // have the same permission on non-team defined units as the default + // permissions for the repo unit. if !found && !repo.IsPrivate && !user.IsRestricted { if _, ok := perm.UnitsMode[u.Type]; !ok { - perm.UnitsMode[u.Type] = perm_model.AccessModeRead + perm.UnitsMode[u.Type] = u.DefaultPermissions.ToAccessMode(perm_model.AccessModeRead) } } } diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index 8a3ba1ee89..b55d3e5de5 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -10,6 +10,7 @@ import ( "strings" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" @@ -39,13 +40,43 @@ func (err ErrUnitTypeNotExist) Unwrap() error { return util.ErrNotExist } +// RepoUnitAccessMode specifies the users access mode to a repo unit +type UnitAccessMode int + +const ( + // UnitAccessModeUnset - no unit mode set + UnitAccessModeUnset UnitAccessMode = iota // 0 + // UnitAccessModeNone no access + UnitAccessModeNone // 1 + // UnitAccessModeRead read access + UnitAccessModeRead // 2 + // UnitAccessModeWrite write access + UnitAccessModeWrite // 3 +) + +func (mode UnitAccessMode) ToAccessMode(modeIfUnset perm.AccessMode) perm.AccessMode { + switch mode { + case UnitAccessModeUnset: + return modeIfUnset + case UnitAccessModeNone: + return perm.AccessModeNone + case UnitAccessModeRead: + return perm.AccessModeRead + case UnitAccessModeWrite: + return perm.AccessModeWrite + default: + return perm.AccessModeNone + } +} + // RepoUnit describes all units of a repository type RepoUnit struct { //revive:disable-line:exported - ID int64 - RepoID int64 `xorm:"INDEX(s)"` - Type unit.Type `xorm:"INDEX(s)"` - Config convert.Conversion `xorm:"TEXT"` - CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"` + ID int64 + RepoID int64 `xorm:"INDEX(s)"` + Type unit.Type `xorm:"INDEX(s)"` + Config convert.Conversion `xorm:"TEXT"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"` + DefaultPermissions UnitAccessMode `xorm:"NOT NULL DEFAULT 0"` } func init() { diff --git a/models/repo/repo_unit_test.go b/models/repo/repo_unit_test.go index a760594013..27a34fd0eb 100644 --- a/models/repo/repo_unit_test.go +++ b/models/repo/repo_unit_test.go @@ -6,6 +6,8 @@ package repo import ( "testing" + "code.gitea.io/gitea/models/perm" + "github.com/stretchr/testify/assert" ) @@ -28,3 +30,10 @@ func TestActionsConfig(t *testing.T) { cfg.DisableWorkflow("test3.yaml") assert.EqualValues(t, "test1.yaml,test2.yaml,test3.yaml", cfg.ToString()) } + +func TestRepoUnitAccessMode(t *testing.T) { + assert.Equal(t, UnitAccessModeNone.ToAccessMode(perm.AccessModeAdmin), perm.AccessModeNone) + assert.Equal(t, UnitAccessModeRead.ToAccessMode(perm.AccessModeAdmin), perm.AccessModeRead) + assert.Equal(t, UnitAccessModeWrite.ToAccessMode(perm.AccessModeAdmin), perm.AccessModeWrite) + assert.Equal(t, UnitAccessModeUnset.ToAccessMode(perm.AccessModeRead), perm.AccessModeRead) +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index c9cc562625..cbddd3d537 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2044,6 +2044,7 @@ settings.branches.update_default_branch = Update Default Branch settings.branches.add_new_rule = Add New Rule settings.advanced_settings = Advanced Settings settings.wiki_desc = Enable Repository Wiki +settings.wiki_globally_editable = Allow anyone to edit the Wiki settings.use_internal_wiki = Use Built-In Wiki settings.use_external_wiki = Use External Wiki settings.external_wiki_url = External Wiki URL diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index bdcd23b082..0334add2ea 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -473,10 +473,17 @@ func SettingsPost(ctx *context.Context) { }) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) } else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() { + var wikiPermissions repo_model.UnitAccessMode + if form.GloballyWriteableWiki { + wikiPermissions = repo_model.UnitAccessModeWrite + } else { + wikiPermissions = repo_model.UnitAccessModeRead + } units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeWiki, - Config: new(repo_model.UnitConfig), + RepoID: repo.ID, + Type: unit_model.TypeWiki, + Config: new(repo_model.UnitConfig), + DefaultPermissions: wikiPermissions, }) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) } else { diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 4d3d705330..7cc07532ef 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -132,6 +132,7 @@ type RepoSettingForm struct { // Advanced settings EnableCode bool EnableWiki bool + GloballyWriteableWiki bool EnableExternalWiki bool ExternalWikiURL string EnableIssues bool diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index b6ad3aacfa..f8a3270e9b 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -318,6 +318,16 @@ + {{if (not .Repository.IsPrivate)}} +
+
+
+ + +
+
+
+ {{end}}
diff --git a/tests/integration/api_wiki_test.go b/tests/integration/api_wiki_test.go index 05d90fc4e3..19fd8c5365 100644 --- a/tests/integration/api_wiki_test.go +++ b/tests/integration/api_wiki_test.go @@ -4,13 +4,18 @@ package integration import ( + "context" "encoding/base64" "fmt" "net/http" "testing" auth_model "code.gitea.io/gitea/models/auth" + repo_model "code.gitea.io/gitea/models/repo" + unit_model "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/models/unittest" api "code.gitea.io/gitea/modules/structs" + repo_service "code.gitea.io/gitea/services/repository" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -209,6 +214,53 @@ func TestAPIEditWikiPage(t *testing.T) { MakeRequest(t, req, http.StatusOK) } +func TestAPIEditOtherWikiPage(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + // (drive-by-user) user, session, and token for a drive-by wiki editor + username := "drive-by-user" + req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{ + "user_name": username, + "email": "drive-by@example.com", + "password": "examplePassword!1", + "retype": "examplePassword!1", + }) + MakeRequest(t, req, http.StatusSeeOther) + session := loginUserWithPassword(t, username, "examplePassword!1") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + // (user2) user for the user whose wiki we're going to edit (as drive-by-user) + otherUsername := "user2" + + // Creating a new Wiki page on user2's repo as user1 fails + testCreateWiki := func(expectedStatusCode int) { + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/new", otherUsername, "repo1") + req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateWikiPageOptions{ + Title: "Globally Edited Page", + ContentBase64: base64.StdEncoding.EncodeToString([]byte("Wiki page content for API unit tests")), + Message: "", + }).AddTokenAuth(token) + session.MakeRequest(t, req, expectedStatusCode) + } + testCreateWiki(http.StatusForbidden) + + // Update the repo settings for user2's repo to enable globally writeable wiki + ctx := context.Background() + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + var units []repo_model.RepoUnit + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeWiki, + Config: new(repo_model.UnitConfig), + DefaultPermissions: repo_model.UnitAccessModeWrite, + }) + err := repo_service.UpdateRepositoryUnits(ctx, repo, units, nil) + assert.NoError(t, err) + + // Creating a new Wiki page on user2's repo works now + testCreateWiki(http.StatusCreated) +} + func TestAPIListPageRevisions(t *testing.T) { defer tests.PrepareTestEnv(t)() username := "user2" From 7b70fa9392cc03121c798407363712d6e5dde536 Mon Sep 17 00:00:00 2001 From: Gusted Date: Fri, 22 Dec 2023 17:20:50 +0100 Subject: [PATCH 49/86] [GITEA] Fix /issues/search endpoint - The endpoint was moved from being an API endpoint to an web endpoint with JSON result. However the API context isn't the same as the web context, for example the `ctx.Error` only takes in the first two arguments into consideration and doesn't do logging, which is not the same behavior as the API context where there's three arguments and does do logging and only reveal the function + error if the user is admin. - Remove any details in the error message and do the logging seperatly, this is somewhat consistent with how other API endpoints behave. - Ref: https://codeberg.org/forgejo/forgejo/issues/1998 (cherry picked from commit fe71e32ace98461398cffe55f99ad31dc1be0b4e) (cherry picked from commit c89e0735fab6b3994ff1769afafb012d1147972f) (cherry picked from commit 4c04dcfc59c1a23b990f9a81c73de7cbfd95d1e3) (cherry picked from commit 66eae1041c3b6bd4f15bbbaf552678313bcae835) --- routers/web/repo/issue.go | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index c8c9924a9e..9da62bb00b 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -2501,7 +2501,8 @@ func UpdatePullReviewRequest(ctx *context.Context) { func SearchIssues(ctx *context.Context) { before, since, err := context.GetQueryBeforeSince(ctx.Base) if err != nil { - ctx.Error(http.StatusUnprocessableEntity, err.Error()) + log.Error("GetQueryBeforeSince: %v", err) + ctx.Error(http.StatusUnprocessableEntity, "invalid before or since") return } @@ -2538,10 +2539,11 @@ func SearchIssues(ctx *context.Context) { if ctx.FormString("owner") != "" { owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner")) if err != nil { + log.Error("GetUserByName: %v", err) if user_model.IsErrUserNotExist(err) { ctx.Error(http.StatusBadRequest, "Owner not found", err.Error()) } else { - ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error()) + ctx.Error(http.StatusInternalServerError) } return } @@ -2552,15 +2554,16 @@ func SearchIssues(ctx *context.Context) { } if ctx.FormString("team") != "" { if ctx.FormString("owner") == "" { - ctx.Error(http.StatusBadRequest, "", "Owner organisation is required for filtering on team") + ctx.Error(http.StatusBadRequest, "Owner organisation is required for filtering on team") return } team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team")) if err != nil { + log.Error("GetTeam: %v", err) if organization.IsErrTeamNotExist(err) { - ctx.Error(http.StatusBadRequest, "Team not found", err.Error()) + ctx.Error(http.StatusBadRequest) } else { - ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error()) + ctx.Error(http.StatusInternalServerError) } return } @@ -2573,7 +2576,8 @@ func SearchIssues(ctx *context.Context) { } repoIDs, _, err = repo_model.SearchRepositoryIDs(ctx, opts) if err != nil { - ctx.Error(http.StatusInternalServerError, "SearchRepositoryIDs", err.Error()) + log.Error("SearchRepositoryIDs: %v", err) + ctx.Error(http.StatusInternalServerError) return } if len(repoIDs) == 0 { @@ -2607,7 +2611,8 @@ func SearchIssues(ctx *context.Context) { } includedAnyLabels, err = issues_model.GetLabelIDsByNames(ctx, includedLabelNames) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetLabelIDsByNames", err.Error()) + log.Error("GetLabelIDsByNames: %v", err) + ctx.Error(http.StatusInternalServerError) return } } @@ -2621,7 +2626,8 @@ func SearchIssues(ctx *context.Context) { } includedMilestones, err = issues_model.GetMilestoneIDsByNames(ctx, includedMilestoneNames) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetMilestoneIDsByNames", err.Error()) + log.Error("GetMilestoneIDsByNames: %v", err) + ctx.Error(http.StatusInternalServerError) return } } @@ -2688,12 +2694,14 @@ func SearchIssues(ctx *context.Context) { ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt) if err != nil { - ctx.Error(http.StatusInternalServerError, "SearchIssues", err.Error()) + log.Error("SearchIssues: %v", err) + ctx.Error(http.StatusInternalServerError) return } issues, err := issues_model.GetIssuesByIDs(ctx, ids, true) if err != nil { - ctx.Error(http.StatusInternalServerError, "FindIssuesByIDs", err.Error()) + log.Error("GetIssuesByIDs: %v", err) + ctx.Error(http.StatusInternalServerError) return } From 23c887f97eeb08ee2a318b28878edb488428f98d Mon Sep 17 00:00:00 2001 From: Gusted Date: Sun, 17 Dec 2023 16:39:58 +0100 Subject: [PATCH 50/86] [GITEA] Avoid `WHERE IN` for comment migration query - Rewrite `UpdateCommentsMigrationsByType` to not use `WHERE IN` as that's a performance diaster for MariaDB, it now use batching to query the the relevant comment IDs via JOINs (which is not possible in a UPDATE query for SQLite) and then update them in a seperate query. - Add unit test. - Resolves https://codeberg.org/forgejo/forgejo/issues/1856 (cherry picked from commit 8098ca9d2e391b17e5e3da5cfa5af042221bfe36) Conflicts: models/issues/comment.go https://codeberg.org/forgejo/forgejo/pulls/2075 (cherry picked from commit ca65deba1cc183ce1643ee6a1f698c5ecb2ac571) (cherry picked from commit 0e1e09e77dd1bc82b1eae02147fddca1d9954469) (cherry picked from commit 19013ba5eac756044e6307abee6fb5d6709c855d) --- models/issues/comment_test.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/models/issues/comment_test.go b/models/issues/comment_test.go index c5bbfdedc2..e08bd7fbf5 100644 --- a/models/issues/comment_test.go +++ b/models/issues/comment_test.go @@ -12,6 +12,7 @@ import ( 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/structs" "github.com/stretchr/testify/assert" ) @@ -97,3 +98,29 @@ func TestMigrate_InsertIssueComments(t *testing.T) { unittest.CheckConsistencyFor(t, &issues_model.Issue{}) } + +func TestUpdateCommentsMigrationsByType(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) + comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1, IssueID: issue.ID}) + + // Set repository to migrated from Gitea. + repo.OriginalServiceType = structs.GiteaService + repo_model.UpdateRepositoryCols(db.DefaultContext, repo, "original_service_type") + + // Set comment to have an original author. + comment.OriginalAuthor = "Example User" + comment.OriginalAuthorID = 1 + comment.PosterID = 0 + _, err := db.GetEngine(db.DefaultContext).ID(comment.ID).Cols("original_author", "original_author_id", "poster_id").Update(comment) + assert.NoError(t, err) + + assert.NoError(t, issues_model.UpdateCommentsMigrationsByType(db.DefaultContext, structs.GiteaService, "1", 513)) + + comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1, IssueID: issue.ID}) + assert.Empty(t, comment.OriginalAuthor) + assert.Empty(t, comment.OriginalAuthorID) + assert.EqualValues(t, 513, comment.PosterID) +} From 1388e7c7bef7a34018b993c24b34e053849eb93a Mon Sep 17 00:00:00 2001 From: Antonin Delpeuch Date: Tue, 19 Dec 2023 22:18:07 +0100 Subject: [PATCH 51/86] [GITEA] pulls: "Edit File" button in "Files Changed" tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1894. Gitea issue: https://github.com/go-gitea/gitea/issues/23848 (cherry picked from commit 79c75164ca70937261b1d9a68420ebfdbdcfa4d4) (cherry picked from commit 58c76aad8f624d7701e3fa6c12264328962cdf58) (cherry picked from commit 5bdb3c6c53527da23ba76a8289ca6a81c6fcecdf) (cherry picked from commit 94e954ce2248f14082f0c3071cc076c118c4a791) --- routers/web/repo/pull.go | 12 ++++++++++ templates/repo/diff/box.tmpl | 3 +++ tests/integration/forgejo_git_test.go | 2 +- tests/integration/git_test.go | 34 +++++++++++++++++++++------ 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index e36d7092af..d58878696d 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -966,6 +966,18 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi return } + // determine if the user viewing the pull request can edit the head branch + if ctx.Doer != nil && pull.HeadRepo != nil && !pull.HasMerged { + headRepoPerm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer) + if err != nil { + ctx.ServerError("GetUserRepoPermission", err) + return + } + ctx.Data["HeadBranchIsEditable"] = pull.HeadRepo.CanEnableEditor() && issues_model.CanMaintainerWriteToBranch(ctx, headRepoPerm, pull.HeadBranch, ctx.Doer) + ctx.Data["SourceRepoLink"] = pull.HeadRepo.Link() + ctx.Data["HeadBranch"] = pull.HeadBranch + } + if ctx.IsSigned && ctx.Doer != nil { if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, issue, ctx.Doer); err != nil { ctx.ServerError("CanMarkConversation", err) diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index be7c7e80f2..05559fc9a7 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -166,6 +166,9 @@ {{ctx.Locale.Tr "repo.diff.view_file"}} {{else}} {{ctx.Locale.Tr "repo.diff.view_file"}} + {{if and $.HeadBranchIsEditable (not $file.IsLFSFile)}} + {{ctx.Locale.Tr "repo.editor.edit_this_file"}} + {{end}} {{end}} {{end}} {{if $isReviewFile}} diff --git a/tests/integration/forgejo_git_test.go b/tests/integration/forgejo_git_test.go index f81521d735..bd0fb207eb 100644 --- a/tests/integration/forgejo_git_test.go +++ b/tests/integration/forgejo_git_test.go @@ -134,6 +134,6 @@ func doActionsUserPR(ctx, doerCtx APITestContext, baseBranch, headBranch string) doerCtx.ExpectedCode = http.StatusCreated t.Run("AutoMergePR", doAPIAutoMergePullRequest(doerCtx, ctx.Username, ctx.Reponame, pr.Index)) // Ensure the PR page works - t.Run("EnsureCanSeePull", doEnsureCanSeePull(ctx, pr)) + t.Run("EnsureCanSeePull", doEnsureCanSeePull(ctx, pr, true)) } } diff --git a/tests/integration/git_test.go b/tests/integration/git_test.go index 0c3a8616f0..0afe9fa580 100644 --- a/tests/integration/git_test.go +++ b/tests/integration/git_test.go @@ -455,8 +455,19 @@ func doMergeFork(ctx, baseCtx APITestContext, baseBranch, headBranch string) fun assert.NoError(t, err) }) - // Ensure the PR page works - t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr)) + // Ensure the PR page works. + // For the base repository owner, the PR is not editable (maintainer edits are not enabled): + t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr, false)) + // For the head repository owner, the PR is editable: + headSession := loginUser(t, "user2") + headToken := getTokenForLoggedInUser(t, headSession, auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopeReadUser) + headCtx := APITestContext{ + Session: headSession, + Token: headToken, + Username: baseCtx.Username, + Reponame: baseCtx.Reponame, + } + t.Run("EnsureCanSeePull", doEnsureCanSeePull(headCtx, pr, true)) // Then get the diff string var diffHash string @@ -470,7 +481,9 @@ func doMergeFork(ctx, baseCtx APITestContext, baseBranch, headBranch string) fun // Now: Merge the PR & make sure that doesn't break the PR page or change its diff t.Run("MergePR", doAPIMergePullRequest(baseCtx, baseCtx.Username, baseCtx.Reponame, pr.Index)) - t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr)) + // for both users the PR is still visible but not editable anymore after it was merged + t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr, false)) + t.Run("EnsureCanSeePull", doEnsureCanSeePull(headCtx, pr, false)) t.Run("CheckPR", func(t *testing.T) { oldMergeBase := pr.MergeBase pr2, err := doAPIGetPullRequest(baseCtx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t) @@ -481,12 +494,12 @@ func doMergeFork(ctx, baseCtx APITestContext, baseBranch, headBranch string) fun // Then: Delete the head branch & make sure that doesn't break the PR page or change its diff t.Run("DeleteHeadBranch", doBranchDelete(baseCtx, baseCtx.Username, baseCtx.Reponame, headBranch)) - t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr)) + t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr, false)) t.Run("EnsureDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength)) // Delete the head repository & make sure that doesn't break the PR page or change its diff t.Run("DeleteHeadRepository", doAPIDeleteRepository(ctx)) - t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr)) + t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr, false)) t.Run("EnsureDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength)) } } @@ -520,12 +533,19 @@ func doCreatePRAndSetManuallyMerged(ctx, baseCtx APITestContext, dstPath, baseBr } } -func doEnsureCanSeePull(ctx APITestContext, pr api.PullRequest) func(t *testing.T) { +func doEnsureCanSeePull(ctx APITestContext, pr api.PullRequest, editable bool) func(t *testing.T) { return func(t *testing.T) { req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index)) ctx.Session.MakeRequest(t, req, http.StatusOK) req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d/files", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index)) - ctx.Session.MakeRequest(t, req, http.StatusOK) + resp := ctx.Session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + editButtonCount := doc.doc.Find("div.diff-file-header-actions a[href*='/_edit/']").Length() + if editable { + assert.Greater(t, editButtonCount, 0, "Expected to find a button to edit a file in the PR diff view but there were none") + } else { + assert.Equal(t, 0, editButtonCount, "Expected not to find any buttons to edit files in PR diff view but there were some") + } req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d/commits", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index)) ctx.Session.MakeRequest(t, req, http.StatusOK) } From 1d5153aaf69bdd114800ebc2a1268896f8dc3ff4 Mon Sep 17 00:00:00 2001 From: Gusted Date: Sat, 30 Dec 2023 15:06:17 +0100 Subject: [PATCH 52/86] [GITEA] Fix NPE in `ToPullReviewList` - Add condition to ensure doer isn't nil when using it. - Added unit test. - Resolves #2055 (cherry picked from commit 8f1a74fb2944c2a1cf3824c2c6f233d6df2df593) (cherry picked from commit 60ac881776c750bc25e1d142e201e78e48e3ac23) (cherry picked from commit 5fdc461ac53ec486e609ad6ac40cde8e701c0fb8) (cherry picked from commit 70623e8da1eb6c7e13a3cef04f1db9d479ffd7a4) --- services/convert/pull_review.go | 2 +- services/convert/pull_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/services/convert/pull_review.go b/services/convert/pull_review.go index aa7ad68a47..05638c2c9b 100644 --- a/services/convert/pull_review.go +++ b/services/convert/pull_review.go @@ -66,7 +66,7 @@ func ToPullReviewList(ctx context.Context, rl []*issues_model.Review, doer *user result := make([]*api.PullReview, 0, len(rl)) for i := range rl { // show pending reviews only for the user who created them - if rl[i].Type == issues_model.ReviewTypePending && !(doer.IsAdmin || doer.ID == rl[i].ReviewerID) { + if rl[i].Type == issues_model.ReviewTypePending && (doer == nil || !(doer.IsAdmin || doer.ID == rl[i].ReviewerID)) { continue } r, err := ToPullReview(ctx, rl[i], doer) diff --git a/services/convert/pull_test.go b/services/convert/pull_test.go index e069fa4a68..66c7313f7d 100644 --- a/services/convert/pull_test.go +++ b/services/convert/pull_test.go @@ -12,6 +12,7 @@ import ( access_model "code.gitea.io/gitea/models/perm/access" 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" "code.gitea.io/gitea/modules/structs" @@ -47,3 +48,30 @@ func TestPullRequest_APIFormat(t *testing.T) { assert.Nil(t, apiPullRequest.Head.Repository) assert.EqualValues(t, -1, apiPullRequest.Head.RepoID) } + +func TestPullReviewList(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + t.Run("Pending review", func(t *testing.T) { + reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + review := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 6, ReviewerID: reviewer.ID}) + rl := []*issues_model.Review{review} + + t.Run("Anonymous", func(t *testing.T) { + prList, err := ToPullReviewList(db.DefaultContext, rl, nil) + assert.NoError(t, err) + assert.Empty(t, prList) + }) + t.Run("Reviewer", func(t *testing.T) { + prList, err := ToPullReviewList(db.DefaultContext, rl, reviewer) + assert.NoError(t, err) + assert.Len(t, prList, 1) + }) + t.Run("Admin", func(t *testing.T) { + adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}, unittest.Cond("id != ?", reviewer.ID)) + prList, err := ToPullReviewList(db.DefaultContext, rl, adminUser) + assert.NoError(t, err) + assert.Len(t, prList, 1) + }) + }) +} From 0157fb9b88fd50832c07b06c11c8dba6e059a465 Mon Sep 17 00:00:00 2001 From: Gergely Nagy Date: Sun, 31 Dec 2023 16:24:05 +0100 Subject: [PATCH 53/86] [GITEA] Configurable clone methods Adds `[repository].DOWNLOAD_OR_CLONE_METHODS` (defaulting to "download-zip,download-targz,download-bundle,vscode-clone"), which lets an instance administrator override the additional clone methods displayed on the repository home view. This is purely display-only, the clone methods not listed here are still available, unless disabled elsewhere. They're just not displayed. Fixes #710. Signed-off-by: Gergely Nagy (cherry picked from commit 2aadcf4946e48ee43800568fe705d00a062c42bf) (cherry picked from commit 42ac34fbf9105eed27ee687b305a85515270f0cc) (cherry picked from commit bd231b02450212aca6be775663c3d24ddf19f990) (cherry picked from commit 3d3366dbbee37621fc665e557a4a87bf08104375) --- modules/setting/repository.go | 11 +++++ modules/web/middleware/data.go | 1 + options/locale/locale_en-US.ini | 1 + templates/repo/clone_script.tmpl | 3 ++ templates/repo/home.tmpl | 31 ++++++++++--- tests/integration/repo_test.go | 78 ++++++++++++++++++++++++++++++++ 6 files changed, 118 insertions(+), 7 deletions(-) diff --git a/modules/setting/repository.go b/modules/setting/repository.go index a722ea2a0f..b6b0b50504 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -7,6 +7,7 @@ import ( "os/exec" "path" "path/filepath" + "slices" "strings" "code.gitea.io/gitea/modules/log" @@ -19,6 +20,8 @@ const ( RepoCreatingPublic = "public" ) +var RecognisedRepositoryDownloadOrCloneMethods = []string{"download-zip", "download-targz", "download-bundle", "vscode-clone", "vscodium-clone", "cite"} + // ItemsPerPage maximum items per page in forks, watchers and stars of a repo const ItemsPerPage = 40 @@ -43,6 +46,7 @@ var ( DisabledRepoUnits []string DefaultRepoUnits []string DefaultForkRepoUnits []string + DownloadOrCloneMethods []string PrefixArchiveFiles bool DisableMigrations bool DisableStars bool `ini:"DISABLE_STARS"` @@ -161,6 +165,7 @@ var ( DisabledRepoUnits: []string{}, DefaultRepoUnits: []string{}, DefaultForkRepoUnits: []string{}, + DownloadOrCloneMethods: []string{"download-zip", "download-targz", "download-bundle", "vscode-clone"}, PrefixArchiveFiles: true, DisableMigrations: false, DisableStars: false, @@ -361,4 +366,10 @@ func loadRepositoryFrom(rootCfg ConfigProvider) { if err := loadRepoArchiveFrom(rootCfg); err != nil { log.Fatal("loadRepoArchiveFrom: %v", err) } + + for _, method := range Repository.DownloadOrCloneMethods { + if !slices.Contains(RecognisedRepositoryDownloadOrCloneMethods, method) { + log.Error("Unrecognised repository download or clone method: %s", method) + } + } } diff --git a/modules/web/middleware/data.go b/modules/web/middleware/data.go index 08d83f94be..c1d1af8528 100644 --- a/modules/web/middleware/data.go +++ b/modules/web/middleware/data.go @@ -53,6 +53,7 @@ func CommonTemplateContextData() ContextData { "ShowMilestonesDashboardPage": setting.Service.ShowMilestonesDashboardPage, "ShowFooterVersion": setting.Other.ShowFooterVersion, "DisableDownloadSourceArchives": setting.Repository.DisableDownloadSourceArchives, + "DownloadOrCloneMethods": setting.Repository.DownloadOrCloneMethods, "EnableSwagger": setting.API.EnableSwagger, "EnableOpenIDSignIn": setting.Service.EnableOpenIDSignIn, diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index cbddd3d537..0e51a35757 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -964,6 +964,7 @@ all_branches = All branches fork_no_valid_owners = This repository can not be forked because there are no valid owners. use_template = Use this template clone_in_vsc = Clone in VS Code +clone_in_vscodium = Clone in VS Codium download_zip = Download ZIP download_tar = Download TAR.GZ download_bundle = Download BUNDLE diff --git a/templates/repo/clone_script.tmpl b/templates/repo/clone_script.tmpl index 0797b400d8..9ff826bc93 100644 --- a/templates/repo/clone_script.tmpl +++ b/templates/repo/clone_script.tmpl @@ -38,5 +38,8 @@ for (const el of document.getElementsByClassName('js-clone-url-vsc')) { el['href'] = 'vscode://vscode.git/clone?url=' + encodeURIComponent(link); } + for (const el of document.getElementsByClassName('js-clone-url-vscodium')) { + el['href'] = 'vscodium://vscode.git/clone?url=' + encodeURIComponent(link); + } })(); diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index d91dc4394e..eb6d96f28e 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -131,15 +131,32 @@ {{template "repo/clone_script" .}}{{/* the script will update `.js-clone-url` and related elements */}} diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go index ed5466bc63..bb341b2ce0 100644 --- a/tests/integration/repo_test.go +++ b/tests/integration/repo_test.go @@ -43,6 +43,84 @@ func TestViewRepo(t *testing.T) { session.MakeRequest(t, req, http.StatusNotFound) } +func TestViewRepoCloneMethods(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + getCloneMethods := func() []string { + req := NewRequest(t, "GET", "/user2/repo1") + resp := MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + cloneMoreMethodsHTML := htmlDoc.doc.Find("#more-btn div a") + + var methods []string + cloneMoreMethodsHTML.Each(func(i int, s *goquery.Selection) { + a, _ := s.Attr("href") + methods = append(methods, a) + }) + + return methods + } + + testCloneMethods := func(expected []string) { + methods := getCloneMethods() + + assert.Len(t, methods, len(expected)) + for i, expectedMethod := range expected { + assert.Contains(t, methods[i], expectedMethod) + } + } + + t.Run("Defaults", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + testCloneMethods([]string{"/master.zip", "/master.tar.gz", "/master.bundle", "vscode://"}) + }) + + t.Run("Customized methods", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.Repository.DownloadOrCloneMethods, []string{"vscodium-clone", "download-targz"})() + + testCloneMethods([]string{"vscodium://", "/master.tar.gz"}) + }) + + t.Run("Individual methods", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + singleMethodTest := func(method, expectedURLPart string) { + t.Run(method, func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.Repository.DownloadOrCloneMethods, []string{method})() + + testCloneMethods([]string{expectedURLPart}) + }) + } + + cases := map[string]string{ + "download-zip": "/master.zip", + "download-targz": "/master.tar.gz", + "download-bundle": "/master.bundle", + "vscode-clone": "vscode://", + "vscodium-clone": "vscodium://", + } + for method, expectedURLPart := range cases { + singleMethodTest(method, expectedURLPart) + } + }) + + t.Run("All methods", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.Repository.DownloadOrCloneMethods, setting.RecognisedRepositoryDownloadOrCloneMethods)() + + methods := getCloneMethods() + // We compare against + // len(setting.RecognisedRepositoryDownloadOrCloneMethods) - 1, because + // the test environment does not currently set things up for the cite + // method to display. + assert.GreaterOrEqual(t, len(methods), len(setting.RecognisedRepositoryDownloadOrCloneMethods)-1) + }) +} + func testViewRepo(t *testing.T) { defer tests.PrepareTestEnv(t)() From 7a343877f1051773e21e9af7bfff26ad03d43f08 Mon Sep 17 00:00:00 2001 From: Gusted Date: Sun, 17 Dec 2023 22:49:58 +0100 Subject: [PATCH 54/86] [GITEA] Remove redundant `syncBranchToDB` - The transaction in combination with Git push was causing deadlocks if you had the `push_update` queue set to `immediate`. This was the root cause of slow integration tests in CI. - Remove the sync branch code as this is already being done in the Git post-receive hook. - Add tests to proof the branch models are in sync even with this code removed. (cherry picked from commit 90110e1f44a40837a6ef5b3979a6ed96bfd614be) (cherry picked from commit a064065cb9a6e39597e38c37a405d066cfabf7f7) (cherry picked from commit 7713e558eb6419a3a7d3f2d1beaa8062899490c8) Conflicts: services/repository/branch.go https://codeberg.org/forgejo/forgejo/pulls/2068 (cherry picked from commit 3bb73e0813b46fd8b518a46d7499ee1c525bc434) (cherry picked from commit c557540926826e82a118a085c3b510e072157cfe) (cherry picked from commit 986be6171a3a34ebab60e757dafeee2e254765a1) --- tests/integration/branches_test.go | 99 +++++++++++---------------- tests/integration/repo_branch_test.go | 16 ++++- 2 files changed, 56 insertions(+), 59 deletions(-) diff --git a/tests/integration/branches_test.go b/tests/integration/branches_test.go index 99d7eef706..26bed8c90a 100644 --- a/tests/integration/branches_test.go +++ b/tests/integration/branches_test.go @@ -4,72 +4,55 @@ package integration import ( + "fmt" "net/http" "net/url" "testing" - "code.gitea.io/gitea/modules/translation" - "code.gitea.io/gitea/tests" + git_model "code.gitea.io/gitea/models/git" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + gitea_context "code.gitea.io/gitea/modules/context" "github.com/stretchr/testify/assert" ) -func TestViewBranches(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - req := NewRequest(t, "GET", "/user2/repo1/branches") - resp := MakeRequest(t, req, http.StatusOK) - - htmlDoc := NewHTMLParser(t, resp.Body) - _, exists := htmlDoc.doc.Find(".delete-branch-button").Attr("data-url") - assert.False(t, exists, "The template has changed") -} - -func TestDeleteBranch(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - deleteBranch(t) -} - -func TestUndoDeleteBranch(t *testing.T) { +func TestBranchActions(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { - deleteBranch(t) - htmlDoc, name := branchAction(t, ".restore-branch-button") - assert.Contains(t, - htmlDoc.doc.Find(".ui.positive.message").Text(), - translation.NewLocale("en-US").Tr("repo.branch.restore_success", name), - ) + session := loginUser(t, "user2") + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + branch3 := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 3, RepoID: repo1.ID}) + branchesLink := repo1.FullName() + "/branches" + + t.Run("View", func(t *testing.T) { + req := NewRequest(t, "GET", branchesLink) + MakeRequest(t, req, http.StatusOK) + }) + + t.Run("Delete branch", func(t *testing.T) { + link := fmt.Sprintf("/%s/branches/delete?name=%s", repo1.FullName(), branch3.Name) + req := NewRequestWithValues(t, "POST", link, map[string]string{ + "_csrf": GetCSRF(t, session, branchesLink), + }) + session.MakeRequest(t, req, http.StatusOK) + flashCookie := session.GetCookie(gitea_context.CookieNameFlash) + assert.NotNil(t, flashCookie) + assert.Contains(t, flashCookie.Value, "success%3DBranch%2B%2522branch2%2522%2Bhas%2Bbeen%2Bdeleted.") + + assert.True(t, unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 3, RepoID: repo1.ID}).IsDeleted) + }) + + t.Run("Restore branch", func(t *testing.T) { + link := fmt.Sprintf("/%s/branches/restore?branch_id=%d&name=%s", repo1.FullName(), branch3.ID, branch3.Name) + req := NewRequestWithValues(t, "POST", link, map[string]string{ + "_csrf": GetCSRF(t, session, branchesLink), + }) + session.MakeRequest(t, req, http.StatusOK) + flashCookie := session.GetCookie(gitea_context.CookieNameFlash) + assert.NotNil(t, flashCookie) + assert.Contains(t, flashCookie.Value, "success%3DBranch%2B%2522branch2%2522%2Bhas%2Bbeen%2Brestored") + + assert.False(t, unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 3, RepoID: repo1.ID}).IsDeleted) + }) }) } - -func deleteBranch(t *testing.T) { - htmlDoc, name := branchAction(t, ".delete-branch-button") - assert.Contains(t, - htmlDoc.doc.Find(".ui.positive.message").Text(), - translation.NewLocale("en-US").Tr("repo.branch.deletion_success", name), - ) -} - -func branchAction(t *testing.T, button string) (*HTMLDoc, string) { - session := loginUser(t, "user2") - req := NewRequest(t, "GET", "/user2/repo1/branches") - resp := session.MakeRequest(t, req, http.StatusOK) - - htmlDoc := NewHTMLParser(t, resp.Body) - link, exists := htmlDoc.doc.Find(button).Attr("data-url") - if !assert.True(t, exists, "The template has changed") { - t.Skip() - } - - req = NewRequestWithValues(t, "POST", link, map[string]string{ - "_csrf": htmlDoc.GetCSRF(), - }) - session.MakeRequest(t, req, http.StatusOK) - - url, err := url.Parse(link) - assert.NoError(t, err) - req = NewRequest(t, "GET", "/user2/repo1/branches") - resp = session.MakeRequest(t, req, http.StatusOK) - - return NewHTMLParser(t, resp.Body), url.Query().Get("name") -} diff --git a/tests/integration/repo_branch_test.go b/tests/integration/repo_branch_test.go index 91674ddc82..9eb17eda00 100644 --- a/tests/integration/repo_branch_test.go +++ b/tests/integration/repo_branch_test.go @@ -10,6 +10,8 @@ import ( "strings" "testing" + git_model "code.gitea.io/gitea/models/git" + "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation" @@ -47,12 +49,14 @@ func testCreateBranches(t *testing.T, giteaURL *url.URL) { CreateRelease string FlashMessage string ExpectedStatus int + CheckBranch bool }{ { OldRefSubURL: "branch/master", NewBranch: "feature/test1", ExpectedStatus: http.StatusSeeOther, FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature/test1"), + CheckBranch: true, }, { OldRefSubURL: "branch/master", @@ -65,6 +69,7 @@ func testCreateBranches(t *testing.T, giteaURL *url.URL) { NewBranch: "feature=test1", ExpectedStatus: http.StatusSeeOther, FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature=test1"), + CheckBranch: true, }, { OldRefSubURL: "branch/master", @@ -94,6 +99,7 @@ func testCreateBranches(t *testing.T, giteaURL *url.URL) { NewBranch: "feature/test3", ExpectedStatus: http.StatusSeeOther, FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature/test3"), + CheckBranch: true, }, { OldRefSubURL: "branch/master", @@ -108,10 +114,15 @@ func testCreateBranches(t *testing.T, giteaURL *url.URL) { CreateRelease: "v1.0.1", ExpectedStatus: http.StatusSeeOther, FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature/test4"), + CheckBranch: true, }, } + + session := loginUser(t, "user2") for _, test := range tests { - session := loginUser(t, "user2") + if test.CheckBranch { + unittest.AssertNotExistsBean(t, &git_model.Branch{RepoID: 1, Name: test.NewBranch}) + } if test.CreateRelease != "" { createNewRelease(t, session, "/user2/repo1", test.CreateRelease, test.CreateRelease, false, false) } @@ -125,6 +136,9 @@ func testCreateBranches(t *testing.T, giteaURL *url.URL) { test.FlashMessage, ) } + if test.CheckBranch { + unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: 1, Name: test.NewBranch}) + } } } From 9266b1916f1577075b0bf2ff14c7412cbd7cae43 Mon Sep 17 00:00:00 2001 From: Gergely Nagy Date: Sun, 31 Dec 2023 17:54:43 +0100 Subject: [PATCH 55/86] [GITEA] repo: Don't redirect the repo to external units When displaying the repo home view, do not redirect to unit types that can't be defaults (which, at the moment, are the external wiki and issue tracker unit types). If we'd redirect to those, that would mean that a repository with the Code unit disabled, and an external issue tracker would immediately redirect to the external issue tracker, making it harder to reach other, non-external units of the repo. Fixes #1965. Signed-off-by: Gergely Nagy (cherry picked from commit 44078e546022e25f5c805ef047fbc3b7c6075ec0) (cherry picked from commit 1868dec2e4c2ba8e6807336e6dabd83e6138bcac) (cherry picked from commit c3a8e9887092c8c089462a1cdb22a404aa11beb6) --- routers/web/repo/view.go | 2 +- tests/integration/repo_test.go | 103 +++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 167be7b559..5d96aa0efc 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -732,7 +732,7 @@ func checkHomeCodeViewable(ctx *context.Context) { } unit, ok := unit_model.Units[repoUnit.Type] - if ok && (firstUnit == nil || !firstUnit.IsLessThan(unit)) { + if ok && (firstUnit == nil || !firstUnit.IsLessThan(unit)) && repoUnit.Type.CanBeDefault() { firstUnit = &unit } } diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go index bb341b2ce0..0265e99b7c 100644 --- a/tests/integration/repo_test.go +++ b/tests/integration/repo_test.go @@ -11,8 +11,13 @@ import ( "testing" "time" + "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/models/unittest" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" + repo_service "code.gitea.io/gitea/services/repository" "code.gitea.io/gitea/tests" "github.com/PuerkitoBio/goquery" @@ -710,3 +715,101 @@ func TestCommitView(t *testing.T) { assert.Contains(t, commitTitle, "Initial commit") }) } + +func TestRepoHomeViewRedirect(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + t.Run("Code", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/user2/repo1") + resp := MakeRequest(t, req, http.StatusOK) + + doc := NewHTMLParser(t, resp.Body) + l := doc.Find("#repo-desc").Length() + assert.Equal(t, 1, l) + }) + + t.Run("No Code redirects to Issues", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Disable the Code unit + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + err := repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, nil, []unit_model.Type{ + unit_model.TypeCode, + }) + assert.NoError(t, err) + + // The repo home should redirect to the built-in issue tracker + req := NewRequest(t, "GET", "/user2/repo1") + resp := MakeRequest(t, req, http.StatusSeeOther) + redir := resp.Header().Get("Location") + + assert.Equal(t, "/user2/repo1/issues", redir) + }) + + t.Run("No Code and ExternalTracker redirects to Pulls", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Replace the internal tracker with an external one + // Disable Code, Projects, Packages, and Actions + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + err := repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, []repo_model.RepoUnit{{ + RepoID: repo.ID, + Type: unit_model.TypeExternalTracker, + Config: &repo_model.ExternalTrackerConfig{ + ExternalTrackerURL: "https://example.com", + }, + }}, []unit_model.Type{ + unit_model.TypeCode, + unit_model.TypeIssues, + unit_model.TypeProjects, + unit_model.TypePackages, + unit_model.TypeActions, + }) + assert.NoError(t, err) + + // The repo home should redirect to pull requests + req := NewRequest(t, "GET", "/user2/repo1") + resp := MakeRequest(t, req, http.StatusSeeOther) + redir := resp.Header().Get("Location") + + assert.Equal(t, "/user2/repo1/pulls", redir) + }) + + t.Run("Only external wiki results in 404", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Replace the internal wiki with an external, and disable everything + // else. + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + err := repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, []repo_model.RepoUnit{{ + RepoID: repo.ID, + Type: unit_model.TypeExternalWiki, + Config: &repo_model.ExternalWikiConfig{ + ExternalWikiURL: "https://example.com", + }, + }}, []unit_model.Type{ + unit_model.TypeCode, + unit_model.TypeIssues, + unit_model.TypeExternalTracker, + unit_model.TypeProjects, + unit_model.TypePackages, + unit_model.TypeActions, + unit_model.TypePullRequests, + unit_model.TypeReleases, + unit_model.TypeWiki, + }) + assert.NoError(t, err) + + // The repo home ends up being 404 + req := NewRequest(t, "GET", "/user2/repo1") + req.Header.Set("Accept", "text/html") + resp := MakeRequest(t, req, http.StatusNotFound) + + // The external wiki is linked to from the 404 page + doc := NewHTMLParser(t, resp.Body) + txt := strings.TrimSpace(doc.Find(`a[href="https://example.com"]`).Text()) + assert.Equal(t, "Wiki", txt) + }) +} From 35cff45eb86177e750cd22e82a201880a5efe045 Mon Sep 17 00:00:00 2001 From: Gergely Nagy Date: Mon, 1 Jan 2024 13:38:49 +0100 Subject: [PATCH 56/86] [GITEA] Add support for shields.io-based badges Adds a new `/{username}/{repo}/badges` family of routes, which redirect to various shields.io badges. The goal is to not reimplement badge generation, and delegate it to shields.io (or a similar service), which are already used by many. This way, we get all the goodies that come with it: different styles, colors, logos, you name it. So these routes are just thin wrappers around shields.io that make it easier to display the information we want. The URL is configurable via `app.ini`, and is templatable, allowing to use alternative badge generator services with slightly different URL patterns. Additionally, for compatibility with GitHub, there's an `/{username}/{repo}/actions/workflows/{workflow_file}/badge.svg` route that works much the same way as on GitHub. Change the hostname in the URL, and done. Fixes gitea#5633, gitea#23688, and also fixes #126. Work sponsored by Codeberg e.V. Signed-off-by: Gergely Nagy (cherry picked from commit fcd0f61212d8febd4bdfc27e61a4e13cbdd16d49) (cherry picked from commit 20d14f784490a880c51ca0f0a6a5988a01887635) (cherry picked from commit 4359741431bb39de4cf24de8b0cfb513f5233f55) --- custom/conf/app.example.ini | 8 + models/actions/run.go | 15 ++ modules/setting/badges.go | 24 +++ modules/setting/setting.go | 1 + routers/web/repo/badges/badges.go | 165 ++++++++++++++++++ routers/web/web.go | 21 +++ tests/integration/repo_badges_test.go | 237 ++++++++++++++++++++++++++ 7 files changed, 471 insertions(+) create mode 100644 modules/setting/badges.go create mode 100644 routers/web/repo/badges/badges.go create mode 100644 tests/integration/repo_badges_test.go diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 2a21bd64b1..31b981d260 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -912,6 +912,14 @@ LEVEL = Info ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;[badges] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Enable repository badges (via shields.io or a similar generator) +;ENABLED = true +;; Template for the badge generator. +;GENERATOR_URL_TEMPLATE = https://img.shields.io/badge/{{.label}}-{{.text}}-{{.color}} + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;[repository] ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/models/actions/run.go b/models/actions/run.go index db0f380049..5d4e3b74dd 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -323,6 +323,21 @@ func GetLatestRun(ctx context.Context, repoID int64) (*ActionRun, error) { return &run, nil } +func GetLatestRunForBranchAndWorkflow(ctx context.Context, repoID int64, branch, workflowFile, event string) (*ActionRun, error) { + var run ActionRun + q := db.GetEngine(ctx).Where("repo_id=?", repoID).And("ref=?", branch).And("workflow_id=?", workflowFile) + if event != "" { + q = q.And("event=?", event) + } + has, err := q.Desc("id").Get(&run) + if err != nil { + return nil, err + } else if !has { + return nil, util.NewNotExistErrorf("run with repo_id %d, ref %s, workflow_id %s", repoID, branch, workflowFile) + } + return &run, nil +} + func GetRunByID(ctx context.Context, id int64) (*ActionRun, error) { var run ActionRun has, err := db.GetEngine(ctx).Where("id=?", id).Get(&run) diff --git a/modules/setting/badges.go b/modules/setting/badges.go new file mode 100644 index 0000000000..e0c1cb55ec --- /dev/null +++ b/modules/setting/badges.go @@ -0,0 +1,24 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "text/template" +) + +// Badges settings +var Badges = struct { + Enabled bool `ini:"ENABLED"` + GeneratorURLTemplate string `ini:"GENERATOR_URL_TEMPLATE"` + GeneratorURLTemplateTemplate *template.Template `ini:"-"` +}{ + Enabled: true, + GeneratorURLTemplate: "https://img.shields.io/badge/{{.label}}-{{.text}}-{{.color}}", +} + +func loadBadgesFrom(rootCfg ConfigProvider) { + mustMapSetting(rootCfg, "badges", &Badges) + + Badges.GeneratorURLTemplateTemplate = template.Must(template.New("").Parse(Badges.GeneratorURLTemplate)) +} diff --git a/modules/setting/setting.go b/modules/setting/setting.go index ebfd3b27be..c0d8d0ee23 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -147,6 +147,7 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error { loadUIFrom(cfg) loadAdminFrom(cfg) loadAPIFrom(cfg) + loadBadgesFrom(cfg) loadMetricsFrom(cfg) loadCamoFrom(cfg) loadI18nFrom(cfg) diff --git a/routers/web/repo/badges/badges.go b/routers/web/repo/badges/badges.go new file mode 100644 index 0000000000..8fe99c7fc1 --- /dev/null +++ b/routers/web/repo/badges/badges.go @@ -0,0 +1,165 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package badges + +import ( + "fmt" + "net/url" + "strings" + + actions_model "code.gitea.io/gitea/models/actions" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" + context_module "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" +) + +func getBadgeURL(ctx *context_module.Context, label, text, color string) string { + sb := &strings.Builder{} + _ = setting.Badges.GeneratorURLTemplateTemplate.Execute(sb, map[string]string{ + "label": url.PathEscape(label), + "text": url.PathEscape(text), + "color": url.PathEscape(color), + }) + + badgeURL := sb.String() + q := ctx.Req.URL.Query() + // Remove any `branch` or `event` query parameters. They're used by the + // workflow badge route, and do not need forwarding to the badge generator. + delete(q, "branch") + delete(q, "event") + if len(q) > 0 { + return fmt.Sprintf("%s?%s", badgeURL, q.Encode()) + } + return badgeURL +} + +func redirectToBadge(ctx *context_module.Context, label, text, color string) { + ctx.Redirect(getBadgeURL(ctx, label, text, color)) +} + +func errorBadge(ctx *context_module.Context, label, text string) { + ctx.Redirect(getBadgeURL(ctx, label, text, "crimson")) +} + +func GetWorkflowBadge(ctx *context_module.Context) { + branch := ctx.Req.URL.Query().Get("branch") + if branch == "" { + branch = ctx.Repo.Repository.DefaultBranch + } + branch = fmt.Sprintf("refs/heads/%s", branch) + event := ctx.Req.URL.Query().Get("event") + + workflowFile := ctx.Params("workflow_name") + run, err := actions_model.GetLatestRunForBranchAndWorkflow(ctx, ctx.Repo.Repository.ID, branch, workflowFile, event) + if err != nil { + errorBadge(ctx, workflowFile, "Not found") + return + } + + var color string + switch run.Status { + case actions_model.StatusUnknown: + color = "lightgrey" + case actions_model.StatusWaiting: + color = "lightgrey" + case actions_model.StatusRunning: + color = "gold" + case actions_model.StatusSuccess: + color = "brightgreen" + case actions_model.StatusFailure: + color = "crimson" + case actions_model.StatusCancelled: + color = "orange" + case actions_model.StatusSkipped: + color = "blue" + case actions_model.StatusBlocked: + color = "yellow" + default: + color = "lightgrey" + } + + redirectToBadge(ctx, workflowFile, run.Status.String(), color) +} + +func getIssueOrPullBadge(ctx *context_module.Context, label, variant string, num int) { + var text string + if len(variant) > 0 { + text = fmt.Sprintf("%d %s", num, variant) + } else { + text = fmt.Sprintf("%d", num) + } + redirectToBadge(ctx, label, text, "blue") +} + +func getIssueBadge(ctx *context_module.Context, variant string, num int) { + if !ctx.Repo.CanRead(unit.TypeIssues) && + !ctx.Repo.CanRead(unit.TypeExternalTracker) { + errorBadge(ctx, "issues", "Not found") + return + } + + _, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker) + if err == nil { + errorBadge(ctx, "issues", "Not found") + return + } + + getIssueOrPullBadge(ctx, "issues", variant, num) +} + +func getPullBadge(ctx *context_module.Context, variant string, num int) { + if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(unit.TypePullRequests) { + errorBadge(ctx, "pulls", "Not found") + return + } + + getIssueOrPullBadge(ctx, "pulls", variant, num) +} + +func GetOpenIssuesBadge(ctx *context_module.Context) { + getIssueBadge(ctx, "open", ctx.Repo.Repository.NumOpenIssues) +} + +func GetClosedIssuesBadge(ctx *context_module.Context) { + getIssueBadge(ctx, "closed", ctx.Repo.Repository.NumClosedIssues) +} + +func GetTotalIssuesBadge(ctx *context_module.Context) { + getIssueBadge(ctx, "", ctx.Repo.Repository.NumIssues) +} + +func GetOpenPullsBadge(ctx *context_module.Context) { + getPullBadge(ctx, "open", ctx.Repo.Repository.NumOpenPulls) +} + +func GetClosedPullsBadge(ctx *context_module.Context) { + getPullBadge(ctx, "closed", ctx.Repo.Repository.NumClosedPulls) +} + +func GetTotalPullsBadge(ctx *context_module.Context) { + getPullBadge(ctx, "", ctx.Repo.Repository.NumPulls) +} + +func GetStarsBadge(ctx *context_module.Context) { + redirectToBadge(ctx, "stars", fmt.Sprintf("%d", ctx.Repo.Repository.NumStars), "blue") +} + +func GetLatestReleaseBadge(ctx *context_module.Context) { + release, err := repo_model.GetLatestReleaseByRepoID(ctx, ctx.Repo.Repository.ID) + if err != nil { + if repo_model.IsErrReleaseNotExist(err) { + errorBadge(ctx, "release", "Not found") + return + } + ctx.ServerError("GetLatestReleaseByRepoID", err) + } + + if err := release.LoadAttributes(ctx); err != nil { + ctx.ServerError("LoadAttributes", err) + return + } + + redirectToBadge(ctx, "release", release.TagName, "blue") +} diff --git a/routers/web/web.go b/routers/web/web.go index 942bab67e5..c15d1800f3 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -37,6 +37,7 @@ import ( org_setting "code.gitea.io/gitea/routers/web/org/setting" "code.gitea.io/gitea/routers/web/repo" "code.gitea.io/gitea/routers/web/repo/actions" + "code.gitea.io/gitea/routers/web/repo/badges" repo_setting "code.gitea.io/gitea/routers/web/repo/setting" "code.gitea.io/gitea/routers/web/user" user_setting "code.gitea.io/gitea/routers/web/user/setting" @@ -1316,6 +1317,24 @@ func registerRoutes(m *web.Route) { m.Get("/packages", repo.Packages) } + if setting.Badges.Enabled { + m.Group("/badges", func() { + m.Get("/workflows/{workflow_name}/badge.svg", badges.GetWorkflowBadge) + m.Group("/issues", func() { + m.Get(".svg", badges.GetTotalIssuesBadge) + m.Get("/open.svg", badges.GetOpenIssuesBadge) + m.Get("/closed.svg", badges.GetClosedIssuesBadge) + }) + m.Group("/pulls", func() { + m.Get(".svg", badges.GetTotalPullsBadge) + m.Get("/open.svg", badges.GetOpenPullsBadge) + m.Get("/closed.svg", badges.GetClosedPullsBadge) + }) + m.Get("/stars.svg", badges.GetStarsBadge) + m.Get("/release.svg", badges.GetLatestReleaseBadge) + }) + } + m.Group("/projects", func() { m.Get("", repo.Projects) m.Get("/{id}", repo.ViewProject) @@ -1367,6 +1386,8 @@ func registerRoutes(m *web.Route) { m.Post("/rerun", reqRepoActionsWriter, actions.Rerun) }) }) + + m.Get("/workflows/{workflow_name}/badge.svg", badges.GetWorkflowBadge) }, reqRepoActionsReader, actions.MustEnableActions) m.Group("/wiki", func() { diff --git a/tests/integration/repo_badges_test.go b/tests/integration/repo_badges_test.go new file mode 100644 index 0000000000..e4b634d1a8 --- /dev/null +++ b/tests/integration/repo_badges_test.go @@ -0,0 +1,237 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + actions_model "code.gitea.io/gitea/models/actions" + "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/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/test" + repo_service "code.gitea.io/gitea/services/repository" + files_service "code.gitea.io/gitea/services/repository/files" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func assertBadge(t *testing.T, resp *httptest.ResponseRecorder, badge string) { + assert.Equal(t, fmt.Sprintf("https://img.shields.io/badge/%s", badge), test.RedirectURL(resp)) +} + +func createMinimalRepo(t *testing.T) func() { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + // Create a new repository + repo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{ + Name: "minimal", + Description: "minimal repo for badge testing", + AutoInit: true, + Gitignores: "Go", + License: "MIT", + Readme: "Default", + DefaultBranch: "main", + IsPrivate: false, + }) + assert.NoError(t, err) + assert.NotEmpty(t, repo) + + // Enable Actions, and disable Issues, PRs and Releases + err = repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, []repo_model.RepoUnit{{ + RepoID: repo.ID, + Type: unit_model.TypeActions, + }}, []unit_model.Type{unit_model.TypeIssues, unit_model.TypePullRequests, unit_model.TypeReleases}) + assert.NoError(t, err) + + return func() { + repo_service.DeleteRepository(db.DefaultContext, user2, repo, false) + } +} + +func addWorkflow(t *testing.T) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "minimal") + assert.NoError(t, err) + + // Add a workflow file to the repo + addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: ".gitea/workflows/pr.yml", + ContentReader: strings.NewReader("name: test\non:\n push:\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo helloworld\n"), + }, + }, + Message: "add workflow", + OldBranch: "main", + NewBranch: "main", + Author: &files_service.IdentityOptions{ + Name: user2.Name, + Email: user2.Email, + }, + Committer: &files_service.IdentityOptions{ + Name: user2.Name, + Email: user2.Email, + }, + Dates: &files_service.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }) + assert.NoError(t, err) + assert.NotEmpty(t, addWorkflowToBaseResp) + + assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID})) +} + +func TestWorkflowBadges(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + defer tests.PrintCurrentTest(t)() + defer createMinimalRepo(t)() + + addWorkflow(t) + + // Actions disabled + req := NewRequest(t, "GET", "/user2/repo1/badges/workflows/test.yaml/badge.svg") + resp := MakeRequest(t, req, http.StatusSeeOther) + assertBadge(t, resp, "test.yaml-Not%20found-crimson") + + req = NewRequest(t, "GET", "/user2/repo1/badges/workflows/test.yaml/badge.svg?branch=no-such-branch") + resp = MakeRequest(t, req, http.StatusSeeOther) + assertBadge(t, resp, "test.yaml-Not%20found-crimson") + + // Actions enabled + req = NewRequest(t, "GET", "/user2/minimal/badges/workflows/pr.yml/badge.svg") + resp = MakeRequest(t, req, http.StatusSeeOther) + assertBadge(t, resp, "pr.yml-waiting-lightgrey") + + req = NewRequest(t, "GET", "/user2/minimal/badges/workflows/pr.yml/badge.svg?branch=main") + resp = MakeRequest(t, req, http.StatusSeeOther) + assertBadge(t, resp, "pr.yml-waiting-lightgrey") + + req = NewRequest(t, "GET", "/user2/minimal/badges/workflows/pr.yml/badge.svg?branch=no-such-branch") + resp = MakeRequest(t, req, http.StatusSeeOther) + assertBadge(t, resp, "pr.yml-Not%20found-crimson") + + req = NewRequest(t, "GET", "/user2/minimal/badges/workflows/pr.yml/badge.svg?event=cron") + resp = MakeRequest(t, req, http.StatusSeeOther) + assertBadge(t, resp, "pr.yml-Not%20found-crimson") + + // GitHub compatibility + req = NewRequest(t, "GET", "/user2/minimal/actions/workflows/pr.yml/badge.svg") + resp = MakeRequest(t, req, http.StatusSeeOther) + assertBadge(t, resp, "pr.yml-waiting-lightgrey") + + req = NewRequest(t, "GET", "/user2/minimal/actions/workflows/pr.yml/badge.svg?branch=main") + resp = MakeRequest(t, req, http.StatusSeeOther) + assertBadge(t, resp, "pr.yml-waiting-lightgrey") + + req = NewRequest(t, "GET", "/user2/minimal/actions/workflows/pr.yml/badge.svg?branch=no-such-branch") + resp = MakeRequest(t, req, http.StatusSeeOther) + assertBadge(t, resp, "pr.yml-Not%20found-crimson") + + req = NewRequest(t, "GET", "/user2/minimal/actions/workflows/pr.yml/badge.svg?event=cron") + resp = MakeRequest(t, req, http.StatusSeeOther) + assertBadge(t, resp, "pr.yml-Not%20found-crimson") + }) +} + +func TestBadges(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + t.Run("Stars", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/user2/repo1/badges/stars.svg") + resp := MakeRequest(t, req, http.StatusSeeOther) + + assertBadge(t, resp, "stars-0-blue") + }) + + t.Run("Issues", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer createMinimalRepo(t)() + + // Issues enabled + req := NewRequest(t, "GET", "/user2/repo1/badges/issues.svg") + resp := MakeRequest(t, req, http.StatusSeeOther) + assertBadge(t, resp, "issues-2-blue") + + req = NewRequest(t, "GET", "/user2/repo1/badges/issues/open.svg") + resp = MakeRequest(t, req, http.StatusSeeOther) + assertBadge(t, resp, "issues-1%20open-blue") + + req = NewRequest(t, "GET", "/user2/repo1/badges/issues/closed.svg") + resp = MakeRequest(t, req, http.StatusSeeOther) + assertBadge(t, resp, "issues-1%20closed-blue") + + // Issues disabled + req = NewRequest(t, "GET", "/user2/minimal/badges/issues.svg") + resp = MakeRequest(t, req, http.StatusSeeOther) + assertBadge(t, resp, "issues-Not%20found-crimson") + + req = NewRequest(t, "GET", "/user2/minimal/badges/issues/open.svg") + resp = MakeRequest(t, req, http.StatusSeeOther) + assertBadge(t, resp, "issues-Not%20found-crimson") + + req = NewRequest(t, "GET", "/user2/minimal/badges/issues/closed.svg") + resp = MakeRequest(t, req, http.StatusSeeOther) + assertBadge(t, resp, "issues-Not%20found-crimson") + }) + + t.Run("Pulls", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer createMinimalRepo(t)() + + // Pull requests enabled + req := NewRequest(t, "GET", "/user2/repo1/badges/pulls.svg") + resp := MakeRequest(t, req, http.StatusSeeOther) + assertBadge(t, resp, "pulls-3-blue") + + req = NewRequest(t, "GET", "/user2/repo1/badges/pulls/open.svg") + resp = MakeRequest(t, req, http.StatusSeeOther) + assertBadge(t, resp, "pulls-3%20open-blue") + + req = NewRequest(t, "GET", "/user2/repo1/badges/pulls/closed.svg") + resp = MakeRequest(t, req, http.StatusSeeOther) + assertBadge(t, resp, "pulls-0%20closed-blue") + + // Pull requests disabled + req = NewRequest(t, "GET", "/user2/minimal/badges/pulls.svg") + resp = MakeRequest(t, req, http.StatusSeeOther) + assertBadge(t, resp, "pulls-Not%20found-crimson") + + req = NewRequest(t, "GET", "/user2/minimal/badges/pulls/open.svg") + resp = MakeRequest(t, req, http.StatusSeeOther) + assertBadge(t, resp, "pulls-Not%20found-crimson") + + req = NewRequest(t, "GET", "/user2/minimal/badges/pulls/closed.svg") + resp = MakeRequest(t, req, http.StatusSeeOther) + assertBadge(t, resp, "pulls-Not%20found-crimson") + }) + + t.Run("Release", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer createMinimalRepo(t)() + + req := NewRequest(t, "GET", "/user2/repo1/badges/release.svg") + resp := MakeRequest(t, req, http.StatusSeeOther) + assertBadge(t, resp, "release-v1.1-blue") + + req = NewRequest(t, "GET", "/user2/minimal/badges/release.svg") + resp = MakeRequest(t, req, http.StatusSeeOther) + assertBadge(t, resp, "release-Not%20found-crimson") + }) +} From a2be4fab27b98b2932486f2b03635b044742f964 Mon Sep 17 00:00:00 2001 From: Gusted Date: Thu, 4 Jan 2024 23:29:28 +0100 Subject: [PATCH 57/86] [GITEA] Check for Commit in opengraph - It's possible that `PageIsDiff` is set but not `Commit` resulting in a NPE in the template. This can happen when the requested commit doesn't exist. - Regression of c802c46a9beeaed44d41f50de31a4db146cdd8f7 & 5743d7cb5bcd85c88ad7d128e0162893a074418b - Added 'hacky' integration test. (cherry picked from commit 8db2d5e4a76f05b34e4f889e7a00ecd6578d3639) (cherry picked from commit 8c737a802bcae54195f1bb15bb0b8aca824ef395) (cherry picked from commit 6b7c7d18dcdcfa135ff2657fbac8ce157eaf0dfa) --- tests/integration/repo_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go index 0265e99b7c..15292bbec1 100644 --- a/tests/integration/repo_test.go +++ b/tests/integration/repo_test.go @@ -683,7 +683,12 @@ func TestCommitView(t *testing.T) { defer tests.PrintCurrentTest(t)() req := NewRequest(t, "GET", "/user2/repo1/commit/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") - MakeRequest(t, req, http.StatusNotFound) + req.SetHeader("Accept", "text/html") + resp := MakeRequest(t, req, http.StatusNotFound) + + // Really ensure that 404 is being sent back. + doc := NewHTMLParser(t, resp.Body) + doc.AssertElement(t, `[aria-label="Page Not Found"]`, true) }) t.Run("Too short commit ID", func(t *testing.T) { From dee4a18423151ac7f22221e6fce12d863921c200 Mon Sep 17 00:00:00 2001 From: Gergely Nagy Date: Fri, 5 Jan 2024 10:44:33 +0100 Subject: [PATCH 58/86] [GITEA] Find README.md for user profiles case insensitively When trying to find a `README.md` in a `.profile` repo, do so case insensitively. This change does not make it possible to render readmes in formats other than Markdown, it just removes the hard-coded "README.md". Also adds a few tests to make sure the change works. Fixes #1494. Signed-off-by: Gergely Nagy (cherry picked from commit edd219d8e9d69becb9814ab0a8359555e80fcd4f) (cherry picked from commit 2c0105ef17b9673e6892a66aa689af7c5c87b8a1) (cherry picked from commit 3975a9f3aaf8ed3ceb5788abc325dbe8e89225d3) --- modules/git/tree_blob.go | 21 +++++ routers/web/shared/user/header.go | 2 +- tests/integration/user_profile_test.go | 120 +++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 tests/integration/user_profile_test.go diff --git a/modules/git/tree_blob.go b/modules/git/tree_blob.go index 31d9f3d73b..e60c1f915b 100644 --- a/modules/git/tree_blob.go +++ b/modules/git/tree_blob.go @@ -1,9 +1,12 @@ // Copyright 2015 The Gogs Authors. All rights reserved. // Copyright 2019 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. // SPDX-License-Identifier: MIT package git +import "strings" + // GetBlobByPath get the blob object according the path func (t *Tree) GetBlobByPath(relpath string) (*Blob, error) { entry, err := t.GetTreeEntryByPath(relpath) @@ -17,3 +20,21 @@ func (t *Tree) GetBlobByPath(relpath string) (*Blob, error) { return nil, ErrNotExist{"", relpath} } + +// GetBlobByFoldedPath returns the blob object at relpath, regardless of the +// case of relpath. If there are multiple files with the same case-insensitive +// name, the first one found will be returned. +func (t *Tree) GetBlobByFoldedPath(relpath string) (*Blob, error) { + entries, err := t.ListEntries() + if err != nil { + return nil, err + } + + for _, entry := range entries { + if strings.EqualFold(entry.Name(), relpath) { + return t.GetBlobByPath(entry.Name()) + } + } + + return nil, ErrNotExist{"", relpath} +} diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index 0f8d64e7b2..7129c7abc0 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -96,7 +96,7 @@ func FindUserProfileReadme(ctx *context.Context, doer *user_model.User) (profile if commit, err := profileGitRepo.GetBranchCommit(profileDbRepo.DefaultBranch); err != nil { log.Error("FindUserProfileReadme failed to GetBranchCommit: %v", err) } else { - profileReadmeBlob, _ = commit.GetBlobByPath("README.md") + profileReadmeBlob, _ = commit.GetBlobByFoldedPath("README.md") } } } diff --git a/tests/integration/user_profile_test.go b/tests/integration/user_profile_test.go new file mode 100644 index 0000000000..001e061674 --- /dev/null +++ b/tests/integration/user_profile_test.go @@ -0,0 +1,120 @@ +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "net/url" + "strings" + "testing" + "time" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + repo_service "code.gitea.io/gitea/services/repository" + files_service "code.gitea.io/gitea/services/repository/files" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func createProfileRepo(t *testing.T, readmeName string) func() { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + // Create a new repository + repo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{ + Name: ".profile", + DefaultBranch: "main", + IsPrivate: false, + AutoInit: true, + Readme: "Default", + }) + assert.NoError(t, err) + assert.NotEmpty(t, repo) + + deleteInitialReadmeResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, + &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + { + Operation: "delete", + TreePath: "README.md", + }, + }, + Message: "Delete the initial readme", + Author: &files_service.IdentityOptions{ + Name: user2.Name, + Email: user2.Email, + }, + Committer: &files_service.IdentityOptions{ + Name: user2.Name, + Email: user2.Email, + }, + Dates: &files_service.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }) + assert.NoError(t, err) + assert.NotEmpty(t, deleteInitialReadmeResp) + + if readmeName != "" { + addReadmeResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, + &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: readmeName, + ContentReader: strings.NewReader("# Hi!\n"), + }, + }, + Message: "Add a readme", + Author: &files_service.IdentityOptions{ + Name: user2.Name, + Email: user2.Email, + }, + Committer: &files_service.IdentityOptions{ + Name: user2.Name, + Email: user2.Email, + }, + Dates: &files_service.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }) + + assert.NoError(t, err) + assert.NotEmpty(t, addReadmeResp) + } + + return func() { + repo_service.DeleteRepository(db.DefaultContext, user2, repo, false) + } +} + +func TestUserProfile(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + checkReadme := func(t *testing.T, title, readmeFilename string, expectedCount int) { + t.Run(title, func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer createProfileRepo(t, readmeFilename)() + + req := NewRequest(t, "GET", "/user2") + resp := MakeRequest(t, req, http.StatusOK) + + doc := NewHTMLParser(t, resp.Body) + readmeCount := doc.Find("#readme_profile").Length() + + assert.Equal(t, expectedCount, readmeCount) + }) + } + + checkReadme(t, "No readme", "", 0) + checkReadme(t, "README.md", "README.md", 1) + checkReadme(t, "readme.md", "readme.md", 1) + checkReadme(t, "ReadMe.mD", "ReadMe.mD", 1) + checkReadme(t, "readme.org does not render", "README.org", 0) + }) +} From c4589d1fce5eac383dd8530427140183a7aeff46 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Mon, 8 Jan 2024 17:53:02 +0100 Subject: [PATCH 59/86] [GITEA] add option for banning dots in usernames (squash) set in test (cherry picked from commit b005b586c354119d2b6aaf6e4c18eb3f1ddfb615) (cherry picked from commit 0077b2661e7e5be7b2e3772113abeb401f4085d5) --- models/user/user_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/models/user/user_test.go b/models/user/user_test.go index 0e08529156..d89f99b871 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -556,6 +556,11 @@ func Test_ValidateUser(t *testing.T) { } func Test_NormalizeUserFromEmail(t *testing.T) { + oldSetting := setting.Service.AllowDotsInUsernames + defer func() { + setting.Service.AllowDotsInUsernames = oldSetting + }() + setting.Service.AllowDotsInUsernames = true testCases := []struct { Input string Expected string From 69b45c3feaf92454853ef9b02c9d75092780dabf Mon Sep 17 00:00:00 2001 From: Gergely Nagy Date: Sat, 6 Jan 2024 10:06:17 +0100 Subject: [PATCH 60/86] [GITEA] Disable the RSS feed in file view for non-branches Files can have an RSS feed, but those only make sense when taken in the context of a branch. There is no history to make a feed of on a tag or a commit: they're static. Forgejo does not provide a feed for them for this reason. However, the file view on the web UI was offering a link to these non-existent feeds. With this patch, it does that no longer, and only provides a link when viewing the file in the context of a branch. Fixes #2102. Signed-off-by: Gergely Nagy (cherry picked from commit 4b48d21ea7459539dfb1ca5cadd6f9cb99e65fc7) (cherry picked from commit 70cb2667603bcdb9a8c9bb20c482877ab3f6de39) --- options/locale/locale_en-US.ini | 2 ++ templates/repo/view_file.tmpl | 12 +++++++++--- tests/integration/repo_test.go | 34 +++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 0e51a35757..675e96dd48 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -938,6 +938,8 @@ visibility.private = Private visibility.private_tooltip = Visible only to members of organizations you have joined [repo] +rss.must_be_on_branch = You must be on a branch to have an RSS feed. + new_repo_helper = A repository contains all project files, including revision history. Already hosting one elsewhere? Migrate repository. owner = Owner owner_helper = Some organizations may not show up in the dropdown due to a maximum repository count limit. diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index e6591df7e3..854ae962c6 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -59,9 +59,15 @@ {{svg "octicon-download"}} {{svg "octicon-copy" 14}} {{if .EnableFeed}} - - {{svg "octicon-rss" 14}} - + {{if .IsViewBranch}} + + {{svg "octicon-rss" 14}} + + {{else}} + + {{svg "octicon-rss" 14}} + + {{end}} {{end}} {{if .Repository.CanEnableEditor}} {{if .CanEditFile}} diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go index 15292bbec1..3f2755870e 100644 --- a/tests/integration/repo_test.go +++ b/tests/integration/repo_test.go @@ -407,6 +407,40 @@ func TestViewFileInRepo(t *testing.T) { assert.EqualValues(t, 0, repoSummary.Length()) } +func TestViewFileInRepoRSSFeed(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + hasFileRSSFeed := func(t *testing.T, ref string) bool { + t.Helper() + + req := NewRequestf(t, "GET", "/user2/repo1/src/%s/README.md", ref) + resp := MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + fileFeed := htmlDoc.doc.Find(`a[href*="/user2/repo1/rss/"]`) + + return fileFeed.Length() != 0 + } + + t.Run("branch", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + assert.True(t, hasFileRSSFeed(t, "branch/master")) + }) + + t.Run("tag", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + assert.False(t, hasFileRSSFeed(t, "tag/v1.1")) + }) + + t.Run("commit", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + assert.False(t, hasFileRSSFeed(t, "commit/65f1bf27bc3bf70f64657658635e66094edbcb4d")) + }) +} + // TestBlameFileInRepo repo description, topics and summary should not be displayed when running blame on a file func TestBlameFileInRepo(t *testing.T) { defer tests.PrepareTestEnv(t)() From 2f8b0414892f6099f519bda63a9e0fbc8ba6cfc7 Mon Sep 17 00:00:00 2001 From: Gergely Nagy Date: Thu, 4 Jan 2024 14:28:19 +0100 Subject: [PATCH 61/86] [FEAT] Repository flags This implements "repository flags", a way for instance administrators to assign custom flags to repositories. The idea is that custom templates can look at these flags, and display banners based on them, Forgejo does not provide anything built on top of it, just the foundation. The feature is optional, and disabled by default. To enable it, set `[repository].ENABLE_FLAGS = true`. On the UI side, instance administrators will see a new "Manage flags" tab on repositories, and a list of enabled tags (if any) on the repository home page. The "Manage flags" page allows them to remove existing flags, or add any new ones that are listed in `[repository].SETTABLE_FLAGS`. The model does not enforce that only the `SETTABLE_FLAGS` are present. If the setting is changed, old flags may remain present in the database, and anything that uses them, will still work. The repository flag management page will allow an instance administrator to remove them, but not set them, once removed. Signed-off-by: Gergely Nagy (cherry picked from commit ba735ce2228f8dd7ca105e94b9baa1be058ebe37) (cherry picked from commit f09f6e029b4fb2714b86cd32dc19255078ecc0ee) --- models/forgejo_migrations/migrate.go | 2 + models/forgejo_migrations/v1_22/v5.go | 22 +++ models/repo/repo_flags.go | 102 ++++++++++ models/repo/repo_flags_test.go | 114 +++++++++++ modules/setting/repository.go | 7 + modules/templates/helper.go | 3 + options/locale/locale_en-US.ini | 6 + routers/web/repo/flags/manage.go | 49 +++++ routers/web/web.go | 8 + templates/custom/repo_flag_banners.tmpl | 0 templates/repo/admin_flags.tmpl | 8 + templates/repo/flags.tmpl | 33 ++++ templates/repo/header.tmpl | 6 + templates/repo/home.tmpl | 8 + tests/integration/repo_flags_test.go | 242 ++++++++++++++++++++++++ 15 files changed, 610 insertions(+) create mode 100644 models/forgejo_migrations/v1_22/v5.go create mode 100644 models/repo/repo_flags.go create mode 100644 models/repo/repo_flags_test.go create mode 100644 routers/web/repo/flags/manage.go create mode 100644 templates/custom/repo_flag_banners.tmpl create mode 100644 templates/repo/admin_flags.tmpl create mode 100644 templates/repo/flags.tmpl create mode 100644 tests/integration/repo_flags_test.go diff --git a/models/forgejo_migrations/migrate.go b/models/forgejo_migrations/migrate.go index 8ac2fb63f2..f0e22c046f 100644 --- a/models/forgejo_migrations/migrate.go +++ b/models/forgejo_migrations/migrate.go @@ -46,6 +46,8 @@ var migrations = []*Migration{ NewMigration("create the forgejo_auth_token table", forgejo_v1_20.CreateAuthorizationTokenTable), // v3 -> v4 NewMigration("Add default_permissions to repo_unit", forgejo_v1_22.AddDefaultPermissionsToRepoUnit), + // v4 -> v5 + NewMigration("create the forgejo_repo_flag table", forgejo_v1_22.CreateRepoFlagTable), } // GetCurrentDBVersion returns the current Forgejo database version. diff --git a/models/forgejo_migrations/v1_22/v5.go b/models/forgejo_migrations/v1_22/v5.go new file mode 100644 index 0000000000..55f9fe1338 --- /dev/null +++ b/models/forgejo_migrations/v1_22/v5.go @@ -0,0 +1,22 @@ +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "xorm.io/xorm" +) + +type RepoFlag struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"UNIQUE(s) INDEX"` + Name string `xorm:"UNIQUE(s) INDEX"` +} + +func (RepoFlag) TableName() string { + return "forgejo_repo_flag" +} + +func CreateRepoFlagTable(x *xorm.Engine) error { + return x.Sync(new(RepoFlag)) +} diff --git a/models/repo/repo_flags.go b/models/repo/repo_flags.go new file mode 100644 index 0000000000..de76ed2b37 --- /dev/null +++ b/models/repo/repo_flags.go @@ -0,0 +1,102 @@ +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "context" + + "code.gitea.io/gitea/models/db" + + "xorm.io/builder" +) + +// RepoFlag represents a single flag against a repository +type RepoFlag struct { //revive:disable-line:exported + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"UNIQUE(s) INDEX"` + Name string `xorm:"UNIQUE(s) INDEX"` +} + +func init() { + db.RegisterModel(new(RepoFlag)) +} + +// TableName provides the real table name +func (RepoFlag) TableName() string { + return "forgejo_repo_flag" +} + +// ListFlags returns the array of flags on the repo. +func (repo *Repository) ListFlags(ctx context.Context) ([]RepoFlag, error) { + var flags []RepoFlag + err := db.GetEngine(ctx).Table(&RepoFlag{}).Where("repo_id = ?", repo.ID).Find(&flags) + if err != nil { + return nil, err + } + return flags, nil +} + +// IsFlagged returns whether a repo has any flags or not +func (repo *Repository) IsFlagged(ctx context.Context) bool { + has, _ := db.Exist[RepoFlag](ctx, builder.Eq{"repo_id": repo.ID}) + return has +} + +// GetFlag returns a single RepoFlag based on its name +func (repo *Repository) GetFlag(ctx context.Context, flagName string) (bool, *RepoFlag, error) { + flag, has, err := db.Get[RepoFlag](ctx, builder.Eq{"repo_id": repo.ID, "name": flagName}) + if err != nil { + return false, nil, err + } + return has, flag, nil +} + +// HasFlag returns true if a repo has a given flag, false otherwise +func (repo *Repository) HasFlag(ctx context.Context, flagName string) bool { + has, _ := db.Exist[RepoFlag](ctx, builder.Eq{"repo_id": repo.ID, "name": flagName}) + return has +} + +// AddFlag adds a new flag to the repo +func (repo *Repository) AddFlag(ctx context.Context, flagName string) error { + return db.Insert(ctx, RepoFlag{ + RepoID: repo.ID, + Name: flagName, + }) +} + +// DeleteFlag removes a flag from the repo +func (repo *Repository) DeleteFlag(ctx context.Context, flagName string) (int64, error) { + return db.DeleteByBean(ctx, &RepoFlag{RepoID: repo.ID, Name: flagName}) +} + +// ReplaceAllFlags replaces all flags of a repo with a new set +func (repo *Repository) ReplaceAllFlags(ctx context.Context, flagNames []string) error { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + + if err := db.DeleteBeans(ctx, &RepoFlag{RepoID: repo.ID}); err != nil { + return err + } + + if len(flagNames) == 0 { + return committer.Commit() + } + + var flags []RepoFlag + for _, name := range flagNames { + flags = append(flags, RepoFlag{ + RepoID: repo.ID, + Name: name, + }) + } + if err := db.Insert(ctx, &flags); err != nil { + return err + } + + return committer.Commit() +} diff --git a/models/repo/repo_flags_test.go b/models/repo/repo_flags_test.go new file mode 100644 index 0000000000..0e4f5c1ba9 --- /dev/null +++ b/models/repo/repo_flags_test.go @@ -0,0 +1,114 @@ +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo_test + +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 TestRepositoryFlags(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10}) + + // ******************** + // ** NEGATIVE TESTS ** + // ******************** + + // Unless we add flags, the repo has none + flags, err := repo.ListFlags(db.DefaultContext) + assert.NoError(t, err) + assert.Empty(t, flags) + + // If the repo has no flags, it is not flagged + flagged := repo.IsFlagged(db.DefaultContext) + assert.False(t, flagged) + + // Trying to find a flag when there is none + has := repo.HasFlag(db.DefaultContext, "foo") + assert.False(t, has) + + // Trying to retrieve a non-existent flag indicates not found + has, _, err = repo.GetFlag(db.DefaultContext, "foo") + assert.NoError(t, err) + assert.False(t, has) + + // Deleting a non-existent flag fails + deleted, err := repo.DeleteFlag(db.DefaultContext, "no-such-flag") + assert.NoError(t, err) + assert.Equal(t, int64(0), deleted) + + // ******************** + // ** POSITIVE TESTS ** + // ******************** + + // Adding a flag works + err = repo.AddFlag(db.DefaultContext, "foo") + assert.NoError(t, err) + + // Adding it again fails + err = repo.AddFlag(db.DefaultContext, "foo") + assert.Error(t, err) + + // Listing flags includes the one we added + flags, err = repo.ListFlags(db.DefaultContext) + assert.NoError(t, err) + assert.Len(t, flags, 1) + assert.Equal(t, "foo", flags[0].Name) + + // With a flag added, the repo is flagged + flagged = repo.IsFlagged(db.DefaultContext) + assert.True(t, flagged) + + // The flag can be found + has = repo.HasFlag(db.DefaultContext, "foo") + assert.True(t, has) + + // Added flag can be retrieved + _, flag, err := repo.GetFlag(db.DefaultContext, "foo") + assert.NoError(t, err) + assert.Equal(t, "foo", flag.Name) + + // Deleting a flag works + deleted, err = repo.DeleteFlag(db.DefaultContext, "foo") + assert.NoError(t, err) + assert.Equal(t, int64(1), deleted) + + // The list is now empty + flags, err = repo.ListFlags(db.DefaultContext) + assert.NoError(t, err) + assert.Empty(t, flags) + + // Replacing an empty list works + err = repo.ReplaceAllFlags(db.DefaultContext, []string{"bar"}) + assert.NoError(t, err) + + // The repo is now flagged with "bar" + has = repo.HasFlag(db.DefaultContext, "bar") + assert.True(t, has) + + // Replacing a tag set with another works + err = repo.ReplaceAllFlags(db.DefaultContext, []string{"baz", "quux"}) + assert.NoError(t, err) + + // The repo now has two tags + flags, err = repo.ListFlags(db.DefaultContext) + assert.NoError(t, err) + assert.Len(t, flags, 2) + assert.Equal(t, "baz", flags[0].Name) + assert.Equal(t, "quux", flags[1].Name) + + // Replacing flags with an empty set deletes all flags + err = repo.ReplaceAllFlags(db.DefaultContext, []string{}) + assert.NoError(t, err) + + // The repo is now unflagged + flagged = repo.IsFlagged(db.DefaultContext) + assert.False(t, flagged) +} diff --git a/modules/setting/repository.go b/modules/setting/repository.go index b6b0b50504..fd17fbf55d 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -113,6 +113,9 @@ var ( Wiki []string DefaultTrustModel string } `ini:"repository.signing"` + + SettableFlags []string + EnableFlags bool }{ DetectedCharsetsOrder: []string{ "UTF-8", @@ -270,6 +273,8 @@ var ( Wiki: []string{"never"}, DefaultTrustModel: "collaborator", }, + + EnableFlags: false, } RepoRootPath string ScriptType = "bash" @@ -372,4 +377,6 @@ func loadRepositoryFrom(rootCfg ConfigProvider) { log.Error("Unrecognised repository download or clone method: %s", method) } } + + Repository.EnableFlags = sec.Key("ENABLE_FLAGS").MustBool() } diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 96cdd9ca46..bcb94bff25 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -96,6 +96,9 @@ func NewFuncMap() template.FuncMap { "AppDomain": func() string { // documented in mail-templates.md return setting.Domain }, + "RepoFlagsEnabled": func() bool { + return setting.Repository.EnableFlags + }, "AssetVersion": func() string { return setting.AssetVersion }, diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 675e96dd48..25e8751232 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -940,6 +940,12 @@ visibility.private_tooltip = Visible only to members of organizations you have j [repo] rss.must_be_on_branch = You must be on a branch to have an RSS feed. +admin.manage_flags = Manage flags +admin.enabled_flags = Flags enabled for the repository: +admin.update_flags = Update flags +admin.failed_to_replace_flags = Failed to replace repository flags +admin.flags_replaced = Repository flags replaced + new_repo_helper = A repository contains all project files, including revision history. Already hosting one elsewhere? Migrate repository. owner = Owner owner_helper = Some organizations may not show up in the dropdown due to a maximum repository count limit. diff --git a/routers/web/repo/flags/manage.go b/routers/web/repo/flags/manage.go new file mode 100644 index 0000000000..840f6c3773 --- /dev/null +++ b/routers/web/repo/flags/manage.go @@ -0,0 +1,49 @@ +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package flags + +import ( + "net/http" + + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" +) + +const ( + tplRepoFlags base.TplName = "repo/flags" +) + +func Manage(ctx *context.Context) { + ctx.Data["IsRepoFlagsPage"] = true + ctx.Data["Title"] = ctx.Tr("repo.admin.manage_flags") + + flags := map[string]bool{} + for _, f := range setting.Repository.SettableFlags { + flags[f] = false + } + repoFlags, _ := ctx.Repo.Repository.ListFlags(ctx) + for _, f := range repoFlags { + flags[f.Name] = true + } + + ctx.Data["Flags"] = flags + + ctx.HTML(http.StatusOK, tplRepoFlags) +} + +func ManagePost(ctx *context.Context) { + newFlags := ctx.FormStrings("flags") + + err := ctx.Repo.Repository.ReplaceAllFlags(ctx, newFlags) + if err != nil { + ctx.Flash.Error(ctx.Tr("repo.admin.failed_to_replace_flags")) + log.Error("Error replacing repository flags for repo %d: %v", ctx.Repo.Repository.ID, err) + } else { + ctx.Flash.Success(ctx.Tr("repo.admin.flags_replaced")) + } + + ctx.Redirect(ctx.Repo.Repository.HTMLURL() + "/flags") +} diff --git a/routers/web/web.go b/routers/web/web.go index c15d1800f3..f9611b5f4b 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -38,6 +38,7 @@ import ( "code.gitea.io/gitea/routers/web/repo" "code.gitea.io/gitea/routers/web/repo/actions" "code.gitea.io/gitea/routers/web/repo/badges" + repo_flags "code.gitea.io/gitea/routers/web/repo/flags" repo_setting "code.gitea.io/gitea/routers/web/repo/setting" "code.gitea.io/gitea/routers/web/user" user_setting "code.gitea.io/gitea/routers/web/user/setting" @@ -1574,6 +1575,13 @@ func registerRoutes(m *web.Route) { gitHTTPRouters(m) }) }) + + if setting.Repository.EnableFlags { + m.Group("/{username}/{reponame}/flags", func() { + m.Get("", repo_flags.Manage) + m.Post("", repo_flags.ManagePost) + }, adminReq, context.RepoAssignment, context.UnitTypes()) + } // ***** END: Repository ***** m.Group("/notifications", func() { diff --git a/templates/custom/repo_flag_banners.tmpl b/templates/custom/repo_flag_banners.tmpl new file mode 100644 index 0000000000..e69de29bb2 diff --git a/templates/repo/admin_flags.tmpl b/templates/repo/admin_flags.tmpl new file mode 100644 index 0000000000..2a65c9c687 --- /dev/null +++ b/templates/repo/admin_flags.tmpl @@ -0,0 +1,8 @@ +{{if .Repository.IsFlagged $.Context}} +
+ {{ctx.Locale.Tr "repo.admin.enabled_flags"}} + {{range .Repository.ListFlags $.Context}} + {{.Name}} + {{end}} +
+{{end}} diff --git a/templates/repo/flags.tmpl b/templates/repo/flags.tmpl new file mode 100644 index 0000000000..3928cf0e17 --- /dev/null +++ b/templates/repo/flags.tmpl @@ -0,0 +1,33 @@ +{{template "base/head" .}} +
+ {{template "repo/header" .}} +
+ {{template "base/alert" .}} +
+

+ {{ctx.Locale.Tr "repo.admin.manage_flags"}} +

+
+
+ {{.CsrfTokenHtml}} + {{ctx.Locale.Tr "repo.admin.enabled_flags"}} +
+ {{range $flag, $checked := .Flags}} +
+
+ + +
+
+ {{end}} +
+ +
+ +
+
+
+
+
+
+{{template "base/footer" .}} diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index dac30af600..201a3b9e79 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -240,6 +240,12 @@ {{template "custom/extra_tabs" .}} + {{if and RepoFlagsEnabled .SignedUser.IsAdmin}} + + {{svg "octicon-milestone"}} {{ctx.Locale.Tr "repo.admin.manage_flags"}} + + {{end}} + {{if .Permission.IsAdmin}} {{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}} diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index eb6d96f28e..5e27d9160c 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -52,6 +52,14 @@
{{end}} + + {{if RepoFlagsEnabled}} + {{template "custom/repo_flag_banners" .}} + {{if .SignedUser.IsAdmin}} + {{template "repo/admin_flags" .}} + {{end}} + {{end}} + {{if .Repository.IsArchived}}
{{if .Repository.ArchivedUnix.IsZero}} diff --git a/tests/integration/repo_flags_test.go b/tests/integration/repo_flags_test.go new file mode 100644 index 0000000000..a335ca9adf --- /dev/null +++ b/tests/integration/repo_flags_test.go @@ -0,0 +1,242 @@ +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "code.gitea.io/gitea/models/db" + 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/setting" + "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/routers" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestRepositoryFlagsUIDisabled(t *testing.T) { + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.Repository.EnableFlags, false)() + defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() + + admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}) + session := loginUser(t, admin.Name) + + // With the repo flags feature disabled, the /flags route is 404 + req := NewRequest(t, "GET", "/user2/repo1/flags") + session.MakeRequest(t, req, http.StatusNotFound) + + // With the repo flags feature disabled, the "Modify flags" tab does not + // appear for instance admins + req = NewRequest(t, "GET", "/user2/repo1") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + flagsLinkCount := doc.Find(fmt.Sprintf(`a[href="%s/flags"]`, "/user2/repo1")).Length() + assert.Equal(t, 0, flagsLinkCount) +} + +func TestRepositoryFlagsUI(t *testing.T) { + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.Repository.EnableFlags, true)() + defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() + + // ******************* + // ** Preparations ** + // ******************* + flaggedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + unflaggedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) + + // ************** + // ** Helpers ** + // ************** + + adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}).Name + flaggedOwner := "user2" + flaggedRepoURLStr := "/user2/repo1" + unflaggedOwner := "user5" + unflaggedRepoURLStr := "/user5/repo4" + otherUser := "user4" + + ensureFlags := func(repo *repo_model.Repository, flags []string) func() { + repo.ReplaceAllFlags(db.DefaultContext, flags) + + return func() { + repo.ReplaceAllFlags(db.DefaultContext, flags) + } + } + + // Tests: + // - Presence of the link + // - Number of flags listed in the admin-only message box + // - Whether there's a link to /user/repo/flags + // - Whether /user/repo/flags is OK or Forbidden + assertFlagAccessAndCount := func(t *testing.T, user, repoURL string, hasAccess bool, expectedFlagCount int) { + t.Helper() + + var expectedLinkCount int + var expectedStatus int + if hasAccess { + expectedLinkCount = 1 + expectedStatus = http.StatusOK + } else { + expectedLinkCount = 0 + if user != "" { + expectedStatus = http.StatusForbidden + } else { + expectedStatus = http.StatusSeeOther + } + } + + var resp *httptest.ResponseRecorder + var session *TestSession + req := NewRequest(t, "GET", repoURL) + if user != "" { + session = loginUser(t, user) + resp = session.MakeRequest(t, req, http.StatusOK) + } else { + resp = MakeRequest(t, req, http.StatusOK) + } + doc := NewHTMLParser(t, resp.Body) + + flagsLinkCount := doc.Find(fmt.Sprintf(`a[href="%s/flags"]`, repoURL)).Length() + assert.Equal(t, expectedLinkCount, flagsLinkCount) + + flagCount := doc.Find(".ui.info.message .ui.label").Length() + assert.Equal(t, expectedFlagCount, flagCount) + + req = NewRequest(t, "GET", fmt.Sprintf("%s/flags", repoURL)) + if user != "" { + session.MakeRequest(t, req, expectedStatus) + } else { + MakeRequest(t, req, expectedStatus) + } + } + + // Ensures that given a repo owner and a repo: + // - An instance admin has access to flags, and sees the list on the repo home + // - A repo admin does not have access to either, and does not see the list + // - A passer by has no access to either, and does not see the list + runTests := func(t *testing.T, ownerUser, repoURL string, expectedFlagCount int) { + t.Run("as instance admin", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + assertFlagAccessAndCount(t, adminUser, repoURL, true, expectedFlagCount) + }) + t.Run("as owner", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + assertFlagAccessAndCount(t, ownerUser, repoURL, false, 0) + }) + t.Run("as other user", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + assertFlagAccessAndCount(t, otherUser, repoURL, false, 0) + }) + t.Run("as non-logged in user", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + assertFlagAccessAndCount(t, "", repoURL, false, 0) + }) + } + + // ************************** + // ** The tests themselves ** + // ************************** + t.Run("unflagged repo", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer ensureFlags(unflaggedRepo, []string{})() + + runTests(t, unflaggedOwner, unflaggedRepoURLStr, 0) + }) + + t.Run("flagged repo", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer ensureFlags(flaggedRepo, []string{"test-flag"})() + + runTests(t, flaggedOwner, flaggedRepoURLStr, 1) + }) + + t.Run("modifying flags", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + session := loginUser(t, adminUser) + flaggedRepoManageURL := fmt.Sprintf("%s/flags", flaggedRepoURLStr) + unflaggedRepoManageURL := fmt.Sprintf("%s/flags", unflaggedRepoURLStr) + + assertUIFlagStates := func(t *testing.T, url string, flagStates map[string]bool) { + t.Helper() + + req := NewRequest(t, "GET", url) + resp := session.MakeRequest(t, req, http.StatusOK) + + doc := NewHTMLParser(t, resp.Body) + flagBoxes := doc.Find(`input[name="flags"]`) + assert.Equal(t, len(flagStates), flagBoxes.Length()) + + for name, state := range flagStates { + _, checked := doc.Find(fmt.Sprintf(`input[value="%s"]`, name)).Attr("checked") + assert.Equal(t, state, checked) + } + } + + t.Run("flag presence on the UI", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer ensureFlags(flaggedRepo, []string{"test-flag"})() + + assertUIFlagStates(t, flaggedRepoManageURL, map[string]bool{"test-flag": true}) + }) + + t.Run("setting.Repository.SettableFlags is respected", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.Repository.SettableFlags, []string{"featured", "no-license"})() + defer ensureFlags(flaggedRepo, []string{"test-flag"})() + + assertUIFlagStates(t, flaggedRepoManageURL, map[string]bool{ + "test-flag": true, + "featured": false, + "no-license": false, + }) + }) + + t.Run("removing flags", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer ensureFlags(flaggedRepo, []string{"test-flag"})() + + flagged := flaggedRepo.IsFlagged(db.DefaultContext) + assert.True(t, flagged) + + req := NewRequestWithValues(t, "POST", flaggedRepoManageURL, map[string]string{ + "_csrf": GetCSRF(t, session, flaggedRepoManageURL), + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + flagged = flaggedRepo.IsFlagged(db.DefaultContext) + assert.False(t, flagged) + + assertUIFlagStates(t, flaggedRepoManageURL, map[string]bool{}) + }) + + t.Run("adding flags", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer ensureFlags(unflaggedRepo, []string{})() + + flagged := unflaggedRepo.IsFlagged(db.DefaultContext) + assert.False(t, flagged) + + req := NewRequestWithValues(t, "POST", unflaggedRepoManageURL, map[string]string{ + "_csrf": GetCSRF(t, session, unflaggedRepoManageURL), + "flags": "test-flag", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + assertUIFlagStates(t, unflaggedRepoManageURL, map[string]bool{"test-flag": true}) + }) + }) +} From 95d9fe19cf3ed5787855ac2a442d29104498aa36 Mon Sep 17 00:00:00 2001 From: Gergely Nagy Date: Fri, 5 Jan 2024 13:45:10 +0100 Subject: [PATCH 62/86] [FEAT] API support for repository flags Expose the repository flags feature over the API, so the flags can be managed by a site administrator without using the web API. Signed-off-by: Gergely Nagy (cherry picked from commit bac9f0225d47e159afa90e5bbea9562cbc860dae) (cherry picked from commit e7f5c1ba141ac7f8c7834b5048d0ffd3ce50900b) --- modules/structs/repo_flags.go | 9 + routers/api/v1/api.go | 12 ++ routers/api/v1/repo/flags.go | 245 ++++++++++++++++++++++++ routers/api/v1/swagger/options.go | 3 + templates/swagger/v1_json.tmpl | 268 +++++++++++++++++++++++++++ tests/integration/repo_flags_test.go | 149 +++++++++++++++ 6 files changed, 686 insertions(+) create mode 100644 modules/structs/repo_flags.go create mode 100644 routers/api/v1/repo/flags.go diff --git a/modules/structs/repo_flags.go b/modules/structs/repo_flags.go new file mode 100644 index 0000000000..5db714545c --- /dev/null +++ b/modules/structs/repo_flags.go @@ -0,0 +1,9 @@ +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package structs + +// ReplaceFlagsOption options when replacing the flags of a repository +type ReplaceFlagsOption struct { + Flags []string `json:"flags"` +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index fc995c44ab..cd5c18235f 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1096,6 +1096,18 @@ func Routes() *web.Route { m.Get("/permission", repo.GetRepoPermissions) }) }, reqToken()) + if setting.Repository.EnableFlags { + m.Group("/flags", func() { + m.Combo("").Get(repo.ListFlags). + Put(bind(api.ReplaceFlagsOption{}), repo.ReplaceAllFlags). + Delete(repo.DeleteAllFlags) + m.Group("/{flag}", func() { + m.Combo("").Get(repo.HasFlag). + Put(repo.AddFlag). + Delete(repo.DeleteFlag) + }) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin()) + } m.Get("/assignees", reqToken(), reqAnyRepoReader(), repo.GetAssignees) m.Get("/reviewers", reqToken(), reqAnyRepoReader(), repo.GetReviewers) m.Group("/teams", func() { diff --git a/routers/api/v1/repo/flags.go b/routers/api/v1/repo/flags.go new file mode 100644 index 0000000000..cbb2c95914 --- /dev/null +++ b/routers/api/v1/repo/flags.go @@ -0,0 +1,245 @@ +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + + "code.gitea.io/gitea/modules/context" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" +) + +func ListFlags(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/flags repository repoListFlags + // --- + // summary: List a repository's flags + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/StringSlice" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + repoFlags, err := ctx.Repo.Repository.ListFlags(ctx) + if err != nil { + ctx.InternalServerError(err) + return + } + + flags := make([]string, len(repoFlags)) + for i := range repoFlags { + flags[i] = repoFlags[i].Name + } + + ctx.SetTotalCountHeader(int64(len(repoFlags))) + ctx.JSON(http.StatusOK, flags) +} + +func ReplaceAllFlags(ctx *context.APIContext) { + // swagger:operation PUT /repos/{owner}/{repo}/flags repository repoReplaceAllFlags + // --- + // summary: Replace all flags of a repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/ReplaceFlagsOption" + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + flagsForm := web.GetForm(ctx).(*api.ReplaceFlagsOption) + + if err := ctx.Repo.Repository.ReplaceAllFlags(ctx, flagsForm.Flags); err != nil { + ctx.InternalServerError(err) + return + } + + ctx.Status(http.StatusNoContent) +} + +func DeleteAllFlags(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/flags repository repoDeleteAllFlags + // --- + // summary: Remove all flags from a repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + if err := ctx.Repo.Repository.ReplaceAllFlags(ctx, nil); err != nil { + ctx.InternalServerError(err) + return + } + + ctx.Status(http.StatusNoContent) +} + +func HasFlag(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/flags/{flag} repository repoCheckFlag + // --- + // summary: Check if a repository has a given flag + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: flag + // in: path + // description: name of the flag + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + hasFlag := ctx.Repo.Repository.HasFlag(ctx, ctx.Params(":flag")) + if hasFlag { + ctx.Status(http.StatusNoContent) + } else { + ctx.NotFound() + } +} + +func AddFlag(ctx *context.APIContext) { + // swagger:operation PUT /repos/{owner}/{repo}/flags/{flag} repository repoAddFlag + // --- + // summary: Add a flag to a repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: flag + // in: path + // description: name of the flag + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + flag := ctx.Params(":flag") + + if ctx.Repo.Repository.HasFlag(ctx, flag) { + ctx.Status(http.StatusNoContent) + return + } + + if err := ctx.Repo.Repository.AddFlag(ctx, flag); err != nil { + ctx.InternalServerError(err) + return + } + ctx.Status(http.StatusNoContent) +} + +func DeleteFlag(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/flags/{flag} repository repoDeleteFlag + // --- + // summary: Remove a flag from a repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: flag + // in: path + // description: name of the flag + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + flag := ctx.Params(":flag") + + if _, err := ctx.Repo.Repository.DeleteFlag(ctx, flag); err != nil { + ctx.InternalServerError(err) + return + } + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index b5efbe916d..cca6d2d572 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -17,6 +17,9 @@ type swaggerParameterBodies struct { // in:body AddCollaboratorOption api.AddCollaboratorOption + // in:body + ReplaceFlagsOption api.ReplaceFlagsOption + // in:body CreateEmailOption api.CreateEmailOption // in:body diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 52dface646..b4f82ab93f 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -4992,6 +4992,260 @@ } } }, + "/repos/{owner}/{repo}/flags": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "List a repository's flags", + "operationId": "repoListFlags", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/StringSlice" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Replace all flags of a repository", + "operationId": "repoReplaceAllFlags", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/ReplaceFlagsOption" + } + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Remove all flags from a repository", + "operationId": "repoDeleteAllFlags", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, + "/repos/{owner}/{repo}/flags/{flag}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Check if a repository has a given flag", + "operationId": "repoCheckFlag", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the flag", + "name": "flag", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Add a flag to a repository", + "operationId": "repoAddFlag", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the flag", + "name": "flag", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Remove a flag from a repository", + "operationId": "repoDeleteFlag", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the flag", + "name": "flag", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/forks": { "get": { "produces": [ @@ -22008,6 +22262,20 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "ReplaceFlagsOption": { + "description": "ReplaceFlagsOption options when replacing the flags of a repository", + "type": "object", + "properties": { + "flags": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "Flags" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "RepoCollaboratorPermission": { "description": "RepoCollaboratorPermission to get repository permission for a collaborator", "type": "object", diff --git a/tests/integration/repo_flags_test.go b/tests/integration/repo_flags_test.go index a335ca9adf..8b64776a5a 100644 --- a/tests/integration/repo_flags_test.go +++ b/tests/integration/repo_flags_test.go @@ -7,13 +7,16 @@ import ( "fmt" "net/http" "net/http/httptest" + "slices" "testing" + auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" 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/setting" + api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/routers" "code.gitea.io/gitea/tests" @@ -42,6 +45,152 @@ func TestRepositoryFlagsUIDisabled(t *testing.T) { assert.Equal(t, 0, flagsLinkCount) } +func TestRepositoryFlagsAPI(t *testing.T) { + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.Repository.EnableFlags, true)() + defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() + + // ************* + // ** Helpers ** + // ************* + + adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}).Name + normalUserBean := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + assert.False(t, normalUserBean.IsAdmin) + normalUser := normalUserBean.Name + + assertAccess := func(t *testing.T, user, method, uri string, expectedStatus int) { + session := loginUser(t, user) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeReadAdmin) + + req := NewRequestf(t, method, "/api/v1/repos/user2/repo1/flags%s", uri).AddTokenAuth(token) + MakeRequest(t, req, expectedStatus) + } + + // *********** + // ** Tests ** + // *********** + + t.Run("API access", func(t *testing.T) { + t.Run("as admin", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + assertAccess(t, adminUser, "GET", "", http.StatusOK) + }) + + t.Run("as normal user", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + assertAccess(t, normalUser, "GET", "", http.StatusForbidden) + }) + }) + + t.Run("token scopes", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Trying to access the API with a token that lacks permissions, will + // fail, even if the token owner is an instance admin. + session := loginUser(t, adminUser) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/flags").AddTokenAuth(token) + MakeRequest(t, req, http.StatusForbidden) + }) + + t.Run("setting.Repository.EnableFlags is respected", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.Repository.EnableFlags, false)() + defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() + + t.Run("as admin", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + assertAccess(t, adminUser, "GET", "", http.StatusNotFound) + }) + + t.Run("as normal user", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + assertAccess(t, normalUser, "GET", "", http.StatusNotFound) + }) + }) + + t.Run("API functionality", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) + defer func() { + repo.ReplaceAllFlags(db.DefaultContext, []string{}) + }() + + baseURLFmtStr := "/api/v1/repos/user5/repo4/flags%s" + + session := loginUser(t, adminUser) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteAdmin) + + // Listing flags + req := NewRequestf(t, "GET", baseURLFmtStr, "").AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var flags []string + DecodeJSON(t, resp, &flags) + assert.Empty(t, flags) + + // Replacing all tags works, twice in a row + for i := 0; i < 2; i++ { + req = NewRequestWithJSON(t, "PUT", fmt.Sprintf(baseURLFmtStr, ""), &api.ReplaceFlagsOption{ + Flags: []string{"flag-1", "flag-2", "flag-3"}, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + } + + // The list now includes all three flags + req = NewRequestf(t, "GET", baseURLFmtStr, "").AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &flags) + assert.Len(t, flags, 3) + for _, flag := range []string{"flag-1", "flag-2", "flag-3"} { + assert.True(t, slices.Contains(flags, flag)) + } + + // Check a flag that is on the repo + req = NewRequestf(t, "GET", baseURLFmtStr, "/flag-1").AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + // Check a flag that isn't on the repo + req = NewRequestf(t, "GET", baseURLFmtStr, "/no-such-flag").AddTokenAuth(token) + MakeRequest(t, req, http.StatusNotFound) + + // We can add the same flag twice + for i := 0; i < 2; i++ { + req = NewRequestf(t, "PUT", baseURLFmtStr, "/brand-new-flag").AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + } + + // The new flag is there + req = NewRequestf(t, "GET", baseURLFmtStr, "/brand-new-flag").AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + // We can delete a flag, twice + for i := 0; i < 2; i++ { + req = NewRequestf(t, "DELETE", baseURLFmtStr, "/flag-3").AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + } + + // We can delete a flag that wasn't there + req = NewRequestf(t, "DELETE", baseURLFmtStr, "/no-such-flag").AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + // We can delete all of the flags in one go, too + req = NewRequestf(t, "DELETE", baseURLFmtStr, "").AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + // ..once all flags are deleted, none are listed, either + req = NewRequestf(t, "GET", baseURLFmtStr, "").AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &flags) + assert.Empty(t, flags) + }) +} + func TestRepositoryFlagsUI(t *testing.T) { defer tests.PrepareTestEnv(t)() defer test.MockVariableValue(&setting.Repository.EnableFlags, true)() From c321af3d5f210474548fadbe907c8144284132bb Mon Sep 17 00:00:00 2001 From: Gusted Date: Fri, 5 Jan 2024 20:48:09 +0100 Subject: [PATCH 63/86] [GITEA] Improve 404 screen on mobile - Remove `container` to remove unnecessary margins being added to the whole page. - Specify max width for the 404 image to avoid overflow of the image. (cherry picked from commit b1ced72ce50af987a6c77149705402eedee02eae) (cherry picked from commit ef5e1b01b82155b4f38e6ead718ae2889b78c701) --- templates/status/404.tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/status/404.tmpl b/templates/status/404.tmpl index 74bb8762bd..695dd78c85 100644 --- a/templates/status/404.tmpl +++ b/templates/status/404.tmpl @@ -1,8 +1,8 @@ {{template "base/head" .}} -
+
{{if .IsRepo}}{{template "repo/header" .}}{{end}}
-

404

+

404

{{if .NotFoundPrompt}}{{.NotFoundPrompt}}{{else}}{{ctx.Locale.Tr "error404" | Safe}}{{end}}

{{if .NotFoundGoBackURL}}
{{ctx.Locale.Tr "go_back"}}{{end}} From b173a0ccee6cc0dadf40ec55e5d88987314c1cc4 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Tue, 9 Jan 2024 12:49:18 +0100 Subject: [PATCH 64/86] [GITEA] POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments Refs: https://codeberg.org/forgejo/forgejo/issues/2109 (cherry picked from commit 8b4ba3dce7fc99fa328444ef27383dccca49c237) (cherry picked from commit 196edea0f972a9a027c4cacb9df36330cf676d2f) [GITEA] POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments (squash) do not implicitly create a review If a comment already exists in a review, the comment is added. If it is the first comment added to a review, it will implicitly create a new review instead of adding to the existing one. The pull_service.CreateCodeComment function is responsibe for this behavior and it will defer to createCodeComment once the review is determined, either because it was found or because it was created. Rename createCodeComment into CreateCodeCommentKnownReviewID to expose it and change the API endpoint to use it instead. Since the review is provided by the user and verified to exist already, there is no need for the logic implemented by CreateCodeComment. The tests are modified to remove the initial comment from the fixture because it was creating the false positive. I was verified to fail without this fix. (cherry picked from commit 6a555996dca6ba71c65818e14ab0eeafa1af6dc2) --- modules/structs/pull_review.go | 3 + routers/api/v1/api.go | 3 +- routers/api/v1/repo/pull_review.go | 83 ++++++++++++++++++++++ routers/api/v1/swagger/options.go | 3 + services/convert/pull_review.go | 50 +++++++------ services/pull/review.go | 6 +- templates/swagger/v1_json.tmpl | 65 +++++++++++++++++ tests/integration/api_pull_review_test.go | 85 +++++++++++++++++++++++ 8 files changed, 274 insertions(+), 24 deletions(-) diff --git a/modules/structs/pull_review.go b/modules/structs/pull_review.go index 810be8f521..c77ebea07d 100644 --- a/modules/structs/pull_review.go +++ b/modules/structs/pull_review.go @@ -89,6 +89,9 @@ type CreatePullReviewComment struct { NewLineNum int64 `json:"new_position"` } +// CreatePullReviewCommentOptions are options to create a pull review comment +type CreatePullReviewCommentOptions CreatePullReviewComment + // SubmitPullReviewOptions are options to submit a pending pull review type SubmitPullReviewOptions struct { Event ReviewStateType `json:"event"` diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index cd5c18235f..f061b8044a 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1228,7 +1228,8 @@ func Routes() *web.Route { Delete(reqToken(), repo.DeletePullReview). Post(reqToken(), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview) m.Combo("/comments"). - Get(repo.GetPullReviewComments) + Get(repo.GetPullReviewComments). + Post(reqToken(), bind(api.CreatePullReviewCommentOptions{}), repo.CreatePullReviewComment) m.Post("/dismissals", reqToken(), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview) m.Post("/undismissals", reqToken(), repo.UnDismissPullReview) }) diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go index 7b9445be4c..99441e1f6b 100644 --- a/routers/api/v1/repo/pull_review.go +++ b/routers/api/v1/repo/pull_review.go @@ -208,6 +208,89 @@ func GetPullReviewComments(ctx *context.APIContext) { ctx.JSON(http.StatusOK, apiComments) } +// CreatePullReviewComments add a new comment to a pull request review +func CreatePullReviewComment(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments repository repoCreatePullReviewComment + // --- + // summary: Add a new comment to a pull request review + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: index + // in: path + // description: index of the pull request + // type: integer + // format: int64 + // required: true + // - name: id + // in: path + // description: id of the review + // type: integer + // format: int64 + // required: true + // - name: body + // in: body + // required: true + // schema: + // "$ref": "#/definitions/CreatePullReviewCommentOptions" + // responses: + // "200": + // "$ref": "#/responses/PullReviewComment" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + + opts := web.GetForm(ctx).(*api.CreatePullReviewCommentOptions) + + review, pr, statusSet := prepareSingleReview(ctx) + if statusSet { + return + } + + if err := pr.Issue.LoadRepo(ctx); err != nil { + ctx.InternalServerError(err) + return + } + + line := opts.NewLineNum + if opts.OldLineNum > 0 { + line = opts.OldLineNum * -1 + } + + comment, err := pull_service.CreateCodeCommentKnownReviewID(ctx, + ctx.Doer, + pr.Issue.Repo, + pr.Issue, + opts.Body, + opts.Path, + line, + review.ID, + ) + if err != nil { + ctx.InternalServerError(err) + return + } + + apiComment, err := convert.ToPullReviewComment(ctx, review, comment, ctx.Doer) + if err != nil { + ctx.InternalServerError(err) + return + } + + ctx.JSON(http.StatusOK, apiComment) +} + // DeletePullReview delete a specific review from a pull request func DeletePullReview(ctx *context.APIContext) { // swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoDeletePullReview diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index cca6d2d572..2886b865e8 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -161,6 +161,9 @@ type swaggerParameterBodies struct { // in:body CreatePullReviewComment api.CreatePullReviewComment + // in:body + CreatePullReviewCommentOptions api.CreatePullReviewCommentOptions + // in:body SubmitPullReviewOptions api.SubmitPullReviewOptions diff --git a/services/convert/pull_review.go b/services/convert/pull_review.go index 05638c2c9b..f7990e7a5c 100644 --- a/services/convert/pull_review.go +++ b/services/convert/pull_review.go @@ -78,6 +78,33 @@ func ToPullReviewList(ctx context.Context, rl []*issues_model.Review, doer *user return result, nil } +// ToPullReviewCommentList convert the CodeComments of an review to it's api format +func ToPullReviewComment(ctx context.Context, review *issues_model.Review, comment *issues_model.Comment, doer *user_model.User) (*api.PullReviewComment, error) { + apiComment := &api.PullReviewComment{ + ID: comment.ID, + Body: comment.Content, + Poster: ToUser(ctx, comment.Poster, doer), + Resolver: ToUser(ctx, comment.ResolveDoer, doer), + ReviewID: review.ID, + Created: comment.CreatedUnix.AsTime(), + Updated: comment.UpdatedUnix.AsTime(), + Path: comment.TreePath, + CommitID: comment.CommitSHA, + OrigCommitID: comment.OldRef, + DiffHunk: patch2diff(comment.Patch), + HTMLURL: comment.HTMLURL(ctx), + HTMLPullURL: review.Issue.HTMLURL(), + } + + if comment.Line < 0 { + apiComment.OldLineNum = comment.UnsignedLine() + } else { + apiComment.LineNum = comment.UnsignedLine() + } + + return apiComment, nil +} + // ToPullReviewCommentList convert the CodeComments of an review to it's api format func ToPullReviewCommentList(ctx context.Context, review *issues_model.Review, doer *user_model.User) ([]*api.PullReviewComment, error) { if err := review.LoadAttributes(ctx); err != nil { @@ -92,26 +119,9 @@ func ToPullReviewCommentList(ctx context.Context, review *issues_model.Review, d for _, lines := range review.CodeComments { for _, comments := range lines { for _, comment := range comments { - apiComment := &api.PullReviewComment{ - ID: comment.ID, - Body: comment.Content, - Poster: ToUser(ctx, comment.Poster, doer), - Resolver: ToUser(ctx, comment.ResolveDoer, doer), - ReviewID: review.ID, - Created: comment.CreatedUnix.AsTime(), - Updated: comment.UpdatedUnix.AsTime(), - Path: comment.TreePath, - CommitID: comment.CommitSHA, - OrigCommitID: comment.OldRef, - DiffHunk: patch2diff(comment.Patch), - HTMLURL: comment.HTMLURL(ctx), - HTMLPullURL: review.Issue.HTMLURL(), - } - - if comment.Line < 0 { - apiComment.OldLineNum = comment.UnsignedLine() - } else { - apiComment.LineNum = comment.UnsignedLine() + apiComment, err := ToPullReviewComment(ctx, review, comment, doer) + if err != nil { + return nil, err } apiComments = append(apiComments, apiComment) } diff --git a/services/pull/review.go b/services/pull/review.go index e48f380154..33bbec54ca 100644 --- a/services/pull/review.go +++ b/services/pull/review.go @@ -95,7 +95,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git. return nil, err } - comment, err := createCodeComment(ctx, + comment, err := CreateCodeCommentKnownReviewID(ctx, doer, issue.Repo, issue, @@ -135,7 +135,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git. } } - comment, err := createCodeComment(ctx, + comment, err := CreateCodeCommentKnownReviewID(ctx, doer, issue.Repo, issue, @@ -161,7 +161,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git. } // createCodeComment creates a plain code comment at the specified line / path -func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content, treePath string, line, reviewID int64) (*issues_model.Comment, error) { +func CreateCodeCommentKnownReviewID(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content, treePath string, line, reviewID int64) (*issues_model.Comment, error) { var commitID, patch string if err := issue.LoadPullRequest(ctx); err != nil { return nil, fmt.Errorf("LoadPullRequest: %w", err) diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index b4f82ab93f..5a20163238 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -11542,6 +11542,67 @@ "$ref": "#/responses/notFound" } } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Add a new comment to a pull request review", + "operationId": "repoCreatePullReviewComment", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "index of the pull request", + "name": "index", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "id of the review", + "name": "id", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreatePullReviewCommentOptions" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/PullReviewComment" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } } }, "/repos/{owner}/{repo}/pulls/{index}/reviews/{id}/dismissals": { @@ -18528,6 +18589,10 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "CreatePullReviewCommentOptions": { + "description": "CreatePullReviewCommentOptions are options to create a pull review comment", + "$ref": "#/definitions/CreatePullReviewComment" + }, "CreatePullReviewOptions": { "description": "CreatePullReviewOptions are options to create a pull review", "type": "object", diff --git a/tests/integration/api_pull_review_test.go b/tests/integration/api_pull_review_test.go index daa136b21e..4d4df739d7 100644 --- a/tests/integration/api_pull_review_test.go +++ b/tests/integration/api_pull_review_test.go @@ -18,8 +18,93 @@ import ( "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +func TestAPIPullReviewCreateComment(t *testing.T) { + defer tests.PrepareTestEnv(t)() + pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}) + assert.NoError(t, pullIssue.LoadAttributes(db.DefaultContext)) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.RepoID}) + + username := "user2" + session := loginUser(t, username) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + // as of e522e774cae2240279fc48c349fc513c9d3353ee + // There should be no reason for CreateComment to behave differently + // depending on the event associated with the review. But the logic of the implementation + // at this point in time is very involved and deserves these seemingly redundant + // test. + for _, event := range []api.ReviewStateType{ + api.ReviewStatePending, + api.ReviewStateRequestChanges, + api.ReviewStateApproved, + api.ReviewStateComment, + } { + t.Run("Event_"+string(event), func(t *testing.T) { + path := "README.md" + var review api.PullReview + var reviewLine int64 = 1 + + { + req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/pulls/%d/reviews", repo.FullName(), pullIssue.Index), &api.CreatePullReviewOptions{ + Body: "body1", + Event: event, + }).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &review) + require.EqualValues(t, string(event), review.State) + require.EqualValues(t, 0, review.CodeCommentsCount) + } + + { + req := NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/pulls/%d/reviews/%d", repo.FullName(), pullIssue.Index, review.ID). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var getReview api.PullReview + DecodeJSON(t, resp, &getReview) + require.EqualValues(t, getReview, review) + } + + newCommentBody := "first new line" + + { + req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/pulls/%d/reviews/%d/comments", repo.FullName(), pullIssue.Index, review.ID), &api.CreatePullReviewCommentOptions{ + Path: path, + Body: newCommentBody, + OldLineNum: reviewLine, + }).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var reviewComment *api.PullReviewComment + DecodeJSON(t, resp, &reviewComment) + assert.EqualValues(t, review.ID, reviewComment.ReviewID) + } + + { + req := NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/pulls/%d/reviews/%d/comments", repo.FullName(), pullIssue.Index, review.ID). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + + var reviewComments []*api.PullReviewComment + DecodeJSON(t, resp, &reviewComments) + assert.Len(t, reviewComments, 2) + assert.EqualValues(t, existingCommentBody, reviewComments[0].Body) + assert.EqualValues(t, reviewComments[0].OldLineNum, reviewComments[1].OldLineNum) + assert.EqualValues(t, reviewComments[0].LineNum, reviewComments[1].LineNum) + assert.EqualValues(t, newCommentBody, reviewComments[1].Body) + assert.EqualValues(t, path, reviewComments[1].Path) + } + + { + req := NewRequestf(t, http.MethodDelete, "/api/v1/repos/%s/pulls/%d/reviews/%d", repo.FullName(), pullIssue.Index, review.ID). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + } + }) + } +} + func TestAPIPullReview(t *testing.T) { defer tests.PrepareTestEnv(t)() pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}) From 19be82b7ef11fe6e0656434dcc69c9ff2f24c702 Mon Sep 17 00:00:00 2001 From: Gusted Date: Sat, 13 Jan 2024 11:55:33 +0100 Subject: [PATCH 65/86] [GITEA] Fix panic in `canSoftDeleteContentHistory` - It's possible that `canSoftDeleteContentHistory` is called without `ctx.Doer` being set, such as an anonymous user requesting the `/content-history/detail` endpoint. - Add a simple condition to always set to `canSoftDelete` to false if an anonymous user is requesting this, this avoids a panic in the code that assumes `ctx.Doer` is set. - Added integration testing. (cherry picked from commit 0b5db0dcc608e9a9e79ead094a20a7775c4f9559) (cherry picked from commit 30d168bcc867387f3c94582a4668cce62f77c171) --- routers/web/repo/issue_content_history.go | 2 + .../issue_content_history.yml | 17 ++++++ tests/integration/issue_test.go | 52 +++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 tests/integration/fixtures/TestGetContentHistory/issue_content_history.yml diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go index 0f376db145..31e6ac608c 100644 --- a/routers/web/repo/issue_content_history.go +++ b/routers/web/repo/issue_content_history.go @@ -94,6 +94,8 @@ func canSoftDeleteContentHistory(ctx *context.Context, issue *issues_model.Issue // CanWrite means the doer can manage the issue/PR list if ctx.Repo.IsOwner() || ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { canSoftDelete = true + } else if ctx.Doer == nil { + canSoftDelete = false } else { // for read-only users, they could still post issues or comments, // they should be able to delete the history related to their own issue/comment, a case is: diff --git a/tests/integration/fixtures/TestGetContentHistory/issue_content_history.yml b/tests/integration/fixtures/TestGetContentHistory/issue_content_history.yml new file mode 100644 index 0000000000..6633b50374 --- /dev/null +++ b/tests/integration/fixtures/TestGetContentHistory/issue_content_history.yml @@ -0,0 +1,17 @@ +- + id: 1 + issue_id: 1 + comment_id: 3 + edited_unix: 1687612839 + content_text: Original Text + is_first_created: true + is_deleted: false + +- + id: 2 + issue_id: 1 + comment_id: 3 + edited_unix: 1687612840 + content_text: "meh..." # This has to be consistent with comment.yml + is_first_created: false + is_deleted: false diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go index de4113a37b..4da8aeae0c 100644 --- a/tests/integration/issue_test.go +++ b/tests/integration/issue_test.go @@ -718,3 +718,55 @@ func TestIssueReferenceURL(t *testing.T) { ref, _ = htmlDoc.Find(`.timeline-item.comment:not(.first) .reference-issue`).Attr("data-reference") assert.EqualValues(t, "/user2/repo1/issues/1#issuecomment-2", ref) } + +func TestGetContentHistory(t *testing.T) { + defer tests.AddFixtures("tests/integration/fixtures/TestGetContentHistory/")() + defer tests.PrepareTestEnv(t)() + + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) + issueURL := fmt.Sprintf("%s/issues/%d", repo.FullName(), issue.Index) + contentHistory := unittest.AssertExistsAndLoadBean(t, &issues_model.ContentHistory{ID: 2, IssueID: issue.ID}) + contentHistoryURL := fmt.Sprintf("%s/issues/%d/content-history/detail?comment_id=%d&history_id=%d", repo.FullName(), issue.Index, contentHistory.CommentID, contentHistory.ID) + + type contentHistoryResp struct { + CanSoftDelete bool `json:"canSoftDelete"` + HistoryID int `json:"historyId"` + PrevHistoryID int `json:"prevHistoryId"` + } + + testCase := func(t *testing.T, session *TestSession, canDelete bool) { + t.Helper() + contentHistoryURL := contentHistoryURL + "&_csrf=" + GetCSRF(t, session, issueURL) + + req := NewRequest(t, "GET", contentHistoryURL) + resp := session.MakeRequest(t, req, http.StatusOK) + + var respJSON contentHistoryResp + DecodeJSON(t, resp, &respJSON) + + assert.EqualValues(t, canDelete, respJSON.CanSoftDelete) + assert.EqualValues(t, contentHistory.ID, respJSON.HistoryID) + assert.EqualValues(t, contentHistory.ID-1, respJSON.PrevHistoryID) + } + + t.Run("Anonymous", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + testCase(t, emptyTestSession(t), false) + }) + + t.Run("Another user", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + testCase(t, loginUser(t, "user8"), false) + }) + + t.Run("Repo owner", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + testCase(t, loginUser(t, "user2"), true) + }) + + t.Run("Poster", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + testCase(t, loginUser(t, "user5"), true) + }) +} From 44dd80552ca63c6d22f4a139a0297486f1a2e655 Mon Sep 17 00:00:00 2001 From: Gergely Nagy Date: Sat, 13 Jan 2024 21:02:49 +0100 Subject: [PATCH 66/86] [GITEA] services: Gracefully handle missing branches services: in loadOneBranch, return if CountDivergingCommits fail If we can't count the number of diverging commits for one reason or another (such as the branch being in the database, but missing from disk), rather than logging an error and continuing into a crash (because `divergence` will be nil), return an error instead. Signed-off-by: Gergely Nagy (cherry picked from commit 8266105f24eb76b1dfb4c79d9bfde2ef9a98417a) services: Gracefully handle missing branches When loading branches, if loading one fails, log an error, and ignore the branch, rather than returning and causing an internal server error. Ideally, we would only ignore the error if it was caused by a missing branch, and do it silently, like the respective API endpoint does. However, veryfing that at this place is not very practical, so for the time being, ignore any and all branch loading errors. Signed-off-by: Gergely Nagy (cherry picked from commit e552a8fd629b11503569f605c824c1c0b01eeab2) tests: Add a testcase for missing branches This tests the scenario reported in Codeberg/Community#1408: a branch that is recorded in the database, but missing on disk was causing internal server errors. With recent changes, that is no longer the case, the error is logged and then ignored. This test case tests this behaviour, that the repo's branches page on the web UI functions even if the git branch is missing. Signed-off-by: Gergely Nagy (cherry picked from commit e20eb7b3853e25ab29d4ca63b015517b44e4954f) tests: More testing in TestDatabaseMissingABranch In the `TestDatabaseMissingABranch` testcase, make sure that the branches are in sync between the db and git before deleting a branch via git, then compare the branch count from the web UI, making sure that it returns an out-of-sync value first, and the correct one after another sync. This is currently tested by scraping the UI, and relies on the fact that the branch counter is out of date before syncing. If that issue gets resolved, we'll have to adjust the test to verify the sync another way. Signed-off-by: Gergely Nagy (cherry picked from commit 8c2ccfcecec6182dd80d463f58223acbf16b039b) (cherry picked from commit 439fadf5635c47c2a1be9cc83614b60f76ac05d0) --- services/repository/branch.go | 10 ++++- tests/integration/repo_branch_test.go | 54 +++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/services/repository/branch.go b/services/repository/branch.go index c1e6625ed4..7e3246ee41 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -96,7 +96,13 @@ func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git for i := range dbBranches { branch, err := loadOneBranch(ctx, repo, dbBranches[i], &rules, repoIDToRepo, repoIDToGitRepo) if err != nil { - return nil, nil, 0, fmt.Errorf("loadOneBranch: %v", err) + log.Error("loadOneBranch() on repo #%d, branch '%s' failed: %v", repo.ID, dbBranches[i].Name, err) + + // TODO: Ideally, we would only do this if the branch doesn't exist + // anymore. That is not practical to check here currently, so we do + // this for all kinds of errors. + totalNumOfBranches-- + continue } branches = append(branches, branch) @@ -132,7 +138,7 @@ func loadOneBranch(ctx context.Context, repo *repo_model.Repository, dbBranch *g var err error divergence, err = files_service.CountDivergingCommits(ctx, repo, git.BranchPrefix+branchName) if err != nil { - log.Error("CountDivergingCommits: %v", err) + return nil, fmt.Errorf("CountDivergingCommits: %v", err) } } diff --git a/tests/integration/repo_branch_test.go b/tests/integration/repo_branch_test.go index 9eb17eda00..d6b2b39d9a 100644 --- a/tests/integration/repo_branch_test.go +++ b/tests/integration/repo_branch_test.go @@ -1,4 +1,5 @@ // Copyright 2017 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. // SPDX-License-Identifier: MIT package integration @@ -7,14 +8,21 @@ import ( "net/http" "net/url" "path" + "strconv" "strings" "testing" + "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" + "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation" + repo_service "code.gitea.io/gitea/services/repository" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -159,3 +167,49 @@ func TestCreateBranchInvalidCSRF(t *testing.T) { strings.TrimSpace(htmlDoc.doc.Find(".ui.message").Text()), ) } + +func TestDatabaseMissingABranch(t *testing.T) { + onGiteaRun(t, func(t *testing.T, URL *url.URL) { + adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}) + session := loginUser(t, "user2") + + // Create two branches + testCreateBranch(t, session, "user2", "repo1", "branch/master", "will-be-present", http.StatusSeeOther) + testCreateBranch(t, session, "user2", "repo1", "branch/master", "will-be-missing", http.StatusSeeOther) + + // Run the repo branch sync, to ensure the db and git agree. + err2 := repo_service.AddAllRepoBranchesToSyncQueue(graceful.GetManager().ShutdownContext(), adminUser.ID) + assert.NoError(t, err2) + + // Delete one branch from git only, leaving it in the database + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + cmd := git.NewCommand(db.DefaultContext, "branch", "-D").AddDynamicArguments("will-be-missing") + _, _, err := cmd.RunStdString(&git.RunOpts{Dir: repo.RepoPath()}) + assert.NoError(t, err) + + // Verify that loading the repo's branches page works still, and that it + // reports at least three branches (master, will-be-present, and + // will-be-missing). + req := NewRequest(t, "GET", "/user2/repo1/branches") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + firstBranchCount, _ := strconv.Atoi(doc.Find(".repository-menu a[href*='/branches'] b").Text()) + assert.GreaterOrEqual(t, firstBranchCount, 3) + + // Run the repo branch sync again + err2 = repo_service.AddAllRepoBranchesToSyncQueue(graceful.GetManager().ShutdownContext(), adminUser.ID) + assert.NoError(t, err2) + + // Verify that loading the repo's branches page works still, and that it + // reports one branch less than the first time. + // + // NOTE: This assumes that the branch counter on the web UI is out of + // date before the sync. If that problem gets resolved, we'll have to + // find another way to test that the syncing works. + req = NewRequest(t, "GET", "/user2/repo1/branches") + resp = session.MakeRequest(t, req, http.StatusOK) + doc = NewHTMLParser(t, resp.Body) + secondBranchCount, _ := strconv.Atoi(doc.Find(".repository-menu a[href*='/branches'] b").Text()) + assert.Equal(t, firstBranchCount-1, secondBranchCount) + }) +} From e165510317b3fe3be4fe49ad3ff0a055e7a92124 Mon Sep 17 00:00:00 2001 From: Gusted Date: Sun, 14 Jan 2024 00:01:39 +0100 Subject: [PATCH 67/86] [GITEA] Log SQL queries when the database return error - When the database returns an error about the SQL query, the error is logged but not the SQL query and arguments, which is just as valuable as the vague deeply hidden documented error that the database returns. It's possible to log the SQL query by logging **all** SQL queries. For bigger instances such as Codeberg, this is not a viable option. - Adds a new hook, enabled by default, to log SQL queries with their arguments and the error returned by the database when the database returns an error. - This likely needs some fine tuning in the future to decide when to enable this, as the error is already logged and if people have the `[database].LOG_SQL` option enabled, the SQL would be logged twice. But given that it's an rare occurence for SQL queries to error, it's fine to leave that as-is. - Ref: https://codeberg.org/forgejo/forgejo/issues/1998 (cherry picked from commit 866229bc323619bc8686bad99951f95d5d46fe19) (cherry picked from commit 96dd3e87cf5f75ac33bfad647e44852667c307d0) --- models/db/engine.go | 20 ++++++++++++++++++++ models/db/engine_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/models/db/engine.go b/models/db/engine.go index ad8ce7ecff..d78583df31 100755 --- a/models/db/engine.go +++ b/models/db/engine.go @@ -153,6 +153,9 @@ func InitEngine(ctx context.Context) error { Logger: log.GetLogger("xorm"), }) } + xormEngine.AddHook(&ErrorQueryHook{ + Logger: log.GetLogger("xorm"), + }) SetDefaultEngine(ctx, xormEngine) return nil @@ -327,3 +330,20 @@ func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error { } return nil } + +type ErrorQueryHook struct { + Logger log.Logger +} + +var _ contexts.Hook = &ErrorQueryHook{} + +func (ErrorQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) { + return c.Ctx, nil +} + +func (h *ErrorQueryHook) AfterProcess(c *contexts.ContextHook) error { + if c.Err != nil { + h.Logger.Log(8, log.ERROR, "[Error SQL Query] %s %v - %v", c.SQL, c.Args, c.Err) + } + return nil +} diff --git a/models/db/engine_test.go b/models/db/engine_test.go index ba922821b0..e8f1c19b1c 100644 --- a/models/db/engine_test.go +++ b/models/db/engine_test.go @@ -123,3 +123,31 @@ func TestSlowQuery(t *testing.T) { _, stopped = lc.Check(100 * time.Millisecond) assert.True(t, stopped) } + +func TestErrorQuery(t *testing.T) { + lc, cleanup := test.NewLogChecker("error-query") + lc.StopMark("[Error SQL Query]") + defer cleanup() + + e := db.GetEngine(db.DefaultContext) + engine, ok := e.(*xorm.Engine) + assert.True(t, ok) + + // It's not possible to clean this up with XORM, but it's luckily not harmful + // to leave around. + engine.AddHook(&db.ErrorQueryHook{ + Logger: log.GetLogger("error-query"), + }) + + // Valid query. + e.Exec("SELECT 1 WHERE false;") + + _, stopped := lc.Check(100 * time.Millisecond) + assert.False(t, stopped) + + // Table doesn't exist. + e.Exec("SELECT column FROM table;") + + _, stopped = lc.Check(100 * time.Millisecond) + assert.True(t, stopped) +} From 9205c9266a7d2b058100d03f5f3272f670f35866 Mon Sep 17 00:00:00 2001 From: Gergely Nagy Date: Sun, 14 Jan 2024 01:04:14 +0100 Subject: [PATCH 68/86] [GITEA] Fix the topic search paging When searching for repository topics, either via the API, or via Explore, paging did not work correctly, because it only applied when the `page` parameter was non-zero. Paging should have applied when the page size is greater than zero, which is what this patch does. As a result, both the API, and the Explore endpoint will return paged results (30 by default). As such, when managing topics on the frontend, the offered completions will also be limited to a pageful of results, based on what the user has already typed. This drastically reduces the amount of traffic, and also the number of the topics to choose from, and thus, the rendering time too. The topics will be returned by popularity, with most used topics first. A single page will contain `[api].DEFAULT_PAGING_NUM` (30 by default) items that match the query. That's plenty to choose from. Fixes #132. Signed-off-by: Gergely Nagy (cherry picked from commit 64d4ff41dbab7b3b84571b595158c3b451f53af7) (cherry picked from commit 06b808fa2c0ddd52ca4569157892a0c7fc154b1f) --- models/repo/topic.go | 2 +- tests/integration/api_repo_topic_test.go | 30 +++++++++++++++++++++ tests/integration/repo_topic_test.go | 34 ++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/models/repo/topic.go b/models/repo/topic.go index b71f43bc88..dd61c48551 100644 --- a/models/repo/topic.go +++ b/models/repo/topic.go @@ -199,7 +199,7 @@ func FindTopics(ctx context.Context, opts *FindTopicOptions) ([]*Topic, int64, e sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id") orderBy = "topic.name" // when render topics for a repo, it's better to sort them by name, to get consistent result } - if opts.PageSize != 0 && opts.Page != 0 { + if opts.PageSize > 0 { sess = db.SetSessionPagination(sess, opts) } topics := make([]*Topic, 0, 10) diff --git a/tests/integration/api_repo_topic_test.go b/tests/integration/api_repo_topic_test.go index c41bc4abb6..528bd66eb2 100644 --- a/tests/integration/api_repo_topic_test.go +++ b/tests/integration/api_repo_topic_test.go @@ -1,4 +1,5 @@ // Copyright 2019 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. // SPDX-License-Identifier: MIT package integration @@ -19,6 +20,35 @@ import ( "github.com/stretchr/testify/assert" ) +func TestAPITopicSearchPaging(t *testing.T) { + defer tests.PrepareTestEnv(t)() + var topics struct { + TopicNames []*api.TopicResponse `json:"topics"` + } + + // Add 20 unique topics to user2/repo2, and 20 unique ones to user2/repo3 + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + token2 := getUserToken(t, user2.Name, auth_model.AccessTokenScopeWriteRepository) + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) + for i := 0; i < 20; i++ { + req := NewRequestf(t, "PUT", "/api/v1/repos/%s/%s/topics/paging-topic-%d", user2.Name, repo2.Name, i). + AddTokenAuth(token2) + MakeRequest(t, req, http.StatusNoContent) + req = NewRequestf(t, "PUT", "/api/v1/repos/%s/%s/topics/paging-topic-%d", user2.Name, repo3.Name, i+30). + AddTokenAuth(token2) + MakeRequest(t, req, http.StatusNoContent) + } + + res := MakeRequest(t, NewRequest(t, "GET", "/api/v1/topics/search"), http.StatusOK) + DecodeJSON(t, res, &topics) + assert.Len(t, topics.TopicNames, 30) + + res = MakeRequest(t, NewRequest(t, "GET", "/api/v1/topics/search?page=2"), http.StatusOK) + DecodeJSON(t, res, &topics) + assert.Greater(t, len(topics.TopicNames), 0) +} + func TestAPITopicSearch(t *testing.T) { defer tests.PrepareTestEnv(t)() searchURL, _ := url.Parse("/api/v1/topics/search") diff --git a/tests/integration/repo_topic_test.go b/tests/integration/repo_topic_test.go index 58fee8418f..2ca8fe64cd 100644 --- a/tests/integration/repo_topic_test.go +++ b/tests/integration/repo_topic_test.go @@ -1,4 +1,5 @@ // Copyright 2022 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. // SPDX-License-Identifier: MIT package integration @@ -8,6 +9,10 @@ import ( "net/url" "testing" + auth_model "code.gitea.io/gitea/models/auth" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/tests" @@ -45,3 +50,32 @@ func TestTopicSearch(t *testing.T) { assert.EqualValues(t, 1, topics.TopicNames[0].RepoCount) } } + +func TestTopicSearchPaging(t *testing.T) { + defer tests.PrepareTestEnv(t)() + var topics struct { + TopicNames []*api.TopicResponse `json:"topics"` + } + + // Add 20 unique topics to user2/repo2, and 20 unique ones to user2/repo3 + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + token2 := getUserToken(t, user2.Name, auth_model.AccessTokenScopeWriteRepository) + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) + for i := 0; i < 20; i++ { + req := NewRequestf(t, "PUT", "/api/v1/repos/%s/%s/topics/paging-topic-%d", user2.Name, repo2.Name, i). + AddTokenAuth(token2) + MakeRequest(t, req, http.StatusNoContent) + req = NewRequestf(t, "PUT", "/api/v1/repos/%s/%s/topics/paging-topic-%d", user2.Name, repo3.Name, i+30). + AddTokenAuth(token2) + MakeRequest(t, req, http.StatusNoContent) + } + + res := MakeRequest(t, NewRequest(t, "GET", "/explore/topics/search"), http.StatusOK) + DecodeJSON(t, res, &topics) + assert.Len(t, topics.TopicNames, 30) + + res = MakeRequest(t, NewRequest(t, "GET", "/explore/topics/search?page=2"), http.StatusOK) + DecodeJSON(t, res, &topics) + assert.Greater(t, len(topics.TopicNames), 0) +} From 0ddefdf9f43d002d29085355a30b6c08a3969181 Mon Sep 17 00:00:00 2001 From: Gergely Nagy Date: Sun, 14 Jan 2024 13:37:16 +0100 Subject: [PATCH 69/86] [GITEA] Include a branch link in the recently pushed banner The message telling us that we recently pushed on a branch should include a link to said branch, not just a "New pull request" button. Signed-off-by: Gergely Nagy (cherry picked from commit d9662d03a407aaa69166d87fdc6e125417e292c1) (cherry picked from commit 2527e09125a653e93ee95ac69049bd5ebd249bdc) --- options/locale/locale_en-US.ini | 2 +- templates/repo/code/recently_pushed_new_branches.tmpl | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 25e8751232..2664129a66 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1851,7 +1851,7 @@ pulls.auto_merge_canceled_schedule_comment = `canceled auto merging this pull re pulls.delete.title = Delete this pull request? pulls.delete.text = Do you really want to delete this pull request? (This will permanently remove all content. Consider closing it instead, if you intend to keep it archived) -pulls.recently_pushed_new_branches = You pushed on branch %[1]s %[2]s +pulls.recently_pushed_new_branches = You pushed on branch %[1]s %[2]s pull.deleted_branch = (deleted):%s diff --git a/templates/repo/code/recently_pushed_new_branches.tmpl b/templates/repo/code/recently_pushed_new_branches.tmpl index 8910a9e5b6..2b9948d214 100644 --- a/templates/repo/code/recently_pushed_new_branches.tmpl +++ b/templates/repo/code/recently_pushed_new_branches.tmpl @@ -2,7 +2,8 @@
{{$timeSince := TimeSince .CommitTime.AsTime ctx.Locale}} - {{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" (Escape .Name) $timeSince | Safe}} + {{$branchLink := (print $.RepoLink "/src/branch/" (PathEscapeSegments .Name))}} + {{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" (Escape .Name) $timeSince $branchLink | Safe}}
{{ctx.Locale.Tr "repo.pulls.compare_changes"}} From 862144920945575c26e026281aab6e9bf3e00c5c Mon Sep 17 00:00:00 2001 From: Gusted Date: Sat, 13 Jan 2024 22:01:05 +0100 Subject: [PATCH 70/86] [GITEA] Fix test `TestWebhookProxy` with http proxy env - Unset the http proxies environments for the `TestWebhookProxy`. - Resolves #2132 (cherry picked from commit 244b9786fc431c362c6f5ac971ac4d04b97f78a2) (cherry picked from commit 8602dfa6a21e1ac9fa0fc6f5952da219a57b2613) --- services/webhook/deliver_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/services/webhook/deliver_test.go b/services/webhook/deliver_test.go index 72aa00478a..eca2ba244b 100644 --- a/services/webhook/deliver_test.go +++ b/services/webhook/deliver_test.go @@ -8,6 +8,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "os" "testing" "time" @@ -25,9 +26,15 @@ import ( func TestWebhookProxy(t *testing.T) { oldWebhook := setting.Webhook + oldHTTPProxy := os.Getenv("http_proxy") + oldHTTPSProxy := os.Getenv("https_proxy") t.Cleanup(func() { setting.Webhook = oldWebhook + os.Setenv("http_proxy", oldHTTPProxy) + os.Setenv("https_proxy", oldHTTPSProxy) }) + os.Unsetenv("http_proxy") + os.Unsetenv("https_proxy") setting.Webhook.ProxyURL = "http://localhost:8080" setting.Webhook.ProxyURLFixed, _ = url.Parse(setting.Webhook.ProxyURL) From f44830c3cba0b9416505a2b0b560cfa096ffeb7c Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Thu, 11 Jan 2024 11:54:16 +0100 Subject: [PATCH 71/86] [GITEA] API commentAssignment() to verify the id belongs Instead of repeating the tests that verify the ID of a comment is related to the repository of the API endpoint, add the middleware function commentAssignment() to assign ctx.Comment if the ID of the comment is verified to be related to the repository. There already are integration tests for cases of potential unrelated comment IDs that cover some of the modified endpoints which covers the commentAssignment() function logic. * TestAPICommentReactions - GetIssueCommentReactions * TestAPICommentReactions - PostIssueCommentReaction * TestAPICommentReactions - DeleteIssueCommentReaction * TestAPIEditComment - EditIssueComment * TestAPIDeleteComment - DeleteIssueComment * TestAPIGetCommentAttachment - GetIssueCommentAttachment The other modified endpoints do not have tests to verify cases of potential unrelated comment IDs. They no longer need to because they no longer implement the logic to enforce this. They however all have integration tests that verify the commentAssignment() they now rely on does not introduce a regression. * TestAPIGetComment - GetIssueComment * TestAPIListCommentAttachments - ListIssueCommentAttachments * TestAPICreateCommentAttachment - CreateIssueCommentAttachment * TestAPIEditCommentAttachment - EditIssueCommentAttachment * TestAPIDeleteCommentAttachment - DeleteIssueCommentAttachment (cherry picked from commit d414376d749041da1be288c02fdaa24fddeafd5c) (cherry picked from commit 09db07aeaed167edc66cb832b0aa54b31d14f0d8) --- modules/context/api.go | 2 + routers/api/v1/api.go | 36 +++++++++- routers/api/v1/repo/issue_comment.go | 68 ++----------------- .../api/v1/repo/issue_comment_attachment.go | 62 +++++------------ routers/api/v1/repo/issue_reaction.go | 52 +------------- 5 files changed, 61 insertions(+), 159 deletions(-) diff --git a/modules/context/api.go b/modules/context/api.go index f41228ad76..f7aa088e65 100644 --- a/modules/context/api.go +++ b/modules/context/api.go @@ -11,6 +11,7 @@ import ( "net/url" "strings" + issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -38,6 +39,7 @@ type APIContext struct { ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer Repo *Repository + Comment *issues_model.Comment Org *APIOrganization Package *Package } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index f061b8044a..5c30526203 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -73,6 +73,7 @@ import ( actions_model "code.gitea.io/gitea/models/actions" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" @@ -230,6 +231,39 @@ func repoAssignment() func(ctx *context.APIContext) { } } +// must be used within a group with a call to repoAssignment() to set ctx.Repo +func commentAssignment(idParam string) func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(idParam)) + if err != nil { + if issues_model.IsErrCommentNotExist(err) { + ctx.NotFound(err) + } else { + ctx.InternalServerError(err) + } + return + } + + if err = comment.LoadIssue(ctx); err != nil { + ctx.InternalServerError(err) + return + } + if comment.Issue == nil || comment.Issue.RepoID != ctx.Repo.Repository.ID { + ctx.NotFound() + return + } + + if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { + ctx.NotFound() + return + } + + comment.Issue.Repo = ctx.Repo.Repository + + ctx.Comment = comment + } +} + func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() { @@ -1333,7 +1367,7 @@ func Routes() *web.Route { Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueCommentAttachment). Delete(reqToken(), mustNotBeArchived, repo.DeleteIssueCommentAttachment) }, mustEnableAttachments) - }) + }, commentAssignment(":id")) }) m.Group("/{index}", func() { m.Combo("").Get(repo.GetIssue). diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go index a589354730..91de21db40 100644 --- a/routers/api/v1/repo/issue_comment.go +++ b/routers/api/v1/repo/issue_comment.go @@ -450,29 +450,7 @@ func GetIssueComment(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) - if err != nil { - if issues_model.IsErrCommentNotExist(err) { - ctx.NotFound(err) - } else { - ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) - } - return - } - - if err = comment.LoadIssue(ctx); err != nil { - ctx.InternalServerError(err) - return - } - if comment.Issue.RepoID != ctx.Repo.Repository.ID { - ctx.Status(http.StatusNotFound) - return - } - - if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { - ctx.NotFound() - return - } + comment := ctx.Comment if comment.Type != issues_model.CommentTypeComment { ctx.Status(http.StatusNoContent) @@ -583,25 +561,7 @@ func EditIssueCommentDeprecated(ctx *context.APIContext) { } func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) { - comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) - if err != nil { - if issues_model.IsErrCommentNotExist(err) { - ctx.NotFound(err) - } else { - ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) - } - return - } - - if err := comment.LoadIssue(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadIssue", err) - return - } - - if comment.Issue.RepoID != ctx.Repo.Repository.ID { - ctx.Status(http.StatusNotFound) - return - } + comment := ctx.Comment if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { ctx.Status(http.StatusForbidden) @@ -613,7 +573,7 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) return } - err = comment.LoadIssue(ctx) + err := comment.LoadIssue(ctx) if err != nil { ctx.Error(http.StatusInternalServerError, "LoadIssue", err) return @@ -707,25 +667,7 @@ func DeleteIssueCommentDeprecated(ctx *context.APIContext) { } func deleteIssueComment(ctx *context.APIContext) { - comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) - if err != nil { - if issues_model.IsErrCommentNotExist(err) { - ctx.NotFound(err) - } else { - ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) - } - return - } - - if err := comment.LoadIssue(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadIssue", err) - return - } - - if comment.Issue.RepoID != ctx.Repo.Repository.ID { - ctx.Status(http.StatusNotFound) - return - } + comment := ctx.Comment if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { ctx.Status(http.StatusForbidden) @@ -735,7 +677,7 @@ func deleteIssueComment(ctx *context.APIContext) { return } - if err = issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil { + if err := issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil { ctx.Error(http.StatusInternalServerError, "DeleteCommentByID", err) return } diff --git a/routers/api/v1/repo/issue_comment_attachment.go b/routers/api/v1/repo/issue_comment_attachment.go index 4f4b88750e..5622a9292a 100644 --- a/routers/api/v1/repo/issue_comment_attachment.go +++ b/routers/api/v1/repo/issue_comment_attachment.go @@ -55,11 +55,8 @@ func GetIssueCommentAttachment(ctx *context.APIContext) { // "404": // "$ref": "#/responses/error" - comment := getIssueCommentSafe(ctx) - if comment == nil { - return - } - attachment := getIssueCommentAttachmentSafeRead(ctx, comment) + comment := ctx.Comment + attachment := getIssueCommentAttachmentSafeRead(ctx) if attachment == nil { return } @@ -101,10 +98,7 @@ func ListIssueCommentAttachments(ctx *context.APIContext) { // "$ref": "#/responses/AttachmentList" // "404": // "$ref": "#/responses/error" - comment := getIssueCommentSafe(ctx) - if comment == nil { - return - } + comment := ctx.Comment if err := comment.LoadAttachments(ctx); err != nil { ctx.Error(http.StatusInternalServerError, "LoadAttachments", err) @@ -166,14 +160,12 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) { // "$ref": "#/responses/repoArchivedError" // Check if comment exists and load comment - comment := getIssueCommentSafe(ctx) - if comment == nil { + + if !canUserWriteIssueCommentAttachment(ctx) { return } - if !canUserWriteIssueCommentAttachment(ctx, comment) { - return - } + comment := ctx.Comment updatedAt := ctx.Req.FormValue("updated_at") if len(updatedAt) != 0 { @@ -341,42 +333,17 @@ func DeleteIssueCommentAttachment(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } -func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment { - comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64("id")) - if err != nil { - ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err) - return nil - } - if err := comment.LoadIssue(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "comment.LoadIssue", err) - return nil - } - if comment.Issue == nil || comment.Issue.RepoID != ctx.Repo.Repository.ID { - ctx.Error(http.StatusNotFound, "", "no matching issue comment found") - return nil - } - - if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { - return nil - } - - comment.Issue.Repo = ctx.Repo.Repository - - return comment -} - func getIssueCommentAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment { - comment := getIssueCommentSafe(ctx) - if comment == nil { + if !canUserWriteIssueCommentAttachment(ctx) { return nil } - if !canUserWriteIssueCommentAttachment(ctx, comment) { - return nil - } - return getIssueCommentAttachmentSafeRead(ctx, comment) + return getIssueCommentAttachmentSafeRead(ctx) } -func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues_model.Comment) bool { +func canUserWriteIssueCommentAttachment(ctx *context.APIContext) bool { + // ctx.Comment is assumed to be set in a safe way via a middleware + comment := ctx.Comment + canEditComment := ctx.IsSigned && (ctx.Doer.ID == comment.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin()) && ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) if !canEditComment { ctx.Error(http.StatusForbidden, "", "user should have permission to edit comment") @@ -386,7 +353,10 @@ func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues return true } -func getIssueCommentAttachmentSafeRead(ctx *context.APIContext, comment *issues_model.Comment) *repo_model.Attachment { +func getIssueCommentAttachmentSafeRead(ctx *context.APIContext) *repo_model.Attachment { + // ctx.Comment is assumed to be set in a safe way via a middleware + comment := ctx.Comment + attachment, err := repo_model.GetAttachmentByID(ctx, ctx.ParamsInt64("attachment_id")) if err != nil { ctx.NotFoundOrServerError("GetAttachmentByID", repo_model.IsErrAttachmentNotExist, err) diff --git a/routers/api/v1/repo/issue_reaction.go b/routers/api/v1/repo/issue_reaction.go index c886bd71b7..33724ebbd7 100644 --- a/routers/api/v1/repo/issue_reaction.go +++ b/routers/api/v1/repo/issue_reaction.go @@ -49,30 +49,7 @@ func GetIssueCommentReactions(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) - if err != nil { - if issues_model.IsErrCommentNotExist(err) { - ctx.NotFound(err) - } else { - ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) - } - return - } - - if err := comment.LoadIssue(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "comment.LoadIssue", err) - return - } - - if comment.Issue.RepoID != ctx.Repo.Repository.ID { - ctx.NotFound() - return - } - - if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { - ctx.Error(http.StatusForbidden, "GetIssueCommentReactions", errors.New("no permission to get reactions")) - return - } + comment := ctx.Comment reactions, _, err := issues_model.FindCommentReactions(ctx, comment.IssueID, comment.ID) if err != nil { @@ -186,30 +163,7 @@ func DeleteIssueCommentReaction(ctx *context.APIContext) { } func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) { - comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) - if err != nil { - if issues_model.IsErrCommentNotExist(err) { - ctx.NotFound(err) - } else { - ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) - } - return - } - - if err = comment.LoadIssue(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "comment.LoadIssue() failed", err) - return - } - - if comment.Issue.RepoID != ctx.Repo.Repository.ID { - ctx.NotFound() - return - } - - if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { - ctx.NotFound() - return - } + comment := ctx.Comment if comment.Issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) { ctx.Error(http.StatusForbidden, "ChangeIssueCommentReaction", errors.New("no permission to change reaction")) @@ -241,7 +195,7 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp }) } else { // DeleteIssueCommentReaction part - err = issues_model.DeleteCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction) + err := issues_model.DeleteCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction) if err != nil { ctx.Error(http.StatusInternalServerError, "DeleteCommentReaction", err) return From 119d10d9e277e7f738eba8087ce6cf4905e183e8 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Thu, 11 Jan 2024 12:32:18 +0100 Subject: [PATCH 72/86] [GITEA] GET /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments/{comment} Refs: https://codeberg.org/forgejo/forgejo/issues/2109 (cherry picked from commit 69fcf26dee0e6886460b533575f29e5b818bbc17) (cherry picked from commit 1296f4d115e1441657cfdac53807743a8b7ca6ba) --- routers/api/v1/api.go | 9 ++-- routers/api/v1/repo/pull_review.go | 63 +++++++++++++++++++++++ templates/swagger/v1_json.tmpl | 63 +++++++++++++++++++++++ tests/integration/api_pull_review_test.go | 19 ++++--- 4 files changed, 141 insertions(+), 13 deletions(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 5c30526203..f9c9017a6b 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1261,9 +1261,12 @@ func Routes() *web.Route { Get(repo.GetPullReview). Delete(reqToken(), repo.DeletePullReview). Post(reqToken(), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview) - m.Combo("/comments"). - Get(repo.GetPullReviewComments). - Post(reqToken(), bind(api.CreatePullReviewCommentOptions{}), repo.CreatePullReviewComment) + m.Group("/comments", func() { + m.Combo(""). + Get(repo.GetPullReviewComments). + Post(reqToken(), bind(api.CreatePullReviewCommentOptions{}), repo.CreatePullReviewComment) + m.Get("/{comment}", commentAssignment("comment"), repo.GetPullReviewComment) + }) m.Post("/dismissals", reqToken(), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview) m.Post("/undismissals", reqToken(), repo.UnDismissPullReview) }) diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go index 99441e1f6b..fc06a90e94 100644 --- a/routers/api/v1/repo/pull_review.go +++ b/routers/api/v1/repo/pull_review.go @@ -208,6 +208,69 @@ func GetPullReviewComments(ctx *context.APIContext) { ctx.JSON(http.StatusOK, apiComments) } +// GetPullReviewComment get a pull review comment +func GetPullReviewComment(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments/{comment} repository repoGetPullReviewComment + // --- + // summary: Get a pull review comment + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: index + // in: path + // description: index of the pull request + // type: integer + // format: int64 + // required: true + // - name: id + // in: path + // description: id of the review + // type: integer + // format: int64 + // required: true + // - name: comment + // in: path + // description: id of the comment + // type: integer + // format: int64 + // required: true + // responses: + // "200": + // "$ref": "#/responses/PullReviewComment" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + review, _, statusSet := prepareSingleReview(ctx) + if statusSet { + return + } + + if err := ctx.Comment.LoadPoster(ctx); err != nil { + ctx.InternalServerError(err) + return + } + + apiComment, err := convert.ToPullReviewComment(ctx, review, ctx.Comment, ctx.Doer) + if err != nil { + ctx.InternalServerError(err) + return + } + + ctx.JSON(http.StatusOK, apiComment) +} + // CreatePullReviewComments add a new comment to a pull request review func CreatePullReviewComment(ctx *context.APIContext) { // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments repository repoCreatePullReviewComment diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 5a20163238..91f4593e67 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -11605,6 +11605,69 @@ } } }, + "/repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments/{comment}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get a pull review comment", + "operationId": "repoGetPullReviewComment", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "index of the pull request", + "name": "index", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "id of the review", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "id of the comment", + "name": "comment", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/PullReviewComment" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/pulls/{index}/reviews/{id}/dismissals": { "post": { "produces": [ diff --git a/tests/integration/api_pull_review_test.go b/tests/integration/api_pull_review_test.go index 4d4df739d7..db44e1ade5 100644 --- a/tests/integration/api_pull_review_test.go +++ b/tests/integration/api_pull_review_test.go @@ -68,6 +68,7 @@ func TestAPIPullReviewCreateComment(t *testing.T) { } newCommentBody := "first new line" + var reviewComment api.PullReviewComment { req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/pulls/%d/reviews/%d/comments", repo.FullName(), pullIssue.Index, review.ID), &api.CreatePullReviewCommentOptions{ @@ -76,24 +77,22 @@ func TestAPIPullReviewCreateComment(t *testing.T) { OldLineNum: reviewLine, }).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) - var reviewComment *api.PullReviewComment DecodeJSON(t, resp, &reviewComment) assert.EqualValues(t, review.ID, reviewComment.ReviewID) + assert.EqualValues(t, newCommentBody, reviewComment.Body) + assert.EqualValues(t, reviewLine, reviewComment.OldLineNum) + assert.EqualValues(t, 0, reviewComment.LineNum) + assert.EqualValues(t, path, reviewComment.Path) } { - req := NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/pulls/%d/reviews/%d/comments", repo.FullName(), pullIssue.Index, review.ID). + req := NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/pulls/%d/reviews/%d/comments/%d", repo.FullName(), pullIssue.Index, review.ID, reviewComment.ID). AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) - var reviewComments []*api.PullReviewComment - DecodeJSON(t, resp, &reviewComments) - assert.Len(t, reviewComments, 2) - assert.EqualValues(t, existingCommentBody, reviewComments[0].Body) - assert.EqualValues(t, reviewComments[0].OldLineNum, reviewComments[1].OldLineNum) - assert.EqualValues(t, reviewComments[0].LineNum, reviewComments[1].LineNum) - assert.EqualValues(t, newCommentBody, reviewComments[1].Body) - assert.EqualValues(t, path, reviewComments[1].Path) + var comment api.PullReviewComment + DecodeJSON(t, resp, &comment) + assert.EqualValues(t, reviewComment, comment) } { From ee1ead81891d7a0d4e62e5ba89ebee9db6359e76 Mon Sep 17 00:00:00 2001 From: Gergely Nagy Date: Thu, 11 Jan 2024 00:20:32 +0100 Subject: [PATCH 73/86] [GITEA] Improved Linguist compatibility Recognise the `linguist-documentation` and `linguist-detectable` attributes in `.gitattributes` files, and use them in `GetLanguageStats()` to make a decision whether to include a particular file in the stats or not. This allows one more control over which files in their repositories contribute toward the language statistics, so that for a project that is mostly documentation, the language stats can reflect that. Fixes #1672. Signed-off-by: Gergely Nagy (cherry picked from commit 6d4e02fe5f2e79fceb6cf672f6f822714db6d0fe) --- modules/git/repo_attribute.go | 2 +- modules/git/repo_language_stats.go | 12 + modules/git/repo_language_stats_gogit.go | 40 +-- modules/git/repo_language_stats_nogogit.go | 39 ++- tests/integration/repo_lang_stats_test.go | 276 +++++++++++++++++++++ 5 files changed, 341 insertions(+), 28 deletions(-) create mode 100644 tests/integration/repo_lang_stats_test.go diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go index 2b34f117f7..3c5a1429a9 100644 --- a/modules/git/repo_attribute.go +++ b/modules/git/repo_attribute.go @@ -291,7 +291,7 @@ func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeRe } checker := &CheckAttributeReader{ - Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language"}, + Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language", "linguist-documentation", "linguist-detectable"}, Repo: repo, IndexFile: indexFilename, WorkTree: worktree, diff --git a/modules/git/repo_language_stats.go b/modules/git/repo_language_stats.go index c40d6937b5..7ed2dc1587 100644 --- a/modules/git/repo_language_stats.go +++ b/modules/git/repo_language_stats.go @@ -13,6 +13,18 @@ const ( bigFileSize int64 = 1024 * 1024 // 1 MiB ) +type LinguistBoolAttrib struct { + Value string +} + +func (attrib *LinguistBoolAttrib) IsTrue() bool { + return attrib.Value == "set" || attrib.Value == "true" +} + +func (attrib *LinguistBoolAttrib) IsFalse() bool { + return attrib.Value == "unset" || attrib.Value == "false" +} + // mergeLanguageStats mergers language names with different cases. The name with most upper case letters is used. func mergeLanguageStats(stats map[string]int64) map[string]int64 { names := map[string]struct { diff --git a/modules/git/repo_language_stats_gogit.go b/modules/git/repo_language_stats_gogit.go index 4c6fbd6c7e..558a83af74 100644 --- a/modules/git/repo_language_stats_gogit.go +++ b/modules/git/repo_language_stats_gogit.go @@ -1,4 +1,5 @@ // Copyright 2020 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. // SPDX-License-Identifier: MIT //go:build gogit @@ -57,23 +58,25 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err return nil } - notVendored := false - notGenerated := false + isVendored := LinguistBoolAttrib{} + isGenerated := LinguistBoolAttrib{} + isDocumentation := LinguistBoolAttrib{} + isDetectable := LinguistBoolAttrib{} if checker != nil { attrs, err := checker.CheckPath(f.Name) if err == nil { if vendored, has := attrs["linguist-vendored"]; has { - if vendored == "set" || vendored == "true" { - return nil - } - notVendored = vendored == "false" + isVendored = LinguistBoolAttrib{Value: vendored} } if generated, has := attrs["linguist-generated"]; has { - if generated == "set" || generated == "true" { - return nil - } - notGenerated = generated == "false" + isGenerated = LinguistBoolAttrib{Value: generated} + } + if documentation, has := attrs["linguist-documentation"]; has { + isDocumentation = LinguistBoolAttrib{Value: documentation} + } + if detectable, has := attrs["linguist-detectable"]; has { + isDetectable = LinguistBoolAttrib{Value: detectable} } if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" { // group languages, such as Pug -> HTML; SCSS -> CSS @@ -105,8 +108,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err } } - if (!notVendored && analyze.IsVendor(f.Name)) || enry.IsDotFile(f.Name) || - enry.IsDocumentation(f.Name) || enry.IsConfiguration(f.Name) { + if isDetectable.IsFalse() || isVendored.IsTrue() || isDocumentation.IsTrue() || + (!isVendored.IsFalse() && analyze.IsVendor(f.Name)) || + enry.IsDotFile(f.Name) || + enry.IsConfiguration(f.Name) || + (!isDocumentation.IsFalse() && enry.IsDocumentation(f.Name)) { return nil } @@ -115,12 +121,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err if f.Size <= bigFileSize { content, _ = readFile(f, fileSizeLimit) } - if !notGenerated && enry.IsGenerated(f.Name, content) { + if !isGenerated.IsTrue() && enry.IsGenerated(f.Name, content) { return nil } // TODO: Use .gitattributes file for linguist overrides - language := analyze.GetCodeLanguage(f.Name, content) if language == enry.OtherLanguage || language == "" { return nil @@ -136,6 +141,13 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err if !checked { langtype := enry.GetLanguageType(language) included = langtype == enry.Programming || langtype == enry.Markup + if !included { + if isDetectable.IsTrue() { + included = true + } else { + return nil + } + } includedLanguage[language] = included } if included { diff --git a/modules/git/repo_language_stats_nogogit.go b/modules/git/repo_language_stats_nogogit.go index 1d94ad6c00..13876094cc 100644 --- a/modules/git/repo_language_stats_nogogit.go +++ b/modules/git/repo_language_stats_nogogit.go @@ -1,4 +1,5 @@ // Copyright 2020 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. // SPDX-License-Identifier: MIT //go:build !gogit @@ -90,23 +91,25 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err continue } - notVendored := false - notGenerated := false + isVendored := LinguistBoolAttrib{} + isGenerated := LinguistBoolAttrib{} + isDocumentation := LinguistBoolAttrib{} + isDetectable := LinguistBoolAttrib{} if checker != nil { attrs, err := checker.CheckPath(f.Name()) if err == nil { if vendored, has := attrs["linguist-vendored"]; has { - if vendored == "set" || vendored == "true" { - continue - } - notVendored = vendored == "false" + isVendored = LinguistBoolAttrib{Value: vendored} } if generated, has := attrs["linguist-generated"]; has { - if generated == "set" || generated == "true" { - continue - } - notGenerated = generated == "false" + isGenerated = LinguistBoolAttrib{Value: generated} + } + if documentation, has := attrs["linguist-documentation"]; has { + isDocumentation = LinguistBoolAttrib{Value: documentation} + } + if detectable, has := attrs["linguist-detectable"]; has { + isDetectable = LinguistBoolAttrib{Value: detectable} } if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" { // group languages, such as Pug -> HTML; SCSS -> CSS @@ -139,8 +142,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err } } - if (!notVendored && analyze.IsVendor(f.Name())) || enry.IsDotFile(f.Name()) || - enry.IsDocumentation(f.Name()) || enry.IsConfiguration(f.Name()) { + if isDetectable.IsFalse() || isVendored.IsTrue() || isDocumentation.IsTrue() || + (!isVendored.IsFalse() && analyze.IsVendor(f.Name())) || + enry.IsDotFile(f.Name()) || + enry.IsConfiguration(f.Name()) || + (!isDocumentation.IsFalse() && enry.IsDocumentation(f.Name())) { continue } @@ -173,7 +179,7 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err return nil, err } } - if !notGenerated && enry.IsGenerated(f.Name(), content) { + if !isGenerated.IsTrue() && enry.IsGenerated(f.Name(), content) { continue } @@ -194,6 +200,13 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err if !checked { langType := enry.GetLanguageType(language) included = langType == enry.Programming || langType == enry.Markup + if !included { + if isDetectable.IsTrue() { + included = true + } else { + continue + } + } includedLanguage[language] = included } if included { diff --git a/tests/integration/repo_lang_stats_test.go b/tests/integration/repo_lang_stats_test.go new file mode 100644 index 0000000000..f3a7e4bc6d --- /dev/null +++ b/tests/integration/repo_lang_stats_test.go @@ -0,0 +1,276 @@ +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "context" + "net/url" + "strings" + "testing" + "time" + + "code.gitea.io/gitea/models/db" + 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" + "code.gitea.io/gitea/modules/indexer/stats" + "code.gitea.io/gitea/modules/queue" + repo_service "code.gitea.io/gitea/services/repository" + files_service "code.gitea.io/gitea/services/repository/files" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func createLangStatTestRepo(t *testing.T) (*repo_model.Repository, func()) { + t.Helper() + + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + // Create a new repository + repo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{ + Name: "lang-stat-test", + Description: "minimal repo for language stats testing", + AutoInit: true, + Gitignores: "Go", + License: "MIT", + Readme: "Default", + DefaultBranch: "main", + IsPrivate: false, + }) + assert.NoError(t, err) + assert.NotEmpty(t, repo) + + return repo, func() { + repo_service.DeleteRepository(db.DefaultContext, user2, repo, false) + } +} + +func addLangStatTestFiles(t *testing.T, repo *repo_model.Repository, contents string) string { + t.Helper() + + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + addFilesResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, owner, &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: ".gitattributes", + ContentReader: strings.NewReader(contents), + }, + { + Operation: "create", + TreePath: "docs.md", + ContentReader: strings.NewReader("This **is** a `markdown` file.\n"), + }, + { + Operation: "create", + TreePath: "foo.c", + ContentReader: strings.NewReader(`#include \nint main() {\n printf("Hello world!\n");\n return 0;\n}\n`), + }, + { + Operation: "create", + TreePath: "foo.nib", + ContentReader: strings.NewReader("Pinky promise, this is not a generated file!\n"), + }, + { + Operation: "create", + TreePath: ".dot.pas", + ContentReader: strings.NewReader("program Hello;\nbegin\n writeln('Hello, world.');\nend.\n"), + }, + { + Operation: "create", + TreePath: "cpplint.py", + ContentReader: strings.NewReader(`#! /usr/bin/env python\n\nprint("Hello world!")\n`), + }, + { + Operation: "create", + TreePath: "some-file.xml", + ContentReader: strings.NewReader(`\n\n Hello\n\n`), + }, + }, + Message: "add files", + OldBranch: "main", + NewBranch: "main", + Author: &files_service.IdentityOptions{ + Name: owner.Name, + Email: owner.Email, + }, + Committer: &files_service.IdentityOptions{ + Name: owner.Name, + Email: owner.Email, + }, + Dates: &files_service.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }) + assert.NoError(t, err) + assert.NotEmpty(t, addFilesResp) + + return addFilesResp.Commit.SHA +} + +func TestRepoLangStats(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + /****************** + ** Preparations ** + ******************/ + prep := func(t *testing.T, attribs string) (*repo_model.Repository, string, func()) { + t.Helper() + + repo, f := createLangStatTestRepo(t) + sha := addLangStatTestFiles(t, repo, attribs) + + return repo, sha, f + } + + getFreshLanguageStats := func(t *testing.T, repo *repo_model.Repository, sha string) repo_model.LanguageStatList { + t.Helper() + + err := stats.UpdateRepoIndexer(repo) + assert.NoError(t, err) + + assert.NoError(t, queue.GetManager().FlushAll(context.Background(), 10*time.Second)) + + status, err := repo_model.GetIndexerStatus(db.DefaultContext, repo, repo_model.RepoIndexerTypeStats) + assert.NoError(t, err) + assert.Equal(t, sha, status.CommitSha) + langs, err := repo_model.GetTopLanguageStats(db.DefaultContext, repo, 5) + assert.NoError(t, err) + + return langs + } + + /*********** + ** Tests ** + ***********/ + + // 1. By default, documentation is not indexed + t.Run("default", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo, sha, f := prep(t, "") + defer f() + + langs := getFreshLanguageStats(t, repo, sha) + + // While this is a fairly short test, this exercises a number of + // things: + // + // - `.gitattributes` is empty, so `isDetectable.IsFalse()`, + // `isVendored.IsTrue()`, and `isDocumentation.IsTrue()` will be + // false for every file, because these are only true if an + // attribute is explicitly set. + // + // - There is `.dot.pas`, which would be considered Pascal source, + // but it is a dotfile (thus, `enry.IsDotFile()` applies), and as + // such, is not considered. + // + // - `some-file.xml` will be skipped because Enry considers XML + // configuration, and `enry.IsConfiguration()` will catch it. + // + // - `!isVendored.IsFalse()` evaluates to true, so + // `analyze.isVendor()` will be called on `cpplint.py`, which will + // be considered vendored, even though both the filename and + // contents would otherwise make it Python. + // + // - `!isDocumentation.IsFalse()` evaluates to true, so + // `enry.IsDocumentation()` will be called for `docs.md`, and will + // be considered documentation, thus, skipped. + // + // Thus, this exercises all of the conditions in the first big if + // that is supposed to filter out files early. With two short asserts! + + assert.Len(t, langs, 1) + assert.Equal(t, "C", langs[0].Language) + }) + + // 2. Marking foo.c as non-detectable + t.Run("foo.c non-detectable", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo, sha, f := prep(t, "foo.c linguist-detectable=false\n") + defer f() + + langs := getFreshLanguageStats(t, repo, sha) + assert.Empty(t, langs) + }) + + // 3. Marking Markdown detectable + t.Run("detectable markdown", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo, sha, f := prep(t, "*.md linguist-detectable\n") + defer f() + + langs := getFreshLanguageStats(t, repo, sha) + assert.Len(t, langs, 2) + assert.Equal(t, "C", langs[0].Language) + assert.Equal(t, "Markdown", langs[1].Language) + }) + + // 4. Marking foo.c as documentation + t.Run("foo.c as documentation", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo, sha, f := prep(t, "foo.c linguist-documentation\n") + defer f() + + langs := getFreshLanguageStats(t, repo, sha) + assert.Empty(t, langs) + }) + + // 5. Overriding a generated file + t.Run("linguist-generated=false", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo, sha, f := prep(t, "foo.nib linguist-generated=false\nfoo.nib linguist-language=Perl\n") + defer f() + + langs := getFreshLanguageStats(t, repo, sha) + assert.Len(t, langs, 2) + assert.Equal(t, "C", langs[0].Language) + assert.Equal(t, "Perl", langs[1].Language) + }) + + // 6. Disabling vendoring for a file + t.Run("linguist-vendored=false", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo, sha, f := prep(t, "cpplint.py linguist-vendored=false\n") + defer f() + + langs := getFreshLanguageStats(t, repo, sha) + assert.Len(t, langs, 2) + assert.Equal(t, "C", langs[0].Language) + assert.Equal(t, "Python", langs[1].Language) + }) + + // 7. Disabling vendoring for a file, with -linguist-vendored + t.Run("-linguist-vendored", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo, sha, f := prep(t, "cpplint.py -linguist-vendored\n") + defer f() + + langs := getFreshLanguageStats(t, repo, sha) + assert.Len(t, langs, 2) + assert.Equal(t, "C", langs[0].Language) + assert.Equal(t, "Python", langs[1].Language) + }) + + // 8. Marking foo.c as vendored + t.Run("foo.c as vendored", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + repo, sha, f := prep(t, "foo.c linguist-vendored\n") + defer f() + + langs := getFreshLanguageStats(t, repo, sha) + assert.Empty(t, langs) + }) + }) +} From c67255192e81b356f88f83447ed13d89cb7e0b6e Mon Sep 17 00:00:00 2001 From: Gusted Date: Wed, 17 Jan 2024 17:45:55 +0100 Subject: [PATCH 74/86] [GITEA] Fix relative links rendering - Relative links were not properly being rendered, because the links were being made absolute against the repository URL instead of repository URL + /src/branch, which leads to incorrect links. - Restore the 'old' behaviour. When there's branch information, that should be used as base for links. - Adjusts the test cases. - Regression of 637451a45ecbc3d127ff2adf27437ce1357493ea - Resolves https://codeberg.org/Codeberg/Community/issues/1411 (cherry picked from commit 0e9d52e2918004ac183910c712e9fe486e139e05) --- modules/markup/markdown/goldmark.go | 2 ++ modules/markup/markdown/markdown_test.go | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index b92b90561b..1db3cbad7e 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -137,6 +137,8 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa var base string if ctx.IsWiki { base = ctx.Links.WikiLink() + } else if ctx.Links.HasBranchInfo() { + base = ctx.Links.SrcLink() } else { base = ctx.Links.Base } diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 77afdcbfbd..d6d0e93eb2 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -975,7 +975,7 @@ space

Expected: `

space @mention-user
/just/a/path.bin
https://example.com/file.bin
-local link
+local link
remote link
local link
remote link
@@ -1088,7 +1088,7 @@ space

Expected: `

space @mention-user
/just/a/path.bin
https://example.com/file.bin
-local link
+local link
remote link
local link
remote link
From 28ecd6f5a67891788ad4d989311050df55deb008 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Tue, 16 Jan 2024 10:28:09 +0000 Subject: [PATCH 75/86] [GITEA] DELETE /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments/{comment} * reuse deleteIssueComment by adding the commentType parameter * ensure tests start with a PR with no random reviews from fixtures Refs: https://codeberg.org/forgejo/forgejo/issues/2109 (cherry picked from commit 5b90ab77f67e4c0ac17d8b1101453d7790fa45d2) --- routers/api/v1/api.go | 6 ++- routers/api/v1/repo/issue_comment.go | 8 +-- routers/api/v1/repo/pull_review.go | 47 +++++++++++++++++ templates/swagger/v1_json.tmpl | 61 +++++++++++++++++++++++ tests/integration/api_pull_review_test.go | 40 ++++++++++++++- 5 files changed, 156 insertions(+), 6 deletions(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index f9c9017a6b..0cb647f870 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1265,7 +1265,11 @@ func Routes() *web.Route { m.Combo(""). Get(repo.GetPullReviewComments). Post(reqToken(), bind(api.CreatePullReviewCommentOptions{}), repo.CreatePullReviewComment) - m.Get("/{comment}", commentAssignment("comment"), repo.GetPullReviewComment) + m.Group("/{comment}", func() { + m.Combo(""). + Get(repo.GetPullReviewComment). + Delete(reqToken(), repo.DeletePullReviewComment) + }, commentAssignment("comment")) }) m.Post("/dismissals", reqToken(), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview) m.Post("/undismissals", reqToken(), repo.UnDismissPullReview) diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go index 91de21db40..7cd44762e3 100644 --- a/routers/api/v1/repo/issue_comment.go +++ b/routers/api/v1/repo/issue_comment.go @@ -624,7 +624,7 @@ func DeleteIssueComment(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - deleteIssueComment(ctx) + deleteIssueComment(ctx, issues_model.CommentTypeComment) } // DeleteIssueCommentDeprecated delete a comment from an issue @@ -663,16 +663,16 @@ func DeleteIssueCommentDeprecated(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - deleteIssueComment(ctx) + deleteIssueComment(ctx, issues_model.CommentTypeComment) } -func deleteIssueComment(ctx *context.APIContext) { +func deleteIssueComment(ctx *context.APIContext, commentType issues_model.CommentType) { comment := ctx.Comment if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { ctx.Status(http.StatusForbidden) return - } else if comment.Type != issues_model.CommentTypeComment { + } else if comment.Type != commentType { ctx.Status(http.StatusNoContent) return } diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go index fc06a90e94..3959449560 100644 --- a/routers/api/v1/repo/pull_review.go +++ b/routers/api/v1/repo/pull_review.go @@ -1015,6 +1015,53 @@ func UnDismissPullReview(ctx *context.APIContext) { dismissReview(ctx, "", false, false) } +// DeletePullReviewComment delete a pull review comment +func DeletePullReviewComment(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments/{comment} repository repoDeletePullReviewComment + // --- + // summary: Delete a pull review comment + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: index + // in: path + // description: index of the pull request + // type: integer + // format: int64 + // required: true + // - name: id + // in: path + // description: id of the review + // type: integer + // format: int64 + // required: true + // - name: comment + // in: path + // description: id of the comment + // type: integer + // format: int64 + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + deleteIssueComment(ctx, issues_model.CommentTypeCode) +} + func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors bool) { if !ctx.Repo.IsAdmin() { ctx.Error(http.StatusForbidden, "", "Must be repo admin") diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 91f4593e67..7cebaa875f 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -11666,6 +11666,67 @@ "$ref": "#/responses/notFound" } } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Delete a pull review comment", + "operationId": "repoDeletePullReviewComment", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "index of the pull request", + "name": "index", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "id of the review", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "id of the comment", + "name": "comment", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } } }, "/repos/{owner}/{repo}/pulls/{index}/reviews/{id}/dismissals": { diff --git a/tests/integration/api_pull_review_test.go b/tests/integration/api_pull_review_test.go index db44e1ade5..c66c7d752d 100644 --- a/tests/integration/api_pull_review_test.go +++ b/tests/integration/api_pull_review_test.go @@ -21,7 +21,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestAPIPullReviewCreateComment(t *testing.T) { +func TestAPIPullReviewCreateDeleteComment(t *testing.T) { defer tests.PrepareTestEnv(t)() pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}) assert.NoError(t, pullIssue.LoadAttributes(db.DefaultContext)) @@ -47,6 +47,30 @@ func TestAPIPullReviewCreateComment(t *testing.T) { var review api.PullReview var reviewLine int64 = 1 + // cleanup + { + session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll) + + req := NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/pulls/%d/reviews", repo.FullName(), pullIssue.Index).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var reviews []*api.PullReview + DecodeJSON(t, resp, &reviews) + for _, review := range reviews { + req := NewRequestf(t, http.MethodDelete, "/api/v1/repos/%s/pulls/%d/reviews/%d", repo.FullName(), pullIssue.Index, review.ID). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + } + } + + requireReviewCount := func(count int) { + req := NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/pulls/%d/reviews", repo.FullName(), pullIssue.Index).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var reviews []*api.PullReview + DecodeJSON(t, resp, &reviews) + require.EqualValues(t, count, len(reviews)) + } + { req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/pulls/%d/reviews", repo.FullName(), pullIssue.Index), &api.CreatePullReviewOptions{ Body: "body1", @@ -66,6 +90,7 @@ func TestAPIPullReviewCreateComment(t *testing.T) { DecodeJSON(t, resp, &getReview) require.EqualValues(t, getReview, review) } + requireReviewCount(1) newCommentBody := "first new line" var reviewComment api.PullReviewComment @@ -95,11 +120,24 @@ func TestAPIPullReviewCreateComment(t *testing.T) { assert.EqualValues(t, reviewComment, comment) } + { + req := NewRequestf(t, http.MethodDelete, "/api/v1/repos/%s/pulls/%d/reviews/%d/comments/%d", repo.FullName(), pullIssue.Index, review.ID, reviewComment.ID). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + } + + { + req := NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/pulls/%d/reviews/%d/comments/%d", repo.FullName(), pullIssue.Index, review.ID, reviewComment.ID). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusNotFound) + } + { req := NewRequestf(t, http.MethodDelete, "/api/v1/repos/%s/pulls/%d/reviews/%d", repo.FullName(), pullIssue.Index, review.ID). AddTokenAuth(token) MakeRequest(t, req, http.StatusNoContent) } + requireReviewCount(0) }) } } From 3f74bcb14df99ee75a170813979beb5ce04c8027 Mon Sep 17 00:00:00 2001 From: Gusted Date: Fri, 19 Jan 2024 01:14:49 +0100 Subject: [PATCH 76/86] [GITEA] Fix API inconsistencies - Document the correct content types for Git archives. Add code that actually sets the correct application type for `.zip` and `.tar.gz`. - When an action (POST/PUT/DELETE method) was successful, an 204 status code should be returned instead of status code 200. - Add and adjust integration testing. - Resolves #2180 - Resolves #2181 (cherry picked from commit 6c8c4512b530e966557a5584efbbb757638b3429) --- routers/api/v1/admin/user.go | 2 +- routers/api/v1/repo/file.go | 14 +++++++++++++- templates/swagger/v1_json.tmpl | 4 +++- tests/integration/api_admin_test.go | 6 +++--- tests/integration/api_repo_archive_test.go | 3 +++ 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index b4cc42ea5d..cfd9ff9cab 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -545,5 +545,5 @@ func RenameUser(ctx *context.APIContext) { } log.Trace("User name changed: %s -> %s", oldName, newName) - ctx.Status(http.StatusOK) + ctx.Status(http.StatusNoContent) } diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 94e634461c..0c3807492b 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -256,7 +256,9 @@ func GetArchive(ctx *context.APIContext) { // --- // summary: Get an archive of a repository // produces: - // - application/json + // - application/octet-stream + // - application/zip + // - application/gzip // parameters: // - name: owner // in: path @@ -337,7 +339,17 @@ func download(ctx *context.APIContext, archiveName string, archiver *repo_model. } defer fr.Close() + contentType := "" + switch archiver.Type { + case git.ZIP: + contentType = "application/zip" + case git.TARGZ: + // Per RFC6713. + contentType = "application/gzip" + } + ctx.ServeContent(fr, &context.ServeHeaderOptions{ + ContentType: contentType, Filename: downloadName, LastModified: archiver.CreatedUnix.AsLocalTime(), }) diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 7cebaa875f..40c323b9f1 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -3533,7 +3533,9 @@ "/repos/{owner}/{repo}/archive/{archive}": { "get": { "produces": [ - "application/json" + "application/octet-stream", + "application/zip", + "application/gzip" ], "tags": [ "repository" diff --git a/tests/integration/api_admin_test.go b/tests/integration/api_admin_test.go index ff7c2ddca3..3c80401e0f 100644 --- a/tests/integration/api_admin_test.go +++ b/tests/integration/api_admin_test.go @@ -254,14 +254,14 @@ func TestAPIRenameUser(t *testing.T) { // required "new_name": "User2", }).AddTokenAuth(token) - MakeRequest(t, req, http.StatusOK) + MakeRequest(t, req, http.StatusNoContent) urlStr = fmt.Sprintf("/api/v1/admin/users/%s/rename", "User2") req = NewRequestWithValues(t, "POST", urlStr, map[string]string{ // required "new_name": "User2-2-2", }).AddTokenAuth(token) - MakeRequest(t, req, http.StatusOK) + MakeRequest(t, req, http.StatusNoContent) req = NewRequestWithValues(t, "POST", urlStr, map[string]string{ // required @@ -281,7 +281,7 @@ func TestAPIRenameUser(t *testing.T) { // required "new_name": "user2", }).AddTokenAuth(token) - MakeRequest(t, req, http.StatusOK) + MakeRequest(t, req, http.StatusNoContent) } func TestAPICron(t *testing.T) { diff --git a/tests/integration/api_repo_archive_test.go b/tests/integration/api_repo_archive_test.go index 57d3abfe84..c574d49450 100644 --- a/tests/integration/api_repo_archive_test.go +++ b/tests/integration/api_repo_archive_test.go @@ -32,18 +32,21 @@ func TestAPIDownloadArchive(t *testing.T) { bs, err := io.ReadAll(resp.Body) assert.NoError(t, err) assert.Len(t, bs, 320) + assert.EqualValues(t, "application/zip", resp.Header().Get("Content-Type")) link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/archive/master.tar.gz", user2.Name, repo.Name)) resp = MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK) bs, err = io.ReadAll(resp.Body) assert.NoError(t, err) assert.Len(t, bs, 266) + assert.EqualValues(t, "application/gzip", resp.Header().Get("Content-Type")) link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/archive/master.bundle", user2.Name, repo.Name)) resp = MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK) bs, err = io.ReadAll(resp.Body) assert.NoError(t, err) assert.Len(t, bs, 382) + assert.EqualValues(t, "application/octet-stream", resp.Header().Get("Content-Type")) link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/archive/master", user2.Name, repo.Name)) MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusBadRequest) From 68fceb9b7a34246a33cdbc2d6669ce80d310f4e9 Mon Sep 17 00:00:00 2001 From: Gusted Date: Sat, 20 Jan 2024 22:33:36 +0100 Subject: [PATCH 77/86] [GITEA] Adjust name of operation - The name could be conflucted with the `GET /user/applications/oauth2/{id}` operation, as it only differed in a single letter being uppercase. Change it to be userGetOAuth2Application**s**, as that's also more accurate for this function. - Resolves #2163 (cherry picked from commit 1891dac5478f095453c4e1eb3b884926b5344deb) --- routers/api/v1/user/app.go | 2 +- templates/swagger/v1_json.tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/routers/api/v1/user/app.go b/routers/api/v1/user/app.go index f045fb4d5d..eb35d8031a 100644 --- a/routers/api/v1/user/app.go +++ b/routers/api/v1/user/app.go @@ -244,7 +244,7 @@ func CreateOauth2Application(ctx *context.APIContext) { // ListOauth2Applications list all the Oauth2 application func ListOauth2Applications(ctx *context.APIContext) { - // swagger:operation GET /user/applications/oauth2 user userGetOauth2Application + // swagger:operation GET /user/applications/oauth2 user userGetOAuth2Applications // --- // summary: List the authenticated user's oauth2 applications // produces: diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 40c323b9f1..d2ad8dd532 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -15169,7 +15169,7 @@ "user" ], "summary": "List the authenticated user's oauth2 applications", - "operationId": "userGetOauth2Application", + "operationId": "userGetOAuth2Applications", "parameters": [ { "type": "integer", From 70c5e2021d7c385b9285622f0b2d878d3807d33c Mon Sep 17 00:00:00 2001 From: Gergely Nagy Date: Sun, 21 Jan 2024 15:18:24 +0100 Subject: [PATCH 78/86] [GITEA] Rework when recently pushed branches are displayed With this change, the "You pushed on branch xyz" banner will be displayed when either the viewed repository or its base repo (if the current one's a fork) has pull requests enabled. Previously it only displayed if the viewed repo had PRs enabled. Furthermore, if the viewed repository is an original repository that the viewing user has a fork of, if the forked repository has recently pushed branches, then the banner will appear for the original repository too. In this case, the notification will include branches from the viewing user's fork, and branches they pushed to the base repo, too. Refs: https://codeberg.org/forgejo/forgejo/pulls/2195 Signed-off-by: Gergely Nagy (cherry picked from commit a29f10661d59f6c33c5cfbee723f03f981aa6b72) --- models/git/branch.go | 4 + routers/web/repo/view.go | 41 ++++- .../code/recently_pushed_new_branches.tmpl | 11 +- tests/integration/pull_create_test.go | 171 ++++++++++++++++++ 4 files changed, 215 insertions(+), 12 deletions(-) diff --git a/models/git/branch.go b/models/git/branch.go index db02fc9b28..a96d7ff8cb 100644 --- a/models/git/branch.go +++ b/models/git/branch.go @@ -128,6 +128,10 @@ func (b *Branch) LoadDeletedBy(ctx context.Context) (err error) { return err } +func (b *Branch) GetRepo(ctx context.Context) (*repo_model.Repository, error) { + return repo_model.GetRepositoryByID(ctx, b.RepoID) +} + func (b *Branch) LoadPusher(ctx context.Context) (err error) { if b.Pusher == nil && b.PusherID > 0 { b.Pusher, err = user_model.GetUserByID(ctx, b.PusherID) diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 5d96aa0efc..3630d85170 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -1020,20 +1020,43 @@ func renderCode(ctx *context.Context) { return } - showRecentlyPushedNewBranches := true - if ctx.Repo.Repository.IsMirror || - !ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypePullRequests) { - showRecentlyPushedNewBranches = false + // If the repo is a mirror, don't display recently pushed branches. + if ctx.Repo.Repository.IsMirror { + goto PostRecentBranchCheck } - if showRecentlyPushedNewBranches { - ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID, ctx.Repo.Repository.DefaultBranch) - if err != nil { - ctx.ServerError("GetRecentlyPushedBranches", err) - return + + // If pull requests aren't enabled for either the current repo, or its + // base, don't display recently pushed branches. + if !(ctx.Repo.Repository.AllowsPulls(ctx) || + (ctx.Repo.Repository.BaseRepo != nil && ctx.Repo.Repository.BaseRepo.AllowsPulls(ctx))) { + goto PostRecentBranchCheck + } + + // Find recently pushed new branches to *this* repo. + branches, err := git_model.FindRecentlyPushedNewBranches(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID, ctx.Repo.Repository.DefaultBranch) + if err != nil { + ctx.ServerError("FindRecentlyPushedBranches", err) + return + } + + // If this is not a fork, check if the signed in user has a fork, and + // check branches there. + if !ctx.Repo.Repository.IsFork { + repo := repo_model.GetForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) + if repo != nil { + baseBranches, err := git_model.FindRecentlyPushedNewBranches(ctx, repo.ID, ctx.Doer.ID, repo.DefaultBranch) + if err != nil { + ctx.ServerError("FindRecentlyPushedBranches", err) + return + } + branches = append(branches, baseBranches...) } } + + ctx.Data["RecentlyPushedNewBranches"] = branches } +PostRecentBranchCheck: var treeNames []string paths := make([]string, 0, 5) if len(ctx.Repo.TreePath) > 0 { diff --git a/templates/repo/code/recently_pushed_new_branches.tmpl b/templates/repo/code/recently_pushed_new_branches.tmpl index 2b9948d214..176ae3df00 100644 --- a/templates/repo/code/recently_pushed_new_branches.tmpl +++ b/templates/repo/code/recently_pushed_new_branches.tmpl @@ -2,10 +2,15 @@

{{$timeSince := TimeSince .CommitTime.AsTime ctx.Locale}} - {{$branchLink := (print $.RepoLink "/src/branch/" (PathEscapeSegments .Name))}} - {{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" (Escape .Name) $timeSince $branchLink | Safe}} + {{$repo := .GetRepo $.Context}} + {{$name := .Name}} + {{if ne $repo.ID $.Repository.ID}} + {{$name = (print $repo.FullName ":" .Name)}} + {{end}} + {{$branchLink := (print ($repo.Link) "/src/branch/" (PathEscapeSegments .Name))}} + {{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" (Escape $name) $timeSince $branchLink | Safe}}
- + {{ctx.Locale.Tr "repo.pulls.compare_changes"}}
diff --git a/tests/integration/pull_create_test.go b/tests/integration/pull_create_test.go index 01965ecbef..ba52b5a8d4 100644 --- a/tests/integration/pull_create_test.go +++ b/tests/integration/pull_create_test.go @@ -1,4 +1,5 @@ // Copyright 2017 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. // SPDX-License-Identifier: MIT package integration @@ -12,7 +13,11 @@ import ( "strings" "testing" + "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/test" + repo_service "code.gitea.io/gitea/services/repository" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -175,3 +180,169 @@ func TestPullBranchDelete(t *testing.T) { session.MakeRequest(t, req, http.StatusOK) }) } + +func TestRecentlyPushed(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + session := loginUser(t, "user1") + testRepoFork(t, session, "user2", "repo1", "user1", "repo1") + + testCreateBranch(t, session, "user1", "repo1", "branch/master", "recent-push", http.StatusSeeOther) + testEditFile(t, session, "user1", "repo1", "recent-push", "README.md", "Hello recently!\n") + + testCreateBranch(t, session, "user2", "repo1", "branch/master", "recent-push-base", http.StatusSeeOther) + testEditFile(t, session, "user2", "repo1", "recent-push-base", "README.md", "Hello, recently, from base!\n") + + baseRepo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "repo1") + assert.NoError(t, err) + repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user1", "repo1") + assert.NoError(t, err) + + enablePRs := func(t *testing.T, repo *repo_model.Repository) { + t.Helper() + + err := repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, + []repo_model.RepoUnit{{ + RepoID: repo.ID, + Type: unit_model.TypePullRequests, + }}, + nil) + assert.NoError(t, err) + } + + disablePRs := func(t *testing.T, repo *repo_model.Repository) { + t.Helper() + + err := repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, nil, + []unit_model.Type{unit_model.TypePullRequests}) + assert.NoError(t, err) + } + + testBanner := func(t *testing.T) { + t.Helper() + + req := NewRequest(t, "GET", "/user1/repo1") + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + message := strings.TrimSpace(htmlDoc.Find(".ui.message").Text()) + link, _ := htmlDoc.Find(".ui.message a").Attr("href") + expectedMessage := "You pushed on branch recent-push" + + assert.Contains(t, message, expectedMessage) + assert.Equal(t, "/user1/repo1/src/branch/recent-push", link) + } + + // Test that there's a recently pushed branches banner, and it contains + // a link to the branch. + t.Run("recently-pushed-banner", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + testBanner(t) + }) + + // Test that it is still there if the fork has PRs disabled, but the + // base repo still has them enabled. + t.Run("with-fork-prs-disabled", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer func() { + enablePRs(t, repo) + }() + + disablePRs(t, repo) + testBanner(t) + }) + + // Test that it is still there if the fork has PRs enabled, but the base + // repo does not. + t.Run("with-base-prs-disabled", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer func() { + enablePRs(t, baseRepo) + }() + + disablePRs(t, baseRepo) + testBanner(t) + }) + + // Test that the banner is not present if both the base and current + // repo have PRs disabled. + t.Run("with-prs-disabled", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer func() { + enablePRs(t, baseRepo) + enablePRs(t, repo) + }() + + disablePRs(t, repo) + disablePRs(t, baseRepo) + + req := NewRequest(t, "GET", "/user1/repo1") + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + htmlDoc.AssertElement(t, ".ui.message", false) + }) + + // Test that visiting the base repo has the banner too, and includes + // recent push notifications from both the fork, and the base repo. + t.Run("on the base repo", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Count recently pushed branches on the fork + req := NewRequest(t, "GET", "/user1/repo1") + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + htmlDoc.AssertElement(t, ".ui.message", true) + + // Count recently pushed branches on the base repo + req = NewRequest(t, "GET", "/user2/repo1") + resp = session.MakeRequest(t, req, http.StatusOK) + htmlDoc = NewHTMLParser(t, resp.Body) + messageCountOnBase := htmlDoc.Find(".ui.message").Length() + + // We have two messages on the base: one from the fork, one on the + // base itself. + assert.Equal(t, 2, messageCountOnBase) + }) + + // Test that the banner's links point to the right repos + t.Run("link validity", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // We're testing against the origin repo, because that has both + // local branches, and another from a fork, so we can test both in + // one test! + + req := NewRequest(t, "GET", "/user2/repo1") + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + messages := htmlDoc.Find(".ui.message") + + prButtons := messages.Find("a[role='button']") + branchLinks := messages.Find("a[href*='/src/branch/']") + + // ** base repo tests ** + basePRLink, _ := prButtons.First().Attr("href") + baseBranchLink, _ := branchLinks.First().Attr("href") + baseBranchName := branchLinks.First().Text() + + // branch in the same repo does not have a `user/repo:` qualifier. + assert.Equal(t, "recent-push-base", baseBranchName) + // branch link points to the same repo + assert.Equal(t, "/user2/repo1/src/branch/recent-push-base", baseBranchLink) + // PR link compares against the correct rep, and unqualified branch name + assert.Equal(t, "/user2/repo1/compare/master...recent-push-base", basePRLink) + + // ** forked repo tests ** + forkPRLink, _ := prButtons.Last().Attr("href") + forkBranchLink, _ := branchLinks.Last().Attr("href") + forkBranchName := branchLinks.Last().Text() + + // branch in the forked repo has a `user/repo:` qualifier. + assert.Equal(t, "user1/repo1:recent-push", forkBranchName) + // branch link points to the forked repo + assert.Equal(t, "/user1/repo1/src/branch/recent-push", forkBranchLink) + // PR link compares against the correct rep, and qualified branch name + assert.Equal(t, "/user2/repo1/compare/master...user1/repo1:recent-push", forkPRLink) + }) + }) +} From 957990b36a25d0e51d9b75432a577dd63fb6dad2 Mon Sep 17 00:00:00 2001 From: Gergely Nagy Date: Sun, 21 Jan 2024 13:22:07 +0100 Subject: [PATCH 79/86] [GITEA] Fix misleading comparisons when comparing branches When comparing branches, only offer those branches to use as a base where the repository allows pull requests. Those that do not allow pull request would result in a 404, so offering them as an option would be misleading. Refs: https://codeberg.org/forgejo/forgejo/pulls/2194 Signed-off-by: Gergely Nagy (cherry picked from commit 022d0e0d71a92c31302176c5c8ba1e7169bbbf3e) --- templates/repo/diff/compare.tmpl | 10 ++--- tests/integration/compare_test.go | 64 +++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl index 15574ad988..c2a114ba4e 100644 --- a/templates/repo/diff/compare.tmpl +++ b/templates/repo/diff/compare.tmpl @@ -67,12 +67,12 @@ {{range .Branches}}
{{$BaseCompareName}}:{{.}}
{{end}} - {{if not .PullRequestCtx.SameRepo}} + {{if and (not .PullRequestCtx.SameRepo) ($.HeadRepo.AllowsPulls ctx)}} {{range .HeadBranches}}
{{$HeadCompareName}}:{{.}}
{{end}} {{end}} - {{if .OwnForkRepo}} + {{if and .OwnForkRepo (.OwnForkRepo.AllowsPulls ctx)}} {{range .OwnForkRepoBranches}}
{{$OwnForkCompareName}}:{{.}}
{{end}} @@ -87,17 +87,17 @@ {{range .Tags}}
{{$BaseCompareName}}:{{.}}
{{end}} - {{if not .PullRequestCtx.SameRepo}} + {{if and (not .PullRequestCtx.SameRepo) ($.HeadRepo.AllowsPulls ctx)}} {{range .HeadTags}}
{{$HeadCompareName}}:{{.}}
{{end}} {{end}} - {{if .OwnForkRepo}} + {{if and .OwnForkRepo (.OwnForkRepo.AllowsPulls ctx)}} {{range .OwnForkRepoTags}}
{{$OwnForkCompareName}}:{{.}}
{{end}} {{end}} - {{if .RootRepo}} + {{if and .RootRepo (.RootRepo.AllowsPulls ctx)}} {{range .RootRepoTags}}
{{$RootRepoCompareName}}:{{.}}
{{end}} diff --git a/tests/integration/compare_test.go b/tests/integration/compare_test.go index 509524ca56..cf0bac4c8a 100644 --- a/tests/integration/compare_test.go +++ b/tests/integration/compare_test.go @@ -1,4 +1,5 @@ // Copyright 2021 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. // SPDX-License-Identifier: MIT package integration @@ -6,9 +7,14 @@ package integration import ( "fmt" "net/http" + "net/url" "strings" "testing" + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + unit_model "code.gitea.io/gitea/models/unit" + repo_service "code.gitea.io/gitea/services/repository" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -118,3 +124,61 @@ func TestCompareBranches(t *testing.T) { inspectCompare(t, htmlDoc, diffCount, diffChanges) } + +func TestCompareWithPRsDisabled(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + session := loginUser(t, "user1") + testRepoFork(t, session, "user2", "repo1", "user1", "repo1") + testCreateBranch(t, session, "user1", "repo1", "branch/master", "recent-push", http.StatusSeeOther) + testEditFile(t, session, "user1", "repo1", "recent-push", "README.md", "Hello recently!\n") + + repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user1", "repo1") + assert.NoError(t, err) + + defer func() { + // Reenable PRs on the repo + err := repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, + []repo_model.RepoUnit{{ + RepoID: repo.ID, + Type: unit_model.TypePullRequests, + }}, + nil) + assert.NoError(t, err) + }() + + // Disable PRs on the repo + err = repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, nil, + []unit_model.Type{unit_model.TypePullRequests}) + assert.NoError(t, err) + + t.Run("branch view doesn't offer creating PRs", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/user1/repo1/branches") + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + htmlDoc.AssertElement(t, "a[href='/user1/repo1/compare/master...recent-push']", false) + }) + + t.Run("compare doesn't offer local branches", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/user2/repo1/compare/master...user1/repo1:recent-push") + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + branches := htmlDoc.Find(".choose.branch .menu .reference-list-menu.base-branch-list .item, .choose.branch .menu .reference-list-menu.base-tag-list .item") + + expectedPrefix := "user2:" + for i := 0; i < len(branches.Nodes); i++ { + assert.True(t, strings.HasPrefix(branches.Eq(i).Text(), expectedPrefix)) + } + }) + + t.Run("comparing against a disabled-PR repo is 404", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/user1/repo1/compare/master...recent-push") + session.MakeRequest(t, req, http.StatusNotFound) + }) + }) +} From 22cff4158564a3e69bef83c458cf1f129e1b688b Mon Sep 17 00:00:00 2001 From: Gusted Date: Sun, 21 Jan 2024 23:53:43 +0100 Subject: [PATCH 80/86] [GITEA] Document correct status code for creating Tag - When there's a succesful POST operation, it should return a 201 status code (which is the status code for succesful created) and additionally the created object. - Currently for the `POST /repos/{owner}/{repo}/tags` endpoint an 200 status code was documented in the OpenAPI specification, while an 201 status code was actually being returned. In this case the code is correct and the documented status code needs to be adjusted. - Resolves #2200 (cherry picked from commit a2939116f5ce21295981a3a9aa84a73fe289b8b2) --- routers/api/v1/repo/tag.go | 2 +- templates/swagger/v1_json.tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/routers/api/v1/repo/tag.go b/routers/api/v1/repo/tag.go index 2f19f95e66..ad812ace56 100644 --- a/routers/api/v1/repo/tag.go +++ b/routers/api/v1/repo/tag.go @@ -176,7 +176,7 @@ func CreateTag(ctx *context.APIContext) { // schema: // "$ref": "#/definitions/CreateTagOption" // responses: - // "200": + // "201": // "$ref": "#/responses/Tag" // "404": // "$ref": "#/responses/notFound" diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index d2ad8dd532..e8da6e4997 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -13349,7 +13349,7 @@ } ], "responses": { - "200": { + "201": { "$ref": "#/responses/Tag" }, "404": { From b4509aa4c79573be973feae96ae808c195637634 Mon Sep 17 00:00:00 2001 From: voltagex Date: Sun, 21 Jan 2024 07:06:48 +0000 Subject: [PATCH 81/86] [GITEA] API comment update routers/api/v1/shared/runners.go Refs: https://codeberg.org/forgejo/forgejo/pulls/2191 (cherry picked from commit 1e89dd95b9d24377bf50a952c323d9e8b6895bf3) (cherry picked from commit fecc14a16c0f85f84127196609b7eafcb6605e80) --- routers/api/v1/shared/runners.go | 2 +- templates/swagger/v1_json.tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/routers/api/v1/shared/runners.go b/routers/api/v1/shared/runners.go index a342bd4b63..fe1584d2e7 100644 --- a/routers/api/v1/shared/runners.go +++ b/routers/api/v1/shared/runners.go @@ -12,7 +12,7 @@ import ( "code.gitea.io/gitea/modules/util" ) -// RegistrationToken is response related to registeration token +// RegistrationToken is a string used to register a runner with a server // swagger:response RegistrationToken type RegistrationToken struct { Token string `json:"token"` diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index e8da6e4997..4779553782 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -24372,7 +24372,7 @@ } }, "RegistrationToken": { - "description": "RegistrationToken is response related to registeration token", + "description": "RegistrationToken is a string used to register a runner with a server", "headers": { "token": { "type": "string" From 2d3c81d4f2676c58e026e5a06cfc8d84ad0d48fa Mon Sep 17 00:00:00 2001 From: Gergely Nagy Date: Sun, 21 Jan 2024 18:03:04 +0100 Subject: [PATCH 82/86] [GITEA] Don't consider orphan branches as recently pushed When displaying the recently pushed branches banner, don't display branches that have no common history with the default branch. These branches are usually not meant to be merged, so the banner is just noise in this case. Refs: https://codeberg.org/forgejo/forgejo/pulls/2196 Signed-off-by: Gergely Nagy (cherry picked from commit e1fba517f4c28c3027feaea73561045264f1f591) --- routers/web/repo/view.go | 38 +++++++++++++++++- tests/integration/pull_create_test.go | 58 +++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 3630d85170..a9bb854897 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -1053,7 +1053,43 @@ func renderCode(ctx *context.Context) { } } - ctx.Data["RecentlyPushedNewBranches"] = branches + // Filter out branches that have no relation to the default branch of + // the repository. + var filteredBranches []*git_model.Branch + for _, branch := range branches { + repo, err := branch.GetRepo(ctx) + if err != nil { + continue + } + gitRepo, err := git.OpenRepository(ctx, repo.RepoPath()) + if err != nil { + continue + } + defer gitRepo.Close() + head, err := gitRepo.GetCommit(branch.CommitID) + if err != nil { + continue + } + defaultBranch, err := gitRepo.GetDefaultBranch() + if err != nil { + continue + } + defaultBranchHead, err := gitRepo.GetCommit(defaultBranch) + if err != nil { + continue + } + + hasMergeBase, err := head.HasPreviousCommit(defaultBranchHead.ID) + if err != nil { + continue + } + + if hasMergeBase { + filteredBranches = append(filteredBranches, branch) + } + } + + ctx.Data["RecentlyPushedNewBranches"] = filteredBranches } PostRecentBranchCheck: diff --git a/tests/integration/pull_create_test.go b/tests/integration/pull_create_test.go index ba52b5a8d4..0aeecd5880 100644 --- a/tests/integration/pull_create_test.go +++ b/tests/integration/pull_create_test.go @@ -16,8 +16,13 @@ import ( "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/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/test" repo_service "code.gitea.io/gitea/services/repository" + files_service "code.gitea.io/gitea/services/repository/files" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -344,5 +349,58 @@ func TestRecentlyPushed(t *testing.T) { // PR link compares against the correct rep, and qualified branch name assert.Equal(t, "/user2/repo1/compare/master...user1/repo1:recent-push", forkPRLink) }) + + t.Run("unrelated branches are not shown", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}) + + // Create a new branch with no relation to the default branch. + // 1. Create a new Tree object + cmd := git.NewCommand(db.DefaultContext, "write-tree") + treeID, _, gitErr := cmd.RunStdString(&git.RunOpts{Dir: repo.RepoPath()}) + assert.NoError(t, gitErr) + treeID = strings.TrimSpace(treeID) + // 2. Create a new (empty) commit + cmd = git.NewCommand(db.DefaultContext, "commit-tree", "-m", "Initial orphan commit").AddDynamicArguments(treeID) + commitID, _, gitErr := cmd.RunStdString(&git.RunOpts{Dir: repo.RepoPath()}) + assert.NoError(t, gitErr) + commitID = strings.TrimSpace(commitID) + // 3. Create a new ref pointing to the orphaned commit + cmd = git.NewCommand(db.DefaultContext, "update-ref", "refs/heads/orphan1").AddDynamicArguments(commitID) + _, _, gitErr = cmd.RunStdString(&git.RunOpts{Dir: repo.RepoPath()}) + assert.NoError(t, gitErr) + // 4. Sync the git repo to the database + syncErr := repo_service.AddAllRepoBranchesToSyncQueue(graceful.GetManager().ShutdownContext(), adminUser.ID) + assert.NoError(t, syncErr) + // 5. Add a fresh commit, so that FindRecentlyPushedBranches has + // something to find. + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"}) + changeResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, owner, + &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: "README.md", + ContentReader: strings.NewReader("a readme file"), + }, + }, + Message: "Add README.md", + OldBranch: "orphan1", + NewBranch: "orphan1", + }) + assert.NoError(t, err) + assert.NotEmpty(t, changeResp) + + // Check that we only have 1 message on the main repo, the orphaned + // one is not shown. + req := NewRequest(t, "GET", "/user1/repo1") + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + htmlDoc.AssertElement(t, ".ui.message", true) + link, _ := htmlDoc.Find(".ui.message a[href*='/src/branch/']").Attr("href") + assert.Equal(t, "/user1/repo1/src/branch/recent-push", link) + }) }) } From b263ac67e08fdd315f8bbb8de9eff81d85a579c1 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Mon, 22 Jan 2024 15:06:50 +0000 Subject: [PATCH 83/86] Revert "Fix schedule not trigger bug because matching full ref name with short ref name (#28874)" This reverts commit 23efd9d2781c2ac22594a83afa75182d276b1571. --- services/actions/notifier_helper.go | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 2419301384..63ae9c3aba 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -159,28 +159,24 @@ func notify(ctx context.Context, input *notifyInput) error { workflows, schedules, err := actions_module.DetectWorkflows(gitRepo, commit, input.Event, input.Payload, - input.Event == webhook_module.HookEventPush && git.RefName(input.Ref).BranchName() == input.Repo.DefaultBranch, + input.Event == webhook_module.HookEventPush && input.Ref == input.Repo.DefaultBranch, ) if err != nil { return fmt.Errorf("DetectWorkflows: %w", err) } - log.Trace("repo %s with commit %s event %s find %d workflows and %d schedules", - input.Repo.RepoPath(), - commit.ID, - input.Event, - len(workflows), - len(schedules), - ) + if len(workflows) == 0 { + log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RepoPath(), commit.ID) + } else { + for _, wf := range workflows { + if actionsConfig.IsWorkflowDisabled(wf.EntryName) { + log.Trace("repo %s has disable workflows %s", input.Repo.RepoPath(), wf.EntryName) + continue + } - for _, wf := range workflows { - if actionsConfig.IsWorkflowDisabled(wf.EntryName) { - log.Trace("repo %s has disable workflows %s", input.Repo.RepoPath(), wf.EntryName) - continue - } - - if wf.TriggerEvent.Name != actions_module.GithubEventPullRequestTarget { - detectedWorkflows = append(detectedWorkflows, wf) + if wf.TriggerEvent.Name != actions_module.GithubEventPullRequestTarget { + detectedWorkflows = append(detectedWorkflows, wf) + } } } From 83e5eba0311dc601518fb1a07a7e8538e573a837 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Mon, 22 Jan 2024 15:07:17 +0000 Subject: [PATCH 84/86] Revert "Fix schedule tasks bugs (#28691)" This reverts commit 97292da96048b036cbe36b3ea66503ac568a73e7. --- models/actions/run.go | 11 ++-- models/actions/run_list.go | 5 -- models/actions/schedule.go | 20 ------- models/git/branch.go | 4 +- models/git/branch_test.go | 3 +- models/repo/repo_unit.go | 26 +++++++++ modules/actions/github.go | 4 -- modules/actions/workflows.go | 26 ++++----- modules/actions/workflows_test.go | 7 --- modules/webhook/type.go | 1 - routers/api/v1/repo/repo.go | 2 +- routers/web/repo/setting/default_branch.go | 28 ++++++--- routers/web/repo/setting/setting.go | 2 +- services/actions/notifier_helper.go | 35 +++++++----- services/actions/schedule_tasks.go | 2 - services/repository/branch.go | 66 +--------------------- services/repository/setting.go | 47 --------------- services/wiki/wiki.go | 3 +- tests/integration/actions_trigger_test.go | 4 +- 19 files changed, 90 insertions(+), 206 deletions(-) delete mode 100644 services/repository/setting.go diff --git a/models/actions/run.go b/models/actions/run.go index 5d4e3b74dd..9c7f049bbc 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -171,14 +171,13 @@ func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) err } // CancelRunningJobs cancels all running and waiting jobs associated with a specific workflow. -func CancelRunningJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error { +func CancelRunningJobs(ctx context.Context, repoID int64, ref, workflowID string) error { // Find all runs in the specified repository, reference, and workflow with statuses 'Running' or 'Waiting'. runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{ - RepoID: repoID, - Ref: ref, - WorkflowID: workflowID, - TriggerEvent: event, - Status: []Status{StatusRunning, StatusWaiting}, + RepoID: repoID, + Ref: ref, + WorkflowID: workflowID, + Status: []Status{StatusRunning, StatusWaiting}, }) if err != nil { return err diff --git a/models/actions/run_list.go b/models/actions/run_list.go index 388bfc4f86..375c46221b 100644 --- a/models/actions/run_list.go +++ b/models/actions/run_list.go @@ -10,7 +10,6 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" - webhook_module "code.gitea.io/gitea/modules/webhook" "xorm.io/builder" ) @@ -72,7 +71,6 @@ type FindRunOptions struct { WorkflowID string Ref string // the commit/tag/… that caused this workflow TriggerUserID int64 - TriggerEvent webhook_module.HookEventType Approved bool // not util.OptionalBool, it works only when it's true Status []Status } @@ -100,9 +98,6 @@ func (opts FindRunOptions) ToConds() builder.Cond { if opts.Ref != "" { cond = cond.And(builder.Eq{"ref": opts.Ref}) } - if opts.TriggerEvent != "" { - cond = cond.And(builder.Eq{"trigger_event": opts.TriggerEvent}) - } return cond } diff --git a/models/actions/schedule.go b/models/actions/schedule.go index d450e7aa07..34d23f1c01 100644 --- a/models/actions/schedule.go +++ b/models/actions/schedule.go @@ -5,7 +5,6 @@ package actions import ( "context" - "fmt" "time" "code.gitea.io/gitea/models/db" @@ -119,22 +118,3 @@ func DeleteScheduleTaskByRepo(ctx context.Context, id int64) error { return committer.Commit() } - -func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) error { - // If actions disabled when there is schedule task, this will remove the outdated schedule tasks - // There is no other place we can do this because the app.ini will be changed manually - if err := DeleteScheduleTaskByRepo(ctx, repo.ID); err != nil { - return fmt.Errorf("DeleteCronTaskByRepo: %v", err) - } - // cancel running cron jobs of this repository and delete old schedules - if err := CancelRunningJobs( - ctx, - repo.ID, - repo.DefaultBranch, - "", - webhook_module.HookEventSchedule, - ); err != nil { - return fmt.Errorf("CancelRunningJobs: %v", err) - } - return nil -} diff --git a/models/git/branch.go b/models/git/branch.go index a96d7ff8cb..6baad65ab4 100644 --- a/models/git/branch.go +++ b/models/git/branch.go @@ -287,7 +287,7 @@ func FindRenamedBranch(ctx context.Context, repoID int64, from string) (branch * } // RenameBranch rename a branch -func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to string, gitAction func(ctx context.Context, isDefault bool) error) (err error) { +func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to string, gitAction func(isDefault bool) error) (err error) { ctx, committer, err := db.TxContext(ctx) if err != nil { return err @@ -362,7 +362,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str } // 5. do git action - if err = gitAction(ctx, isDefault); err != nil { + if err = gitAction(isDefault); err != nil { return err } diff --git a/models/git/branch_test.go b/models/git/branch_test.go index fd5d6519e9..d480e2ec30 100644 --- a/models/git/branch_test.go +++ b/models/git/branch_test.go @@ -4,7 +4,6 @@ package git_test import ( - "context" "testing" "code.gitea.io/gitea/models/db" @@ -133,7 +132,7 @@ func TestRenameBranch(t *testing.T) { }, git_model.WhitelistOptions{})) assert.NoError(t, committer.Commit()) - assert.NoError(t, git_model.RenameBranch(db.DefaultContext, repo1, "master", "main", func(ctx context.Context, isDefault bool) error { + assert.NoError(t, git_model.RenameBranch(db.DefaultContext, repo1, "master", "main", func(isDefault bool) error { _isDefault = isDefault return nil })) diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index b55d3e5de5..3df5236ea7 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -314,3 +314,29 @@ func UpdateRepoUnit(ctx context.Context, unit *RepoUnit) error { _, err := db.GetEngine(ctx).ID(unit.ID).Update(unit) return err } + +// UpdateRepositoryUnits updates a repository's units +func UpdateRepositoryUnits(ctx context.Context, repo *Repository, units []RepoUnit, deleteUnitTypes []unit.Type) (err error) { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + + // Delete existing settings of units before adding again + for _, u := range units { + deleteUnitTypes = append(deleteUnitTypes, u.Type) + } + + if _, err = db.GetEngine(ctx).Where("repo_id = ?", repo.ID).In("type", deleteUnitTypes).Delete(new(RepoUnit)); err != nil { + return err + } + + if len(units) > 0 { + if err = db.Insert(ctx, units); err != nil { + return err + } + } + + return committer.Commit() +} diff --git a/modules/actions/github.go b/modules/actions/github.go index fafea4e11a..71f81a8903 100644 --- a/modules/actions/github.go +++ b/modules/actions/github.go @@ -22,7 +22,6 @@ const ( GithubEventRelease = "release" GithubEventPullRequestComment = "pull_request_comment" GithubEventGollum = "gollum" - GithubEventSchedule = "schedule" ) // canGithubEventMatch check if the input Github event can match any Gitea event. @@ -70,9 +69,6 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent return false } - case GithubEventSchedule: - return triggedEvent == webhook_module.HookEventSchedule - default: return eventName == string(triggedEvent) } diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 8a44e9dbe2..c49cf3193a 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -22,7 +22,7 @@ import ( type DetectedWorkflow struct { EntryName string - TriggerEvent *jobparser.Event + TriggerEvent string Content []byte } @@ -103,7 +103,6 @@ func DetectWorkflows( commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, - detectSchedule bool, ) ([]*DetectedWorkflow, []*DetectedWorkflow, error) { entries, err := ListWorkflows(commit) if err != nil { @@ -118,7 +117,6 @@ func DetectWorkflows( return nil, nil, err } - // one workflow may have multiple events events, err := GetEventsFromContent(content) if err != nil { log.Warn("ignore invalid workflow %q: %v", entry.Name(), err) @@ -127,18 +125,17 @@ func DetectWorkflows( for _, evt := range events { log.Trace("detect workflow %q for event %#v matching %q", entry.Name(), evt, triggedEvent) if evt.IsSchedule() { - if detectSchedule { - dwf := &DetectedWorkflow{ - EntryName: entry.Name(), - TriggerEvent: evt, - Content: content, - } - schedules = append(schedules, dwf) - } - } else if detectMatched(gitRepo, commit, triggedEvent, payload, evt) { dwf := &DetectedWorkflow{ EntryName: entry.Name(), - TriggerEvent: evt, + TriggerEvent: evt.Name, + Content: content, + } + schedules = append(schedules, dwf) + } + if detectMatched(gitRepo, commit, triggedEvent, payload, evt) { + dwf := &DetectedWorkflow{ + EntryName: entry.Name(), + TriggerEvent: evt.Name, Content: content, } workflows = append(workflows, dwf) @@ -159,8 +156,7 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web webhook_module.HookEventCreate, webhook_module.HookEventDelete, webhook_module.HookEventFork, - webhook_module.HookEventWiki, - webhook_module.HookEventSchedule: + webhook_module.HookEventWiki: if len(evt.Acts()) != 0 { log.Warn("Ignore unsupported %s event arguments %v", triggedEvent, evt.Acts()) } diff --git a/modules/actions/workflows_test.go b/modules/actions/workflows_test.go index c8e1e553fe..2d57f19488 100644 --- a/modules/actions/workflows_test.go +++ b/modules/actions/workflows_test.go @@ -118,13 +118,6 @@ func TestDetectMatched(t *testing.T) { yamlOn: "on: gollum", expected: true, }, - { - desc: "HookEventSchedue(schedule) matches GithubEventSchedule(schedule)", - triggedEvent: webhook_module.HookEventSchedule, - payload: nil, - yamlOn: "on: schedule", - expected: true, - }, } for _, tc := range testCases { diff --git a/modules/webhook/type.go b/modules/webhook/type.go index 0013691c02..7042d391b7 100644 --- a/modules/webhook/type.go +++ b/modules/webhook/type.go @@ -31,7 +31,6 @@ const ( HookEventRepository HookEventType = "repository" HookEventRelease HookEventType = "release" HookEventPackage HookEventType = "package" - HookEventSchedule HookEventType = "schedule" ) // Event returns the HookEventType as an event string diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 9810e461de..2dd61eca0e 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -983,7 +983,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { } if len(units)+len(deleteUnitTypes) > 0 { - if err := repo_service.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil { + if err := repo_model.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil { ctx.Error(http.StatusInternalServerError, "UpdateRepositoryUnits", err) return err } diff --git a/routers/web/repo/setting/default_branch.go b/routers/web/repo/setting/default_branch.go index c8a576e576..9bf54e706a 100644 --- a/routers/web/repo/setting/default_branch.go +++ b/routers/web/repo/setting/default_branch.go @@ -6,12 +6,13 @@ package setting import ( "net/http" - git_model "code.gitea.io/gitea/models/git" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/routers/web/repo" - repo_service "code.gitea.io/gitea/services/repository" + notify_service "code.gitea.io/gitea/services/notify" ) // SetDefaultBranchPost set default branch @@ -34,14 +35,23 @@ func SetDefaultBranchPost(ctx *context.Context) { } branch := ctx.FormString("branch") - if err := repo_service.SetRepoDefaultBranch(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, branch); err != nil { - switch { - case git_model.IsErrBranchNotExist(err): - ctx.Status(http.StatusNotFound) - default: - ctx.ServerError("SetDefaultBranch", err) - } + if !ctx.Repo.GitRepo.IsBranchExist(branch) { + ctx.Status(http.StatusNotFound) return + } else if repo.DefaultBranch != branch { + repo.DefaultBranch = branch + if err := ctx.Repo.GitRepo.SetDefaultBranch(branch); err != nil { + if !git.IsErrUnsupportedVersion(err) { + ctx.ServerError("SetDefaultBranch", err) + return + } + } + if err := repo_model.UpdateDefaultBranch(ctx, repo); err != nil { + ctx.ServerError("SetDefaultBranch", err) + return + } + + notify_service.ChangeDefaultBranch(ctx, repo) } log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 0334add2ea..6d294d1f26 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -601,7 +601,7 @@ func SettingsPost(ctx *context.Context) { return } - if err := repo_service.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil { + if err := repo_model.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil { ctx.ServerError("UpdateRepositoryUnits", err) return } diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 63ae9c3aba..035e5e6c25 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -117,9 +117,6 @@ func notify(ctx context.Context, input *notifyInput) error { return nil } if unit_model.TypeActions.UnitGlobalDisabled() { - if err := actions_model.CleanRepoScheduleTasks(ctx, input.Repo); err != nil { - log.Error("CleanRepoScheduleTasks: %v", err) - } return nil } if err := input.Repo.LoadUnits(ctx); err != nil { @@ -156,11 +153,7 @@ func notify(ctx context.Context, input *notifyInput) error { var detectedWorkflows []*actions_module.DetectedWorkflow actionsConfig := input.Repo.MustGetUnit(ctx, unit_model.TypeActions).ActionsConfig() - workflows, schedules, err := actions_module.DetectWorkflows(gitRepo, commit, - input.Event, - input.Payload, - input.Event == webhook_module.HookEventPush && input.Ref == input.Repo.DefaultBranch, - ) + workflows, schedules, err := actions_module.DetectWorkflows(gitRepo, commit, input.Event, input.Payload) if err != nil { return fmt.Errorf("DetectWorkflows: %w", err) } @@ -174,7 +167,7 @@ func notify(ctx context.Context, input *notifyInput) error { continue } - if wf.TriggerEvent.Name != actions_module.GithubEventPullRequestTarget { + if wf.TriggerEvent != actions_module.GithubEventPullRequestTarget { detectedWorkflows = append(detectedWorkflows, wf) } } @@ -187,7 +180,7 @@ func notify(ctx context.Context, input *notifyInput) error { if err != nil { return fmt.Errorf("gitRepo.GetCommit: %w", err) } - baseWorkflows, _, err := actions_module.DetectWorkflows(gitRepo, baseCommit, input.Event, input.Payload, false) + baseWorkflows, _, err := actions_module.DetectWorkflows(gitRepo, baseCommit, input.Event, input.Payload) if err != nil { return fmt.Errorf("DetectWorkflows: %w", err) } @@ -195,7 +188,7 @@ func notify(ctx context.Context, input *notifyInput) error { log.Trace("repo %s with commit %s couldn't find pull_request_target workflows", input.Repo.RepoPath(), baseCommit.ID) } else { for _, wf := range baseWorkflows { - if wf.TriggerEvent.Name == actions_module.GithubEventPullRequestTarget { + if wf.TriggerEvent == actions_module.GithubEventPullRequestTarget { detectedWorkflows = append(detectedWorkflows, wf) } } @@ -272,7 +265,7 @@ func handleWorkflows( IsForkPullRequest: isForkPullRequest, Event: input.Event, EventPayload: string(p), - TriggerEvent: dwf.TriggerEvent.Name, + TriggerEvent: dwf.TriggerEvent, Status: actions_model.StatusWaiting, } if need, err := ifNeedApproval(ctx, run, input.Repo, input.Doer); err != nil { @@ -296,7 +289,6 @@ func handleWorkflows( run.RepoID, run.Ref, run.WorkflowID, - run.Event, ); err != nil { log.Error("CancelRunningJobs: %v", err) } @@ -422,8 +414,8 @@ func handleSchedules( log.Error("CountSchedules: %v", err) return err } else if count > 0 { - if err := actions_model.CleanRepoScheduleTasks(ctx, input.Repo); err != nil { - log.Error("CleanRepoScheduleTasks: %v", err) + if err := actions_model.DeleteScheduleTaskByRepo(ctx, input.Repo.ID); err != nil { + log.Error("DeleteCronTaskByRepo: %v", err) } } @@ -468,6 +460,19 @@ func handleSchedules( Specs: schedules, Content: dwf.Content, } + + // cancel running jobs if the event is push + if run.Event == webhook_module.HookEventPush { + // cancel running jobs of the same workflow + if err := actions_model.CancelRunningJobs( + ctx, + run.RepoID, + run.Ref, + run.WorkflowID, + ); err != nil { + log.Error("CancelRunningJobs: %v", err) + } + } crons = append(crons, run) } diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go index e7aa4a39ac..8eef2b67bd 100644 --- a/services/actions/schedule_tasks.go +++ b/services/actions/schedule_tasks.go @@ -59,7 +59,6 @@ func startTasks(ctx context.Context) error { row.RepoID, row.Schedule.Ref, row.Schedule.WorkflowID, - webhook_module.HookEventSchedule, ); err != nil { log.Error("CancelRunningJobs: %v", err) } @@ -114,7 +113,6 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule) CommitSHA: cron.CommitSHA, Event: cron.Event, EventPayload: cron.EventPayload, - TriggerEvent: string(webhook_module.HookEventSchedule), ScheduleID: cron.ID, Status: actions_model.StatusWaiting, } diff --git a/services/repository/branch.go b/services/repository/branch.go index 7e3246ee41..7cd2c34d25 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -10,7 +10,6 @@ import ( "strings" "code.gitea.io/gitea/models" - actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" @@ -23,7 +22,6 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" - webhook_module "code.gitea.io/gitea/modules/webhook" notify_service "code.gitea.io/gitea/services/notify" files_service "code.gitea.io/gitea/services/repository/files" @@ -315,28 +313,13 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m return "from_not_exist", nil } - if err := git_model.RenameBranch(ctx, repo, from, to, func(ctx context.Context, isDefault bool) error { + if err := git_model.RenameBranch(ctx, repo, from, to, func(isDefault bool) error { err2 := gitRepo.RenameBranch(from, to) if err2 != nil { return err2 } if isDefault { - // if default branch changed, we need to delete all schedules and cron jobs - if err := actions_model.DeleteScheduleTaskByRepo(ctx, repo.ID); err != nil { - log.Error("DeleteCronTaskByRepo: %v", err) - } - // cancel running cron jobs of this repository and delete old schedules - if err := actions_model.CancelRunningJobs( - ctx, - repo.ID, - from, - "", - webhook_module.HookEventSchedule, - ); err != nil { - log.Error("CancelRunningJobs: %v", err) - } - err2 = gitRepo.SetDefaultBranch(to) if err2 != nil { return err2 @@ -472,50 +455,3 @@ func AddAllRepoBranchesToSyncQueue(ctx context.Context, doerID int64) error { } return nil } - -func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, newBranchName string) error { - if repo.DefaultBranch == newBranchName { - return nil - } - - if !gitRepo.IsBranchExist(newBranchName) { - return git_model.ErrBranchNotExist{ - BranchName: newBranchName, - } - } - - oldDefaultBranchName := repo.DefaultBranch - repo.DefaultBranch = newBranchName - if err := db.WithTx(ctx, func(ctx context.Context) error { - if err := repo_model.UpdateDefaultBranch(ctx, repo); err != nil { - return err - } - - if err := actions_model.DeleteScheduleTaskByRepo(ctx, repo.ID); err != nil { - log.Error("DeleteCronTaskByRepo: %v", err) - } - // cancel running cron jobs of this repository and delete old schedules - if err := actions_model.CancelRunningJobs( - ctx, - repo.ID, - oldDefaultBranchName, - "", - webhook_module.HookEventSchedule, - ); err != nil { - log.Error("CancelRunningJobs: %v", err) - } - - if err := gitRepo.SetDefaultBranch(newBranchName); err != nil { - if !git.IsErrUnsupportedVersion(err) { - return err - } - } - return nil - }); err != nil { - return err - } - - notify_service.ChangeDefaultBranch(ctx, repo) - - return nil -} diff --git a/services/repository/setting.go b/services/repository/setting.go deleted file mode 100644 index 6496ac4014..0000000000 --- a/services/repository/setting.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package repository - -import ( - "context" - "slices" - - actions_model "code.gitea.io/gitea/models/actions" - "code.gitea.io/gitea/models/db" - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/log" -) - -// UpdateRepositoryUnits updates a repository's units -func UpdateRepositoryUnits(ctx context.Context, repo *repo_model.Repository, units []repo_model.RepoUnit, deleteUnitTypes []unit.Type) (err error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - // Delete existing settings of units before adding again - for _, u := range units { - deleteUnitTypes = append(deleteUnitTypes, u.Type) - } - - if slices.Contains(deleteUnitTypes, unit.TypeActions) { - if err := actions_model.CleanRepoScheduleTasks(ctx, repo); err != nil { - log.Error("CleanRepoScheduleTasks: %v", err) - } - } - - if _, err = db.GetEngine(ctx).Where("repo_id = ?", repo.ID).In("type", deleteUnitTypes).Delete(new(repo_model.RepoUnit)); err != nil { - return err - } - - if len(units) > 0 { - if err = db.Insert(ctx, units); err != nil { - return err - } - } - - return committer.Commit() -} diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index ce54a00da7..f98854c8dd 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -19,7 +19,6 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/sync" asymkey_service "code.gitea.io/gitea/services/asymkey" - repo_service "code.gitea.io/gitea/services/repository" ) // TODO: use clustered lock (unique queue? or *abuse* cache) @@ -351,7 +350,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model // DeleteWiki removes the actual and local copy of repository wiki. func DeleteWiki(ctx context.Context, repo *repo_model.Repository) error { - if err := repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit.Type{unit.TypeWiki}); err != nil { + if err := repo_model.UpdateRepositoryUnits(ctx, repo, nil, []unit.Type{unit.TypeWiki}); err != nil { return err } diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index 7744f33e57..684b93ed1d 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -47,7 +47,7 @@ func TestPullRequestTargetEvent(t *testing.T) { assert.NotEmpty(t, baseRepo) // enable actions - err = repo_service.UpdateRepositoryUnits(db.DefaultContext, baseRepo, []repo_model.RepoUnit{{ + err = repo_model.UpdateRepositoryUnits(db.DefaultContext, baseRepo, []repo_model.RepoUnit{{ RepoID: baseRepo.ID, Type: unit_model.TypeActions, }}, nil) @@ -216,7 +216,7 @@ func TestSkipCI(t *testing.T) { assert.NotEmpty(t, repo) // enable actions - err = repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, []repo_model.RepoUnit{{ + err = repo_model.UpdateRepositoryUnits(db.DefaultContext, repo, []repo_model.RepoUnit{{ RepoID: repo.ID, Type: unit_model.TypeActions, }}, nil) From 86f4d1871e5605c1592cb83b178f8ebe69ccdfa8 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Thu, 18 Jan 2024 16:40:33 +0000 Subject: [PATCH 85/86] Revert "Fix schedule tasks bugs (#28691)" (part 2) This function is now being used elsewhere and cannot be reverted. Only the part that was modified in addition to being moved is deleted. (cherry picked from commit 72954836a492f552ccc03250ba560951eedc199d) --- .deadcode-out | 1 + services/repository/setting.go | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 services/repository/setting.go diff --git a/.deadcode-out b/.deadcode-out index a64c5b936f..92937ab0ed 100644 --- a/.deadcode-out +++ b/.deadcode-out @@ -324,6 +324,7 @@ package "code.gitea.io/gitea/services/pull" package "code.gitea.io/gitea/services/repository" func GetBranchCommitID func IsErrForkAlreadyExist + func UpdateRepositoryUnits package "code.gitea.io/gitea/services/repository/archiver" func ArchiveRepository diff --git a/services/repository/setting.go b/services/repository/setting.go new file mode 100644 index 0000000000..7dded5d6be --- /dev/null +++ b/services/repository/setting.go @@ -0,0 +1,38 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "context" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" +) + +// UpdateRepositoryUnits updates a repository's units +func UpdateRepositoryUnits(ctx context.Context, repo *repo_model.Repository, units []repo_model.RepoUnit, deleteUnitTypes []unit.Type) (err error) { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + + // Delete existing settings of units before adding again + for _, u := range units { + deleteUnitTypes = append(deleteUnitTypes, u.Type) + } + + if _, err = db.GetEngine(ctx).Where("repo_id = ?", repo.ID).In("type", deleteUnitTypes).Delete(new(repo_model.RepoUnit)); err != nil { + return err + } + + if len(units) > 0 { + if err = db.Insert(ctx, units); err != nil { + return err + } + } + + return committer.Commit() +} From aca2ae23907ded7b959362d033e039c4caa71478 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Sat, 23 Dec 2023 12:14:02 +0100 Subject: [PATCH 86/86] [ACTIONS] on.schedule: the event is always "schedule" handleSchedules() is called every time an event is received and will check the content of the main branch to (re)create scheduled events. There is no reason why intput.Event will be relevant when the schedule workflow runs. (cherry picked from commit 9a712bb276f2103cd7bccc4bb07b6cc669537e38) (cherry picked from commit 41af36da818eb1f4ceb18c0447f2b6e099d4e04c) (cherry picked from commit bb83604fa2e6f29d995378c3daf5037a468c0858) (cherry picked from commit 65e4503a7a875db0098d4e25611a0240104d1048) (cherry picked from commit e562b6f7a0b3da9bfea9b88107eb53bae4a225da) --- modules/actions/github.go | 4 ++++ modules/actions/workflows.go | 1 + modules/actions/workflows_test.go | 7 +++++++ modules/webhook/type.go | 1 + services/actions/notifier_helper.go | 2 +- services/actions/schedule_tasks.go | 1 + 6 files changed, 15 insertions(+), 1 deletion(-) diff --git a/modules/actions/github.go b/modules/actions/github.go index 71f81a8903..a988b2a124 100644 --- a/modules/actions/github.go +++ b/modules/actions/github.go @@ -22,6 +22,7 @@ const ( GithubEventRelease = "release" GithubEventPullRequestComment = "pull_request_comment" GithubEventGollum = "gollum" + GithubEventSchedule = "schedule" ) // canGithubEventMatch check if the input Github event can match any Gitea event. @@ -34,6 +35,9 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent case GithubEventGollum: return triggedEvent == webhook_module.HookEventWiki + case GithubEventSchedule: + return triggedEvent == webhook_module.HookEventSchedule + case GithubEventIssues: switch triggedEvent { case webhook_module.HookEventIssues, diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index c49cf3193a..00d83e06d7 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -153,6 +153,7 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web switch triggedEvent { case // events with no activity types + webhook_module.HookEventSchedule, webhook_module.HookEventCreate, webhook_module.HookEventDelete, webhook_module.HookEventFork, diff --git a/modules/actions/workflows_test.go b/modules/actions/workflows_test.go index 2d57f19488..c8e1e553fe 100644 --- a/modules/actions/workflows_test.go +++ b/modules/actions/workflows_test.go @@ -118,6 +118,13 @@ func TestDetectMatched(t *testing.T) { yamlOn: "on: gollum", expected: true, }, + { + desc: "HookEventSchedue(schedule) matches GithubEventSchedule(schedule)", + triggedEvent: webhook_module.HookEventSchedule, + payload: nil, + yamlOn: "on: schedule", + expected: true, + }, } for _, tc := range testCases { diff --git a/modules/webhook/type.go b/modules/webhook/type.go index 7042d391b7..0013691c02 100644 --- a/modules/webhook/type.go +++ b/modules/webhook/type.go @@ -31,6 +31,7 @@ const ( HookEventRepository HookEventType = "repository" HookEventRelease HookEventType = "release" HookEventPackage HookEventType = "package" + HookEventSchedule HookEventType = "schedule" ) // Event returns the HookEventType as an event string diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 035e5e6c25..66566a29a7 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -455,7 +455,7 @@ func handleSchedules( TriggerUserID: input.Doer.ID, Ref: input.Repo.DefaultBranch, CommitSHA: commit.ID.String(), - Event: input.Event, + Event: webhook_module.HookEventType(api.HookScheduleCreated), EventPayload: string(p), Specs: schedules, Content: dwf.Content, diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go index 8eef2b67bd..dfde34d994 100644 --- a/services/actions/schedule_tasks.go +++ b/services/actions/schedule_tasks.go @@ -112,6 +112,7 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule) Ref: cron.Ref, CommitSHA: cron.CommitSHA, Event: cron.Event, + TriggerEvent: string(webhook_module.HookEventSchedule), EventPayload: cron.EventPayload, ScheduleID: cron.ID, Status: actions_model.StatusWaiting,