From 2ad9ef4984f0b68ef38241fd6b557d8427d851d8 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 24 Apr 2024 09:58:24 +0800 Subject: [PATCH 01/20] Fix some bug on migrations (#30647) Fix https://github.com/go-gitea/gitea/pull/23894#discussion_r1573718690 --- models/migrations/v1_16/v210.go | 5 ----- models/migrations/v1_22/v286.go | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/models/migrations/v1_16/v210.go b/models/migrations/v1_16/v210.go index 533bb4bf80..51b7d81e99 100644 --- a/models/migrations/v1_16/v210.go +++ b/models/migrations/v1_16/v210.go @@ -43,11 +43,6 @@ func RemigrateU2FCredentials(x *xorm.Engine) error { if err != nil { return err } - case schemas.ORACLE: - _, err := x.Exec("ALTER TABLE webauthn_credential MODIFY credential_id VARCHAR(410)") - if err != nil { - return err - } case schemas.MSSQL: // This column has an index on it. I could write all of the code to attempt to change the index OR // I could just use recreate table. diff --git a/models/migrations/v1_22/v286.go b/models/migrations/v1_22/v286.go index fbbd87344f..f46d494dfe 100644 --- a/models/migrations/v1_22/v286.go +++ b/models/migrations/v1_22/v286.go @@ -53,7 +53,7 @@ func expandHashReferencesToSha256(x *xorm.Engine) error { if setting.Database.Type.IsMySQL() { _, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` MODIFY COLUMN `%s` VARCHAR(64)", alts[0], alts[1])) } else if setting.Database.Type.IsMSSQL() { - _, err = db.Exec(fmt.Sprintf("ALTER TABLE [%s] ALTER COLUMN [%s] VARCHAR(64)", alts[0], alts[1])) + _, err = db.Exec(fmt.Sprintf("ALTER TABLE [%s] ALTER COLUMN [%s] NVARCHAR(64)", alts[0], alts[1])) } else { _, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` ALTER COLUMN `%s` TYPE VARCHAR(64)", alts[0], alts[1])) } From 8b3632435ef72c351af34a154b13511b34323471 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 24 Apr 2024 13:26:50 +0800 Subject: [PATCH 02/20] Fix a panic bug when head repository deleting (#30674) When visiting a pull request files which head repository has been deleted, it will panic because headrepo is nil. --- routers/web/repo/pull.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 71f25db11b..acdba4bcdc 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -863,21 +863,21 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi if pull.HeadRepo != nil { ctx.Data["SourcePath"] = pull.HeadRepo.Link() + "/src/branch/" + util.PathEscapeSegments(pull.HeadBranch) - } - if !pull.HasMerged && ctx.Doer != nil { - perm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer) - if err != nil { - ctx.ServerError("GetUserRepoPermission", err) - return - } + if !pull.HasMerged && ctx.Doer != nil { + perm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer) + if err != nil { + ctx.ServerError("GetUserRepoPermission", err) + return + } - if perm.CanWrite(unit.TypeCode) || issues_model.CanMaintainerWriteToBranch(ctx, perm, pull.HeadBranch, ctx.Doer) { - ctx.Data["CanEditFile"] = true - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file") - ctx.Data["HeadRepoLink"] = pull.HeadRepo.Link() - ctx.Data["HeadBranchName"] = pull.HeadBranch - ctx.Data["BackToLink"] = setting.AppSubURL + ctx.Req.URL.RequestURI() + if perm.CanWrite(unit.TypeCode) || issues_model.CanMaintainerWriteToBranch(ctx, perm, pull.HeadBranch, ctx.Doer) { + ctx.Data["CanEditFile"] = true + ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file") + ctx.Data["HeadRepoLink"] = pull.HeadRepo.Link() + ctx.Data["HeadBranchName"] = pull.HeadBranch + ctx.Data["BackToLink"] = setting.AppSubURL + ctx.Req.URL.RequestURI() + } } } } From 3f19a6334575e1d2849999e8339f1b515cefaf1a Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 24 Apr 2024 15:11:52 +0200 Subject: [PATCH 03/20] Fix border-radius of header+segment boxes (#30667) This is a very old bug with the bottom border-radiuses not being there and the `:has` selector now makes it possible to cleanly solve it. It affects all header+segment boxes, which there are many throughout the UI: Screenshot 2024-04-23 at 20 47 21 --- web_src/css/modules/segment.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/web_src/css/modules/segment.css b/web_src/css/modules/segment.css index bbd39c385f..994ac1779a 100644 --- a/web_src/css/modules/segment.css +++ b/web_src/css/modules/segment.css @@ -151,6 +151,11 @@ border-top: none; } +.ui.attached.segment:has(+ .ui[class*="top attached"].header), +.ui.attached.segment:last-child { + border-radius: 0 0 0.28571429rem 0.28571429rem; +} + .ui[class*="top attached"].segment { bottom: 0; margin-bottom: 0; From a19d2bbd90dd22a897220425a1221af8ed065ba7 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 24 Apr 2024 22:11:49 +0800 Subject: [PATCH 04/20] Add test for #30674 (#30679) --- tests/integration/pull_compare_test.go | 35 ++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/integration/pull_compare_test.go b/tests/integration/pull_compare_test.go index 5ce8ea3031..b814207b2f 100644 --- a/tests/integration/pull_compare_test.go +++ b/tests/integration/pull_compare_test.go @@ -5,8 +5,14 @@ package integration import ( "net/http" + "net/url" "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" + repo_service "code.gitea.io/gitea/services/repository" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -32,4 +38,33 @@ func TestPullCompare(t *testing.T) { doc := NewHTMLParser(t, resp.Body) editButtonCount := doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length() assert.Greater(t, editButtonCount, 0, "Expected to find a button to edit a file in the PR diff view but there were none") + + onGiteaRun(t, func(t *testing.T, u *url.URL) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user1") + testRepoFork(t, session, "user2", "repo1", "user1", "repo1") + testCreateBranch(t, session, "user1", "repo1", "branch/master", "master1", http.StatusSeeOther) + testEditFile(t, session, "user1", "repo1", "master1", "README.md", "Hello, World (Edited)\n") + resp = testPullCreate(t, session, "user1", "repo1", false, "master", "master1", "This is a pull title") + + // the max value on issue_index.yml for repo_id=1 is 5 + req = NewRequest(t, "GET", "/user2/repo1/pulls/6/files") + resp = session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + editButtonCount := doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length() + assert.Greater(t, editButtonCount, 0, "Expected to find a button to edit a file in the PR diff view but there were none") + + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + repoForked := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: "repo1"}) + // delete the head repository and revisit the PR diff view + err := repo_service.DeleteRepositoryDirectly(db.DefaultContext, user2, repoForked.ID) + assert.NoError(t, err) + + req = NewRequest(t, "GET", "/user2/repo1/pulls/6/files") + resp = session.MakeRequest(t, req, http.StatusOK) + doc = NewHTMLParser(t, resp.Body) + editButtonCount = doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length() + assert.EqualValues(t, editButtonCount, 0, "Expected not to find a button to edit a file in the PR diff view because head repository has been deleted") + }) } From a63f14b90839821a480fb56fd9b45a27864b77d1 Mon Sep 17 00:00:00 2001 From: Jiaxin Zhu Date: Thu, 25 Apr 2024 08:07:38 +0800 Subject: [PATCH 05/20] Fix view of readme file in the home code page. (#30564) Gitea attempts to display image file, pdf file, etc. named readme in the home code page (but it cannot). I think only the markdown and plain-text file should be displayed, which is also the behavior of GitHub. Co-authored-by: jxshin --- templates/repo/view_list.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl index fb257bd474..7ec9acc84e 100644 --- a/templates/repo/view_list.tmpl +++ b/templates/repo/view_list.tmpl @@ -68,6 +68,6 @@ {{end}} -{{if .ReadmeExist}} +{{if and .ReadmeExist (or .IsMarkup .IsPlainText)}} {{template "repo/view_file" .}} {{end}} From 4ff54933f8093f1c1d84797d687ccb640739ec32 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Thu, 25 Apr 2024 00:26:13 +0000 Subject: [PATCH 06/20] [skip ci] Updated translations via Crowdin --- options/locale/locale_pt-PT.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 64798d6d65..4b7604a5cb 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -763,6 +763,8 @@ manage_themes=Escolher o tema padrão manage_openid=Gerir endereços OpenID email_desc=O seu endereço de email principal irá ser usado para notificações, recuperação de senha e, desde que não esteja oculto, operações Git baseados na web. theme_desc=Este será o seu tema padrão em todo o sítio. +theme_colorblindness_help=Suporte a temas para daltónicos +theme_colorblindness_prompt=O Gitea acabou de obter alguns temas com suporte básico para daltónicos que têm apenas algumas cores definidas. O trabalho ainda está em andamento. Poderiam ser feitos mais melhoramentos se fossem definidas mais cores nos ficheiros CSS do tema. primary=Principal activated=Em uso requires_activation=Tem que ser habilitado From c685eefe4a8ff6e32bc859a3457a5090f58ea8c5 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 25 Apr 2024 17:14:23 +0800 Subject: [PATCH 07/20] If a repository return no commitstatus, then still cache it but not query it from database (#30700) The previous repository default branch commit status cache will only store if the commit status has value. So the repository which have no any commit status will always be fetched from database. This PR will store the empty state of commit status of a repository into cache because the cache will be updated once there is a commit status stored. --- .../repository/commitstatus/commitstatus.go | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/services/repository/commitstatus/commitstatus.go b/services/repository/commitstatus/commitstatus.go index 8a62a603d4..444ae04d0c 100644 --- a/services/repository/commitstatus/commitstatus.go +++ b/services/repository/commitstatus/commitstatus.go @@ -38,12 +38,10 @@ func getCommitStatusCache(repoID int64, branchName string) *commitStatusCacheVal if ok && statusStr != "" { var cv commitStatusCacheValue err := json.Unmarshal([]byte(statusStr), &cv) - if err == nil && cv.State != "" { + if err == nil { return &cv } - if err != nil { - log.Warn("getCommitStatusCache: json.Unmarshal failed: %v", err) - } + log.Warn("getCommitStatusCache: json.Unmarshal failed: %v", err) } return nil } @@ -128,15 +126,22 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato // FindReposLastestCommitStatuses loading repository default branch latest combinded commit status with cache func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Repository) ([]*git_model.CommitStatus, error) { results := make([]*git_model.CommitStatus, len(repos)) + allCached := true for i, repo := range repos { if cv := getCommitStatusCache(repo.ID, repo.DefaultBranch); cv != nil { results[i] = &git_model.CommitStatus{ State: api.CommitStatusState(cv.State), TargetURL: cv.TargetURL, } + } else { + allCached = false } } + if allCached { + return results, nil + } + // collect the latest commit of each repo // at most there are dozens of repos (limited by MaxResponseItems), so it's not a big problem at the moment repoBranchNames := make(map[int64]string, len(repos)) @@ -165,10 +170,10 @@ func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Rep for i, repo := range repos { if repo.ID == summary.RepoID { results[i] = summary - _ = slices.DeleteFunc(repoSHAs, func(repoSHA git_model.RepoSHA) bool { + repoSHAs = slices.DeleteFunc(repoSHAs, func(repoSHA git_model.RepoSHA) bool { return repoSHA.RepoID == repo.ID }) - if results[i].State != "" { + if results[i] != nil { if err := updateCommitStatusCache(repo.ID, repo.DefaultBranch, results[i].State, results[i].TargetURL); err != nil { log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err) } @@ -177,6 +182,9 @@ func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Rep } } } + if len(repoSHAs) == 0 { + return results, nil + } // call the database O(1) times to get the commit statuses for all repos repoToItsLatestCommitStatuses, err := git_model.GetLatestCommitStatusForPairs(ctx, repoSHAs) @@ -187,7 +195,7 @@ func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Rep for i, repo := range repos { if results[i] == nil { results[i] = git_model.CalcCommitStatus(repoToItsLatestCommitStatuses[repo.ID]) - if results[i].State != "" { + if results[i] != nil { if err := updateCommitStatusCache(repo.ID, repo.DefaultBranch, results[i].State, results[i].TargetURL); err != nil { log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err) } From d0bfc978de802683b9a44720b7f5a8a8394d38be Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 25 Apr 2024 12:53:39 +0200 Subject: [PATCH 08/20] Fix active item in tab menu (#30690) Before, item would also resize on hover because of font weight: Screenshot 2024-04-25 at 01 28 53 After: Screenshot 2024-04-25 at 01 28 40 Co-authored-by: Giteabot --- web_src/css/modules/menu.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/css/modules/menu.css b/web_src/css/modules/menu.css index a392ffb5e9..e393ec5186 100644 --- a/web_src/css/modules/menu.css +++ b/web_src/css/modules/menu.css @@ -403,7 +403,7 @@ background: var(--color-body); border-top-width: 1px; border-color: var(--color-secondary); - font-weight: var(--font-weight-medium); + color: var(--color-text-dark); margin-bottom: -1px; border-radius: 0.28571429rem 0.28571429rem 0 0 !important; } From bffbbf547063fa170cc52ae2e757d5badb336632 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 25 Apr 2024 19:22:32 +0800 Subject: [PATCH 09/20] Improve oauth2 client "preferred username field" logic and the error handling (#30622) Follow #30454 And fix #24957 When using "preferred_username", if no such field, `extractUserNameFromOAuth2` (old `getUserName`) shouldn't return an error. All other USERNAME options do not return such error. And fine tune some logic and error messages, make code more stable and more friendly to end users. --- custom/conf/app.example.ini | 4 +- .../config-cheat-sheet.en-us.md | 2 +- models/unittest/testdb.go | 5 +++ models/user/user.go | 18 ++++----- models/user/user_test.go | 7 ++-- modules/session/mock.go | 26 ++++++++++++ modules/session/store.go | 23 ++++++++++- modules/setting/oauth2.go | 14 +++---- options/locale/locale_en-US.ini | 1 + routers/web/auth/auth.go | 14 +++---- routers/web/auth/auth_test.go | 40 +++++++++++++++++++ routers/web/auth/linkaccount.go | 20 ++++++---- routers/web/auth/oauth.go | 37 ++++++++++------- services/context/context.go | 5 +-- services/contexttest/context_tests.go | 14 +++++-- templates/user/auth/link_account.tmpl | 11 ++--- 16 files changed, 173 insertions(+), 68 deletions(-) create mode 100644 modules/session/mock.go diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 12588c1387..62db26fb02 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1558,8 +1558,8 @@ LEVEL = Info ;; email = use the username part of the email attribute ;; Note: `nickname`, `preferred_username` and `email` options will normalize input strings using the following criteria: ;; - diacritics are removed -;; - the characters in the set `['´\x60]` are removed -;; - the characters in the set `[\s~+]` are replaced with `-` +;; - the characters in the set ['´`] are removed +;; - the characters in the set [\s~+] are replaced with "-" ;USERNAME = nickname ;; ;; Update avatar if available from oauth2 provider. diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md index b295ddf53a..14f562fc21 100644 --- a/docs/content/administration/config-cheat-sheet.en-us.md +++ b/docs/content/administration/config-cheat-sheet.en-us.md @@ -612,7 +612,7 @@ And the following unique queues: - `email` - use the username part of the email attribute - Note: `nickname`, `preferred_username` and `email` options will normalize input strings using the following criteria: - diacritics are removed - - the characters in the set `['´\x60]` are removed + - the characters in the set ```['´`]``` are removed - the characters in the set `[\s~+]` are replaced with `-` - `UPDATE_AVATAR`: **false**: Update avatar if available from oauth2 provider. Update will be performed on each login. - `ACCOUNT_LINKING`: **login**: How to handle if an account / email already exists: diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index cb90c12f2b..51de18fa9b 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/modules/auth/password/hash" "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting/config" @@ -106,6 +107,7 @@ func MainTest(m *testing.M, testOpts ...*TestOptions) { fatalTestError("Error creating test engine: %v\n", err) } + setting.IsInTesting = true setting.AppURL = "https://try.gitea.io/" setting.RunUser = "runuser" setting.SSH.User = "sshuser" @@ -148,6 +150,9 @@ func MainTest(m *testing.M, testOpts ...*TestOptions) { config.SetDynGetter(system.NewDatabaseDynKeyGetter()) + if err = cache.Init(); err != nil { + fatalTestError("cache.Init: %v\n", err) + } if err = storage.Init(); err != nil { fatalTestError("storage.Init: %v\n", err) } diff --git a/models/user/user.go b/models/user/user.go index 7056aecab0..a5a5b5bdf6 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -501,19 +501,19 @@ func GetUserSalt() (string, error) { // Note: The set of characters here can safely expand without a breaking change, // but characters removed from this set can cause user account linking to break var ( - customCharsReplacement = strings.NewReplacer("Æ", "AE") - removeCharsRE = regexp.MustCompile(`['´\x60]`) - removeDiacriticsTransform = transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC) - replaceCharsHyphenRE = regexp.MustCompile(`[\s~+]`) + customCharsReplacement = strings.NewReplacer("Æ", "AE") + removeCharsRE = regexp.MustCompile("['`´]") + transformDiacritics = transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC) + replaceCharsHyphenRE = regexp.MustCompile(`[\s~+]`) ) -// normalizeUserName returns a string with single-quotes and diacritics -// removed, and any other non-supported username characters replaced with -// a `-` character +// NormalizeUserName only takes the name part if it is an email address, transforms it diacritics to ASCII characters. +// It returns a string with the single-quotes removed, and any other non-supported username characters are replaced with a `-` character func NormalizeUserName(s string) (string, error) { - strDiacriticsRemoved, n, err := transform.String(removeDiacriticsTransform, customCharsReplacement.Replace(s)) + s, _, _ = strings.Cut(s, "@") + strDiacriticsRemoved, n, err := transform.String(transformDiacritics, customCharsReplacement.Replace(s)) if err != nil { - return "", fmt.Errorf("Failed to normalize character `%v` in provided username `%v`", s[n], s) + return "", fmt.Errorf("failed to normalize the string of provided username %q at position %d", s, n) } return replaceCharsHyphenRE.ReplaceAllLiteralString(removeCharsRE.ReplaceAllLiteralString(strDiacriticsRemoved, ""), "-"), nil } diff --git a/models/user/user_test.go b/models/user/user_test.go index a4550fa655..b4ffa1f322 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -506,15 +506,16 @@ func Test_NormalizeUserFromEmail(t *testing.T) { Expected string IsNormalizedValid bool }{ - {"test", "test", true}, + {"name@example.com", "name", true}, + {"test'`´name", "testname", true}, {"Sinéad.O'Connor", "Sinead.OConnor", true}, {"Æsir", "AEsir", true}, - // \u00e9\u0065\u0301 - {"éé", "ee", true}, + {"éé", "ee", true}, // \u00e9\u0065\u0301 {"Awareness Hub", "Awareness-Hub", true}, {"double__underscore", "double__underscore", false}, // We should consider squashing double non-alpha characters {".bad.", ".bad.", false}, {"new😀user", "new😀user", false}, // No plans to support + {`"quoted"`, `"quoted"`, false}, // No plans to support } for _, testCase := range testCases { normalizedName, err := user_model.NormalizeUserName(testCase.Input) diff --git a/modules/session/mock.go b/modules/session/mock.go new file mode 100644 index 0000000000..95231a3655 --- /dev/null +++ b/modules/session/mock.go @@ -0,0 +1,26 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package session + +import ( + "net/http" + + "gitea.com/go-chi/session" +) + +type MockStore struct { + *session.MemStore +} + +func (m *MockStore) Destroy(writer http.ResponseWriter, request *http.Request) error { + return nil +} + +type mockStoreContextKeyStruct struct{} + +var MockStoreContextKey = mockStoreContextKeyStruct{} + +func NewMockStore(sid string) *MockStore { + return &MockStore{session.NewMemStore(sid)} +} diff --git a/modules/session/store.go b/modules/session/store.go index 70988fcdc5..09d1ef44dd 100644 --- a/modules/session/store.go +++ b/modules/session/store.go @@ -6,6 +6,8 @@ package session import ( "net/http" + "code.gitea.io/gitea/modules/setting" + "gitea.com/go-chi/session" ) @@ -14,6 +16,10 @@ type Store interface { Get(any) any Set(any, any) error Delete(any) error + ID() string + Release() error + Flush() error + Destroy(http.ResponseWriter, *http.Request) error } // RegenerateSession regenerates the underlying session and returns the new store @@ -21,8 +27,21 @@ func RegenerateSession(resp http.ResponseWriter, req *http.Request) (Store, erro for _, f := range BeforeRegenerateSession { f(resp, req) } - s, err := session.RegenerateSession(resp, req) - return s, err + if setting.IsInTesting { + if store, ok := req.Context().Value(MockStoreContextKey).(*MockStore); ok { + return store, nil + } + } + return session.RegenerateSession(resp, req) +} + +func GetContextSession(req *http.Request) Store { + if setting.IsInTesting { + if store, ok := req.Context().Value(MockStoreContextKey).(*MockStore); ok { + return store + } + } + return session.GetSession(req) } // BeforeRegenerateSession is a list of functions that are called before a session is regenerated. diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go index 34e1a336dc..e59f54420b 100644 --- a/modules/setting/oauth2.go +++ b/modules/setting/oauth2.go @@ -16,14 +16,10 @@ import ( type OAuth2UsernameType string const ( - // OAuth2UsernameUserid oauth2 userid field will be used as gitea name - OAuth2UsernameUserid OAuth2UsernameType = "userid" - // OAuth2UsernameNickname oauth2 nickname field will be used as gitea name - OAuth2UsernameNickname OAuth2UsernameType = "nickname" - // OAuth2UsernameEmail username of oauth2 email field will be used as gitea name - OAuth2UsernameEmail OAuth2UsernameType = "email" - // OAuth2UsernameEmail username of oauth2 preferred_username field will be used as gitea name - OAuth2UsernamePreferredUsername OAuth2UsernameType = "preferred_username" + OAuth2UsernameUserid OAuth2UsernameType = "userid" // use user id (sub) field as gitea's username + OAuth2UsernameNickname OAuth2UsernameType = "nickname" // use nickname field + OAuth2UsernameEmail OAuth2UsernameType = "email" // use email field + OAuth2UsernamePreferredUsername OAuth2UsernameType = "preferred_username" // use preferred_username field ) func (username OAuth2UsernameType) isValid() bool { @@ -71,8 +67,8 @@ func loadOAuth2ClientFrom(rootCfg ConfigProvider) { OAuth2Client.EnableAutoRegistration = sec.Key("ENABLE_AUTO_REGISTRATION").MustBool() OAuth2Client.Username = OAuth2UsernameType(sec.Key("USERNAME").MustString(string(OAuth2UsernameNickname))) if !OAuth2Client.Username.isValid() { - log.Warn("Username setting is not valid: '%s', will fallback to '%s'", OAuth2Client.Username, OAuth2UsernameNickname) OAuth2Client.Username = OAuth2UsernameNickname + log.Warn("[oauth2_client].USERNAME setting is invalid, falls back to %q", OAuth2Client.Username) } OAuth2Client.UpdateAvatar = sec.Key("UPDATE_AVATAR").MustBool() OAuth2Client.AccountLinking = OAuth2AccountLinkingType(sec.Key("ACCOUNT_LINKING").MustString(string(OAuth2AccountLinkingLogin))) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 4f17b1a6db..fb591be393 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -436,6 +436,7 @@ oauth_signin_submit = Link Account oauth.signin.error = There was an error processing the authorization request. If this error persists, please contact the site administrator. oauth.signin.error.access_denied = The authorization request was denied. oauth.signin.error.temporarily_unavailable = Authorization failed because the authentication server is temporarily unavailable. Please try again later. +oauth_callback_unable_auto_reg = Auto Registration is enabled, but OAuth2 Provider %[1]s returned missing fields: %[2]s, unable to create an account automatically, please create or link to an account, or contact the site administrator. openid_connect_submit = Connect openid_connect_title = Connect to an existing account openid_connect_desc = The chosen OpenID URI is unknown. Associate it with a new account here. diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 9ef32ebdb1..7c873796fe 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -382,17 +382,17 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe return setting.AppSubURL + "/" } -func getUserName(gothUser *goth.User) (string, error) { +// extractUserNameFromOAuth2 tries to extract a normalized username from the given OAuth2 user. +// It returns ("", nil) if the required field doesn't exist. +func extractUserNameFromOAuth2(gothUser *goth.User) (string, error) { switch setting.OAuth2Client.Username { case setting.OAuth2UsernameEmail: - return user_model.NormalizeUserName(strings.Split(gothUser.Email, "@")[0]) + return user_model.NormalizeUserName(gothUser.Email) case setting.OAuth2UsernamePreferredUsername: - preferredUsername, exists := gothUser.RawData["preferred_username"] - if exists { - return user_model.NormalizeUserName(preferredUsername.(string)) - } else { - return "", fmt.Errorf("preferred_username is missing in received user data but configured as username source for user_id %q. Check if OPENID_CONNECT_SCOPES contains profile", gothUser.UserID) + if preferredUsername, ok := gothUser.RawData["preferred_username"].(string); ok { + return user_model.NormalizeUserName(preferredUsername) } + return "", nil case setting.OAuth2UsernameNickname: return user_model.NormalizeUserName(gothUser.NickName) default: // OAuth2UsernameUserid diff --git a/routers/web/auth/auth_test.go b/routers/web/auth/auth_test.go index c6afbf877c..45525a5c6f 100644 --- a/routers/web/auth/auth_test.go +++ b/routers/web/auth/auth_test.go @@ -8,12 +8,31 @@ import ( "net/url" "testing" + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/session" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/contexttest" + "github.com/markbates/goth" + "github.com/markbates/goth/gothic" "github.com/stretchr/testify/assert" ) +func addOAuth2Source(t *testing.T, authName string, cfg oauth2.Source) { + cfg.Provider = util.IfZero(cfg.Provider, "gitea") + err := auth_model.CreateSource(db.DefaultContext, &auth_model.Source{ + Type: auth_model.OAuth2, + Name: authName, + IsActive: true, + Cfg: &cfg, + }) + assert.NoError(t, err) +} + func TestUserLogin(t *testing.T) { ctx, resp := contexttest.MockContext(t, "/user/login") SignIn(ctx) @@ -41,3 +60,24 @@ func TestUserLogin(t *testing.T) { SignIn(ctx) assert.Equal(t, "/", test.RedirectURL(resp)) } + +func TestSignUpOAuth2ButMissingFields(t *testing.T) { + defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)() + defer test.MockVariableValue(&gothic.CompleteUserAuth, func(res http.ResponseWriter, req *http.Request) (goth.User, error) { + return goth.User{Provider: "dummy-auth-source", UserID: "dummy-user"}, nil + })() + + addOAuth2Source(t, "dummy-auth-source", oauth2.Source{}) + + mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockStore("dummy-sid")} + ctx, resp := contexttest.MockContext(t, "/user/oauth2/dummy-auth-source/callback?code=dummy-code", mockOpt) + ctx.SetParams("provider", "dummy-auth-source") + SignInOAuthCallback(ctx) + assert.Equal(t, http.StatusSeeOther, resp.Code) + assert.Equal(t, "/user/link_account", test.RedirectURL(resp)) + + // then the user will be redirected to the link account page, and see a message about the missing fields + ctx, _ = contexttest.MockContext(t, "/user/link_account", mockOpt) + LinkAccount(ctx) + assert.EqualValues(t, "auth.oauth_callback_unable_auto_reg:dummy-auth-source,email", ctx.Data["AutoRegistrationFailedPrompt"]) +} diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go index f744a57a43..24130df634 100644 --- a/routers/web/auth/linkaccount.go +++ b/routers/web/auth/linkaccount.go @@ -48,23 +48,27 @@ func LinkAccount(ctx *context.Context) { ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin" ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup" - gothUser := ctx.Session.Get("linkAccountGothUser") - if gothUser == nil { - ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session")) + gothUser, ok := ctx.Session.Get("linkAccountGothUser").(goth.User) + if !ok { + // no account in session, so just redirect to the login page, then the user could restart the process + ctx.Redirect(setting.AppSubURL + "/user/login") return } - gu, _ := gothUser.(goth.User) - uname, err := getUserName(&gu) + if missingFields, ok := gothUser.RawData["__giteaAutoRegMissingFields"].([]string); ok { + ctx.Data["AutoRegistrationFailedPrompt"] = ctx.Tr("auth.oauth_callback_unable_auto_reg", gothUser.Provider, strings.Join(missingFields, ",")) + } + + uname, err := extractUserNameFromOAuth2(&gothUser) if err != nil { ctx.ServerError("UserSignIn", err) return } - email := gu.Email + email := gothUser.Email ctx.Data["user_name"] = uname ctx.Data["email"] = email - if len(email) != 0 { + if email != "" { u, err := user_model.GetUserByEmail(ctx, email) if err != nil && !user_model.IsErrUserNotExist(err) { ctx.ServerError("UserSignIn", err) @@ -73,7 +77,7 @@ func LinkAccount(ctx *context.Context) { if u != nil { ctx.Data["user_exists"] = true } - } else if len(uname) != 0 { + } else if uname != "" { u, err := user_model.GetUserByName(ctx, uname) if err != nil && !user_model.IsErrUserNotExist(err) { ctx.ServerError("UserSignIn", err) diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index 3189d1372e..c9cb7859cd 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -934,7 +934,7 @@ func SignInOAuthCallback(ctx *context.Context) { if u == nil { if ctx.Doer != nil { - // attach user to already logged in user + // attach user to the current signed-in user err = externalaccount.LinkAccountToUser(ctx, ctx.Doer, gothUser) if err != nil { ctx.ServerError("UserLinkAccount", err) @@ -952,23 +952,32 @@ func SignInOAuthCallback(ctx *context.Context) { if gothUser.Email == "" { missingFields = append(missingFields, "email") } - if setting.OAuth2Client.Username == setting.OAuth2UsernameNickname && gothUser.NickName == "" { - 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) - return - } - uname, err := getUserName(&gothUser) + uname, err := extractUserNameFromOAuth2(&gothUser) if err != nil { ctx.ServerError("UserSignIn", err) return } + if uname == "" { + if setting.OAuth2Client.Username == setting.OAuth2UsernameNickname { + missingFields = append(missingFields, "nickname") + } else if setting.OAuth2Client.Username == setting.OAuth2UsernamePreferredUsername { + missingFields = append(missingFields, "preferred_username") + } // else: "UserID" and "Email" have been handled above separately + } + if len(missingFields) > 0 { + log.Error(`OAuth2 auto registration (ENABLE_AUTO_REGISTRATION) is enabled but OAuth2 provider %q doesn't return required fields: %s. `+ + `Suggest to: disable auto registration, or make OPENID_CONNECT_SCOPES (for OpenIDConnect) / Authentication Source Scopes (for Admin panel) to request all required fields, and the fields shouldn't be empty.`, + authSource.Name, strings.Join(missingFields, ",")) + // The RawData is the only way to pass the missing fields to the another page at the moment, other ways all have various problems: + // by session or cookie: difficult to clean or reset; by URL: could be injected with uncontrollable content; by ctx.Flash: the link_account page is a mess ... + // Since the RawData is for the provider's data, so we need to use our own prefix here to avoid conflict. + if gothUser.RawData == nil { + gothUser.RawData = make(map[string]any) + } + gothUser.RawData["__giteaAutoRegMissingFields"] = missingFields + showLinkingLogin(ctx, gothUser) + return + } u = &user_model.User{ Name: uname, FullName: gothUser.Name, diff --git a/services/context/context.go b/services/context/context.go index 88ab5cae0e..aab0485f1a 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -20,14 +20,13 @@ import ( "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/httpcache" + "code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web/middleware" web_types "code.gitea.io/gitea/modules/web/types" - - "gitea.com/go-chi/session" ) // Render represents a template render @@ -154,7 +153,7 @@ func Contexter() func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { base, baseCleanUp := NewBaseContext(resp, req) defer baseCleanUp() - ctx := NewWebContext(base, rnd, session.GetSession(req)) + ctx := NewWebContext(base, rnd, session.GetContextSession(req)) ctx.Data.MergeFrom(middleware.CommonTemplateContextData()) ctx.Data["Context"] = ctx // TODO: use "ctx" in template and remove this diff --git a/services/contexttest/context_tests.go b/services/contexttest/context_tests.go index 3064c56590..0c1e5ee54f 100644 --- a/services/contexttest/context_tests.go +++ b/services/contexttest/context_tests.go @@ -19,7 +19,9 @@ 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/cache" "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/web/middleware" @@ -43,7 +45,8 @@ func mockRequest(t *testing.T, reqPath string) *http.Request { } type MockContextOption struct { - Render context.Render + Render context.Render + SessionStore *session.MockStore } // MockContext mock context for unit tests @@ -62,12 +65,17 @@ func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*cont base.Data = middleware.GetContextData(req.Context()) base.Locale = &translation.MockLocale{} + chiCtx := chi.NewRouteContext() ctx := context.NewWebContext(base, opt.Render, nil) ctx.AppendContextValue(context.WebContextKey, ctx) + ctx.AppendContextValue(chi.RouteCtxKey, chiCtx) + if opt.SessionStore != nil { + ctx.AppendContextValue(session.MockStoreContextKey, opt.SessionStore) + ctx.Session = opt.SessionStore + } + ctx.Cache = cache.GetCache() ctx.PageData = map[string]any{} ctx.Data["PageStartTime"] = time.Now() - chiCtx := chi.NewRouteContext() - ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx) return ctx, resp } diff --git a/templates/user/auth/link_account.tmpl b/templates/user/auth/link_account.tmpl index 8dd49ccd60..a99e172d05 100644 --- a/templates/user/auth/link_account.tmpl +++ b/templates/user/auth/link_account.tmpl @@ -17,15 +17,12 @@
-
+
+ {{if .AutoRegistrationFailedPrompt}}
{{.AutoRegistrationFailedPrompt}}
{{end}} {{template "user/auth/signup_inner" .}}
-
- +
+ {{template "user/auth/signin_inner" .}}
From fd63b96f6a4c5b3ea9e53d37af85e0d2d09715b9 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 25 Apr 2024 21:01:38 +0800 Subject: [PATCH 10/20] Refactor imagediff and fix regression bug (#30694) Fix #30683 --- web_src/js/features/imagediff.js | 204 +++++++++++++++---------------- 1 file changed, 102 insertions(+), 102 deletions(-) diff --git a/web_src/js/features/imagediff.js b/web_src/js/features/imagediff.js index 192a642834..d1b139ffde 100644 --- a/web_src/js/features/imagediff.js +++ b/web_src/js/features/imagediff.js @@ -1,6 +1,6 @@ import $ from 'jquery'; import {GET} from '../modules/fetch.js'; -import {hideElem, loadElem} from '../utils/dom.js'; +import {hideElem, loadElem, queryElemChildren} from '../utils/dom.js'; import {parseDom} from '../utils.js'; function getDefaultSvgBoundsIfUndefined(text, src) { @@ -38,36 +38,36 @@ function getDefaultSvgBoundsIfUndefined(text, src) { return null; } +function createContext(imageAfter, imageBefore) { + const sizeAfter = { + width: imageAfter?.width || 0, + height: imageAfter?.height || 0, + }; + const sizeBefore = { + width: imageBefore?.width || 0, + height: imageBefore?.height || 0, + }; + const maxSize = { + width: Math.max(sizeBefore.width, sizeAfter.width), + height: Math.max(sizeBefore.height, sizeAfter.height), + }; + + return { + imageAfter, + imageBefore, + sizeAfter, + sizeBefore, + maxSize, + ratio: [ + Math.floor(maxSize.width - sizeAfter.width) / 2, + Math.floor(maxSize.height - sizeAfter.height) / 2, + Math.floor(maxSize.width - sizeBefore.width) / 2, + Math.floor(maxSize.height - sizeBefore.height) / 2, + ], + }; +} + export function initImageDiff() { - function createContext(image1, image2) { - const size1 = { - width: image1 && image1.width || 0, - height: image1 && image1.height || 0, - }; - const size2 = { - width: image2 && image2.width || 0, - height: image2 && image2.height || 0, - }; - const max = { - width: Math.max(size2.width, size1.width), - height: Math.max(size2.height, size1.height), - }; - - return { - $image1: $(image1), - $image2: $(image2), - size1, - size2, - max, - ratio: [ - Math.floor(max.width - size1.width) / 2, - Math.floor(max.height - size1.height) / 2, - Math.floor(max.width - size2.width) / 2, - Math.floor(max.height - size2.height) / 2, - ], - }; - } - $('.image-diff:not([data-image-diff-loaded])').each(async function() { const $container = $(this); this.setAttribute('data-image-diff-loaded', 'true'); @@ -116,94 +116,96 @@ export function initImageDiff() { initOverlay(createContext($imagesAfter[2], $imagesBefore[2])); } - this.querySelector(':scope > .image-diff-tabs')?.classList.remove('is-loading'); + queryElemChildren(this, '.image-diff-tabs', (el) => el.classList.remove('is-loading')); function initSideBySide(container, sizes) { let factor = 1; - if (sizes.max.width > (diffContainerWidth - 24) / 2) { - factor = (diffContainerWidth - 24) / 2 / sizes.max.width; + if (sizes.maxSize.width > (diffContainerWidth - 24) / 2) { + factor = (diffContainerWidth - 24) / 2 / sizes.maxSize.width; } - const widthChanged = sizes.$image1.length !== 0 && sizes.$image2.length !== 0 && sizes.$image1[0].naturalWidth !== sizes.$image2[0].naturalWidth; - const heightChanged = sizes.$image1.length !== 0 && sizes.$image2.length !== 0 && sizes.$image1[0].naturalHeight !== sizes.$image2[0].naturalHeight; - if (sizes.$image1?.length) { + const widthChanged = sizes.imageAfter && sizes.imageBefore && sizes.imageAfter.naturalWidth !== sizes.imageBefore.naturalWidth; + const heightChanged = sizes.imageAfter && sizes.imageBefore && sizes.imageAfter.naturalHeight !== sizes.imageBefore.naturalHeight; + if (sizes.imageAfter) { const boundsInfoAfterWidth = container.querySelector('.bounds-info-after .bounds-info-width'); - boundsInfoAfterWidth.textContent = `${sizes.$image1[0].naturalWidth}px`; - if (widthChanged) boundsInfoAfterWidth.classList.add('green'); - + if (boundsInfoAfterWidth) { + boundsInfoAfterWidth.textContent = `${sizes.imageAfter.naturalWidth}px`; + boundsInfoAfterWidth.classList.toggle('green', widthChanged); + } const boundsInfoAfterHeight = container.querySelector('.bounds-info-after .bounds-info-height'); - boundsInfoAfterHeight.textContent = `${sizes.$image1[0].naturalHeight}px`; - if (heightChanged) boundsInfoAfterHeight.classList.add('green'); + if (boundsInfoAfterHeight) { + boundsInfoAfterHeight.textContent = `${sizes.imageAfter.naturalHeight}px`; + boundsInfoAfterHeight.classList.toggle('green', heightChanged); + } } - if (sizes.$image2?.length) { + if (sizes.imageBefore) { const boundsInfoBeforeWidth = container.querySelector('.bounds-info-before .bounds-info-width'); - boundsInfoBeforeWidth.textContent = `${sizes.$image2[0].naturalWidth}px`; - if (widthChanged) boundsInfoBeforeWidth.classList.add('red'); - + if (boundsInfoBeforeWidth) { + boundsInfoBeforeWidth.textContent = `${sizes.imageBefore.naturalWidth}px`; + boundsInfoBeforeWidth.classList.toggle('red', widthChanged); + } const boundsInfoBeforeHeight = container.querySelector('.bounds-info-before .bounds-info-height'); - boundsInfoBeforeHeight.textContent = `${sizes.$image2[0].naturalHeight}px`; - if (heightChanged) boundsInfoBeforeHeight.classList.add('red'); + if (boundsInfoBeforeHeight) { + boundsInfoBeforeHeight.textContent = `${sizes.imageBefore.naturalHeight}px`; + boundsInfoBeforeHeight.classList.add('red', heightChanged); + } } - const image1 = sizes.$image1[0]; - if (image1) { - const container = image1.parentNode; - image1.style.width = `${sizes.size1.width * factor}px`; - image1.style.height = `${sizes.size1.height * factor}px`; + if (sizes.imageAfter) { + const container = sizes.imageAfter.parentNode; + sizes.imageAfter.style.width = `${sizes.sizeAfter.width * factor}px`; + sizes.imageAfter.style.height = `${sizes.sizeAfter.height * factor}px`; container.style.margin = '10px auto'; - container.style.width = `${sizes.size1.width * factor + 2}px`; - container.style.height = `${sizes.size1.height * factor + 2}px`; + container.style.width = `${sizes.sizeAfter.width * factor + 2}px`; + container.style.height = `${sizes.sizeAfter.height * factor + 2}px`; } - const image2 = sizes.$image2[0]; - if (image2) { - const container = image2.parentNode; - image2.style.width = `${sizes.size2.width * factor}px`; - image2.style.height = `${sizes.size2.height * factor}px`; + if (sizes.imageBefore) { + const container = sizes.imageBefore.parentNode; + sizes.imageBefore.style.width = `${sizes.sizeBefore.width * factor}px`; + sizes.imageBefore.style.height = `${sizes.sizeBefore.height * factor}px`; container.style.margin = '10px auto'; - container.style.width = `${sizes.size2.width * factor + 2}px`; - container.style.height = `${sizes.size2.height * factor + 2}px`; + container.style.width = `${sizes.sizeBefore.width * factor + 2}px`; + container.style.height = `${sizes.sizeBefore.height * factor + 2}px`; } } function initSwipe(sizes) { let factor = 1; - if (sizes.max.width > diffContainerWidth - 12) { - factor = (diffContainerWidth - 12) / sizes.max.width; + if (sizes.maxSize.width > diffContainerWidth - 12) { + factor = (diffContainerWidth - 12) / sizes.maxSize.width; } - const image1 = sizes.$image1[0]; - if (image1) { - const container = image1.parentNode; + if (sizes.imageAfter) { + const container = sizes.imageAfter.parentNode; const swipeFrame = container.parentNode; - image1.style.width = `${sizes.size1.width * factor}px`; - image1.style.height = `${sizes.size1.height * factor}px`; + sizes.imageAfter.style.width = `${sizes.sizeAfter.width * factor}px`; + sizes.imageAfter.style.height = `${sizes.sizeAfter.height * factor}px`; container.style.margin = `0px ${sizes.ratio[0] * factor}px`; - container.style.width = `${sizes.size1.width * factor + 2}px`; - container.style.height = `${sizes.size1.height * factor + 2}px`; + container.style.width = `${sizes.sizeAfter.width * factor + 2}px`; + container.style.height = `${sizes.sizeAfter.height * factor + 2}px`; swipeFrame.style.padding = `${sizes.ratio[1] * factor}px 0 0 0`; - swipeFrame.style.width = `${sizes.max.width * factor + 2}px`; + swipeFrame.style.width = `${sizes.maxSize.width * factor + 2}px`; } - const image2 = sizes.$image2[0]; - if (image2) { - const container = image2.parentNode; + if (sizes.imageBefore) { + const container = sizes.imageBefore.parentNode; const swipeFrame = container.parentNode; - image2.style.width = `${sizes.size2.width * factor}px`; - image2.style.height = `${sizes.size2.height * factor}px`; + sizes.imageBefore.style.width = `${sizes.sizeBefore.width * factor}px`; + sizes.imageBefore.style.height = `${sizes.sizeBefore.height * factor}px`; container.style.margin = `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`; - container.style.width = `${sizes.size2.width * factor + 2}px`; - container.style.height = `${sizes.size2.height * factor + 2}px`; - swipeFrame.style.width = `${sizes.max.width * factor + 2}px`; - swipeFrame.style.height = `${sizes.max.height * factor + 2}px`; + container.style.width = `${sizes.sizeBefore.width * factor + 2}px`; + container.style.height = `${sizes.sizeBefore.height * factor + 2}px`; + swipeFrame.style.width = `${sizes.maxSize.width * factor + 2}px`; + swipeFrame.style.height = `${sizes.maxSize.height * factor + 2}px`; } // extra height for inner "position: absolute" elements const swipe = $container.find('.diff-swipe')[0]; if (swipe) { - swipe.style.width = `${sizes.max.width * factor + 2}px`; - swipe.style.height = `${sizes.max.height * factor + 30}px`; + swipe.style.width = `${sizes.maxSize.width * factor + 2}px`; + swipe.style.height = `${sizes.maxSize.height * factor + 30}px`; } $container.find('.swipe-bar').on('mousedown', function(e) { @@ -229,39 +231,37 @@ export function initImageDiff() { function initOverlay(sizes) { let factor = 1; - if (sizes.max.width > diffContainerWidth - 12) { - factor = (diffContainerWidth - 12) / sizes.max.width; + if (sizes.maxSize.width > diffContainerWidth - 12) { + factor = (diffContainerWidth - 12) / sizes.maxSize.width; } - const image1 = sizes.$image1[0]; - if (image1) { - const container = image1.parentNode; - image1.style.width = `${sizes.size1.width * factor}px`; - image1.style.height = `${sizes.size1.height * factor}px`; + if (sizes.imageAfter) { + const container = sizes.imageAfter.parentNode; + sizes.imageAfter.style.width = `${sizes.sizeAfter.width * factor}px`; + sizes.imageAfter.style.height = `${sizes.sizeAfter.height * factor}px`; container.style.margin = `${sizes.ratio[1] * factor}px ${sizes.ratio[0] * factor}px`; - container.style.width = `${sizes.size1.width * factor + 2}px`; - container.style.height = `${sizes.size1.height * factor + 2}px`; + container.style.width = `${sizes.sizeAfter.width * factor + 2}px`; + container.style.height = `${sizes.sizeAfter.height * factor + 2}px`; } - const image2 = sizes.$image2[0]; - if (image2) { - const container = image2.parentNode; + if (sizes.imageBefore) { + const container = sizes.imageBefore.parentNode; const overlayFrame = container.parentNode; - image2.style.width = `${sizes.size2.width * factor}px`; - image2.style.height = `${sizes.size2.height * factor}px`; + sizes.imageBefore.style.width = `${sizes.sizeBefore.width * factor}px`; + sizes.imageBefore.style.height = `${sizes.sizeBefore.height * factor}px`; container.style.margin = `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`; - container.style.width = `${sizes.size2.width * factor + 2}px`; - container.style.height = `${sizes.size2.height * factor + 2}px`; + container.style.width = `${sizes.sizeBefore.width * factor + 2}px`; + container.style.height = `${sizes.sizeBefore.height * factor + 2}px`; // some inner elements are `position: absolute`, so the container's height must be large enough - overlayFrame.style.width = `${sizes.max.width * factor + 2}px`; - overlayFrame.style.height = `${sizes.max.height * factor + 2}px`; + overlayFrame.style.width = `${sizes.maxSize.width * factor + 2}px`; + overlayFrame.style.height = `${sizes.maxSize.height * factor + 2}px`; } const rangeInput = $container[0].querySelector('input[type="range"]'); function updateOpacity() { - if (sizes?.$image1?.[0]) { - sizes.$image1[0].parentNode.style.opacity = `${rangeInput.value / 100}`; + if (sizes.imageAfter) { + sizes.imageAfter.parentNode.style.opacity = `${rangeInput.value / 100}`; } } rangeInput?.addEventListener('input', updateOpacity); From 935330b1b98a232f2928d55e96dc425e09bda593 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Fri, 26 Apr 2024 00:26:00 +0000 Subject: [PATCH 11/20] [skip ci] Updated translations via Crowdin --- options/locale/locale_pt-PT.ini | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 4b7604a5cb..444f784af9 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -2358,7 +2358,7 @@ settings.protected_branch.delete_rule=Eliminar regra settings.protected_branch_can_push=Permitir envios? settings.protected_branch_can_push_yes=Pode enviar settings.protected_branch_can_push_no=Não pode enviar -settings.branch_protection=Salvaguarda do ramo '%s' +settings.branch_protection=Regras de salvaguarda do ramo '%s' settings.protect_this_branch=Habilitar salvaguarda do ramo settings.protect_this_branch_desc=Impede a eliminação e restringe envios e integrações do Git no ramo. settings.protect_disable_push=Desabilitar envios @@ -2402,7 +2402,7 @@ settings.protect_patterns=Padrões settings.protect_protected_file_patterns=Padrões de ficheiros protegidos (separados com ponto e vírgula ';'): settings.protect_protected_file_patterns_desc=Ficheiros protegidos não podem ser modificados imediatamente, mesmo que o utilizador tenha direitos para adicionar, editar ou eliminar ficheiros neste ramo. Múltiplos padrões podem ser separados com ponto e vírgula (';'). Veja a documentação em github.com/gobwas/glob para ver a sintaxe. Exemplos: .drone.yml, /docs/**/*.txt. settings.protect_unprotected_file_patterns=Padrões de ficheiros desprotegidos (separados com ponto e vírgula ';'): -settings.protect_unprotected_file_patterns_desc=Ficheiros desprotegidos que podem ser modificados imediatamente se o utilizador tiver direitos de escrita, contornando a restrição no envio. Múltiplos padrões podem ser separados com ponto e vírgula (';'). Veja a documentação em github.com/gobwas/glob para ver a sintaxe. Exemplos: .drone.yml, /docs/**/*.txt. +settings.protect_unprotected_file_patterns_desc=Ficheiros desprotegidos que podem ser modificados imediatamente se o utilizador tiver direitos de escrita, contornando a restrição no envio. Padrões múltiplos podem ser separados com ponto e vírgula (';'). Veja a documentação em github.com/gobwas/glob para ver a sintaxe. Exemplos: .drone.yml, /docs/**/*.txt. settings.add_protected_branch=Habilitar salvaguarda settings.delete_protected_branch=Desabilitar salvaguarda settings.update_protect_branch_success=A salvaguarda do ramo "%s" foi modificada. @@ -2418,7 +2418,7 @@ settings.block_outdated_branch=Bloquear integração se o pedido de integração settings.block_outdated_branch_desc=A integração não será possível quando o ramo de topo estiver abaixo do ramo base. settings.default_branch_desc=Escolha um ramo do repositório como sendo o predefinido para pedidos de integração e cometimentos: settings.merge_style_desc=Estilos de integração -settings.default_merge_style_desc=Tipo de integração predefinido para pedidos de integração: +settings.default_merge_style_desc=Tipo de integração predefinido settings.choose_branch=Escolha um ramo… settings.no_protected_branch=Não existem ramos protegidos. settings.edit_protected_branch=Editar @@ -2788,7 +2788,7 @@ self_check=Auto-verificação identity_access=Identidade e acesso users=Contas de utilizador organizations=Organizações -assets=Recursos de código +assets=Recursos do código-fonte repositories=Repositórios hooks=Automatismos web integrations=Integrações @@ -2869,14 +2869,14 @@ dashboard.mspan_structures_obtained=Estruturas MSpan obtidas dashboard.mcache_structures_usage=Uso das estruturas MCache dashboard.mcache_structures_obtained=Estruturas MCache obtidas dashboard.profiling_bucket_hash_table_obtained=Perfil obtido da tabela de hash do balde -dashboard.gc_metadata_obtained=Metadados da recolha de lixo obtidos +dashboard.gc_metadata_obtained=Metadados obtidos da recolha de lixo dashboard.other_system_allocation_obtained=Outras alocações de sistema obtidas dashboard.next_gc_recycle=Próxima reciclagem da recolha de lixo dashboard.last_gc_time=Tempo decorrido desde a última recolha de lixo dashboard.total_gc_time=Pausa total da recolha de lixo dashboard.total_gc_pause=Pausa total da recolha de lixo dashboard.last_gc_pause=Última pausa da recolha de lixo -dashboard.gc_times=Tempos da recolha de lixo +dashboard.gc_times=N.º de recolhas de lixo dashboard.delete_old_actions=Eliminar todas as operações antigas da base de dados dashboard.delete_old_actions.started=Foi iniciado o processo de eliminação de todas as operações antigas da base de dados. dashboard.update_checker=Verificador de novas versões @@ -3025,7 +3025,7 @@ auths.attribute_surname=Atributo do Sobrenome auths.attribute_mail=Atributo do email auths.attribute_ssh_public_key=Atributo da chave pública SSH auths.attribute_avatar=Atributo do avatar -auths.attributes_in_bind=Buscar os atributos no contexto de Bind DN +auths.attributes_in_bind=Buscar atributos no contexto do Bind DN auths.allow_deactivate_all=Permitir que um resultado de pesquisa vazio desabilite todos os utilizadores auths.use_paged_search=Usar pesquisa paginada auths.search_page_size=Tamanho da página @@ -3224,7 +3224,7 @@ config.session_config=Configuração de sessão config.session_provider=Fornecedor da sessão config.provider_config=Configuração do fornecedor config.cookie_name=Nome do cookie -config.gc_interval_time=Intervalo da recolha do lixo +config.gc_interval_time=Intervalo de tempo entre recolhas do lixo config.session_life_time=Tempo de vida da sessão config.https_only=Apenas HTTPS config.cookie_life_time=Tempo de vida do cookie From 6a0750177fe4c494f828463bc146ea11df08a422 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 26 Apr 2024 09:17:43 +0800 Subject: [PATCH 12/20] Allow to save empty comment (#30706) Fix #29986 --- routers/web/repo/issue.go | 41 +++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 95f0cf3d71..1bc5f343e7 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -3149,13 +3149,10 @@ func UpdateCommentContent(ctx *context.Context) { } oldContent := comment.Content - comment.Content = ctx.FormString("content") - if len(comment.Content) == 0 { - ctx.JSON(http.StatusOK, map[string]any{ - "content": "", - }) - return - } + newContent := ctx.FormString("content") + + // allow to save empty content + comment.Content = newContent if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil { if errors.Is(err, user_model.ErrBlockedUser) { ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user")) @@ -3178,21 +3175,27 @@ func UpdateCommentContent(ctx *context.Context) { } } - content, err := markdown.RenderString(&markup.RenderContext{ - Links: markup.Links{ - Base: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ? - }, - Metas: ctx.Repo.Repository.ComposeMetas(ctx), - GitRepo: ctx.Repo.GitRepo, - Ctx: ctx, - }, comment.Content) - if err != nil { - ctx.ServerError("RenderString", err) - return + var renderedContent template.HTML + if comment.Content != "" { + renderedContent, err = markdown.RenderString(&markup.RenderContext{ + Links: markup.Links{ + Base: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ? + }, + Metas: ctx.Repo.Repository.ComposeMetas(ctx), + GitRepo: ctx.Repo.GitRepo, + Ctx: ctx, + }, comment.Content) + if err != nil { + ctx.ServerError("RenderString", err) + return + } + } else { + contentEmpty := fmt.Sprintf(`%s`, ctx.Tr("repo.issues.no_content")) + renderedContent = template.HTML(contentEmpty) } ctx.JSON(http.StatusOK, map[string]any{ - "content": content, + "content": renderedContent, "attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content), }) } From 2a6418abb1e227f7d0401761a5f68d59a1cea9b2 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 26 Apr 2024 09:52:28 +0800 Subject: [PATCH 13/20] Improve test for TestPullCompare (#30699) --- tests/integration/pull_compare_test.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/integration/pull_compare_test.go b/tests/integration/pull_compare_test.go index b814207b2f..39d9103dfd 100644 --- a/tests/integration/pull_compare_test.go +++ b/tests/integration/pull_compare_test.go @@ -4,11 +4,13 @@ package integration import ( + "fmt" "net/http" "net/url" "testing" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -46,22 +48,25 @@ func TestPullCompare(t *testing.T) { testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testCreateBranch(t, session, "user1", "repo1", "branch/master", "master1", http.StatusSeeOther) testEditFile(t, session, "user1", "repo1", "master1", "README.md", "Hello, World (Edited)\n") - resp = testPullCreate(t, session, "user1", "repo1", false, "master", "master1", "This is a pull title") + testPullCreate(t, session, "user1", "repo1", false, "master", "master1", "This is a pull title") - // the max value on issue_index.yml for repo_id=1 is 5 - req = NewRequest(t, "GET", "/user2/repo1/pulls/6/files") + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"}) + issueIndex := unittest.AssertExistsAndLoadBean(t, &issues_model.IssueIndex{GroupID: repo1.ID}, unittest.OrderBy("group_id ASC")) + prFilesURL := fmt.Sprintf("/user2/repo1/pulls/%d/files", issueIndex.MaxIndex) + req = NewRequest(t, "GET", prFilesURL) resp = session.MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) editButtonCount := doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length() assert.Greater(t, editButtonCount, 0, "Expected to find a button to edit a file in the PR diff view but there were none") - user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) repoForked := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: "repo1"}) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + // delete the head repository and revisit the PR diff view err := repo_service.DeleteRepositoryDirectly(db.DefaultContext, user2, repoForked.ID) assert.NoError(t, err) - req = NewRequest(t, "GET", "/user2/repo1/pulls/6/files") + req = NewRequest(t, "GET", prFilesURL) resp = session.MakeRequest(t, req, http.StatusOK) doc = NewHTMLParser(t, resp.Body) editButtonCount = doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length() From 2a3906d75532ba8689338247d794f21dceb4d359 Mon Sep 17 00:00:00 2001 From: yp05327 <576951401@qq.com> Date: Fri, 26 Apr 2024 11:22:45 +0900 Subject: [PATCH 14/20] Improve job commit description (#30579) Fix https://github.com/go-gitea/gitea/issues/30567 When job is a schedule: ![image](https://github.com/go-gitea/gitea/assets/18380374/b07e9d43-e8b7-4ee2-87b3-a7050c3a8ca5) When it is a normal one: ![image](https://github.com/go-gitea/gitea/assets/18380374/0d58dab9-74bb-421b-8952-0578cdf21a52) also add a 'space' behind `:` ![image](https://github.com/go-gitea/gitea/assets/18380374/4cebece0-bfe6-4ad9-b806-e5c49bb9be43) ![image](https://github.com/go-gitea/gitea/assets/18380374/02da7681-474b-4c0f-9dad-b6558f6cb484) --------- Co-authored-by: wxiaoguang --- models/actions/run.go | 11 ++++++++++ routers/web/repo/actions/view.go | 26 +++++++++++++----------- templates/repo/actions/runs_list.tmpl | 2 +- templates/repo/actions/view.tmpl | 3 +++ web_src/js/components/RepoActionView.vue | 22 +++++++++++++++----- 5 files changed, 46 insertions(+), 18 deletions(-) diff --git a/models/actions/run.go b/models/actions/run.go index b75fa49f3c..d68710f46d 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -74,6 +74,13 @@ func (run *ActionRun) Link() string { return fmt.Sprintf("%s/actions/runs/%d", run.Repo.Link(), run.Index) } +func (run *ActionRun) WorkflowLink() string { + if run.Repo == nil { + return "" + } + return fmt.Sprintf("%s/actions/?workflow=%s", run.Repo.Link(), run.WorkflowID) +} + // RefLink return the url of run's ref func (run *ActionRun) RefLink() string { refName := git.RefName(run.Ref) @@ -156,6 +163,10 @@ func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, err return nil, fmt.Errorf("event %s is not a pull request event", run.Event) } +func (run *ActionRun) IsSchedule() bool { + return run.ScheduleID > 0 +} + func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error { _, err := db.GetEngine(ctx).ID(repo.ID). SetExpr("num_action_runs", diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index db2b11a7ed..3909a64be6 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -67,6 +67,9 @@ type ViewResponse struct { CanRerun bool `json:"canRerun"` CanDeleteArtifact bool `json:"canDeleteArtifact"` Done bool `json:"done"` + WorkflowID string `json:"workflowID"` + WorkflowLink string `json:"workflowLink"` + IsSchedule bool `json:"isSchedule"` Jobs []*ViewJob `json:"jobs"` Commit ViewCommit `json:"commit"` } `json:"run"` @@ -90,12 +93,10 @@ type ViewJob struct { } type ViewCommit struct { - LocaleCommit string `json:"localeCommit"` - LocalePushedBy string `json:"localePushedBy"` - ShortSha string `json:"shortSHA"` - Link string `json:"link"` - Pusher ViewUser `json:"pusher"` - Branch ViewBranch `json:"branch"` + ShortSha string `json:"shortSHA"` + Link string `json:"link"` + Pusher ViewUser `json:"pusher"` + Branch ViewBranch `json:"branch"` } type ViewUser struct { @@ -151,6 +152,9 @@ func ViewPost(ctx *context_module.Context) { resp.State.Run.CanRerun = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions) resp.State.Run.CanDeleteArtifact = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions) resp.State.Run.Done = run.Status.IsDone() + resp.State.Run.WorkflowID = run.WorkflowID + resp.State.Run.WorkflowLink = run.WorkflowLink() + resp.State.Run.IsSchedule = run.IsSchedule() resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json resp.State.Run.Status = run.Status.String() for _, v := range jobs { @@ -172,12 +176,10 @@ func ViewPost(ctx *context_module.Context) { Link: run.RefLink(), } resp.State.Run.Commit = ViewCommit{ - LocaleCommit: ctx.Locale.TrString("actions.runs.commit"), - LocalePushedBy: ctx.Locale.TrString("actions.runs.pushed_by"), - ShortSha: base.ShortSha(run.CommitSHA), - Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA), - Pusher: pusher, - Branch: branch, + ShortSha: base.ShortSha(run.CommitSHA), + Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA), + Pusher: pusher, + Branch: branch, } var task *actions_model.ActionTask diff --git a/templates/repo/actions/runs_list.tmpl b/templates/repo/actions/runs_list.tmpl index 20330b5d62..09a25ce8bd 100644 --- a/templates/repo/actions/runs_list.tmpl +++ b/templates/repo/actions/runs_list.tmpl @@ -15,7 +15,7 @@ {{if .Title}}{{.Title}}{{else}}{{ctx.Locale.Tr "actions.runs.empty_commit_message"}}{{end}}
- {{if not $.CurWorkflow}}{{.WorkflowID}} {{end}}#{{.Index}}: + {{if not $.CurWorkflow}}{{.WorkflowID}} {{end}}#{{.Index}}: {{- if .ScheduleID -}} {{ctx.Locale.Tr "actions.runs.scheduled"}} {{- else -}} diff --git a/templates/repo/actions/view.tmpl b/templates/repo/actions/view.tmpl index f8b106147b..f7b03608ee 100644 --- a/templates/repo/actions/view.tmpl +++ b/templates/repo/actions/view.tmpl @@ -10,6 +10,9 @@ data-locale-cancel="{{ctx.Locale.Tr "cancel"}}" data-locale-rerun="{{ctx.Locale.Tr "rerun"}}" data-locale-rerun-all="{{ctx.Locale.Tr "rerun_all"}}" + data-locale-runs-scheduled="{{ctx.Locale.Tr "actions.runs.scheduled"}}" + data-locale-runs-commit="{{ctx.Locale.Tr "actions.runs.commit"}}" + data-locale-runs-pushed-by="{{ctx.Locale.Tr "actions.runs.pushed_by"}}" data-locale-status-unknown="{{ctx.Locale.Tr "actions.status.unknown"}}" data-locale-status-waiting="{{ctx.Locale.Tr "actions.status.waiting"}}" data-locale-status-running="{{ctx.Locale.Tr "actions.status.running"}}" diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index 16ce3fc80d..8b39d0504b 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -44,6 +44,9 @@ const sfc = { canApprove: false, canRerun: false, done: false, + workflowID: '', + workflowLink: '', + isSchedule: false, jobs: [ // { // id: 0, @@ -338,10 +341,13 @@ export function initRepositoryActionView() { approve: el.getAttribute('data-locale-approve'), cancel: el.getAttribute('data-locale-cancel'), rerun: el.getAttribute('data-locale-rerun'), + rerun_all: el.getAttribute('data-locale-rerun-all'), + scheduled: el.getAttribute('data-locale-runs-scheduled'), + commit: el.getAttribute('data-locale-runs-commit'), + pushedBy: el.getAttribute('data-locale-runs-pushed-by'), artifactsTitle: el.getAttribute('data-locale-artifacts-title'), areYouSure: el.getAttribute('data-locale-are-you-sure'), confirmDeleteArtifact: el.getAttribute('data-locale-confirm-delete-artifact'), - rerun_all: el.getAttribute('data-locale-rerun-all'), showTimeStamps: el.getAttribute('data-locale-show-timestamps'), showLogSeconds: el.getAttribute('data-locale-show-log-seconds'), showFullScreen: el.getAttribute('data-locale-show-full-screen'), @@ -382,10 +388,16 @@ export function initRepositoryActionView() {
- {{ run.commit.localeCommit }} - {{ run.commit.shortSHA }} - {{ run.commit.localePushedBy }} - {{ run.commit.pusher.displayName }} + {{ run.workflowID }}: + + {{ run.commit.branch.name }} From ed8c63cea3da0d0ba3cdec58f6c6d61c73205afe Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 26 Apr 2024 10:53:30 +0800 Subject: [PATCH 15/20] Deduplicate lfs common code (#30704) --- modules/git/pipeline/lfs_common.go | 32 ++++++++++++++++++ modules/git/pipeline/{lfs.go => lfs_gogit.go} | 25 ++------------ modules/git/pipeline/lfs_nogogit.go | 33 ++++--------------- 3 files changed, 42 insertions(+), 48 deletions(-) create mode 100644 modules/git/pipeline/lfs_common.go rename modules/git/pipeline/{lfs.go => lfs_gogit.go} (80%) diff --git a/modules/git/pipeline/lfs_common.go b/modules/git/pipeline/lfs_common.go new file mode 100644 index 0000000000..188e7d4d65 --- /dev/null +++ b/modules/git/pipeline/lfs_common.go @@ -0,0 +1,32 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package pipeline + +import ( + "fmt" + "time" + + "code.gitea.io/gitea/modules/git" +) + +// LFSResult represents commits found using a provided pointer file hash +type LFSResult struct { + Name string + SHA string + Summary string + When time.Time + ParentHashes []git.ObjectID + BranchName string + FullCommitName string +} + +type lfsResultSlice []*LFSResult + +func (a lfsResultSlice) Len() int { return len(a) } +func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) } + +func lfsError(msg string, err error) error { + return fmt.Errorf("LFS error occurred, %s: err: %w", msg, err) +} diff --git a/modules/git/pipeline/lfs.go b/modules/git/pipeline/lfs_gogit.go similarity index 80% rename from modules/git/pipeline/lfs.go rename to modules/git/pipeline/lfs_gogit.go index 6dfca24f29..adcf8ed09c 100644 --- a/modules/git/pipeline/lfs.go +++ b/modules/git/pipeline/lfs_gogit.go @@ -7,12 +7,10 @@ package pipeline import ( "bufio" - "fmt" "io" "sort" "strings" "sync" - "time" "code.gitea.io/gitea/modules/git" @@ -21,23 +19,6 @@ import ( "github.com/go-git/go-git/v5/plumbing/object" ) -// LFSResult represents commits found using a provided pointer file hash -type LFSResult struct { - Name string - SHA string - Summary string - When time.Time - ParentHashes []git.ObjectID - BranchName string - FullCommitName string -} - -type lfsResultSlice []*LFSResult - -func (a lfsResultSlice) Len() int { return len(a) } -func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) } - // FindLFSFile finds commits that contain a provided pointer file hash func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, error) { resultsMap := map[string]*LFSResult{} @@ -51,7 +32,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err All: true, }) if err != nil { - return nil, fmt.Errorf("Failed to get GoGit CommitsIter. Error: %w", err) + return nil, lfsError("failed to get GoGit CommitsIter", err) } err = commitsIter.ForEach(func(gitCommit *object.Commit) error { @@ -85,7 +66,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err return nil }) if err != nil && err != io.EOF { - return nil, fmt.Errorf("Failure in CommitIter.ForEach: %w", err) + return nil, lfsError("failure in CommitIter.ForEach", err) } for _, result := range resultsMap { @@ -156,7 +137,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err select { case err, has := <-errChan: if has { - return nil, fmt.Errorf("Unable to obtain name for LFS files. Error: %w", err) + return nil, lfsError("unable to obtain name for LFS files", err) } default: } diff --git a/modules/git/pipeline/lfs_nogogit.go b/modules/git/pipeline/lfs_nogogit.go index fe320f39f3..349cfbd9ce 100644 --- a/modules/git/pipeline/lfs_nogogit.go +++ b/modules/git/pipeline/lfs_nogogit.go @@ -8,33 +8,14 @@ package pipeline import ( "bufio" "bytes" - "fmt" "io" "sort" "strings" "sync" - "time" "code.gitea.io/gitea/modules/git" ) -// LFSResult represents commits found using a provided pointer file hash -type LFSResult struct { - Name string - SHA string - Summary string - When time.Time - ParentIDs []git.ObjectID - BranchName string - FullCommitName string -} - -type lfsResultSlice []*LFSResult - -func (a lfsResultSlice) Len() int { return len(a) } -func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) } - // FindLFSFile finds commits that contain a provided pointer file hash func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, error) { resultsMap := map[string]*LFSResult{} @@ -137,11 +118,11 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err n += int64(count) if bytes.Equal(binObjectID, objectID.RawValue()) { result := LFSResult{ - Name: curPath + string(fname), - SHA: curCommit.ID.String(), - Summary: strings.Split(strings.TrimSpace(curCommit.CommitMessage), "\n")[0], - When: curCommit.Author.When, - ParentIDs: curCommit.Parents, + Name: curPath + string(fname), + SHA: curCommit.ID.String(), + Summary: strings.Split(strings.TrimSpace(curCommit.CommitMessage), "\n")[0], + When: curCommit.Author.When, + ParentHashes: curCommit.Parents, } resultsMap[curCommit.ID.String()+":"+curPath+string(fname)] = &result } else if string(mode) == git.EntryModeTree.String() { @@ -183,7 +164,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err for _, result := range resultsMap { hasParent := false - for _, parentID := range result.ParentIDs { + for _, parentID := range result.ParentHashes { if _, hasParent = resultsMap[parentID.String()+":"+result.Name]; hasParent { break } @@ -240,7 +221,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err select { case err, has := <-errChan: if has { - return nil, fmt.Errorf("Unable to obtain name for LFS files. Error: %w", err) + return nil, lfsError("unable to obtain name for LFS files", err) } default: } From 68a3e6b5e64b4035aa0659cb6daa1c4d1eec892a Mon Sep 17 00:00:00 2001 From: Yarden Shoham Date: Fri, 26 Apr 2024 10:27:34 +0300 Subject: [PATCH 16/20] Bump htmx version to 1.9.12 (#30711) There are no breaking changes. I tested and everything works as before. Signed-off-by: Yarden Shoham --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 61d86f6b7c..780689a0a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "esbuild-loader": "4.1.0", "escape-goat": "4.0.0", "fast-glob": "3.3.2", - "htmx.org": "1.9.11", + "htmx.org": "1.9.12", "idiomorph": "0.3.0", "jquery": "3.7.1", "katex": "0.16.10", @@ -6728,9 +6728,9 @@ } }, "node_modules/htmx.org": { - "version": "1.9.11", - "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.11.tgz", - "integrity": "sha512-WlVuICn8dfNOOgYmdYzYG8zSnP3++AdHkMHooQAzGZObWpVXYathpz/I37ycF4zikR6YduzfCvEcxk20JkIUsw==" + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.12.tgz", + "integrity": "sha512-VZAohXyF7xPGS52IM8d1T1283y+X4D+Owf3qY1NZ9RuBypyu9l8cGsxUMAG5fEAb/DhT7rDoJ9Hpu5/HxFD3cw==" }, "node_modules/human-signals": { "version": "5.0.0", diff --git a/package.json b/package.json index ff1ae4d49e..b0cb67ed4a 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "esbuild-loader": "4.1.0", "escape-goat": "4.0.0", "fast-glob": "3.3.2", - "htmx.org": "1.9.11", + "htmx.org": "1.9.12", "idiomorph": "0.3.0", "jquery": "3.7.1", "katex": "0.16.10", From 1e749b80d741c3a5c7eff6087d820e4d0d1ba3a2 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 26 Apr 2024 17:09:49 +0800 Subject: [PATCH 17/20] Add route handler info for debugging purpose (#30705) Follow #30519 --- routers/init.go | 6 ++++++ routers/web/web.go | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/routers/init.go b/routers/init.go index aaf95920c2..030ef3c740 100644 --- a/routers/init.go +++ b/routers/init.go @@ -5,6 +5,7 @@ package routers import ( "context" + "net/http" "reflect" "runtime" @@ -25,6 +26,7 @@ import ( "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/modules/web/routing" actions_router "code.gitea.io/gitea/routers/api/actions" packages_router "code.gitea.io/gitea/routers/api/packages" apiv1 "code.gitea.io/gitea/routers/api/v1" @@ -202,5 +204,9 @@ func NormalRoutes() *web.Route { r.Mount(prefix, actions_router.ArtifactsV4Routes(prefix)) } + r.NotFound(func(w http.ResponseWriter, req *http.Request) { + routing.UpdateFuncInfo(req.Context(), routing.GetFuncInfo(http.NotFound, "GlobalNotFound")) + http.NotFound(w, req) + }) return r } diff --git a/routers/web/web.go b/routers/web/web.go index c6132f0d61..9a6687059b 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1612,7 +1612,7 @@ func registerRoutes(m *web.Route) { m.NotFound(func(w http.ResponseWriter, req *http.Request) { ctx := context.GetWebContext(req) - routing.UpdateFuncInfo(ctx, routing.GetFuncInfo(ctx.NotFound, "GlobalNotFound")) + routing.UpdateFuncInfo(ctx, routing.GetFuncInfo(ctx.NotFound, "WebNotFound")) ctx.NotFound("", nil) }) } From cd70ab31cdee8116055819bf67bcf374e2aa6172 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 26 Apr 2024 17:49:48 +0800 Subject: [PATCH 18/20] Fix incorrect object id hash function (#30708) Great thanks to @oliverpool for figuring out the problem and proposing a fix. Regression of #28138 Incorrect hash causes the user's LFS files get all deleted when running `doctor fix all` (by the way, remove unused/non-standard comments) Co-authored-by: Giteabot --- modules/git/object_format.go | 16 ++++------------ modules/git/object_id.go | 3 --- modules/git/object_id_test.go | 4 ++++ 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/modules/git/object_format.go b/modules/git/object_format.go index a056b20e8a..3de9ff8cf4 100644 --- a/modules/git/object_format.go +++ b/modules/git/object_format.go @@ -33,7 +33,6 @@ type ObjectFormat interface { ComputeHash(t ObjectType, content []byte) ObjectID } -/* SHA1 Type */ type Sha1ObjectFormatImpl struct{} var ( @@ -70,14 +69,10 @@ func (h Sha1ObjectFormatImpl) ComputeHash(t ObjectType, content []byte) ObjectID _, _ = hasher.Write([]byte(" ")) _, _ = hasher.Write([]byte(strconv.FormatInt(int64(len(content)), 10))) _, _ = hasher.Write([]byte{0}) - - // HashSum generates a SHA1 for the provided hash - var sha1 Sha1Hash - copy(sha1[:], hasher.Sum(nil)) - return &sha1 + _, _ = hasher.Write(content) + return h.MustID(hasher.Sum(nil)) } -/* SHA256 Type */ type Sha256ObjectFormatImpl struct{} var ( @@ -116,11 +111,8 @@ func (h Sha256ObjectFormatImpl) ComputeHash(t ObjectType, content []byte) Object _, _ = hasher.Write([]byte(" ")) _, _ = hasher.Write([]byte(strconv.FormatInt(int64(len(content)), 10))) _, _ = hasher.Write([]byte{0}) - - // HashSum generates a SHA256 for the provided hash - var sha256 Sha1Hash - copy(sha256[:], hasher.Sum(nil)) - return &sha256 + _, _ = hasher.Write(content) + return h.MustID(hasher.Sum(nil)) } var ( diff --git a/modules/git/object_id.go b/modules/git/object_id.go index 4f8c39ee1d..33e5085005 100644 --- a/modules/git/object_id.go +++ b/modules/git/object_id.go @@ -16,7 +16,6 @@ type ObjectID interface { Type() ObjectFormat } -/* SHA1 */ type Sha1Hash [20]byte func (h *Sha1Hash) String() string { @@ -40,7 +39,6 @@ func MustIDFromString(hexHash string) ObjectID { return id } -/* SHA256 */ type Sha256Hash [32]byte func (h *Sha256Hash) String() string { @@ -54,7 +52,6 @@ func (h *Sha256Hash) IsZero() bool { func (h *Sha256Hash) RawValue() []byte { return h[:] } func (*Sha256Hash) Type() ObjectFormat { return Sha256ObjectFormat } -/* utility */ func NewIDFromString(hexHash string) (ObjectID, error) { var theObjectFormat ObjectFormat for _, objectFormat := range SupportedObjectFormats { diff --git a/modules/git/object_id_test.go b/modules/git/object_id_test.go index 1ad40096a0..03d0c85d87 100644 --- a/modules/git/object_id_test.go +++ b/modules/git/object_id_test.go @@ -18,4 +18,8 @@ func TestIsValidSHAPattern(t *testing.T) { assert.False(t, h.IsValid("abc")) assert.False(t, h.IsValid("123g")) assert.False(t, h.IsValid("some random text")) + assert.Equal(t, "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", ComputeBlobHash(Sha1ObjectFormat, nil).String()) + assert.Equal(t, "2e65efe2a145dda7ee51d1741299f848e5bf752e", ComputeBlobHash(Sha1ObjectFormat, []byte("a")).String()) + assert.Equal(t, "473a0f4c3be8a93681a267e3b1e9a7dcda1185436fe141f7749120a303721813", ComputeBlobHash(Sha256ObjectFormat, nil).String()) + assert.Equal(t, "eb337bcee2061c5313c9a1392116b6c76039e9e30d71467ae359b36277e17dc7", ComputeBlobHash(Sha256ObjectFormat, []byte("a")).String()) } From 993736d838c36e26951b6cfea9c6a549958addd1 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 26 Apr 2024 19:21:04 +0800 Subject: [PATCH 19/20] Fix code search input for different views (#30678) Now only show the "code search" on the repo home page, because it only does global search. So do not show it when viewing file or directory to avoid misleading users (it doesn't search in a directory) --- routers/web/repo/commit.go | 2 -- routers/web/repo/compare.go | 1 - routers/web/repo/pull.go | 1 - templates/repo/home.tmpl | 22 ++++++++++++---------- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 8543fa44cc..a2c6ac33e8 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -212,8 +212,6 @@ func SearchCommits(ctx *context.Context) { // FileHistory show a file's reversions func FileHistory(ctx *context.Context) { - ctx.Data["IsRepoToolbarCommits"] = true - fileName := ctx.Repo.TreePath if len(fileName) == 0 { Commits(ctx) diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 035a92f228..a55426dab5 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -800,7 +800,6 @@ func CompareDiff(ctx *context.Context) { } ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + separator + base.ShortSha(afterCommitID) - ctx.Data["IsRepoToolbarCommits"] = true ctx.Data["IsDiffCompare"] = true _, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates) diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index acdba4bcdc..7f131f2e98 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1225,7 +1225,6 @@ func CompareAndPullRequestPost(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.pulls.compare_changes") ctx.Data["PageIsComparePull"] = true ctx.Data["IsDiffCompare"] = true - ctx.Data["IsRepoToolbarCommits"] = true ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled upload.AddUploadContext(ctx, "comment") diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index 7b37ac1011..eb9eb9c149 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -90,7 +90,16 @@ {{ctx.Locale.Tr "repo.use_template"}} {{end}} - {{if (not $isHomepage)}} + {{if $isHomepage}} + {{/* only show the "code search" on the repo home page, it only does global search, + so do not show it when viewing file or directory to avoid misleading users (it doesn't search in a directory) */}} +
+
+ + {{template "shared/search/button"}} +
+
+ {{else}} {{StringUtils.EllipsisString .Repository.Name 30}} {{- range $i, $v := .TreeNames -}} @@ -103,13 +112,6 @@ {{- end -}} {{end}} - -
-
- - {{template "shared/search/button"}} -
-
@@ -136,7 +138,7 @@
{{template "repo/cite/cite_modal" .}} {{end}} - {{if and (not $isHomepage) (not .IsViewFile) (not .IsBlame)}} + {{if and (not $isHomepage) (not .IsViewFile) (not .IsBlame)}}{{/* IsViewDirectory (not home), TODO: split the templates, avoid using "if" tricks */}} {{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}} @@ -147,7 +149,7 @@ {{template "repo/view_file" .}} {{else if .IsBlame}} {{template "repo/blame" .}} - {{else}} + {{else}}{{/* IsViewDirectory */}} {{template "repo/view_list" .}} {{end}}
From 852547d0dc70299589c7bf8d00ea462ed709b8e5 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Fri, 26 Apr 2024 21:11:49 +0800 Subject: [PATCH 20/20] feat(api): enhance Actions Secrets Management API for repository (#30656) - Add endpoint to list repository action secrets in API routes - Implement `ListActionsSecrets` function to retrieve action secrets from the database - Update Swagger documentation to include the new `/repos/{owner}/{repo}/actions/secrets` endpoint - Add `actions` package import and define new routes for actions, secrets, variables, and runners in `api.go`. - Refactor action-related API functions into `Action` struct methods in `org/action.go` and `repo/action.go`. - Remove `actionAPI` struct and related functions, replacing them with `NewAction()` calls. - Rename `variables.go` to `action.go` in `org` directory. - Delete `runners.go` and `secrets.go` in both `org` and `repo` directories, consolidating their content into `action.go`. - Update copyright year and add new imports in `org/action.go`. - Implement `API` interface in `services/actions/interface.go` for action-related methods. - Remove individual action-related functions and replace them with methods on the `Action` struct in `repo/action.go`. --------- Signed-off-by: Bo-Yi Wu Signed-off-by: appleboy --- routers/api/v1/api.go | 80 ++++---- .../api/v1/org/{variables.go => action.go} | 192 +++++++++++++++++- routers/api/v1/org/runners.go | 31 --- routers/api/v1/org/secrets.go | 166 --------------- routers/api/v1/repo/action.go | 108 +++++++++- routers/api/v1/repo/runners.go | 34 ---- services/actions/interface.go | 28 +++ templates/swagger/v1_json.tmpl | 48 +++++ tests/integration/api_repo_secrets_test.go | 8 +- 9 files changed, 410 insertions(+), 285 deletions(-) rename routers/api/v1/org/{variables.go => action.go} (58%) delete mode 100644 routers/api/v1/org/runners.go delete mode 100644 routers/api/v1/org/secrets.go delete mode 100644 routers/api/v1/repo/runners.go create mode 100644 services/actions/interface.go diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 5358906f27..73071aa8df 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -93,6 +93,7 @@ import ( "code.gitea.io/gitea/routers/api/v1/settings" "code.gitea.io/gitea/routers/api/v1/user" "code.gitea.io/gitea/routers/common" + "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" @@ -835,6 +836,34 @@ func Routes() *web.Route { SignInRequired: setting.Service.RequireSignInView, })) + addActionsRoutes := func( + m *web.Route, + reqChecker func(ctx *context.APIContext), + act actions.API, + ) { + m.Group("/actions", func() { + m.Group("/secrets", func() { + m.Get("", reqToken(), reqChecker, act.ListActionsSecrets) + m.Combo("/{secretname}"). + Put(reqToken(), reqChecker, bind(api.CreateOrUpdateSecretOption{}), act.CreateOrUpdateSecret). + Delete(reqToken(), reqChecker, act.DeleteSecret) + }) + + m.Group("/variables", func() { + m.Get("", reqToken(), reqChecker, act.ListVariables) + m.Combo("/{variablename}"). + Get(reqToken(), reqChecker, act.GetVariable). + Delete(reqToken(), reqChecker, act.DeleteVariable). + Post(reqToken(), reqChecker, bind(api.CreateVariableOption{}), act.CreateVariable). + Put(reqToken(), reqChecker, bind(api.UpdateVariableOption{}), act.UpdateVariable) + }) + + m.Group("/runners", func() { + m.Get("/registration-token", reqToken(), reqChecker, act.GetRegistrationToken) + }) + }) + } + m.Group("", func() { // Miscellaneous (no scope required) if setting.API.EnableSwagger { @@ -1073,26 +1102,11 @@ func Routes() *web.Route { m.Post("/accept", repo.AcceptTransfer) m.Post("/reject", repo.RejectTransfer) }, reqToken()) - m.Group("/actions", func() { - m.Group("/secrets", func() { - m.Combo("/{secretname}"). - Put(reqToken(), reqOwner(), bind(api.CreateOrUpdateSecretOption{}), repo.CreateOrUpdateSecret). - Delete(reqToken(), reqOwner(), repo.DeleteSecret) - }) - - m.Group("/variables", func() { - m.Get("", reqToken(), reqOwner(), repo.ListVariables) - m.Combo("/{variablename}"). - Get(reqToken(), reqOwner(), repo.GetVariable). - Delete(reqToken(), reqOwner(), repo.DeleteVariable). - Post(reqToken(), reqOwner(), bind(api.CreateVariableOption{}), repo.CreateVariable). - Put(reqToken(), reqOwner(), bind(api.UpdateVariableOption{}), repo.UpdateVariable) - }) - - m.Group("/runners", func() { - m.Get("/registration-token", reqToken(), reqOwner(), repo.GetRegistrationToken) - }) - }) + addActionsRoutes( + m, + reqOwner(), + repo.NewAction(), + ) m.Group("/hooks/git", func() { m.Combo("").Get(repo.ListGitHooks) m.Group("/{id}", func() { @@ -1460,27 +1474,11 @@ func Routes() *web.Route { m.Combo("/{username}").Get(reqToken(), org.IsMember). Delete(reqToken(), reqOrgOwnership(), org.DeleteMember) }) - m.Group("/actions", func() { - m.Group("/secrets", func() { - m.Get("", reqToken(), reqOrgOwnership(), org.ListActionsSecrets) - m.Combo("/{secretname}"). - Put(reqToken(), reqOrgOwnership(), bind(api.CreateOrUpdateSecretOption{}), org.CreateOrUpdateSecret). - Delete(reqToken(), reqOrgOwnership(), org.DeleteSecret) - }) - - m.Group("/variables", func() { - m.Get("", reqToken(), reqOrgOwnership(), org.ListVariables) - m.Combo("/{variablename}"). - Get(reqToken(), reqOrgOwnership(), org.GetVariable). - Delete(reqToken(), reqOrgOwnership(), org.DeleteVariable). - Post(reqToken(), reqOrgOwnership(), bind(api.CreateVariableOption{}), org.CreateVariable). - Put(reqToken(), reqOrgOwnership(), bind(api.UpdateVariableOption{}), org.UpdateVariable) - }) - - m.Group("/runners", func() { - m.Get("/registration-token", reqToken(), reqOrgOwnership(), org.GetRegistrationToken) - }) - }) + addActionsRoutes( + m, + reqOrgOwnership(), + org.NewAction(), + ) m.Group("/public_members", func() { m.Get("", org.ListPublicMembers) m.Combo("/{username}").Get(org.IsPublicMember). diff --git a/routers/api/v1/org/variables.go b/routers/api/v1/org/action.go similarity index 58% rename from routers/api/v1/org/variables.go rename to routers/api/v1/org/action.go index eaf7bdc45b..03a1fa8ccc 100644 --- a/routers/api/v1/org/variables.go +++ b/routers/api/v1/org/action.go @@ -9,16 +9,188 @@ import ( actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" + secret_model "code.gitea.io/gitea/models/secret" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/shared" "code.gitea.io/gitea/routers/api/v1/utils" actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/context" + secret_service "code.gitea.io/gitea/services/secrets" ) +// ListActionsSecrets list an organization's actions secrets +func (Action) ListActionsSecrets(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/actions/secrets organization orgListActionsSecrets + // --- + // summary: List an organization's actions secrets + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/SecretList" + // "404": + // "$ref": "#/responses/notFound" + + opts := &secret_model.FindSecretsOptions{ + OwnerID: ctx.Org.Organization.ID, + ListOptions: utils.GetListOptions(ctx), + } + + secrets, count, err := db.FindAndCount[secret_model.Secret](ctx, opts) + if err != nil { + ctx.InternalServerError(err) + return + } + + apiSecrets := make([]*api.Secret, len(secrets)) + for k, v := range secrets { + apiSecrets[k] = &api.Secret{ + Name: v.Name, + Created: v.CreatedUnix.AsTime(), + } + } + + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, apiSecrets) +} + +// create or update one secret of the organization +func (Action) CreateOrUpdateSecret(ctx *context.APIContext) { + // swagger:operation PUT /orgs/{org}/actions/secrets/{secretname} organization updateOrgSecret + // --- + // summary: Create or Update a secret value in an organization + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of organization + // type: string + // required: true + // - name: secretname + // in: path + // description: name of the secret + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateOrUpdateSecretOption" + // responses: + // "201": + // description: response when creating a secret + // "204": + // description: response when updating a secret + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption) + + _, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Org.Organization.ID, 0, ctx.Params("secretname"), opt.Data) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "CreateOrUpdateSecret", err) + } else if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "CreateOrUpdateSecret", err) + } else { + ctx.Error(http.StatusInternalServerError, "CreateOrUpdateSecret", err) + } + return + } + + if created { + ctx.Status(http.StatusCreated) + } else { + ctx.Status(http.StatusNoContent) + } +} + +// DeleteSecret delete one secret of the organization +func (Action) DeleteSecret(ctx *context.APIContext) { + // swagger:operation DELETE /orgs/{org}/actions/secrets/{secretname} organization deleteOrgSecret + // --- + // summary: Delete a secret in an organization + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of organization + // type: string + // required: true + // - name: secretname + // in: path + // description: name of the secret + // type: string + // required: true + // responses: + // "204": + // description: delete one secret of the organization + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + err := secret_service.DeleteSecretByName(ctx, ctx.Org.Organization.ID, 0, ctx.Params("secretname")) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "DeleteSecret", err) + } else if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "DeleteSecret", err) + } else { + ctx.Error(http.StatusInternalServerError, "DeleteSecret", err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization +// GetRegistrationToken returns the token to register org runners +func (Action) GetRegistrationToken(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/actions/runners/registration-token organization orgGetRunnerRegistrationToken + // --- + // summary: Get an organization's actions runner registration token + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/RegistrationToken" + + shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0) +} + // ListVariables list org-level variables -func ListVariables(ctx *context.APIContext) { +func (Action) ListVariables(ctx *context.APIContext) { // swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList // --- // summary: Get an org-level variables list @@ -70,7 +242,7 @@ func ListVariables(ctx *context.APIContext) { } // GetVariable get an org-level variable -func GetVariable(ctx *context.APIContext) { +func (Action) GetVariable(ctx *context.APIContext) { // swagger:operation GET /orgs/{org}/actions/variables/{variablename} organization getOrgVariable // --- // summary: Get an org-level variable @@ -119,7 +291,7 @@ func GetVariable(ctx *context.APIContext) { } // DeleteVariable delete an org-level variable -func DeleteVariable(ctx *context.APIContext) { +func (Action) DeleteVariable(ctx *context.APIContext) { // swagger:operation DELETE /orgs/{org}/actions/variables/{variablename} organization deleteOrgVariable // --- // summary: Delete an org-level variable @@ -163,7 +335,7 @@ func DeleteVariable(ctx *context.APIContext) { } // CreateVariable create an org-level variable -func CreateVariable(ctx *context.APIContext) { +func (Action) CreateVariable(ctx *context.APIContext) { // swagger:operation POST /orgs/{org}/actions/variables/{variablename} organization createOrgVariable // --- // summary: Create an org-level variable @@ -227,7 +399,7 @@ func CreateVariable(ctx *context.APIContext) { } // UpdateVariable update an org-level variable -func UpdateVariable(ctx *context.APIContext) { +func (Action) UpdateVariable(ctx *context.APIContext) { // swagger:operation PUT /orgs/{org}/actions/variables/{variablename} organization updateOrgVariable // --- // summary: Update an org-level variable @@ -289,3 +461,13 @@ func UpdateVariable(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } + +var _ actions_service.API = new(Action) + +// Action implements actions_service.API +type Action struct{} + +// NewAction creates a new Action service +func NewAction() actions_service.API { + return Action{} +} diff --git a/routers/api/v1/org/runners.go b/routers/api/v1/org/runners.go deleted file mode 100644 index 2a52bd8778..0000000000 --- a/routers/api/v1/org/runners.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package org - -import ( - "code.gitea.io/gitea/routers/api/v1/shared" - "code.gitea.io/gitea/services/context" -) - -// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization - -// GetRegistrationToken returns the token to register org runners -func GetRegistrationToken(ctx *context.APIContext) { - // swagger:operation GET /orgs/{org}/actions/runners/registration-token organization orgGetRunnerRegistrationToken - // --- - // summary: Get an organization's actions runner registration token - // produces: - // - application/json - // parameters: - // - name: org - // in: path - // description: name of the organization - // type: string - // required: true - // responses: - // "200": - // "$ref": "#/responses/RegistrationToken" - - shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0) -} diff --git a/routers/api/v1/org/secrets.go b/routers/api/v1/org/secrets.go deleted file mode 100644 index abb6bb26c4..0000000000 --- a/routers/api/v1/org/secrets.go +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package org - -import ( - "errors" - "net/http" - - "code.gitea.io/gitea/models/db" - secret_model "code.gitea.io/gitea/models/secret" - api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/util" - "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/routers/api/v1/utils" - "code.gitea.io/gitea/services/context" - secret_service "code.gitea.io/gitea/services/secrets" -) - -// ListActionsSecrets list an organization's actions secrets -func ListActionsSecrets(ctx *context.APIContext) { - // swagger:operation GET /orgs/{org}/actions/secrets organization orgListActionsSecrets - // --- - // summary: List an organization's actions secrets - // produces: - // - application/json - // parameters: - // - name: org - // in: path - // description: name of the organization - // type: string - // required: true - // - name: page - // in: query - // description: page number of results to return (1-based) - // type: integer - // - name: limit - // in: query - // description: page size of results - // type: integer - // responses: - // "200": - // "$ref": "#/responses/SecretList" - // "404": - // "$ref": "#/responses/notFound" - - opts := &secret_model.FindSecretsOptions{ - OwnerID: ctx.Org.Organization.ID, - ListOptions: utils.GetListOptions(ctx), - } - - secrets, count, err := db.FindAndCount[secret_model.Secret](ctx, opts) - if err != nil { - ctx.InternalServerError(err) - return - } - - apiSecrets := make([]*api.Secret, len(secrets)) - for k, v := range secrets { - apiSecrets[k] = &api.Secret{ - Name: v.Name, - Created: v.CreatedUnix.AsTime(), - } - } - - ctx.SetTotalCountHeader(count) - ctx.JSON(http.StatusOK, apiSecrets) -} - -// create or update one secret of the organization -func CreateOrUpdateSecret(ctx *context.APIContext) { - // swagger:operation PUT /orgs/{org}/actions/secrets/{secretname} organization updateOrgSecret - // --- - // summary: Create or Update a secret value in an organization - // consumes: - // - application/json - // produces: - // - application/json - // parameters: - // - name: org - // in: path - // description: name of organization - // type: string - // required: true - // - name: secretname - // in: path - // description: name of the secret - // type: string - // required: true - // - name: body - // in: body - // schema: - // "$ref": "#/definitions/CreateOrUpdateSecretOption" - // responses: - // "201": - // description: response when creating a secret - // "204": - // description: response when updating a secret - // "400": - // "$ref": "#/responses/error" - // "404": - // "$ref": "#/responses/notFound" - - opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption) - - _, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Org.Organization.ID, 0, ctx.Params("secretname"), opt.Data) - if err != nil { - if errors.Is(err, util.ErrInvalidArgument) { - ctx.Error(http.StatusBadRequest, "CreateOrUpdateSecret", err) - } else if errors.Is(err, util.ErrNotExist) { - ctx.Error(http.StatusNotFound, "CreateOrUpdateSecret", err) - } else { - ctx.Error(http.StatusInternalServerError, "CreateOrUpdateSecret", err) - } - return - } - - if created { - ctx.Status(http.StatusCreated) - } else { - ctx.Status(http.StatusNoContent) - } -} - -// DeleteSecret delete one secret of the organization -func DeleteSecret(ctx *context.APIContext) { - // swagger:operation DELETE /orgs/{org}/actions/secrets/{secretname} organization deleteOrgSecret - // --- - // summary: Delete a secret in an organization - // consumes: - // - application/json - // produces: - // - application/json - // parameters: - // - name: org - // in: path - // description: name of organization - // type: string - // required: true - // - name: secretname - // in: path - // description: name of the secret - // type: string - // required: true - // responses: - // "204": - // description: delete one secret of the organization - // "400": - // "$ref": "#/responses/error" - // "404": - // "$ref": "#/responses/notFound" - - err := secret_service.DeleteSecretByName(ctx, ctx.Org.Organization.ID, 0, ctx.Params("secretname")) - if err != nil { - if errors.Is(err, util.ErrInvalidArgument) { - ctx.Error(http.StatusBadRequest, "DeleteSecret", err) - } else if errors.Is(err, util.ErrNotExist) { - ctx.Error(http.StatusNotFound, "DeleteSecret", err) - } else { - ctx.Error(http.StatusInternalServerError, "DeleteSecret", err) - } - return - } - - ctx.Status(http.StatusNoContent) -} diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 03321d956d..311cfca6e9 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -9,17 +9,76 @@ import ( actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" + secret_model "code.gitea.io/gitea/models/secret" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/shared" "code.gitea.io/gitea/routers/api/v1/utils" actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/context" secret_service "code.gitea.io/gitea/services/secrets" ) +// ListActionsSecrets list an repo's actions secrets +func (Action) ListActionsSecrets(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/secrets repository repoListActionsSecrets + // --- + // summary: List an repo's actions secrets + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repository + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/SecretList" + // "404": + // "$ref": "#/responses/notFound" + + repo := ctx.Repo.Repository + + opts := &secret_model.FindSecretsOptions{ + RepoID: repo.ID, + ListOptions: utils.GetListOptions(ctx), + } + + secrets, count, err := db.FindAndCount[secret_model.Secret](ctx, opts) + if err != nil { + ctx.InternalServerError(err) + return + } + + apiSecrets := make([]*api.Secret, len(secrets)) + for k, v := range secrets { + apiSecrets[k] = &api.Secret{ + Name: v.Name, + Created: v.CreatedUnix.AsTime(), + } + } + + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, apiSecrets) +} + // create or update one secret of the repository -func CreateOrUpdateSecret(ctx *context.APIContext) { +func (Action) CreateOrUpdateSecret(ctx *context.APIContext) { // swagger:operation PUT /repos/{owner}/{repo}/actions/secrets/{secretname} repository updateRepoSecret // --- // summary: Create or Update a secret value in a repository @@ -82,7 +141,7 @@ func CreateOrUpdateSecret(ctx *context.APIContext) { } // DeleteSecret delete one secret of the repository -func DeleteSecret(ctx *context.APIContext) { +func (Action) DeleteSecret(ctx *context.APIContext) { // swagger:operation DELETE /repos/{owner}/{repo}/actions/secrets/{secretname} repository deleteRepoSecret // --- // summary: Delete a secret in a repository @@ -133,7 +192,7 @@ func DeleteSecret(ctx *context.APIContext) { } // GetVariable get a repo-level variable -func GetVariable(ctx *context.APIContext) { +func (Action) GetVariable(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/actions/variables/{variablename} repository getRepoVariable // --- // summary: Get a repo-level variable @@ -186,7 +245,7 @@ func GetVariable(ctx *context.APIContext) { } // DeleteVariable delete a repo-level variable -func DeleteVariable(ctx *context.APIContext) { +func (Action) DeleteVariable(ctx *context.APIContext) { // swagger:operation DELETE /repos/{owner}/{repo}/actions/variables/{variablename} repository deleteRepoVariable // --- // summary: Delete a repo-level variable @@ -235,7 +294,7 @@ func DeleteVariable(ctx *context.APIContext) { } // CreateVariable create a repo-level variable -func CreateVariable(ctx *context.APIContext) { +func (Action) CreateVariable(ctx *context.APIContext) { // swagger:operation POST /repos/{owner}/{repo}/actions/variables/{variablename} repository createRepoVariable // --- // summary: Create a repo-level variable @@ -302,7 +361,7 @@ func CreateVariable(ctx *context.APIContext) { } // UpdateVariable update a repo-level variable -func UpdateVariable(ctx *context.APIContext) { +func (Action) UpdateVariable(ctx *context.APIContext) { // swagger:operation PUT /repos/{owner}/{repo}/actions/variables/{variablename} repository updateRepoVariable // --- // summary: Update a repo-level variable @@ -369,7 +428,7 @@ func UpdateVariable(ctx *context.APIContext) { } // ListVariables list repo-level variables -func ListVariables(ctx *context.APIContext) { +func (Action) ListVariables(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/actions/variables repository getRepoVariablesList // --- // summary: Get repo-level variables list @@ -423,3 +482,38 @@ func ListVariables(ctx *context.APIContext) { ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, variables) } + +// GetRegistrationToken returns the token to register repo runners +func (Action) GetRegistrationToken(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/runners/registration-token repository repoGetRunnerRegistrationToken + // --- + // summary: Get a repository's actions runner registration token + // 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/RegistrationToken" + + shared.GetRegistrationToken(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID) +} + +var _ actions_service.API = new(Action) + +// Action implements actions_service.API +type Action struct{} + +// NewAction creates a new Action service +func NewAction() actions_service.API { + return Action{} +} diff --git a/routers/api/v1/repo/runners.go b/routers/api/v1/repo/runners.go deleted file mode 100644 index fe133b311d..0000000000 --- a/routers/api/v1/repo/runners.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package repo - -import ( - "code.gitea.io/gitea/routers/api/v1/shared" - "code.gitea.io/gitea/services/context" -) - -// GetRegistrationToken returns the token to register repo runners -func GetRegistrationToken(ctx *context.APIContext) { - // swagger:operation GET /repos/{owner}/{repo}/runners/registration-token repository repoGetRunnerRegistrationToken - // --- - // summary: Get a repository's actions runner registration token - // 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/RegistrationToken" - - shared.GetRegistrationToken(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID) -} diff --git a/services/actions/interface.go b/services/actions/interface.go new file mode 100644 index 0000000000..d4fa782fec --- /dev/null +++ b/services/actions/interface.go @@ -0,0 +1,28 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import "code.gitea.io/gitea/services/context" + +// API for actions of a repository or organization +type API interface { + // ListActionsSecrets list secrets + ListActionsSecrets(*context.APIContext) + // CreateOrUpdateSecret create or update a secret + CreateOrUpdateSecret(*context.APIContext) + // DeleteSecret delete a secret + DeleteSecret(*context.APIContext) + // ListVariables list variables + ListVariables(*context.APIContext) + // GetVariable get a variable + GetVariable(*context.APIContext) + // DeleteVariable delete a variable + DeleteVariable(*context.APIContext) + // CreateVariable create a variable + CreateVariable(*context.APIContext) + // UpdateVariable update a variable + UpdateVariable(*context.APIContext) + // GetRegistrationToken get registration token + GetRegistrationToken(*context.APIContext) +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index faf57454d7..3ed4e43e6d 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -3843,6 +3843,54 @@ } } }, + "/repos/{owner}/{repo}/actions/secrets": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "List an repo's actions secrets", + "operationId": "repoListActionsSecrets", + "parameters": [ + { + "type": "string", + "description": "owner of the repository", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/SecretList" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/actions/secrets/{secretname}": { "put": { "consumes": [ diff --git a/tests/integration/api_repo_secrets_test.go b/tests/integration/api_repo_secrets_test.go index feb9bae2b2..c3074d9ece 100644 --- a/tests/integration/api_repo_secrets_test.go +++ b/tests/integration/api_repo_secrets_test.go @@ -24,6 +24,12 @@ func TestAPIRepoSecrets(t *testing.T) { session := loginUser(t, user.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + t.Run("List", func(t *testing.T) { + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/secrets", repo.FullName())). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusOK) + }) + t.Run("Create", func(t *testing.T) { cases := []struct { Name string @@ -31,7 +37,7 @@ func TestAPIRepoSecrets(t *testing.T) { }{ { Name: "", - ExpectedStatus: http.StatusNotFound, + ExpectedStatus: http.StatusMethodNotAllowed, }, { Name: "-",