From 4f4eedc175b71723c9aec0031bab9773d492afd2 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 29 May 2024 00:10:32 +0000 Subject: [PATCH 01/49] Update dependency @github/text-expander-element to v2.7.1 --- package-lock.json | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index dcfb6f75c9..eee334aebf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@citation-js/plugin-software-formats": "0.6.1", "@github/markdown-toolbar-element": "2.2.3", "@github/relative-time-element": "4.4.0", - "@github/text-expander-element": "2.7.0", + "@github/text-expander-element": "2.7.1", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@primer/octicons": "19.9.0", "add-asset-webpack-plugin": "2.0.1", @@ -1003,13 +1003,13 @@ "integrity": "sha512-CrI6oAecoahG7PF5dsgjdvlF5kCtusVMjg810EULD81TvnDsP+k/FRi/ClFubWLgBo4EGpr2EfvmumtqQFo7ow==" }, "node_modules/@github/text-expander-element": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@github/text-expander-element/-/text-expander-element-2.7.0.tgz", - "integrity": "sha512-zeo7l2L91o6yuGHJfA1Xtpg6UgDuZGq0WCgplDwd+54pVIsNzwsynIo6oTjE03cCtqLQpdYRe1wSQxyKYZOoGw==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@github/text-expander-element/-/text-expander-element-2.7.1.tgz", + "integrity": "sha512-CWxfYxJRkeWVCUhJveproLs6pHsPrWtK8TsjL8ByYVcSCs8CJmNzF8b7ZawrUgfai0F2jb4aIdw2FoBTykj9XA==", "license": "MIT", "dependencies": { "@github/combobox-nav": "^2.0.2", - "dom-input-range": "^1.1.3" + "dom-input-range": "^1.1.6" } }, "node_modules/@humanwhocodes/config-array": { @@ -4813,9 +4813,9 @@ } }, "node_modules/dom-input-range": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/dom-input-range/-/dom-input-range-1.1.5.tgz", - "integrity": "sha512-ITURvugfDoy8Wk8JC6NoI4dKyLPR4qbFnXJ+V+qVpQtTmDgT8HZjH2iNUIMiEU1kkdWEMLgDxYTSXJnPz9aeiA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/dom-input-range/-/dom-input-range-1.1.6.tgz", + "integrity": "sha512-4o/SkTpscD0n81BeErrrtmE58lG8vTks++92vk//ld0NmkQTb4AVJ2rexh2yor6rtBf5IMte26u+fF3EgCppPQ==", "license": "MIT", "workspaces": [ "demos" diff --git a/package.json b/package.json index cd89ca3c5f..2f266b5169 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@citation-js/plugin-software-formats": "0.6.1", "@github/markdown-toolbar-element": "2.2.3", "@github/relative-time-element": "4.4.0", - "@github/text-expander-element": "2.7.0", + "@github/text-expander-element": "2.7.1", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@primer/octicons": "19.9.0", "add-asset-webpack-plugin": "2.0.1", From 288075c72433b2d5948f24b1820f7ada8aabd0d8 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 1 Jun 2024 02:07:47 +0000 Subject: [PATCH 02/49] Update module github.com/golangci/golangci-lint/cmd/golangci-lint to v1.59.0 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d6838a7adf..e6686bafe7 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ XGO_VERSION := go-1.21.x AIR_PACKAGE ?= github.com/cosmtrek/air@v1 # renovate: datasource=go EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v2/cmd/editorconfig-checker@2.8.0 # renovate: datasource=go GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.6.0 # renovate: datasource=go -GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.58.2 # renovate: datasource=go +GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.59.0 # renovate: datasource=go GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11 # renovate: datasource=go MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.5.1 # renovate: datasource=go SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.31.0 # renovate: datasource=go From 6d712590f1741248bfa6e45d307379f2a89c5b75 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Sat, 1 Jun 2024 16:17:07 +0200 Subject: [PATCH 03/49] chore(lint): make golangci-lint to v1.59.0 happy --- services/gitdiff/gitdiff.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index c4430339e2..e6cae04db4 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -1057,7 +1057,7 @@ func readFileName(rd *strings.Reader) (string, bool) { char, _ := rd.ReadByte() _ = rd.UnreadByte() if char == '"' { - fmt.Fscanf(rd, "%q ", &name) + _, _ = fmt.Fscanf(rd, "%q ", &name) if len(name) == 0 { log.Error("Reader has no file name: reader=%+v", rd) return "", true @@ -1069,12 +1069,12 @@ func readFileName(rd *strings.Reader) (string, bool) { } else { // This technique is potentially ambiguous it may not be possible to uniquely identify the filenames from the diff line alone ambiguity = true - fmt.Fscanf(rd, "%s ", &name) + _, _ = fmt.Fscanf(rd, "%s ", &name) char, _ := rd.ReadByte() _ = rd.UnreadByte() for !(char == 0 || char == '"' || char == 'b') { var suffix string - fmt.Fscanf(rd, "%s ", &suffix) + _, _ = fmt.Fscanf(rd, "%s ", &suffix) name += " " + suffix char, _ = rd.ReadByte() _ = rd.UnreadByte() From 1084362f7c53877a912ca9cec7effed52b586a31 Mon Sep 17 00:00:00 2001 From: 0ko <0ko@noreply.codeberg.org> Date: Sat, 1 Jun 2024 15:20:40 +0000 Subject: [PATCH 04/49] CSS improvements around commit status icons (#3948) - make sure margins are all consistent and good, elements are not too close or too apart - this also applies to "Show commit body" button - remove unused code. The class `commit-status-link` doesn't exist in templates, nor I could find it on any related pages in case it's generated in runtime ## Preview ![](/attachments/9cf6d73a-8132-4f30-8094-5687d7dd98e9) Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3948 Reviewed-by: Beowulf --- web_src/css/repo.css | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 91a0c8c336..066e81f63c 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -185,6 +185,14 @@ td .commit-summary { white-space: normal; } +.commit-list .js-toggle-commit-body { + margin: 0 0 0 0.25em; +} + +.commit-list .commit-status { + margin: 0 0 0 0.25em; +} + .latest-commit { display: flex; flex: 1; @@ -193,6 +201,10 @@ td .commit-summary { text-overflow: ellipsis; } +.latest-commit .commit-status { + margin-right: 0.25em; +} + @media (max-width: 767.98px) { .latest-commit .sha { display: none; @@ -876,12 +888,6 @@ td .commit-summary { background: var(--color-orange-badge-hover-bg) !important; } -.repository.view.issue .comment-list .timeline-item.event > .commit-status-link { - float: right; - margin-right: 8px; - margin-top: 4px; -} - .repository.view.issue .comment-list .timeline-item .comparebox { line-height: 32px; vertical-align: middle; @@ -2467,11 +2473,6 @@ tbody.commit-list { } } -.commit-list .commit-status-link { - display: inline-block; - vertical-align: middle; -} - .commit-body { margin: 0.25em 0; white-space: pre-wrap; From 4c2ed3c35d52bb897f143e4c0f3f0fd2ae17289a Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Sat, 1 Jun 2024 13:57:35 +0200 Subject: [PATCH 05/49] test(integration): add t.Helper() to reduce stack polution Without the a testify stack is likely to not show the relevant test. --- tests/integration/git_helper_for_declarative_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/integration/git_helper_for_declarative_test.go b/tests/integration/git_helper_for_declarative_test.go index ff06dab07a..3c05932955 100644 --- a/tests/integration/git_helper_for_declarative_test.go +++ b/tests/integration/git_helper_for_declarative_test.go @@ -89,6 +89,7 @@ func onGiteaRun[T testing.TB](t T, callback func(T, *url.URL)) { func doGitClone(dstLocalPath string, u *url.URL) func(*testing.T) { return func(t *testing.T) { + t.Helper() assert.NoError(t, git.CloneWithArgs(context.Background(), git.AllowLFSFiltersArgs(), u.String(), dstLocalPath, git.CloneRepoOptions{})) exist, err := util.IsExist(filepath.Join(dstLocalPath, "README.md")) assert.NoError(t, err) @@ -98,6 +99,7 @@ func doGitClone(dstLocalPath string, u *url.URL) func(*testing.T) { func doPartialGitClone(dstLocalPath string, u *url.URL) func(*testing.T) { return func(t *testing.T) { + t.Helper() assert.NoError(t, git.CloneWithArgs(context.Background(), git.AllowLFSFiltersArgs(), u.String(), dstLocalPath, git.CloneRepoOptions{ Filter: "blob:none", })) @@ -109,6 +111,7 @@ func doPartialGitClone(dstLocalPath string, u *url.URL) func(*testing.T) { func doGitCloneFail(u *url.URL) func(*testing.T) { return func(t *testing.T) { + t.Helper() tmpDir := t.TempDir() assert.Error(t, git.Clone(git.DefaultContext, u.String(), tmpDir, git.CloneRepoOptions{})) exist, err := util.IsExist(filepath.Join(tmpDir, "README.md")) @@ -119,6 +122,7 @@ func doGitCloneFail(u *url.URL) func(*testing.T) { func doGitInitTestRepository(dstPath string, objectFormat git.ObjectFormat) func(*testing.T) { return func(t *testing.T) { + t.Helper() // Init repository in dstPath assert.NoError(t, git.InitRepository(git.DefaultContext, dstPath, false, objectFormat.Name())) // forcibly set default branch to master @@ -141,6 +145,7 @@ func doGitInitTestRepository(dstPath string, objectFormat git.ObjectFormat) func func doGitAddRemote(dstPath, remoteName string, u *url.URL) func(*testing.T) { return func(t *testing.T) { + t.Helper() _, _, err := git.NewCommand(git.DefaultContext, "remote", "add").AddDynamicArguments(remoteName, u.String()).RunStdString(&git.RunOpts{Dir: dstPath}) assert.NoError(t, err) } @@ -156,6 +161,7 @@ func doGitPushTestRepository(dstPath string, args ...string) func(*testing.T) { func doGitPushTestRepositoryFail(dstPath string, args ...string) func(*testing.T) { return func(t *testing.T) { + t.Helper() _, _, err := git.NewCommand(git.DefaultContext, "push").AddArguments(git.ToTrustedCmdArgs(args)...).RunStdString(&git.RunOpts{Dir: dstPath}) assert.Error(t, err) } @@ -163,6 +169,7 @@ func doGitPushTestRepositoryFail(dstPath string, args ...string) func(*testing.T func doGitCreateBranch(dstPath, branch string) func(*testing.T) { return func(t *testing.T) { + t.Helper() _, _, err := git.NewCommand(git.DefaultContext, "checkout", "-b").AddDynamicArguments(branch).RunStdString(&git.RunOpts{Dir: dstPath}) assert.NoError(t, err) } @@ -170,6 +177,7 @@ func doGitCreateBranch(dstPath, branch string) func(*testing.T) { func doGitCheckoutBranch(dstPath string, args ...string) func(*testing.T) { return func(t *testing.T) { + t.Helper() _, _, err := git.NewCommandContextNoGlobals(git.DefaultContext, git.AllowLFSFiltersArgs()...).AddArguments("checkout").AddArguments(git.ToTrustedCmdArgs(args)...).RunStdString(&git.RunOpts{Dir: dstPath}) assert.NoError(t, err) } @@ -177,6 +185,7 @@ func doGitCheckoutBranch(dstPath string, args ...string) func(*testing.T) { func doGitMerge(dstPath string, args ...string) func(*testing.T) { return func(t *testing.T) { + t.Helper() _, _, err := git.NewCommand(git.DefaultContext, "merge").AddArguments(git.ToTrustedCmdArgs(args)...).RunStdString(&git.RunOpts{Dir: dstPath}) assert.NoError(t, err) } @@ -184,6 +193,7 @@ func doGitMerge(dstPath string, args ...string) func(*testing.T) { func doGitPull(dstPath string, args ...string) func(*testing.T) { return func(t *testing.T) { + t.Helper() _, _, err := git.NewCommandContextNoGlobals(git.DefaultContext, git.AllowLFSFiltersArgs()...).AddArguments("pull").AddArguments(git.ToTrustedCmdArgs(args)...).RunStdString(&git.RunOpts{Dir: dstPath}) assert.NoError(t, err) } From 70aa294cc18c60982f7c5afe7338f269d8165f61 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Sat, 1 Jun 2024 10:35:01 +0200 Subject: [PATCH 06/49] test(integration): refactor doProtectBranch explicitly specify the parameters instead of providing them as arguments so the caller has a more fine grain control over them. --- tests/integration/git_test.go | 54 +++++++++++++++++------------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/tests/integration/git_test.go b/tests/integration/git_test.go index 16f6ea04f8..53c6e29359 100644 --- a/tests/integration/git_test.go +++ b/tests/integration/git_test.go @@ -368,7 +368,7 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes t.Run("PushProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected")) ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository) - t.Run("ProtectProtectedBranchNoWhitelist", doProtectBranch(ctx, "protected", "", "")) + t.Run("ProtectProtectedBranchNoWhitelist", doProtectBranch(ctx, "protected")) t.Run("GenerateCommit", func(t *testing.T) { _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") assert.NoError(t, err) @@ -395,14 +395,22 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes t.Run("MergePR", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) t.Run("PullProtected", doGitPull(dstPath, "origin", "protected")) - t.Run("ProtectProtectedBranchUnprotectedFilePaths", doProtectBranch(ctx, "protected", "", "unprotected-file-*")) + t.Run("ProtectProtectedBranchUnprotectedFilePaths", doProtectBranch(ctx, "protected", parameterProtectBranch{ + "unprotected_file_patterns": "unprotected-file-*", + })) t.Run("GenerateCommit", func(t *testing.T) { _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "unprotected-file-") assert.NoError(t, err) }) t.Run("PushUnprotectedFilesToProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected")) - t.Run("ProtectProtectedBranchWhitelist", doProtectBranch(ctx, "protected", baseCtx.Username, "")) + user, err := user_model.GetUserByName(db.DefaultContext, baseCtx.Username) + assert.NoError(t, err) + t.Run("ProtectProtectedBranchWhitelist", doProtectBranch(ctx, "protected", parameterProtectBranch{ + "enable_push": "whitelist", + "enable_whitelist": "on", + "whitelist_users": strconv.FormatInt(user.ID, 10), + })) t.Run("CheckoutMaster", doGitCheckoutBranch(dstPath, "master")) t.Run("CreateBranchForced", doGitCreateBranch(dstPath, "toforce")) @@ -417,7 +425,9 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes } } -func doProtectBranch(ctx APITestContext, branch, userToWhitelist, unprotectedFilePatterns string) func(t *testing.T) { +type parameterProtectBranch map[string]string + +func doProtectBranch(ctx APITestContext, branch string, addParameter ...parameterProtectBranch) func(t *testing.T) { // We are going to just use the owner to set the protection. return func(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: ctx.Reponame, OwnerName: ctx.Username}) @@ -426,30 +436,20 @@ func doProtectBranch(ctx APITestContext, branch, userToWhitelist, unprotectedFil csrf := GetCSRF(t, ctx.Session, fmt.Sprintf("/%s/%s/settings/branches", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame))) - if userToWhitelist == "" { - // Change branch to protected - req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/edit", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), map[string]string{ - "_csrf": csrf, - "rule_id": strconv.FormatInt(rule.ID, 10), - "rule_name": branch, - "unprotected_file_patterns": unprotectedFilePatterns, - }) - ctx.Session.MakeRequest(t, req, http.StatusSeeOther) - } else { - user, err := user_model.GetUserByName(db.DefaultContext, userToWhitelist) - assert.NoError(t, err) - // Change branch to protected - req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/edit", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), map[string]string{ - "_csrf": csrf, - "rule_name": branch, - "rule_id": strconv.FormatInt(rule.ID, 10), - "enable_push": "whitelist", - "enable_whitelist": "on", - "whitelist_users": strconv.FormatInt(user.ID, 10), - "unprotected_file_patterns": unprotectedFilePatterns, - }) - ctx.Session.MakeRequest(t, req, http.StatusSeeOther) + parameter := parameterProtectBranch{ + "_csrf": csrf, + "rule_id": strconv.FormatInt(rule.ID, 10), + "rule_name": branch, } + if len(addParameter) > 0 { + for k, v := range addParameter[0] { + parameter[k] = v + } + } + + // Change branch to protected + req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/edit", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), parameter) + ctx.Session.MakeRequest(t, req, http.StatusSeeOther) // Check if master branch has been locked successfully flashCookie := ctx.Session.GetCookie(gitea_context.CookieNameFlash) assert.NotNil(t, flashCookie) From 49aea9879bf9cda05ff6d8e41169b10f21b7867a Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Sat, 1 Jun 2024 10:37:35 +0200 Subject: [PATCH 07/49] test(integration): refactor doAPIMergePullRequest * http.StatusMethodNotAllowed can be expected: only retry if the error message is "Please try again later" * split into doAPIMergePullRequestForm which can be called directly if the caller wants to specify extra parameters. --- .../api_helper_for_declarative_test.go | 74 +++++++++++-------- .../git_helper_for_declarative_test.go | 8 -- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/tests/integration/api_helper_for_declarative_test.go b/tests/integration/api_helper_for_declarative_test.go index 3e54e2fe3f..1aceda8241 100644 --- a/tests/integration/api_helper_for_declarative_test.go +++ b/tests/integration/api_helper_for_declarative_test.go @@ -257,41 +257,51 @@ func doAPIGetPullRequest(ctx APITestContext, owner, repo string, index int64) fu func doAPIMergePullRequest(ctx APITestContext, owner, repo string, index int64) func(*testing.T) { return func(t *testing.T) { - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", owner, repo, index) - - var req *RequestWrapper - var resp *httptest.ResponseRecorder - - for i := 0; i < 6; i++ { - req = NewRequestWithJSON(t, http.MethodPost, urlStr, &forms.MergePullRequestForm{ - MergeMessageField: "doAPIMergePullRequest Merge", - Do: string(repo_model.MergeStyleMerge), - }).AddTokenAuth(ctx.Token) - - resp = ctx.Session.MakeRequest(t, req, NoExpectedStatus) - - if resp.Code != http.StatusMethodNotAllowed { - break - } - err := api.APIError{} - DecodeJSON(t, resp, &err) - assert.EqualValues(t, "Please try again later", err.Message) - queue.GetManager().FlushAll(context.Background(), 5*time.Second) - <-time.After(1 * time.Second) - } - - expected := ctx.ExpectedCode - if expected == 0 { - expected = http.StatusOK - } - - if !assert.EqualValues(t, expected, resp.Code, - "Request: %s %s", req.Method, req.URL.String()) { - logUnexpectedResponse(t, resp) - } + t.Helper() + doAPIMergePullRequestForm(t, ctx, owner, repo, index, &forms.MergePullRequestForm{ + MergeMessageField: "doAPIMergePullRequest Merge", + Do: string(repo_model.MergeStyleMerge), + }) } } +func doAPIMergePullRequestForm(t *testing.T, ctx APITestContext, owner, repo string, index int64, merge *forms.MergePullRequestForm) *httptest.ResponseRecorder { + t.Helper() + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", owner, repo, index) + + var req *RequestWrapper + var resp *httptest.ResponseRecorder + + for i := 0; i < 6; i++ { + req = NewRequestWithJSON(t, http.MethodPost, urlStr, merge).AddTokenAuth(ctx.Token) + + resp = ctx.Session.MakeRequest(t, req, NoExpectedStatus) + + if resp.Code != http.StatusMethodNotAllowed { + break + } + err := api.APIError{} + DecodeJSON(t, resp, &err) + if err.Message != "Please try again later" { + break + } + queue.GetManager().FlushAll(context.Background(), 5*time.Second) + <-time.After(1 * time.Second) + } + + expected := ctx.ExpectedCode + if expected == 0 { + expected = http.StatusOK + } + + if !assert.EqualValues(t, expected, resp.Code, + "Request: %s %s", req.Method, req.URL.String()) { + logUnexpectedResponse(t, resp) + } + + return resp +} + func doAPIManuallyMergePullRequest(ctx APITestContext, owner, repo, commitID string, index int64) func(*testing.T) { return func(t *testing.T) { urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", owner, repo, index) diff --git a/tests/integration/git_helper_for_declarative_test.go b/tests/integration/git_helper_for_declarative_test.go index 3c05932955..e9df1d70a4 100644 --- a/tests/integration/git_helper_for_declarative_test.go +++ b/tests/integration/git_helper_for_declarative_test.go @@ -183,14 +183,6 @@ func doGitCheckoutBranch(dstPath string, args ...string) func(*testing.T) { } } -func doGitMerge(dstPath string, args ...string) func(*testing.T) { - return func(t *testing.T) { - t.Helper() - _, _, err := git.NewCommand(git.DefaultContext, "merge").AddArguments(git.ToTrustedCmdArgs(args)...).RunStdString(&git.RunOpts{Dir: dstPath}) - assert.NoError(t, err) - } -} - func doGitPull(dstPath string, args ...string) func(*testing.T) { return func(t *testing.T) { t.Helper() From 20591d966e48ae7dfc146398952b89f8b6a3f897 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Sat, 1 Jun 2024 22:18:17 +0200 Subject: [PATCH 08/49] test(integration): refactor testPullMerge * split into testPullMergeForm which can be called directly if the caller wants to specify extra parameters. * testPullMergeForm can expect something different than StatusOK --- tests/integration/pull_merge_test.go | 33 +++++++++++++++++++--------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index 1d86d9e106..05807cc242 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -45,7 +45,20 @@ import ( "github.com/stretchr/testify/assert" ) +type optionsPullMerge map[string]string + func testPullMerge(t *testing.T, session *TestSession, user, repo, pullnum string, mergeStyle repo_model.MergeStyle, deleteBranch bool) *httptest.ResponseRecorder { + options := optionsPullMerge{ + "do": string(mergeStyle), + } + if deleteBranch { + options["delete_branch_after_merge"] = "on" + } + + return testPullMergeForm(t, session, http.StatusOK, user, repo, pullnum, options) +} + +func testPullMergeForm(t *testing.T, session *TestSession, expectedCode int, user, repo, pullnum string, addOptions optionsPullMerge) *httptest.ResponseRecorder { req := NewRequest(t, "GET", path.Join(user, repo, "pulls", pullnum)) resp := session.MakeRequest(t, req, http.StatusOK) @@ -54,22 +67,22 @@ func testPullMerge(t *testing.T, session *TestSession, user, repo, pullnum strin options := map[string]string{ "_csrf": htmlDoc.GetCSRF(), - "do": string(mergeStyle), } - - if deleteBranch { - options["delete_branch_after_merge"] = "on" + for k, v := range addOptions { + options[k] = v } req = NewRequestWithValues(t, "POST", link, options) - resp = session.MakeRequest(t, req, http.StatusOK) + resp = session.MakeRequest(t, req, expectedCode) - respJSON := struct { - Redirect string - }{} - DecodeJSON(t, resp, &respJSON) + if expectedCode == http.StatusOK { + respJSON := struct { + Redirect string + }{} + DecodeJSON(t, resp, &respJSON) - assert.EqualValues(t, fmt.Sprintf("/%s/%s/pulls/%s", user, repo, pullnum), respJSON.Redirect) + assert.EqualValues(t, fmt.Sprintf("/%s/%s/pulls/%s", user, repo, pullnum), respJSON.Redirect) + } return resp } From 0d8478b82e0a4d24102a582526e4028ba4329d2a Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Sat, 1 Jun 2024 14:13:30 +0200 Subject: [PATCH 09/49] test(integration): refactor doBranchProtectPRMerge * group test cases to clarify their purpose * remove pull request branch protection tests, they are redundant with TestPullMergeBranchProtect --- tests/integration/git_test.go | 91 +++++++++++++++++------------------ 1 file changed, 45 insertions(+), 46 deletions(-) diff --git a/tests/integration/git_test.go b/tests/integration/git_test.go index 53c6e29359..af14c0f288 100644 --- a/tests/integration/git_test.go +++ b/tests/integration/git_test.go @@ -86,7 +86,7 @@ func testGit(t *testing.T, u *url.URL) { t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &httpContext, "test/head")) t.Run("InternalReferences", doInternalReferences(&httpContext, dstPath)) - t.Run("BranchProtectMerge", doBranchProtectPRMerge(&httpContext, dstPath)) + t.Run("BranchProtect", doBranchProtect(&httpContext, dstPath)) t.Run("AutoMerge", doAutoPRMerge(&httpContext, dstPath)) t.Run("CreatePRAndSetManuallyMerged", doCreatePRAndSetManuallyMerged(httpContext, httpContext, dstPath, "master", "test-manually-merge")) t.Run("MergeFork", func(t *testing.T) { @@ -130,7 +130,7 @@ func testGit(t *testing.T, u *url.URL) { t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &sshContext, "test/head2")) t.Run("InternalReferences", doInternalReferences(&sshContext, dstPath)) - t.Run("BranchProtectMerge", doBranchProtectPRMerge(&sshContext, dstPath)) + t.Run("BranchProtect", doBranchProtect(&sshContext, dstPath)) t.Run("MergeFork", func(t *testing.T) { defer tests.PrintCurrentTest(t)() t.Run("CreatePRAndMerge", doMergeFork(sshContext, forkedUserCtx, "master", sshContext.Username+":master")) @@ -361,67 +361,66 @@ func generateCommitWithNewData(size int, repoPath, email, fullName, prefix strin return filepath.Base(tmpFile.Name()), err } -func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) { +func doBranchProtect(baseCtx *APITestContext, dstPath string) func(t *testing.T) { return func(t *testing.T) { defer tests.PrintCurrentTest(t)() t.Run("CreateBranchProtected", doGitCreateBranch(dstPath, "protected")) t.Run("PushProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected")) ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository) - t.Run("ProtectProtectedBranchNoWhitelist", doProtectBranch(ctx, "protected")) - t.Run("GenerateCommit", func(t *testing.T) { - _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") - assert.NoError(t, err) - }) - t.Run("FailToPushToProtectedBranch", doGitPushTestRepositoryFail(dstPath, "origin", "protected")) - t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected")) - var pr api.PullRequest - var err error - t.Run("CreatePullRequest", func(t *testing.T) { - pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "protected", "unprotected")(t) - assert.NoError(t, err) - }) - t.Run("GenerateCommit", func(t *testing.T) { - _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") - assert.NoError(t, err) - }) - t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected-2")) - var pr2 api.PullRequest - t.Run("CreatePullRequest", func(t *testing.T) { - pr2, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "unprotected", "unprotected-2")(t) - assert.NoError(t, err) - }) - t.Run("MergePR2", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr2.Index)) - t.Run("MergePR", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) - t.Run("PullProtected", doGitPull(dstPath, "origin", "protected")) - t.Run("ProtectProtectedBranchUnprotectedFilePaths", doProtectBranch(ctx, "protected", parameterProtectBranch{ - "unprotected_file_patterns": "unprotected-file-*", - })) - t.Run("GenerateCommit", func(t *testing.T) { - _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "unprotected-file-") - assert.NoError(t, err) + t.Run("FailToPushToProtectedBranch", func(t *testing.T) { + t.Run("ProtectProtectedBranch", doProtectBranch(ctx, "protected")) + t.Run("Create modified-protected-branch", doGitCheckoutBranch(dstPath, "-b", "modified-protected-branch", "protected")) + t.Run("GenerateCommit", func(t *testing.T) { + _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") + assert.NoError(t, err) + }) + + doGitPushTestRepositoryFail(dstPath, "origin", "modified-protected-branch:protected")(t) + }) + + t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "modified-protected-branch:unprotected")) + + t.Run("PushUnprotectedFilesToProtectedBranch", func(t *testing.T) { + t.Run("Create modified-unprotected-file-protected-branch", doGitCheckoutBranch(dstPath, "-b", "modified-unprotected-file-protected-branch", "protected")) + t.Run("UnprotectedFilePaths", doProtectBranch(ctx, "protected", parameterProtectBranch{ + "unprotected_file_patterns": "unprotected-file-*", + })) + t.Run("GenerateCommit", func(t *testing.T) { + _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "unprotected-file-") + assert.NoError(t, err) + }) + doGitPushTestRepository(dstPath, "origin", "modified-unprotected-file-protected-branch:protected")(t) + doGitCheckoutBranch(dstPath, "protected")(t) + doGitPull(dstPath, "origin", "protected")(t) }) - t.Run("PushUnprotectedFilesToProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected")) user, err := user_model.GetUserByName(db.DefaultContext, baseCtx.Username) assert.NoError(t, err) - t.Run("ProtectProtectedBranchWhitelist", doProtectBranch(ctx, "protected", parameterProtectBranch{ + t.Run("WhitelistUsers", doProtectBranch(ctx, "protected", parameterProtectBranch{ "enable_push": "whitelist", "enable_whitelist": "on", "whitelist_users": strconv.FormatInt(user.ID, 10), })) - t.Run("CheckoutMaster", doGitCheckoutBranch(dstPath, "master")) - t.Run("CreateBranchForced", doGitCreateBranch(dstPath, "toforce")) - t.Run("GenerateCommit", func(t *testing.T) { - _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") - assert.NoError(t, err) + t.Run("WhitelistedUserFailToForcePushToProtectedBranch", func(t *testing.T) { + t.Run("Create toforce", doGitCheckoutBranch(dstPath, "-b", "toforce", "master")) + t.Run("GenerateCommit", func(t *testing.T) { + _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") + assert.NoError(t, err) + }) + doGitPushTestRepositoryFail(dstPath, "-f", "origin", "toforce:protected")(t) + }) + + t.Run("WhitelistedUserPushToProtectedBranch", func(t *testing.T) { + t.Run("Create topush", doGitCheckoutBranch(dstPath, "-b", "topush", "protected")) + t.Run("GenerateCommit", func(t *testing.T) { + _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") + assert.NoError(t, err) + }) + doGitPushTestRepository(dstPath, "origin", "topush:protected")(t) }) - t.Run("FailToForcePushToProtectedBranch", doGitPushTestRepositoryFail(dstPath, "-f", "origin", "toforce:protected")) - t.Run("MergeProtectedToToforce", doGitMerge(dstPath, "protected")) - t.Run("PushToProtectedBranch", doGitPushTestRepository(dstPath, "origin", "toforce:protected")) - t.Run("CheckoutMasterAgain", doGitCheckoutBranch(dstPath, "master")) } } From e0eba21ab7d05a29290b4a7b53908adf79b2f2f9 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Sat, 1 Jun 2024 22:28:42 +0200 Subject: [PATCH 10/49] test(integration): add protected file to doBranchProtect A protected file pushed to a protected branch branch is not allowed. --- tests/integration/git_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/integration/git_test.go b/tests/integration/git_test.go index af14c0f288..b7469a04cf 100644 --- a/tests/integration/git_test.go +++ b/tests/integration/git_test.go @@ -382,6 +382,20 @@ func doBranchProtect(baseCtx *APITestContext, dstPath string) func(t *testing.T) t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "modified-protected-branch:unprotected")) + t.Run("FailToPushProtectedFilesToProtectedBranch", func(t *testing.T) { + t.Run("Create modified-protected-file-protected-branch", doGitCheckoutBranch(dstPath, "-b", "modified-protected-file-protected-branch", "protected")) + t.Run("GenerateCommit", func(t *testing.T) { + _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "protected-file-") + assert.NoError(t, err) + }) + + t.Run("ProtectedFilePathsApplyToAdmins", doProtectBranch(ctx, "protected")) + doGitPushTestRepositoryFail(dstPath, "origin", "modified-protected-file-protected-branch:protected")(t) + + doGitCheckoutBranch(dstPath, "protected")(t) + doGitPull(dstPath, "origin", "protected")(t) + }) + t.Run("PushUnprotectedFilesToProtectedBranch", func(t *testing.T) { t.Run("Create modified-unprotected-file-protected-branch", doGitCheckoutBranch(dstPath, "-b", "modified-unprotected-file-protected-branch", "protected")) t.Run("UnprotectedFilePaths", doProtectBranch(ctx, "protected", parameterProtectBranch{ From 793421bf5990d1b28d79ea78d310d8a6e15bf562 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Sat, 1 Jun 2024 10:58:46 +0200 Subject: [PATCH 11/49] tests(integration): add TestPullMergeBranchProtect Verify variations of branch protection that are in play when merging a pull request as: * instance admin * repository admin / owner * user with write permissions on the repository In all cases the result is expected to be the same when merging the pull request via: * API * web Although the implementations are different. --- .../user5/repo4.git/hooks/post-receive | 7 + .../repo4.git/hooks/post-receive.d/gitea | 2 + .../user5/repo4.git/hooks/pre-receive | 7 + .../user5/repo4.git/hooks/pre-receive.d/gitea | 2 + .../user5/repo4.git/hooks/update | 7 + .../user5/repo4.git/hooks/update.d/gitea | 2 + tests/integration/pull_merge_test.go | 147 ++++++++++++++++++ 7 files changed, 174 insertions(+) create mode 100755 tests/gitea-repositories-meta/user5/repo4.git/hooks/post-receive create mode 100755 tests/gitea-repositories-meta/user5/repo4.git/hooks/post-receive.d/gitea create mode 100755 tests/gitea-repositories-meta/user5/repo4.git/hooks/pre-receive create mode 100755 tests/gitea-repositories-meta/user5/repo4.git/hooks/pre-receive.d/gitea create mode 100755 tests/gitea-repositories-meta/user5/repo4.git/hooks/update create mode 100755 tests/gitea-repositories-meta/user5/repo4.git/hooks/update.d/gitea diff --git a/tests/gitea-repositories-meta/user5/repo4.git/hooks/post-receive b/tests/gitea-repositories-meta/user5/repo4.git/hooks/post-receive new file mode 100755 index 0000000000..4b3d452abc --- /dev/null +++ b/tests/gitea-repositories-meta/user5/repo4.git/hooks/post-receive @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +ORI_DIR=`pwd` +SHELL_FOLDER=$(cd "$(dirname "$0")";pwd) +cd "$ORI_DIR" +for i in `ls "$SHELL_FOLDER/post-receive.d"`; do + sh "$SHELL_FOLDER/post-receive.d/$i" +done \ No newline at end of file diff --git a/tests/gitea-repositories-meta/user5/repo4.git/hooks/post-receive.d/gitea b/tests/gitea-repositories-meta/user5/repo4.git/hooks/post-receive.d/gitea new file mode 100755 index 0000000000..43a948da3a --- /dev/null +++ b/tests/gitea-repositories-meta/user5/repo4.git/hooks/post-receive.d/gitea @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" post-receive diff --git a/tests/gitea-repositories-meta/user5/repo4.git/hooks/pre-receive b/tests/gitea-repositories-meta/user5/repo4.git/hooks/pre-receive new file mode 100755 index 0000000000..4127013053 --- /dev/null +++ b/tests/gitea-repositories-meta/user5/repo4.git/hooks/pre-receive @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +ORI_DIR=`pwd` +SHELL_FOLDER=$(cd "$(dirname "$0")";pwd) +cd "$ORI_DIR" +for i in `ls "$SHELL_FOLDER/pre-receive.d"`; do + sh "$SHELL_FOLDER/pre-receive.d/$i" +done \ No newline at end of file diff --git a/tests/gitea-repositories-meta/user5/repo4.git/hooks/pre-receive.d/gitea b/tests/gitea-repositories-meta/user5/repo4.git/hooks/pre-receive.d/gitea new file mode 100755 index 0000000000..49d0940636 --- /dev/null +++ b/tests/gitea-repositories-meta/user5/repo4.git/hooks/pre-receive.d/gitea @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" pre-receive diff --git a/tests/gitea-repositories-meta/user5/repo4.git/hooks/update b/tests/gitea-repositories-meta/user5/repo4.git/hooks/update new file mode 100755 index 0000000000..c186fe4a18 --- /dev/null +++ b/tests/gitea-repositories-meta/user5/repo4.git/hooks/update @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +ORI_DIR=`pwd` +SHELL_FOLDER=$(cd "$(dirname "$0")";pwd) +cd "$ORI_DIR" +for i in `ls "$SHELL_FOLDER/update.d"`; do + sh "$SHELL_FOLDER/update.d/$i" $1 $2 $3 +done \ No newline at end of file diff --git a/tests/gitea-repositories-meta/user5/repo4.git/hooks/update.d/gitea b/tests/gitea-repositories-meta/user5/repo4.git/hooks/update.d/gitea new file mode 100755 index 0000000000..38101c2426 --- /dev/null +++ b/tests/gitea-repositories-meta/user5/repo4.git/hooks/update.d/gitea @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" update $1 $2 $3 diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index 05807cc242..93bd89afdb 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -37,6 +37,7 @@ import ( "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/services/automerge" + "code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/pull" commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus" files_service "code.gitea.io/gitea/services/repository/files" @@ -696,6 +697,152 @@ func testResetRepo(t *testing.T, repoPath, branch, commitID string) { assert.EqualValues(t, commitID, id) } +func TestPullMergeBranchProtect(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + admin := "user1" + owner := "user5" + notOwner := "user4" + repo := "repo4" + + dstPath := t.TempDir() + + u.Path = fmt.Sprintf("%s/%s.git", owner, repo) + u.User = url.UserPassword(owner, userPassword) + + t.Run("Clone", doGitClone(dstPath, u)) + + for _, testCase := range []struct { + name string + doer string + expectedCode map[string]int + filename string + protectBranch parameterProtectBranch + }{ + { + name: "SuccessAdminNotEnoughMergeRequiredApprovals", + doer: admin, + expectedCode: map[string]int{"api": http.StatusOK, "web": http.StatusOK}, + filename: "branch-data-file-", + protectBranch: parameterProtectBranch{ + "required_approvals": "1", + "apply_to_admins": "true", + }, + }, + { + name: "FailOwnerProtectedFile", + doer: owner, + expectedCode: map[string]int{"api": http.StatusMethodNotAllowed, "web": http.StatusBadRequest}, + filename: "protected-file-", + protectBranch: parameterProtectBranch{ + "protected_file_patterns": "protected-file-*", + "apply_to_admins": "true", + }, + }, + { + name: "OwnerProtectedFile", + doer: owner, + expectedCode: map[string]int{"api": http.StatusOK, "web": http.StatusOK}, + filename: "protected-file-", + protectBranch: parameterProtectBranch{ + "protected_file_patterns": "protected-file-*", + "apply_to_admins": "false", + }, + }, + { + name: "FailNotOwnerProtectedFile", + doer: notOwner, + expectedCode: map[string]int{"api": http.StatusMethodNotAllowed, "web": http.StatusBadRequest}, + filename: "protected-file-", + protectBranch: parameterProtectBranch{ + "protected_file_patterns": "protected-file-*", + }, + }, + { + name: "FailOwnerNotEnoughMergeRequiredApprovals", + doer: owner, + expectedCode: map[string]int{"api": http.StatusMethodNotAllowed, "web": http.StatusBadRequest}, + filename: "branch-data-file-", + protectBranch: parameterProtectBranch{ + "required_approvals": "1", + "apply_to_admins": "true", + }, + }, + { + name: "SuccessOwnerNotEnoughMergeRequiredApprovals", + doer: owner, + expectedCode: map[string]int{"api": http.StatusOK, "web": http.StatusOK}, + filename: "branch-data-file-", + protectBranch: parameterProtectBranch{ + "required_approvals": "1", + "apply_to_admins": "false", + }, + }, + { + name: "FailNotOwnerNotEnoughMergeRequiredApprovals", + doer: notOwner, + expectedCode: map[string]int{"api": http.StatusMethodNotAllowed, "web": http.StatusBadRequest}, + filename: "branch-data-file-", + protectBranch: parameterProtectBranch{ + "required_approvals": "1", + "apply_to_admins": "false", + }, + }, + { + name: "SuccessNotOwner", + doer: notOwner, + expectedCode: map[string]int{"api": http.StatusOK, "web": http.StatusOK}, + filename: "branch-data-file-", + protectBranch: parameterProtectBranch{ + "required_approvals": "0", + }, + }, + } { + mergeWith := func(t *testing.T, ctx APITestContext, apiOrWeb string, expectedCode int, pr int64) { + switch apiOrWeb { + case "api": + ctx.ExpectedCode = expectedCode + doAPIMergePullRequestForm(t, ctx, owner, repo, pr, + &forms.MergePullRequestForm{ + MergeMessageField: "doAPIMergePullRequest Merge", + Do: string(repo_model.MergeStyleMerge), + ForceMerge: true, + }) + ctx.ExpectedCode = 0 + case "web": + testPullMergeForm(t, ctx.Session, expectedCode, owner, repo, fmt.Sprintf("%d", pr), optionsPullMerge{ + "do": string(repo_model.MergeStyleMerge), + "force_merge": "true", + }) + default: + panic(apiOrWeb) + } + } + for _, withAPIOrWeb := range []string{"api", "web"} { + t.Run(testCase.name+" "+withAPIOrWeb, func(t *testing.T) { + branch := testCase.name + "-" + withAPIOrWeb + unprotected := branch + "-unprotected" + doGitCheckoutBranch(dstPath, "master")(t) + doGitCreateBranch(dstPath, branch)(t) + doGitPushTestRepository(dstPath, "origin", branch)(t) + + ctx := NewAPITestContext(t, owner, repo, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + doProtectBranch(ctx, branch, testCase.protectBranch)(t) + + ctx = NewAPITestContext(t, testCase.doer, "not used", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + ctx.Username = owner + ctx.Reponame = repo + _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", testCase.filename) + assert.NoError(t, err) + doGitPushTestRepository(dstPath, "origin", branch+":"+unprotected)(t) + pr, err := doAPICreatePullRequest(ctx, owner, repo, branch, unprotected)(t) + assert.NoError(t, err) + mergeWith(t, ctx, withAPIOrWeb, testCase.expectedCode[withAPIOrWeb], pr.Index) + }) + } + } + }) +} + func TestPullAutoMergeAfterCommitStatusSucceed(t *testing.T) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { // create a pull request From 05f0007437d507e1445fd616594c048e5b9908d8 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Sat, 1 Jun 2024 10:41:10 +0200 Subject: [PATCH 12/49] fix(hook): instance admins wrongly restricted by permissions checks This exception existed for both instance admins and repo admins before ApplyToAdmins was introduced in 79b70893601c33a33d8d44eb0421797dfd846a47. It should have been kept for instance admins only because they are not subject to permission checks. --- routers/private/hook_pre_receive.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go index f06f6071e9..97568b3f65 100644 --- a/routers/private/hook_pre_receive.go +++ b/routers/private/hook_pre_receive.go @@ -397,6 +397,11 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r return } + // If we're an admin for the instance, we can ignore checks + if ctx.user.IsAdmin { + return + } + // It's not allowed t overwrite protected files. Unless if the user is an // admin and the protected branch rule doesn't apply to admins. if changedProtectedfiles && (!ctx.user.IsAdmin || protectBranch.ApplyToAdmins) { From c3d60ed9f78ece91d40a73cff5553877f6ed165e Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Sun, 2 Jun 2024 03:32:36 +0200 Subject: [PATCH 13/49] Cleanup & add missing options to app.example.ini --- custom/conf/app.example.ini | 82 ++++++++++++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 10 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 0aa8a1324b..913c0e7496 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -222,6 +222,9 @@ RUN_USER = ; git ;; Enable exposure of SSH clone URL to anonymous visitors, default is false ;SSH_EXPOSE_ANONYMOUS = false ;; +;; Command template for authorized keys entries +;SSH_AUTHORIZED_KEYS_COMMAND_TEMPLATE = {{.AppPath}} --config={{.CustomConf}} serv key-{{.Key.ID}} +;; ;; Timeout for any write to ssh connections. (Set to -1 to disable all timeouts.) ;; Will default to the PER_WRITE_TIMEOUT. ;SSH_PER_WRITE_TIMEOUT = 30s @@ -407,6 +410,9 @@ USER = root ;; Database connection max life time, default is 0 or 3s mysql (See #6804 & #7071 for reasoning) ;CONN_MAX_LIFETIME = 3s ;; +;; Database connection max idle time, 0 prevents closing due to idle time. +;CONN_MAX_IDLETIME = 0 +;; ;; Database maximum number of open connections, default is 100 which is the lowest default from Postgres (MariaDB + MySQL default to 151). Ensure you only increase the value if you configured your database server accordingly. ;MAX_OPEN_CONNS = 100 ;; @@ -443,9 +449,6 @@ INTERNAL_TOKEN = ;; How long to remember that a user is logged in before requiring relogin (in days) ;LOGIN_REMEMBER_DAYS = 31 ;; -;; Name of the cookie used to store the current username. -;COOKIE_USERNAME = gitea_awesome -;; ;; Name of cookie used to store authentication information. ;COOKIE_REMEMBER_NAME = gitea_incredible ;; @@ -689,6 +692,12 @@ LEVEL = Info ;; Set the default branches range size ;BRANCHES_RANGE_SIZE = 20 ;; +;; Print out verbose infos on push to stdout +;VERBOSE_PUSH = true +;; +;; Delay before verbose push infos are printed to stdout +;VERBOSE_PUSH_DELAY = 5s +;; ;; Arguments for command 'git gc', e.g. "--aggressive --auto" ;; see more on http://git-scm.com/docs/git-gc/ ;GC_ARGS = @@ -784,6 +793,11 @@ LEVEL = Info ;; Enable this to require captcha validation for login ;REQUIRE_CAPTCHA_FOR_LOGIN = false ;; +;; Requires captcha for external registrations +;REQUIRE_EXTERNAL_REGISTRATION_CAPTCHA = false +;; Requires a password for external registrations +;REQUIRE_EXTERNAL_REGISTRATION_PASSWORD = false +;; ;; Type of captcha you want to use. Options: image, recaptcha, hcaptcha, mcaptcha, cfturnstile. ;CAPTCHA_TYPE = image ;; @@ -953,7 +967,7 @@ LEVEL = Info ;; ;; Preferred Licenses to place at the top of the List ;; The name here must match the filename in options/license or custom/options/license -;PREFERRED_LICENSES = Apache License 2.0,MIT License +;PREFERRED_LICENSES = Apache-2.0,MIT ;; ;; Disable the ability to interact with repositories using the HTTP protocol ;DISABLE_HTTP_GIT = false @@ -963,7 +977,7 @@ LEVEL = Info ;ACCESS_CONTROL_ALLOW_ORIGIN = ;; ;; Force ssh:// clone url instead of scp-style uri when default SSH port is used -;USE_COMPAT_SSH_URI = false +;USE_COMPAT_SSH_URI = true ;; ;; Value for the "go get" request returns the repository url as https or ssh, default is https ;GO_GET_CLONE_URL_PROTOCOL = https @@ -1088,6 +1102,9 @@ LEVEL = Info ;; In default merge messages only include approvers who are official ;DEFAULT_MERGE_MESSAGE_OFFICIAL_APPROVERS_ONLY = true ;; +;; If an squash commit's comment should be populated with the commit messages of the squashed commits +;POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES = false +;; ;; Add co-authored-by and co-committed-by trailers if committer does not match author ;ADD_CO_COMMITTER_TRAILERS = true ;; @@ -1103,7 +1120,7 @@ LEVEL = Info ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; List of reasons why a Pull Request or Issue can be locked -;LOCK_REASONS = Too heated,Off-topic,Resolved,Spam +;LOCK_REASONS = Too heated,Off-topic,Spam,Resolved ;; Maximum number of pinned Issues per repo ;; Set to 0 to disable pinning Issues ;MAX_PINNED = 3 @@ -1219,6 +1236,12 @@ LEVEL = Info ;; Number of issues that are displayed on one page ;ISSUE_PAGING_NUM = 20 ;; +;; Number of repositories that are displayed on one page when searching. +;REPO_SEARCH_PAGING_NUM = 20 +;; +;; Number of members that are displayed on one page +;MEMBERS_PAGING_NUM = 20 +;; ;; Number of maximum commits displayed in one activity feed ;FEED_MAX_COMMIT_NUM = 5 ;; @@ -1228,6 +1251,9 @@ LEVEL = Info ;; Number of items that are displayed in a single subsitemap ;SITEMAP_PAGING_NUM = 20 ;; +;; Number of packages that are displayed on one page +;PACKAGES_PAGING_NUM = 20 +;; ;; Number of maximum commits displayed in commit graph. ;GRAPH_MAX_COMMIT_NUM = 100 ;; @@ -1244,9 +1270,14 @@ LEVEL = Info ;SHOW_USER_EMAIL = true ;; ;; Set the default theme for the Gitea install -;DEFAULT_THEME = gitea-auto +;DEFAULT_THEME = forgejo-auto ;; ;; All available themes. Allow users select personalized themes regardless of the value of `DEFAULT_THEME`. +;; By default available: +;; - forgejo-auto, forgejo-light, forgejo-dark +;; - gitea-auto, gitea-light, gitea-dark +;; - forgejo-auto-deuteranopia-protanopia, forgejo-light-deuteranopia-protanopia, forgejo-dark-deuteranopia-protanopia +;; - forgejo-auto-tritanopia, forgejo-light-tritanopia, forgejo-dark-tritanopia ;THEMES = gitea-auto,gitea-light,gitea-dark ;; ;; All available reactions users can choose on issues/prs and comments. @@ -1260,7 +1291,7 @@ LEVEL = Info ;; Additional Emojis not defined in the utf8 standard ;; By default we support gitea (:gitea:), to add more copy them to public/assets/img/emoji/emoji_name.png and add it to this config. ;; Dont mistake it for Reactions. -;CUSTOM_EMOJIS = gitea, codeberg, gitlab, git, github, gogs +;CUSTOM_EMOJIS = gitea, codeberg, gitlab, git, github, gogs, forgejo ;; ;; Whether the full name of the users should be shown where possible. If the full name isn't set, the username will be used. ;DEFAULT_SHOW_FULL_NAME = false @@ -1445,6 +1476,11 @@ LEVEL = Info ;; A comma separated list of glob patterns to exclude from the index; ; default is empty ;REPO_INDEXER_EXCLUDE = ;; +;; If vendored files should be excluded. +;; See https://github.com/go-enry/go-enry for more details which files are considered to be vendored. +;REPO_INDEXER_EXCLUDE_VENDORED = true +;; +;; The maximum filesize to include for indexing ;MAX_FILE_SIZE = 1048576 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1536,7 +1572,7 @@ LEVEL = Info ;; ;; Whether to allow registering via OpenID ;; Do not include to rely on rhw DISABLE_REGISTRATION setting -;;ENABLE_OPENID_SIGNUP = true +;ENABLE_OPENID_SIGNUP = true ;; ;; Allowed URI patterns (POSIX regexp). ;; Space separated. @@ -1810,6 +1846,9 @@ LEVEL = Info ;; Session life time in seconds, default is 86400 (1 day) ;SESSION_LIFE_TIME = 86400 ;; +;; Cookie domain name. Default is empty +;DOMAIN = +;; ;; SameSite settings. Either "none", "lax", or "strict" ;SAME_SITE=lax @@ -1862,7 +1901,7 @@ LEVEL = Info ;ENABLED = true ;; ;; Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types. -;ALLOWED_TYPES = .csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip +;ALLOWED_TYPES = .cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip ;; ;; Max size of each file. Defaults to 2048MB ;MAX_SIZE = 2048 @@ -1894,6 +1933,11 @@ LEVEL = Info ;; Minio bucket to store the attachments only available when STORAGE_TYPE is `minio` ;MINIO_BUCKET = gitea ;; +;; Url lookup for the minio bucket only available when STORAGE_TYPE is `minio` +;; Available values: auto, dns, path +;; If empty, it behaves the same as "auto" was set +;MINIO_BUCKET_LOOKUP = +;; ;; Minio location to create bucket only available when STORAGE_TYPE is `minio` ;MINIO_LOCATION = us-east-1 ;; @@ -2428,6 +2472,9 @@ LEVEL = Info ;; Allow private addresses defined by RFC 1918, RFC 1122, RFC 4632 and RFC 4291 (false by default) ;; If a domain is allowed by ALLOWED_DOMAINS, this option will be ignored. ;ALLOW_LOCALNETWORKS = false +;; +;; If set to true, completely ignores server certificate validation errors. This option is unsafe. +;SKIP_TLS_VERIFY = false ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -2595,6 +2642,11 @@ LEVEL = Info ;; Minio bucket to store the attachments only available when STORAGE_TYPE is `minio` ;MINIO_BUCKET = gitea ;; +;; Url lookup for the minio bucket only available when STORAGE_TYPE is `minio` +;; Available values: auto, dns, path +;; If empty, it behaves the same as "auto" was set +;MINIO_BUCKET_LOOKUP = +;; ;; Minio location to create bucket only available when STORAGE_TYPE is `minio` ;MINIO_LOCATION = us-east-1 ;; @@ -2637,3 +2689,13 @@ LEVEL = Info ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; storage type ;STORAGE_TYPE = local + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; settings for action artifacts, will override storage setting +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;[storage.actions_artifacts] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; storage type +;STORAGE_TYPE = local From 56a6d3f77d395fcb61e84df0e1cd1b5132adbee4 Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Sun, 2 Jun 2024 06:04:53 +0200 Subject: [PATCH 14/49] Adding release note for #3337 --- release-notes/8.0.0/feat/3337.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 release-notes/8.0.0/feat/3337.md diff --git a/release-notes/8.0.0/feat/3337.md b/release-notes/8.0.0/feat/3337.md new file mode 100644 index 0000000000..4ee66ef2ce --- /dev/null +++ b/release-notes/8.0.0/feat/3337.md @@ -0,0 +1 @@ +Added support for grouping of log-lines inside steps between the special `::group::{title}` and `::endgroup::` workflow commands. A runner of v3.4.2 or later is needed. \ No newline at end of file From 9d4f0671f3749acd2596192fa1e4f886624cf9d2 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Mon, 27 May 2024 00:26:27 +0000 Subject: [PATCH 15/49] [skip ci] Updated licenses and gitignores (cherry picked from commit e625813aa9f585718e9c7677fc441f1f3ad69c61) --- ...e-first-lines => BSD-2-Clause-first-lines} | 0 options/license/Gutmann | 2 + options/license/HPND-export2-US | 21 ++++++ options/license/HPND-merchantability-variant | 9 +++ options/license/RRDtool-FLOSS-exception-2.0 | 66 +++++++++++++++++++ 5 files changed, 98 insertions(+) rename options/license/{BSD-2-clause-first-lines => BSD-2-Clause-first-lines} (100%) create mode 100644 options/license/Gutmann create mode 100644 options/license/HPND-export2-US create mode 100644 options/license/HPND-merchantability-variant create mode 100644 options/license/RRDtool-FLOSS-exception-2.0 diff --git a/options/license/BSD-2-clause-first-lines b/options/license/BSD-2-Clause-first-lines similarity index 100% rename from options/license/BSD-2-clause-first-lines rename to options/license/BSD-2-Clause-first-lines diff --git a/options/license/Gutmann b/options/license/Gutmann new file mode 100644 index 0000000000..c33f4ee3a2 --- /dev/null +++ b/options/license/Gutmann @@ -0,0 +1,2 @@ +You can use this code in whatever way you want, as long as you don't try +to claim you wrote it. diff --git a/options/license/HPND-export2-US b/options/license/HPND-export2-US new file mode 100644 index 0000000000..1dda23a88c --- /dev/null +++ b/options/license/HPND-export2-US @@ -0,0 +1,21 @@ +Copyright 2004-2008 Apple Inc. All Rights Reserved. + + Export of this software from the United States of America may + require a specific license from the United States Government. + It is the responsibility of any person or organization + contemplating export to obtain such a license before exporting. + +WITHIN THAT CONSTRAINT, permission to use, copy, modify, and +distribute this software and its documentation for any purpose and +without fee is hereby granted, provided that the above copyright +notice appear in all copies and that both that copyright notice and +this permission notice appear in supporting documentation, and that +the name of Apple Inc. not be used in advertising or publicity +pertaining to distribution of the software without specific, +written prior permission. Apple Inc. makes no representations +about the suitability of this software for any purpose. It is +provided "as is" without express or implied warranty. + +THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED +WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. diff --git a/options/license/HPND-merchantability-variant b/options/license/HPND-merchantability-variant new file mode 100644 index 0000000000..421b9ff96b --- /dev/null +++ b/options/license/HPND-merchantability-variant @@ -0,0 +1,9 @@ +Copyright (C) 2004 Christian Groessler + +Permission to use, copy, modify, and distribute this file +for any purpose is hereby granted without fee, provided that +the above copyright notice and this notice appears in all +copies. + +This file is distributed WITHOUT ANY WARRANTY; without even the implied +warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. diff --git a/options/license/RRDtool-FLOSS-exception-2.0 b/options/license/RRDtool-FLOSS-exception-2.0 new file mode 100644 index 0000000000..d88dae5868 --- /dev/null +++ b/options/license/RRDtool-FLOSS-exception-2.0 @@ -0,0 +1,66 @@ +FLOSS License Exception +======================= +(Adapted from http://www.mysql.com/company/legal/licensing/foss-exception.html) + +I want specified Free/Libre and Open Source Software ("FLOSS") +applications to be able to use specified GPL-licensed RRDtool +libraries (the "Program") despite the fact that not all FLOSS licenses are +compatible with version 2 of the GNU General Public License (the "GPL"). + +As a special exception to the terms and conditions of version 2.0 of the GPL: + +You are free to distribute a Derivative Work that is formed entirely from +the Program and one or more works (each, a "FLOSS Work") licensed under one +or more of the licenses listed below, as long as: + +1. You obey the GPL in all respects for the Program and the Derivative +Work, except for identifiable sections of the Derivative Work which are +not derived from the Program, and which can reasonably be considered +independent and separate works in themselves, + +2. all identifiable sections of the Derivative Work which are not derived +from the Program, and which can reasonably be considered independent and +separate works in themselves, + +1. are distributed subject to one of the FLOSS licenses listed +below, and + +2. the object code or executable form of those sections are +accompanied by the complete corresponding machine-readable source +code for those sections on the same medium and under the same FLOSS +license as the corresponding object code or executable forms of +those sections, and + +3. any works which are aggregated with the Program or with a Derivative +Work on a volume of a storage or distribution medium in accordance with +the GPL, can reasonably be considered independent and separate works in +themselves which are not derivatives of either the Program, a Derivative +Work or a FLOSS Work. + +If the above conditions are not met, then the Program may only be copied, +modified, distributed or used under the terms and conditions of the GPL. + +FLOSS License List +================== +License name Version(s)/Copyright Date +Academic Free License 2.0 +Apache Software License 1.0/1.1/2.0 +Apple Public Source License 2.0 +Artistic license From Perl 5.8.0 +BSD license "July 22 1999" +Common Public License 1.0 +GNU Library or "Lesser" General Public License (LGPL) 2.0/2.1 +IBM Public License, Version 1.0 +Jabber Open Source License 1.0 +MIT License (As listed in file MIT-License.txt) - +Mozilla Public License (MPL) 1.0/1.1 +Open Software License 2.0 +OpenSSL license (with original SSLeay license) "2003" ("1998") +PHP License 3.01 +Python license (CNRI Python License) - +Python Software Foundation License 2.1.1 +Sleepycat License "1999" +W3C License "2001" +X11 License "2001" +Zlib/libpng License - +Zope Public License 2.0/2.1 From 9e495f700ef58b6a7c5560429fe317d1c4bf2f81 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 27 May 2024 13:21:00 +0800 Subject: [PATCH 16/49] Fix possible ui 500 if workflow's job is nil (#31092) Fix #31087 (cherry picked from commit e695ba47557ed4c3999c63b28051a449ca4653de) --- options/locale/locale_en-US.ini | 1 + routers/web/repo/actions/actions.go | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 721ad1605e..3f3b7143ad 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3724,6 +3724,7 @@ runs.workflow = Workflow runs.invalid_workflow_helper = Workflow config file is invalid. Please check your config file: %s runs.no_matching_online_runner_helper = No matching online runner with label: %s runs.no_job_without_needs = The workflow must contain at least one job without dependencies. +runs.no_job = The workflow must contain at least one job runs.actor = Actor runs.status = Status runs.actors_no_select = All actors diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index 6059ad1414..a0f03ec7e9 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -107,7 +107,12 @@ func List(ctx *context.Context) { // The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run. hasJobWithoutNeeds := false // Check whether have matching runner and a job without "needs" + emptyJobsNumber := 0 for _, j := range wf.Jobs { + if j == nil { + emptyJobsNumber++ + continue + } if !hasJobWithoutNeeds && len(j.Needs()) == 0 { hasJobWithoutNeeds = true } @@ -131,6 +136,9 @@ func List(ctx *context.Context) { if !hasJobWithoutNeeds { workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs") } + if emptyJobsNumber == len(wf.Jobs) { + workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job") + } workflows = append(workflows, workflow) } } From ec05ab1e3c99270c46e93fc3113d6b2102a14c57 Mon Sep 17 00:00:00 2001 From: Zettat123 Date: Mon, 27 May 2024 14:15:34 +0800 Subject: [PATCH 17/49] Improve the handling of `jobs..if` (#31070) Fix #25897 Fix #30322 #29464 cannot handle some complex `if` conditions correctly because it only checks `always()` literally. In fact, it's not easy to evaluate the `if` condition on the Gitea side because evaluating it requires a series of contexts. But act_runner is able to evaluate the `if` condition before running the job (for more information, see [`gitea/act`](https://gitea.com/gitea/act/src/commit/517d11c67126bd97c88e2faabda0832fff482258/pkg/runner/run_context.go#L739-L753)) . So we can use act_runner to check the `if` condition. In this PR, how to handle a blocked job depends on its `needs` and `if`: - If not all jobs in `needs` completed successfully and the job's `if` is empty, set the job status to `StatusSkipped` - In other cases, the job status will be set to `StatusWaiting`, and then act_runner will check the `if` condition and run the job if the condition is met (cherry picked from commit 31a0c4dfb4156a7b4d856cceae1e61c7fc1a4a1b) --- services/actions/job_emitter.go | 14 +++++++------- services/actions/job_emitter_test.go | 18 +++++++++--------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/services/actions/job_emitter.go b/services/actions/job_emitter.go index d2bbbd9a7c..1f859fcf70 100644 --- a/services/actions/job_emitter.go +++ b/services/actions/job_emitter.go @@ -7,7 +7,6 @@ import ( "context" "errors" "fmt" - "strings" actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" @@ -141,18 +140,19 @@ func (r *jobStatusResolver) resolve() map[int64]actions_model.Status { if allSucceed { ret[id] = actions_model.StatusWaiting } else { - // If a job's "if" condition is "always()", the job should always run even if some of its dependencies did not succeed. - // See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idneeds - always := false + // Check if the job has an "if" condition + hasIf := false if wfJobs, _ := jobparser.Parse(r.jobMap[id].WorkflowPayload); len(wfJobs) == 1 { _, wfJob := wfJobs[0].Job() - expr := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(wfJob.If.Value, "${{"), "}}")) - always = expr == "always()" + hasIf = len(wfJob.If.Value) > 0 } - if always { + if hasIf { + // act_runner will check the "if" condition ret[id] = actions_model.StatusWaiting } else { + // If the "if" condition is empty and not all dependent jobs completed successfully, + // the job should be skipped. ret[id] = actions_model.StatusSkipped } } diff --git a/services/actions/job_emitter_test.go b/services/actions/job_emitter_test.go index 038df7d4f8..58c2dc3b24 100644 --- a/services/actions/job_emitter_test.go +++ b/services/actions/job_emitter_test.go @@ -71,9 +71,9 @@ func Test_jobStatusResolver_Resolve(t *testing.T) { want: map[int64]actions_model.Status{}, }, { - name: "with ${{ always() }} condition", + name: "`if` is not empty and all jobs in `needs` completed successfully", jobs: actions_model.ActionJobList{ - {ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}}, + {ID: 1, JobID: "job1", Status: actions_model.StatusSuccess, Needs: []string{}}, {ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte( ` name: test @@ -82,15 +82,15 @@ jobs: job2: runs-on: ubuntu-latest needs: job1 - if: ${{ always() }} + if: ${{ always() && needs.job1.result == 'success' }} steps: - - run: echo "always run" + - run: echo "will be checked by act_runner" `)}, }, want: map[int64]actions_model.Status{2: actions_model.StatusWaiting}, }, { - name: "with always() condition", + name: "`if` is not empty and not all jobs in `needs` completed successfully", jobs: actions_model.ActionJobList{ {ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}}, {ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte( @@ -101,15 +101,15 @@ jobs: job2: runs-on: ubuntu-latest needs: job1 - if: always() + if: ${{ always() && needs.job1.result == 'failure' }} steps: - - run: echo "always run" + - run: echo "will be checked by act_runner" `)}, }, want: map[int64]actions_model.Status{2: actions_model.StatusWaiting}, }, { - name: "without always() condition", + name: "`if` is empty and not all jobs in `needs` completed successfully", jobs: actions_model.ActionJobList{ {ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}}, {ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte( @@ -121,7 +121,7 @@ jobs: runs-on: ubuntu-latest needs: job1 steps: - - run: echo "not always run" + - run: echo "should be skipped" `)}, }, want: map[int64]actions_model.Status{2: actions_model.StatusSkipped}, From a7591f9738dbefb2dcddeb2d45175abee3d03c1f Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 27 May 2024 16:59:54 +0800 Subject: [PATCH 18/49] Rename project board -> column to make the UI less confusing (#30170) This PR split the `Board` into two parts. One is the struct has been renamed to `Column` and the second we have a `Template Type`. But to make it easier to review, this PR will not change the database schemas, they are just renames. The database schema changes could be in future PRs. --------- Co-authored-by: silverwind Co-authored-by: yp05327 <576951401@qq.com> (cherry picked from commit 98751108b11dc748cc99230ca0fc1acfdf2c8929) Conflicts: docs/content/administration/config-cheat-sheet.en-us.md docs/content/index.en-us.md docs/content/installation/comparison.en-us.md docs/content/usage/permissions.en-us.md non existent files options/locale/locale_en-US.ini routers/web/web.go templates/repo/header.tmpl templates/repo/settings/options.tmpl trivial context conflicts --- models/activities/statistic.go | 4 +- models/issues/comment.go | 6 +- models/issues/issue_project.go | 38 +- models/issues/issue_search.go | 14 +- models/migrations/v1_22/v293_test.go | 24 +- models/project/board.go | 389 ------------------ models/project/column.go | 359 ++++++++++++++++ .../project/{board_test.go => column_test.go} | 48 +-- models/project/issue.go | 24 +- models/project/project.go | 97 ++--- models/project/project_test.go | 14 +- models/project/template.go | 45 ++ models/unit/unit.go | 2 +- modules/indexer/issues/bleve/bleve.go | 4 +- modules/indexer/issues/db/options.go | 2 +- modules/indexer/issues/dboptions.go | 2 +- .../issues/elasticsearch/elasticsearch.go | 4 +- modules/indexer/issues/indexer_test.go | 4 +- modules/indexer/issues/internal/model.go | 6 +- .../indexer/issues/internal/tests/tests.go | 18 +- .../indexer/issues/meilisearch/meilisearch.go | 4 +- modules/indexer/issues/util.go | 2 +- modules/metrics/collector.go | 14 +- options/locale/locale_en-US.ini | 2 +- routers/web/org/projects.go | 114 +++-- routers/web/org/projects_test.go | 8 +- routers/web/repo/issue.go | 12 +- routers/web/repo/projects.go | 118 +++--- routers/web/repo/projects_test.go | 8 +- routers/web/web.go | 20 +- services/forms/repo_form.go | 50 +-- services/forms/user_form_hidden_comments.go | 2 +- templates/projects/new.tmpl | 6 +- templates/repo/header.tmpl | 2 +- templates/repo/issue/filter_actions.tmpl | 2 +- templates/repo/settings/units/overview.tmpl | 2 +- tests/integration/project_test.go | 14 +- web_src/css/features/projects.css | 2 +- web_src/css/themes/theme-gitea-dark.css | 2 +- web_src/css/themes/theme-gitea-light.css | 2 +- 40 files changed, 720 insertions(+), 770 deletions(-) delete mode 100644 models/project/board.go create mode 100644 models/project/column.go rename models/project/{board_test.go => column_test.go} (69%) create mode 100644 models/project/template.go diff --git a/models/activities/statistic.go b/models/activities/statistic.go index d1a459d1b2..ff81ad78a1 100644 --- a/models/activities/statistic.go +++ b/models/activities/statistic.go @@ -30,7 +30,7 @@ type Statistic struct { Mirror, Release, AuthSource, Webhook, Milestone, Label, HookTask, Team, UpdateTask, Project, - ProjectBoard, Attachment, + ProjectColumn, Attachment, Branches, Tags, CommitStatus int64 IssueByLabel []IssueByLabelCount IssueByRepository []IssueByRepositoryCount @@ -115,6 +115,6 @@ func GetStatistic(ctx context.Context) (stats Statistic) { stats.Counter.Team, _ = e.Count(new(organization.Team)) stats.Counter.Attachment, _ = e.Count(new(repo_model.Attachment)) stats.Counter.Project, _ = e.Count(new(project_model.Project)) - stats.Counter.ProjectBoard, _ = e.Count(new(project_model.Board)) + stats.Counter.ProjectColumn, _ = e.Count(new(project_model.Column)) return stats } diff --git a/models/issues/comment.go b/models/issues/comment.go index e4b5ed12cd..b0f7c47a73 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -100,8 +100,8 @@ const ( CommentTypeMergePull // 28 merge pull request CommentTypePullRequestPush // 29 push to PR head branch - CommentTypeProject // 30 Project changed - CommentTypeProjectBoard // 31 Project board changed + CommentTypeProject // 30 Project changed + CommentTypeProjectColumn // 31 Project column changed CommentTypeDismissReview // 32 Dismiss Review @@ -146,7 +146,7 @@ var commentStrings = []string{ "merge_pull", "pull_push", "project", - "project_board", + "project_board", // FIXME: the name should be project_column "dismiss_review", "change_issue_ref", "pull_scheduled_merge", diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go index e31d2ef151..835ea1db52 100644 --- a/models/issues/issue_project.go +++ b/models/issues/issue_project.go @@ -37,22 +37,22 @@ func (issue *Issue) projectID(ctx context.Context) int64 { return ip.ProjectID } -// ProjectBoardID return project board id if issue was assigned to one -func (issue *Issue) ProjectBoardID(ctx context.Context) int64 { +// ProjectColumnID return project column id if issue was assigned to one +func (issue *Issue) ProjectColumnID(ctx context.Context) int64 { var ip project_model.ProjectIssue has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip) if err != nil || !has { return 0 } - return ip.ProjectBoardID + return ip.ProjectColumnID } -// LoadIssuesFromBoard load issues assigned to this board -func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList, error) { +// LoadIssuesFromColumn load issues assigned to this column +func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column) (IssueList, error) { issueList, err := Issues(ctx, &IssuesOptions{ - ProjectBoardID: b.ID, - ProjectID: b.ProjectID, - SortType: "project-column-sorting", + ProjectColumnID: b.ID, + ProjectID: b.ProjectID, + SortType: "project-column-sorting", }) if err != nil { return nil, err @@ -60,9 +60,9 @@ func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList if b.Default { issues, err := Issues(ctx, &IssuesOptions{ - ProjectBoardID: db.NoConditionID, - ProjectID: b.ProjectID, - SortType: "project-column-sorting", + ProjectColumnID: db.NoConditionID, + ProjectID: b.ProjectID, + SortType: "project-column-sorting", }) if err != nil { return nil, err @@ -77,11 +77,11 @@ func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList return issueList, nil } -// LoadIssuesFromBoardList load issues assigned to the boards -func LoadIssuesFromBoardList(ctx context.Context, bs project_model.BoardList) (map[int64]IssueList, error) { +// LoadIssuesFromColumnList load issues assigned to the columns +func LoadIssuesFromColumnList(ctx context.Context, bs project_model.ColumnList) (map[int64]IssueList, error) { issuesMap := make(map[int64]IssueList, len(bs)) for i := range bs { - il, err := LoadIssuesFromBoard(ctx, bs[i]) + il, err := LoadIssuesFromColumn(ctx, bs[i]) if err != nil { return nil, err } @@ -110,7 +110,7 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo return util.NewPermissionDeniedErrorf("issue %d can't be accessed by project %d", issue.ID, newProject.ID) } if newColumnID == 0 { - newDefaultColumn, err := newProject.GetDefaultBoard(ctx) + newDefaultColumn, err := newProject.GetDefaultColumn(ctx) if err != nil { return err } @@ -153,10 +153,10 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo } newSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0) return db.Insert(ctx, &project_model.ProjectIssue{ - IssueID: issue.ID, - ProjectID: newProjectID, - ProjectBoardID: newColumnID, - Sorting: newSorting, + IssueID: issue.ID, + ProjectID: newProjectID, + ProjectColumnID: newColumnID, + Sorting: newSorting, }) }) } diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go index 921dd9973e..491def1229 100644 --- a/models/issues/issue_search.go +++ b/models/issues/issue_search.go @@ -33,7 +33,7 @@ type IssuesOptions struct { //nolint SubscriberID int64 MilestoneIDs []int64 ProjectID int64 - ProjectBoardID int64 + ProjectColumnID int64 IsClosed optional.Option[bool] IsPull optional.Option[bool] LabelIDs []int64 @@ -169,12 +169,12 @@ func applyProjectCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Sessio return sess } -func applyProjectBoardCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { - // opts.ProjectBoardID == 0 means all project boards, +func applyProjectColumnCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { + // opts.ProjectColumnID == 0 means all project columns, // do not need to apply any condition - if opts.ProjectBoardID > 0 { - sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectBoardID})) - } else if opts.ProjectBoardID == db.NoConditionID { + if opts.ProjectColumnID > 0 { + sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectColumnID})) + } else if opts.ProjectColumnID == db.NoConditionID { sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0})) } return sess @@ -246,7 +246,7 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { applyProjectCondition(sess, opts) - applyProjectBoardCondition(sess, opts) + applyProjectColumnCondition(sess, opts) if opts.IsPull.Has() { sess.And("issue.is_pull=?", opts.IsPull.Value()) diff --git a/models/migrations/v1_22/v293_test.go b/models/migrations/v1_22/v293_test.go index ccc92f39a6..cfe4345143 100644 --- a/models/migrations/v1_22/v293_test.go +++ b/models/migrations/v1_22/v293_test.go @@ -15,7 +15,7 @@ import ( func Test_CheckProjectColumnsConsistency(t *testing.T) { // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(project.Project), new(project.Board)) + x, deferable := base.PrepareTestEnv(t, 0, new(project.Project), new(project.Column)) defer deferable() if x == nil || t.Failed() { return @@ -23,22 +23,22 @@ func Test_CheckProjectColumnsConsistency(t *testing.T) { assert.NoError(t, CheckProjectColumnsConsistency(x)) - // check if default board was added - var defaultBoard project.Board - has, err := x.Where("project_id=? AND `default` = ?", 1, true).Get(&defaultBoard) + // check if default column was added + var defaultColumn project.Column + has, err := x.Where("project_id=? AND `default` = ?", 1, true).Get(&defaultColumn) assert.NoError(t, err) assert.True(t, has) - assert.Equal(t, int64(1), defaultBoard.ProjectID) - assert.True(t, defaultBoard.Default) + assert.Equal(t, int64(1), defaultColumn.ProjectID) + assert.True(t, defaultColumn.Default) // check if multiple defaults, previous were removed and last will be kept - expectDefaultBoard, err := project.GetBoard(db.DefaultContext, 2) + expectDefaultColumn, err := project.GetColumn(db.DefaultContext, 2) assert.NoError(t, err) - assert.Equal(t, int64(2), expectDefaultBoard.ProjectID) - assert.False(t, expectDefaultBoard.Default) + assert.Equal(t, int64(2), expectDefaultColumn.ProjectID) + assert.False(t, expectDefaultColumn.Default) - expectNonDefaultBoard, err := project.GetBoard(db.DefaultContext, 3) + expectNonDefaultColumn, err := project.GetColumn(db.DefaultContext, 3) assert.NoError(t, err) - assert.Equal(t, int64(2), expectNonDefaultBoard.ProjectID) - assert.True(t, expectNonDefaultBoard.Default) + assert.Equal(t, int64(2), expectNonDefaultColumn.ProjectID) + assert.True(t, expectNonDefaultColumn.Default) } diff --git a/models/project/board.go b/models/project/board.go deleted file mode 100644 index a52baa0c18..0000000000 --- a/models/project/board.go +++ /dev/null @@ -1,389 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package project - -import ( - "context" - "errors" - "fmt" - "regexp" - - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" - - "xorm.io/builder" -) - -type ( - // BoardType is used to represent a project board type - BoardType uint8 - - // CardType is used to represent a project board card type - CardType uint8 - - // BoardList is a list of all project boards in a repository - BoardList []*Board -) - -const ( - // BoardTypeNone is a project board type that has no predefined columns - BoardTypeNone BoardType = iota - - // BoardTypeBasicKanban is a project board type that has basic predefined columns - BoardTypeBasicKanban - - // BoardTypeBugTriage is a project board type that has predefined columns suited to hunting down bugs - BoardTypeBugTriage -) - -const ( - // CardTypeTextOnly is a project board card type that is text only - CardTypeTextOnly CardType = iota - - // CardTypeImagesAndText is a project board card type that has images and text - CardTypeImagesAndText -) - -// BoardColorPattern is a regexp witch can validate BoardColor -var BoardColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$") - -// Board is used to represent boards on a project -type Board struct { - ID int64 `xorm:"pk autoincr"` - Title string - Default bool `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific board will be assigned to this board - Sorting int8 `xorm:"NOT NULL DEFAULT 0"` - Color string `xorm:"VARCHAR(7)"` - - ProjectID int64 `xorm:"INDEX NOT NULL"` - CreatorID int64 `xorm:"NOT NULL"` - - CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` - UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` -} - -// TableName return the real table name -func (Board) TableName() string { - return "project_board" -} - -// NumIssues return counter of all issues assigned to the board -func (b *Board) NumIssues(ctx context.Context) int { - c, err := db.GetEngine(ctx).Table("project_issue"). - Where("project_id=?", b.ProjectID). - And("project_board_id=?", b.ID). - GroupBy("issue_id"). - Cols("issue_id"). - Count() - if err != nil { - return 0 - } - return int(c) -} - -func (b *Board) GetIssues(ctx context.Context) ([]*ProjectIssue, error) { - issues := make([]*ProjectIssue, 0, 5) - if err := db.GetEngine(ctx).Where("project_id=?", b.ProjectID). - And("project_board_id=?", b.ID). - OrderBy("sorting, id"). - Find(&issues); err != nil { - return nil, err - } - return issues, nil -} - -func init() { - db.RegisterModel(new(Board)) -} - -// IsBoardTypeValid checks if the project board type is valid -func IsBoardTypeValid(p BoardType) bool { - switch p { - case BoardTypeNone, BoardTypeBasicKanban, BoardTypeBugTriage: - return true - default: - return false - } -} - -// IsCardTypeValid checks if the project board card type is valid -func IsCardTypeValid(p CardType) bool { - switch p { - case CardTypeTextOnly, CardTypeImagesAndText: - return true - default: - return false - } -} - -func createBoardsForProjectsType(ctx context.Context, project *Project) error { - var items []string - - switch project.BoardType { - case BoardTypeBugTriage: - items = setting.Project.ProjectBoardBugTriageType - - case BoardTypeBasicKanban: - items = setting.Project.ProjectBoardBasicKanbanType - case BoardTypeNone: - fallthrough - default: - return nil - } - - board := Board{ - CreatedUnix: timeutil.TimeStampNow(), - CreatorID: project.CreatorID, - Title: "Backlog", - ProjectID: project.ID, - Default: true, - } - if err := db.Insert(ctx, board); err != nil { - return err - } - - if len(items) == 0 { - return nil - } - - boards := make([]Board, 0, len(items)) - - for _, v := range items { - boards = append(boards, Board{ - CreatedUnix: timeutil.TimeStampNow(), - CreatorID: project.CreatorID, - Title: v, - ProjectID: project.ID, - }) - } - - return db.Insert(ctx, boards) -} - -// maxProjectColumns max columns allowed in a project, this should not bigger than 127 -// because sorting is int8 in database -const maxProjectColumns = 20 - -// NewBoard adds a new project board to a given project -func NewBoard(ctx context.Context, board *Board) error { - if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) { - return fmt.Errorf("bad color code: %s", board.Color) - } - res := struct { - MaxSorting int64 - ColumnCount int64 - }{} - if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as column_count").Table("project_board"). - Where("project_id=?", board.ProjectID).Get(&res); err != nil { - return err - } - if res.ColumnCount >= maxProjectColumns { - return fmt.Errorf("NewBoard: maximum number of columns reached") - } - board.Sorting = int8(util.Iif(res.ColumnCount > 0, res.MaxSorting+1, 0)) - _, err := db.GetEngine(ctx).Insert(board) - return err -} - -// DeleteBoardByID removes all issues references to the project board. -func DeleteBoardByID(ctx context.Context, boardID int64) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - if err := deleteBoardByID(ctx, boardID); err != nil { - return err - } - - return committer.Commit() -} - -func deleteBoardByID(ctx context.Context, boardID int64) error { - board, err := GetBoard(ctx, boardID) - if err != nil { - if IsErrProjectBoardNotExist(err) { - return nil - } - - return err - } - - if board.Default { - return fmt.Errorf("deleteBoardByID: cannot delete default board") - } - - // move all issues to the default column - project, err := GetProjectByID(ctx, board.ProjectID) - if err != nil { - return err - } - defaultColumn, err := project.GetDefaultBoard(ctx) - if err != nil { - return err - } - - if err = board.moveIssuesToAnotherColumn(ctx, defaultColumn); err != nil { - return err - } - - if _, err := db.GetEngine(ctx).ID(board.ID).NoAutoCondition().Delete(board); err != nil { - return err - } - return nil -} - -func deleteBoardByProjectID(ctx context.Context, projectID int64) error { - _, err := db.GetEngine(ctx).Where("project_id=?", projectID).Delete(&Board{}) - return err -} - -// GetBoard fetches the current board of a project -func GetBoard(ctx context.Context, boardID int64) (*Board, error) { - board := new(Board) - has, err := db.GetEngine(ctx).ID(boardID).Get(board) - if err != nil { - return nil, err - } else if !has { - return nil, ErrProjectBoardNotExist{BoardID: boardID} - } - - return board, nil -} - -// UpdateBoard updates a project board -func UpdateBoard(ctx context.Context, board *Board) error { - var fieldToUpdate []string - - if board.Sorting != 0 { - fieldToUpdate = append(fieldToUpdate, "sorting") - } - - if board.Title != "" { - fieldToUpdate = append(fieldToUpdate, "title") - } - - if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) { - return fmt.Errorf("bad color code: %s", board.Color) - } - fieldToUpdate = append(fieldToUpdate, "color") - - _, err := db.GetEngine(ctx).ID(board.ID).Cols(fieldToUpdate...).Update(board) - - return err -} - -// GetBoards fetches all boards related to a project -func (p *Project) GetBoards(ctx context.Context) (BoardList, error) { - boards := make([]*Board, 0, 5) - if err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Find(&boards); err != nil { - return nil, err - } - - return boards, nil -} - -// GetDefaultBoard return default board and ensure only one exists -func (p *Project) GetDefaultBoard(ctx context.Context) (*Board, error) { - var board Board - has, err := db.GetEngine(ctx). - Where("project_id=? AND `default` = ?", p.ID, true). - Desc("id").Get(&board) - if err != nil { - return nil, err - } - - if has { - return &board, nil - } - - // create a default board if none is found - board = Board{ - ProjectID: p.ID, - Default: true, - Title: "Uncategorized", - CreatorID: p.CreatorID, - } - if _, err := db.GetEngine(ctx).Insert(&board); err != nil { - return nil, err - } - return &board, nil -} - -// SetDefaultBoard represents a board for issues not assigned to one -func SetDefaultBoard(ctx context.Context, projectID, boardID int64) error { - return db.WithTx(ctx, func(ctx context.Context) error { - if _, err := GetBoard(ctx, boardID); err != nil { - return err - } - - if _, err := db.GetEngine(ctx).Where(builder.Eq{ - "project_id": projectID, - "`default`": true, - }).Cols("`default`").Update(&Board{Default: false}); err != nil { - return err - } - - _, err := db.GetEngine(ctx).ID(boardID). - Where(builder.Eq{"project_id": projectID}). - Cols("`default`").Update(&Board{Default: true}) - return err - }) -} - -// UpdateBoardSorting update project board sorting -func UpdateBoardSorting(ctx context.Context, bs BoardList) error { - return db.WithTx(ctx, func(ctx context.Context) error { - for i := range bs { - if _, err := db.GetEngine(ctx).ID(bs[i].ID).Cols( - "sorting", - ).Update(bs[i]); err != nil { - return err - } - } - return nil - }) -} - -func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (BoardList, error) { - columns := make([]*Board, 0, 5) - if err := db.GetEngine(ctx). - Where("project_id =?", projectID). - In("id", columnsIDs). - OrderBy("sorting").Find(&columns); err != nil { - return nil, err - } - return columns, nil -} - -// MoveColumnsOnProject sorts columns in a project -func MoveColumnsOnProject(ctx context.Context, project *Project, sortedColumnIDs map[int64]int64) error { - return db.WithTx(ctx, func(ctx context.Context) error { - sess := db.GetEngine(ctx) - columnIDs := util.ValuesOfMap(sortedColumnIDs) - movedColumns, err := GetColumnsByIDs(ctx, project.ID, columnIDs) - if err != nil { - return err - } - if len(movedColumns) != len(sortedColumnIDs) { - return errors.New("some columns do not exist") - } - - for _, column := range movedColumns { - if column.ProjectID != project.ID { - return fmt.Errorf("column[%d]'s projectID is not equal to project's ID [%d]", column.ProjectID, project.ID) - } - } - - for sorting, columnID := range sortedColumnIDs { - if _, err := sess.Exec("UPDATE `project_board` SET sorting=? WHERE id=?", sorting, columnID); err != nil { - return err - } - } - return nil - }) -} diff --git a/models/project/column.go b/models/project/column.go new file mode 100644 index 0000000000..222f448599 --- /dev/null +++ b/models/project/column.go @@ -0,0 +1,359 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package project + +import ( + "context" + "errors" + "fmt" + "regexp" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" +) + +type ( + + // CardType is used to represent a project column card type + CardType uint8 + + // ColumnList is a list of all project columns in a repository + ColumnList []*Column +) + +const ( + // CardTypeTextOnly is a project column card type that is text only + CardTypeTextOnly CardType = iota + + // CardTypeImagesAndText is a project column card type that has images and text + CardTypeImagesAndText +) + +// ColumnColorPattern is a regexp witch can validate ColumnColor +var ColumnColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$") + +// Column is used to represent column on a project +type Column struct { + ID int64 `xorm:"pk autoincr"` + Title string + Default bool `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific column will be assigned to this column + Sorting int8 `xorm:"NOT NULL DEFAULT 0"` + Color string `xorm:"VARCHAR(7)"` + + ProjectID int64 `xorm:"INDEX NOT NULL"` + CreatorID int64 `xorm:"NOT NULL"` + + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` +} + +// TableName return the real table name +func (Column) TableName() string { + return "project_board" // TODO: the legacy table name should be project_column +} + +// NumIssues return counter of all issues assigned to the column +func (c *Column) NumIssues(ctx context.Context) int { + total, err := db.GetEngine(ctx).Table("project_issue"). + Where("project_id=?", c.ProjectID). + And("project_board_id=?", c.ID). + GroupBy("issue_id"). + Cols("issue_id"). + Count() + if err != nil { + return 0 + } + return int(total) +} + +func (c *Column) GetIssues(ctx context.Context) ([]*ProjectIssue, error) { + issues := make([]*ProjectIssue, 0, 5) + if err := db.GetEngine(ctx).Where("project_id=?", c.ProjectID). + And("project_board_id=?", c.ID). + OrderBy("sorting, id"). + Find(&issues); err != nil { + return nil, err + } + return issues, nil +} + +func init() { + db.RegisterModel(new(Column)) +} + +// IsCardTypeValid checks if the project column card type is valid +func IsCardTypeValid(p CardType) bool { + switch p { + case CardTypeTextOnly, CardTypeImagesAndText: + return true + default: + return false + } +} + +func createDefaultColumnsForProject(ctx context.Context, project *Project) error { + var items []string + + switch project.TemplateType { + case TemplateTypeBugTriage: + items = setting.Project.ProjectBoardBugTriageType + case TemplateTypeBasicKanban: + items = setting.Project.ProjectBoardBasicKanbanType + case TemplateTypeNone: + fallthrough + default: + return nil + } + + return db.WithTx(ctx, func(ctx context.Context) error { + column := Column{ + CreatedUnix: timeutil.TimeStampNow(), + CreatorID: project.CreatorID, + Title: "Backlog", + ProjectID: project.ID, + Default: true, + } + if err := db.Insert(ctx, column); err != nil { + return err + } + + if len(items) == 0 { + return nil + } + + columns := make([]Column, 0, len(items)) + for _, v := range items { + columns = append(columns, Column{ + CreatedUnix: timeutil.TimeStampNow(), + CreatorID: project.CreatorID, + Title: v, + ProjectID: project.ID, + }) + } + + return db.Insert(ctx, columns) + }) +} + +// maxProjectColumns max columns allowed in a project, this should not bigger than 127 +// because sorting is int8 in database +const maxProjectColumns = 20 + +// NewColumn adds a new project column to a given project +func NewColumn(ctx context.Context, column *Column) error { + if len(column.Color) != 0 && !ColumnColorPattern.MatchString(column.Color) { + return fmt.Errorf("bad color code: %s", column.Color) + } + + res := struct { + MaxSorting int64 + ColumnCount int64 + }{} + if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as column_count").Table("project_board"). + Where("project_id=?", column.ProjectID).Get(&res); err != nil { + return err + } + if res.ColumnCount >= maxProjectColumns { + return fmt.Errorf("NewBoard: maximum number of columns reached") + } + column.Sorting = int8(util.Iif(res.ColumnCount > 0, res.MaxSorting+1, 0)) + _, err := db.GetEngine(ctx).Insert(column) + return err +} + +// DeleteColumnByID removes all issues references to the project column. +func DeleteColumnByID(ctx context.Context, columnID int64) error { + return db.WithTx(ctx, func(ctx context.Context) error { + return deleteColumnByID(ctx, columnID) + }) +} + +func deleteColumnByID(ctx context.Context, columnID int64) error { + column, err := GetColumn(ctx, columnID) + if err != nil { + if IsErrProjectColumnNotExist(err) { + return nil + } + + return err + } + + if column.Default { + return fmt.Errorf("deleteColumnByID: cannot delete default column") + } + + // move all issues to the default column + project, err := GetProjectByID(ctx, column.ProjectID) + if err != nil { + return err + } + defaultColumn, err := project.GetDefaultColumn(ctx) + if err != nil { + return err + } + + if err = column.moveIssuesToAnotherColumn(ctx, defaultColumn); err != nil { + return err + } + + if _, err := db.GetEngine(ctx).ID(column.ID).NoAutoCondition().Delete(column); err != nil { + return err + } + return nil +} + +func deleteColumnByProjectID(ctx context.Context, projectID int64) error { + _, err := db.GetEngine(ctx).Where("project_id=?", projectID).Delete(&Column{}) + return err +} + +// GetColumn fetches the current column of a project +func GetColumn(ctx context.Context, columnID int64) (*Column, error) { + column := new(Column) + has, err := db.GetEngine(ctx).ID(columnID).Get(column) + if err != nil { + return nil, err + } else if !has { + return nil, ErrProjectColumnNotExist{ColumnID: columnID} + } + + return column, nil +} + +// UpdateColumn updates a project column +func UpdateColumn(ctx context.Context, column *Column) error { + var fieldToUpdate []string + + if column.Sorting != 0 { + fieldToUpdate = append(fieldToUpdate, "sorting") + } + + if column.Title != "" { + fieldToUpdate = append(fieldToUpdate, "title") + } + + if len(column.Color) != 0 && !ColumnColorPattern.MatchString(column.Color) { + return fmt.Errorf("bad color code: %s", column.Color) + } + fieldToUpdate = append(fieldToUpdate, "color") + + _, err := db.GetEngine(ctx).ID(column.ID).Cols(fieldToUpdate...).Update(column) + + return err +} + +// GetColumns fetches all columns related to a project +func (p *Project) GetColumns(ctx context.Context) (ColumnList, error) { + columns := make([]*Column, 0, 5) + if err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Find(&columns); err != nil { + return nil, err + } + + return columns, nil +} + +// GetDefaultColumn return default column and ensure only one exists +func (p *Project) GetDefaultColumn(ctx context.Context) (*Column, error) { + var column Column + has, err := db.GetEngine(ctx). + Where("project_id=? AND `default` = ?", p.ID, true). + Desc("id").Get(&column) + if err != nil { + return nil, err + } + + if has { + return &column, nil + } + + // create a default column if none is found + column = Column{ + ProjectID: p.ID, + Default: true, + Title: "Uncategorized", + CreatorID: p.CreatorID, + } + if _, err := db.GetEngine(ctx).Insert(&column); err != nil { + return nil, err + } + return &column, nil +} + +// SetDefaultColumn represents a column for issues not assigned to one +func SetDefaultColumn(ctx context.Context, projectID, columnID int64) error { + return db.WithTx(ctx, func(ctx context.Context) error { + if _, err := GetColumn(ctx, columnID); err != nil { + return err + } + + if _, err := db.GetEngine(ctx).Where(builder.Eq{ + "project_id": projectID, + "`default`": true, + }).Cols("`default`").Update(&Column{Default: false}); err != nil { + return err + } + + _, err := db.GetEngine(ctx).ID(columnID). + Where(builder.Eq{"project_id": projectID}). + Cols("`default`").Update(&Column{Default: true}) + return err + }) +} + +// UpdateColumnSorting update project column sorting +func UpdateColumnSorting(ctx context.Context, cl ColumnList) error { + return db.WithTx(ctx, func(ctx context.Context) error { + for i := range cl { + if _, err := db.GetEngine(ctx).ID(cl[i].ID).Cols( + "sorting", + ).Update(cl[i]); err != nil { + return err + } + } + return nil + }) +} + +func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (ColumnList, error) { + columns := make([]*Column, 0, 5) + if err := db.GetEngine(ctx). + Where("project_id =?", projectID). + In("id", columnsIDs). + OrderBy("sorting").Find(&columns); err != nil { + return nil, err + } + return columns, nil +} + +// MoveColumnsOnProject sorts columns in a project +func MoveColumnsOnProject(ctx context.Context, project *Project, sortedColumnIDs map[int64]int64) error { + return db.WithTx(ctx, func(ctx context.Context) error { + sess := db.GetEngine(ctx) + columnIDs := util.ValuesOfMap(sortedColumnIDs) + movedColumns, err := GetColumnsByIDs(ctx, project.ID, columnIDs) + if err != nil { + return err + } + if len(movedColumns) != len(sortedColumnIDs) { + return errors.New("some columns do not exist") + } + + for _, column := range movedColumns { + if column.ProjectID != project.ID { + return fmt.Errorf("column[%d]'s projectID is not equal to project's ID [%d]", column.ProjectID, project.ID) + } + } + + for sorting, columnID := range sortedColumnIDs { + if _, err := sess.Exec("UPDATE `project_board` SET sorting=? WHERE id=?", sorting, columnID); err != nil { + return err + } + } + return nil + }) +} diff --git a/models/project/board_test.go b/models/project/column_test.go similarity index 69% rename from models/project/board_test.go rename to models/project/column_test.go index da922ff7ad..911649fb72 100644 --- a/models/project/board_test.go +++ b/models/project/column_test.go @@ -14,48 +14,48 @@ import ( "github.com/stretchr/testify/assert" ) -func TestGetDefaultBoard(t *testing.T) { +func TestGetDefaultColumn(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) projectWithoutDefault, err := GetProjectByID(db.DefaultContext, 5) assert.NoError(t, err) - // check if default board was added - board, err := projectWithoutDefault.GetDefaultBoard(db.DefaultContext) + // check if default column was added + column, err := projectWithoutDefault.GetDefaultColumn(db.DefaultContext) assert.NoError(t, err) - assert.Equal(t, int64(5), board.ProjectID) - assert.Equal(t, "Uncategorized", board.Title) + assert.Equal(t, int64(5), column.ProjectID) + assert.Equal(t, "Uncategorized", column.Title) projectWithMultipleDefaults, err := GetProjectByID(db.DefaultContext, 6) assert.NoError(t, err) // check if multiple defaults were removed - board, err = projectWithMultipleDefaults.GetDefaultBoard(db.DefaultContext) + column, err = projectWithMultipleDefaults.GetDefaultColumn(db.DefaultContext) assert.NoError(t, err) - assert.Equal(t, int64(6), board.ProjectID) - assert.Equal(t, int64(9), board.ID) + assert.Equal(t, int64(6), column.ProjectID) + assert.Equal(t, int64(9), column.ID) - // set 8 as default board - assert.NoError(t, SetDefaultBoard(db.DefaultContext, board.ProjectID, 8)) + // set 8 as default column + assert.NoError(t, SetDefaultColumn(db.DefaultContext, column.ProjectID, 8)) - // then 9 will become a non-default board - board, err = GetBoard(db.DefaultContext, 9) + // then 9 will become a non-default column + column, err = GetColumn(db.DefaultContext, 9) assert.NoError(t, err) - assert.Equal(t, int64(6), board.ProjectID) - assert.False(t, board.Default) + assert.Equal(t, int64(6), column.ProjectID) + assert.False(t, column.Default) } func Test_moveIssuesToAnotherColumn(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - column1 := unittest.AssertExistsAndLoadBean(t, &Board{ID: 1, ProjectID: 1}) + column1 := unittest.AssertExistsAndLoadBean(t, &Column{ID: 1, ProjectID: 1}) issues, err := column1.GetIssues(db.DefaultContext) assert.NoError(t, err) assert.Len(t, issues, 1) assert.EqualValues(t, 1, issues[0].ID) - column2 := unittest.AssertExistsAndLoadBean(t, &Board{ID: 2, ProjectID: 1}) + column2 := unittest.AssertExistsAndLoadBean(t, &Column{ID: 2, ProjectID: 1}) issues, err = column2.GetIssues(db.DefaultContext) assert.NoError(t, err) assert.Len(t, issues, 1) @@ -81,7 +81,7 @@ func Test_MoveColumnsOnProject(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1}) - columns, err := project1.GetBoards(db.DefaultContext) + columns, err := project1.GetColumns(db.DefaultContext) assert.NoError(t, err) assert.Len(t, columns, 3) assert.EqualValues(t, 0, columns[0].Sorting) // even if there is no default sorting, the code should also work @@ -95,7 +95,7 @@ func Test_MoveColumnsOnProject(t *testing.T) { }) assert.NoError(t, err) - columnsAfter, err := project1.GetBoards(db.DefaultContext) + columnsAfter, err := project1.GetColumns(db.DefaultContext) assert.NoError(t, err) assert.Len(t, columnsAfter, 3) assert.EqualValues(t, columns[1].ID, columnsAfter[0].ID) @@ -103,23 +103,23 @@ func Test_MoveColumnsOnProject(t *testing.T) { assert.EqualValues(t, columns[0].ID, columnsAfter[2].ID) } -func Test_NewBoard(t *testing.T) { +func Test_NewColumn(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1}) - columns, err := project1.GetBoards(db.DefaultContext) + columns, err := project1.GetColumns(db.DefaultContext) assert.NoError(t, err) assert.Len(t, columns, 3) for i := 0; i < maxProjectColumns-3; i++ { - err := NewBoard(db.DefaultContext, &Board{ - Title: fmt.Sprintf("board-%d", i+4), + err := NewColumn(db.DefaultContext, &Column{ + Title: fmt.Sprintf("column-%d", i+4), ProjectID: project1.ID, }) assert.NoError(t, err) } - err = NewBoard(db.DefaultContext, &Board{ - Title: "board-21", + err = NewColumn(db.DefaultContext, &Column{ + Title: "column-21", ProjectID: project1.ID, }) assert.Error(t, err) diff --git a/models/project/issue.go b/models/project/issue.go index 32e72e909d..3361b533b9 100644 --- a/models/project/issue.go +++ b/models/project/issue.go @@ -18,10 +18,10 @@ type ProjectIssue struct { //revive:disable-line:exported IssueID int64 `xorm:"INDEX"` ProjectID int64 `xorm:"INDEX"` - // ProjectBoardID should not be zero since 1.22. If it's zero, the issue will not be displayed on UI and it might result in errors. - ProjectBoardID int64 `xorm:"INDEX"` + // ProjectColumnID should not be zero since 1.22. If it's zero, the issue will not be displayed on UI and it might result in errors. + ProjectColumnID int64 `xorm:"'project_board_id' INDEX"` - // the sorting order on the board + // the sorting order on the column Sorting int64 `xorm:"NOT NULL DEFAULT 0"` } @@ -76,13 +76,13 @@ func (p *Project) NumOpenIssues(ctx context.Context) int { return int(c) } -// MoveIssuesOnProjectBoard moves or keeps issues in a column and sorts them inside that column -func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs map[int64]int64) error { +// MoveIssuesOnProjectColumn moves or keeps issues in a column and sorts them inside that column +func MoveIssuesOnProjectColumn(ctx context.Context, column *Column, sortedIssueIDs map[int64]int64) error { return db.WithTx(ctx, func(ctx context.Context) error { sess := db.GetEngine(ctx) issueIDs := util.ValuesOfMap(sortedIssueIDs) - count, err := sess.Table(new(ProjectIssue)).Where("project_id=?", board.ProjectID).In("issue_id", issueIDs).Count() + count, err := sess.Table(new(ProjectIssue)).Where("project_id=?", column.ProjectID).In("issue_id", issueIDs).Count() if err != nil { return err } @@ -91,7 +91,7 @@ func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs } for sorting, issueID := range sortedIssueIDs { - _, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", board.ID, sorting, issueID) + _, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", column.ID, sorting, issueID) if err != nil { return err } @@ -100,12 +100,12 @@ func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs }) } -func (b *Board) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Board) error { - if b.ProjectID != newColumn.ProjectID { +func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Column) error { + if c.ProjectID != newColumn.ProjectID { return fmt.Errorf("columns have to be in the same project") } - if b.ID == newColumn.ID { + if c.ID == newColumn.ID { return nil } @@ -121,7 +121,7 @@ func (b *Board) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Board) return err } - issues, err := b.GetIssues(ctx) + issues, err := c.GetIssues(ctx) if err != nil { return err } @@ -132,7 +132,7 @@ func (b *Board) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Board) nextSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0) return db.WithTx(ctx, func(ctx context.Context) error { for i, issue := range issues { - issue.ProjectBoardID = newColumn.ID + issue.ProjectColumnID = newColumn.ID issue.Sorting = nextSorting + int64(i) if _, err := db.GetEngine(ctx).ID(issue.ID).Cols("project_board_id", "sorting").Update(issue); err != nil { return err diff --git a/models/project/project.go b/models/project/project.go index 8be38694c5..fe5d408f64 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -21,13 +21,7 @@ import ( ) type ( - // BoardConfig is used to identify the type of board that is being created - BoardConfig struct { - BoardType BoardType - Translation string - } - - // CardConfig is used to identify the type of board card that is being used + // CardConfig is used to identify the type of column card that is being used CardConfig struct { CardType CardType Translation string @@ -38,7 +32,7 @@ type ( ) const ( - // TypeIndividual is a type of project board that is owned by an individual + // TypeIndividual is a type of project column that is owned by an individual TypeIndividual Type = iota + 1 // TypeRepository is a project that is tied to a repository @@ -68,39 +62,39 @@ func (err ErrProjectNotExist) Unwrap() error { return util.ErrNotExist } -// ErrProjectBoardNotExist represents a "ProjectBoardNotExist" kind of error. -type ErrProjectBoardNotExist struct { - BoardID int64 +// ErrProjectColumnNotExist represents a "ErrProjectColumnNotExist" kind of error. +type ErrProjectColumnNotExist struct { + ColumnID int64 } -// IsErrProjectBoardNotExist checks if an error is a ErrProjectBoardNotExist -func IsErrProjectBoardNotExist(err error) bool { - _, ok := err.(ErrProjectBoardNotExist) +// IsErrProjectColumnNotExist checks if an error is a ErrProjectColumnNotExist +func IsErrProjectColumnNotExist(err error) bool { + _, ok := err.(ErrProjectColumnNotExist) return ok } -func (err ErrProjectBoardNotExist) Error() string { - return fmt.Sprintf("project board does not exist [id: %d]", err.BoardID) +func (err ErrProjectColumnNotExist) Error() string { + return fmt.Sprintf("project column does not exist [id: %d]", err.ColumnID) } -func (err ErrProjectBoardNotExist) Unwrap() error { +func (err ErrProjectColumnNotExist) Unwrap() error { return util.ErrNotExist } -// Project represents a project board +// Project represents a project type Project struct { - ID int64 `xorm:"pk autoincr"` - Title string `xorm:"INDEX NOT NULL"` - Description string `xorm:"TEXT"` - OwnerID int64 `xorm:"INDEX"` - Owner *user_model.User `xorm:"-"` - RepoID int64 `xorm:"INDEX"` - Repo *repo_model.Repository `xorm:"-"` - CreatorID int64 `xorm:"NOT NULL"` - IsClosed bool `xorm:"INDEX"` - BoardType BoardType - CardType CardType - Type Type + ID int64 `xorm:"pk autoincr"` + Title string `xorm:"INDEX NOT NULL"` + Description string `xorm:"TEXT"` + OwnerID int64 `xorm:"INDEX"` + Owner *user_model.User `xorm:"-"` + RepoID int64 `xorm:"INDEX"` + Repo *repo_model.Repository `xorm:"-"` + CreatorID int64 `xorm:"NOT NULL"` + IsClosed bool `xorm:"INDEX"` + TemplateType TemplateType `xorm:"'board_type'"` // TODO: rename the column to template_type + CardType CardType + Type Type RenderedContent template.HTML `xorm:"-"` @@ -172,16 +166,7 @@ func init() { db.RegisterModel(new(Project)) } -// GetBoardConfig retrieves the types of configurations project boards could have -func GetBoardConfig() []BoardConfig { - return []BoardConfig{ - {BoardTypeNone, "repo.projects.type.none"}, - {BoardTypeBasicKanban, "repo.projects.type.basic_kanban"}, - {BoardTypeBugTriage, "repo.projects.type.bug_triage"}, - } -} - -// GetCardConfig retrieves the types of configurations project board cards could have +// GetCardConfig retrieves the types of configurations project column cards could have func GetCardConfig() []CardConfig { return []CardConfig{ {CardTypeTextOnly, "repo.projects.card_type.text_only"}, @@ -251,8 +236,8 @@ func GetSearchOrderByBySortType(sortType string) db.SearchOrderBy { // NewProject creates a new Project func NewProject(ctx context.Context, p *Project) error { - if !IsBoardTypeValid(p.BoardType) { - p.BoardType = BoardTypeNone + if !IsTemplateTypeValid(p.TemplateType) { + p.TemplateType = TemplateTypeNone } if !IsCardTypeValid(p.CardType) { @@ -263,27 +248,19 @@ func NewProject(ctx context.Context, p *Project) error { return util.NewInvalidArgumentErrorf("project type is not valid") } - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - if err := db.Insert(ctx, p); err != nil { - return err - } - - if p.RepoID > 0 { - if _, err := db.Exec(ctx, "UPDATE `repository` SET num_projects = num_projects + 1 WHERE id = ?", p.RepoID); err != nil { + return db.WithTx(ctx, func(ctx context.Context) error { + if err := db.Insert(ctx, p); err != nil { return err } - } - if err := createBoardsForProjectsType(ctx, p); err != nil { - return err - } + if p.RepoID > 0 { + if _, err := db.Exec(ctx, "UPDATE `repository` SET num_projects = num_projects + 1 WHERE id = ?", p.RepoID); err != nil { + return err + } + } - return committer.Commit() + return createDefaultColumnsForProject(ctx, p) + }) } // GetProjectByID returns the projects in a repository @@ -417,7 +394,7 @@ func DeleteProjectByID(ctx context.Context, id int64) error { return err } - if err := deleteBoardByProjectID(ctx, id); err != nil { + if err := deleteColumnByProjectID(ctx, id); err != nil { return err } diff --git a/models/project/project_test.go b/models/project/project_test.go index 8fbbdedecf..dd421b4659 100644 --- a/models/project/project_test.go +++ b/models/project/project_test.go @@ -51,13 +51,13 @@ func TestProject(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) project := &Project{ - Type: TypeRepository, - BoardType: BoardTypeBasicKanban, - CardType: CardTypeTextOnly, - Title: "New Project", - RepoID: 1, - CreatedUnix: timeutil.TimeStampNow(), - CreatorID: 2, + Type: TypeRepository, + TemplateType: TemplateTypeBasicKanban, + CardType: CardTypeTextOnly, + Title: "New Project", + RepoID: 1, + CreatedUnix: timeutil.TimeStampNow(), + CreatorID: 2, } assert.NoError(t, NewProject(db.DefaultContext, project)) diff --git a/models/project/template.go b/models/project/template.go new file mode 100644 index 0000000000..06d5d2af14 --- /dev/null +++ b/models/project/template.go @@ -0,0 +1,45 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package project + +type ( + // TemplateType is used to represent a project template type + TemplateType uint8 + + // TemplateConfig is used to identify the template type of project that is being created + TemplateConfig struct { + TemplateType TemplateType + Translation string + } +) + +const ( + // TemplateTypeNone is a project template type that has no predefined columns + TemplateTypeNone TemplateType = iota + + // TemplateTypeBasicKanban is a project template type that has basic predefined columns + TemplateTypeBasicKanban + + // TemplateTypeBugTriage is a project template type that has predefined columns suited to hunting down bugs + TemplateTypeBugTriage +) + +// GetTemplateConfigs retrieves the template configs of configurations project columns could have +func GetTemplateConfigs() []TemplateConfig { + return []TemplateConfig{ + {TemplateTypeNone, "repo.projects.type.none"}, + {TemplateTypeBasicKanban, "repo.projects.type.basic_kanban"}, + {TemplateTypeBugTriage, "repo.projects.type.bug_triage"}, + } +} + +// IsTemplateTypeValid checks if the project template type is valid +func IsTemplateTypeValid(p TemplateType) bool { + switch p { + case TemplateTypeNone, TemplateTypeBasicKanban, TemplateTypeBugTriage: + return true + default: + return false + } +} diff --git a/models/unit/unit.go b/models/unit/unit.go index 8b4d0caa4c..3beee6a572 100644 --- a/models/unit/unit.go +++ b/models/unit/unit.go @@ -28,7 +28,7 @@ const ( TypeWiki // 5 Wiki TypeExternalWiki // 6 ExternalWiki TypeExternalTracker // 7 ExternalTracker - TypeProjects // 8 Kanban board + TypeProjects // 8 Projects TypePackages // 9 Packages TypeActions // 10 Actions ) diff --git a/modules/indexer/issues/bleve/bleve.go b/modules/indexer/issues/bleve/bleve.go index e3193dbc02..b20fcc6f80 100644 --- a/modules/indexer/issues/bleve/bleve.go +++ b/modules/indexer/issues/bleve/bleve.go @@ -230,8 +230,8 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( if options.ProjectID.Has() { queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectID.Value(), "project_id")) } - if options.ProjectBoardID.Has() { - queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectBoardID.Value(), "project_board_id")) + if options.ProjectColumnID.Has() { + queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectColumnID.Value(), "project_board_id")) } if options.PosterID.Has() { diff --git a/modules/indexer/issues/db/options.go b/modules/indexer/issues/db/options.go index eeaf1696ad..875a4ca279 100644 --- a/modules/indexer/issues/db/options.go +++ b/modules/indexer/issues/db/options.go @@ -61,7 +61,7 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m ReviewedID: convertID(options.ReviewedID), SubscriberID: convertID(options.SubscriberID), ProjectID: convertID(options.ProjectID), - ProjectBoardID: convertID(options.ProjectBoardID), + ProjectColumnID: convertID(options.ProjectColumnID), IsClosed: options.IsClosed, IsPull: options.IsPull, IncludedLabelNames: nil, diff --git a/modules/indexer/issues/dboptions.go b/modules/indexer/issues/dboptions.go index 8f94088742..d9cf9b5e3b 100644 --- a/modules/indexer/issues/dboptions.go +++ b/modules/indexer/issues/dboptions.go @@ -50,7 +50,7 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp } searchOpt.ProjectID = convertID(opts.ProjectID) - searchOpt.ProjectBoardID = convertID(opts.ProjectBoardID) + searchOpt.ProjectColumnID = convertID(opts.ProjectColumnID) searchOpt.PosterID = convertID(opts.PosterID) searchOpt.AssigneeID = convertID(opts.AssigneeID) searchOpt.MentionID = convertID(opts.MentionedID) diff --git a/modules/indexer/issues/elasticsearch/elasticsearch.go b/modules/indexer/issues/elasticsearch/elasticsearch.go index c7cb59f2cf..6f70515009 100644 --- a/modules/indexer/issues/elasticsearch/elasticsearch.go +++ b/modules/indexer/issues/elasticsearch/elasticsearch.go @@ -197,8 +197,8 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( if options.ProjectID.Has() { query.Must(elastic.NewTermQuery("project_id", options.ProjectID.Value())) } - if options.ProjectBoardID.Has() { - query.Must(elastic.NewTermQuery("project_board_id", options.ProjectBoardID.Value())) + if options.ProjectColumnID.Has() { + query.Must(elastic.NewTermQuery("project_board_id", options.ProjectColumnID.Value())) } if options.PosterID.Has() { diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index 0d0cfc8516..e426229f78 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -369,13 +369,13 @@ func searchIssueInProject(t *testing.T) { }, { SearchOptions{ - ProjectBoardID: optional.Some(int64(1)), + ProjectColumnID: optional.Some(int64(1)), }, []int64{1}, }, { SearchOptions{ - ProjectBoardID: optional.Some(int64(0)), // issue with in default board + ProjectColumnID: optional.Some(int64(0)), // issue with in default column }, []int64{2}, }, diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go index e9c4eca559..2dfee8b72e 100644 --- a/modules/indexer/issues/internal/model.go +++ b/modules/indexer/issues/internal/model.go @@ -27,7 +27,7 @@ type IndexerData struct { NoLabel bool `json:"no_label"` // True if LabelIDs is empty MilestoneID int64 `json:"milestone_id"` ProjectID int64 `json:"project_id"` - ProjectBoardID int64 `json:"project_board_id"` + ProjectColumnID int64 `json:"project_board_id"` // the key should be kept as project_board_id to keep compatible PosterID int64 `json:"poster_id"` AssigneeID int64 `json:"assignee_id"` MentionIDs []int64 `json:"mention_ids"` @@ -89,8 +89,8 @@ type SearchOptions struct { MilestoneIDs []int64 // milestones the issues have - ProjectID optional.Option[int64] // project the issues belong to - ProjectBoardID optional.Option[int64] // project board the issues belong to + ProjectID optional.Option[int64] // project the issues belong to + ProjectColumnID optional.Option[int64] // project column the issues belong to PosterID optional.Option[int64] // poster of the issues diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go index 66e396e02c..5f2488a06e 100644 --- a/modules/indexer/issues/internal/tests/tests.go +++ b/modules/indexer/issues/internal/tests/tests.go @@ -352,38 +352,38 @@ var cases = []*testIndexerCase{ }, }, { - Name: "ProjectBoardID", + Name: "ProjectColumnID", SearchOptions: &internal.SearchOptions{ Paginator: &db.ListOptions{ PageSize: 5, }, - ProjectBoardID: optional.Some(int64(1)), + ProjectColumnID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) for _, v := range result.Hits { - assert.Equal(t, int64(1), data[v.ID].ProjectBoardID) + assert.Equal(t, int64(1), data[v.ID].ProjectColumnID) } assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { - return v.ProjectBoardID == 1 + return v.ProjectColumnID == 1 }), result.Total) }, }, { - Name: "no ProjectBoardID", + Name: "no ProjectColumnID", SearchOptions: &internal.SearchOptions{ Paginator: &db.ListOptions{ PageSize: 5, }, - ProjectBoardID: optional.Some(int64(0)), + ProjectColumnID: optional.Some(int64(0)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) for _, v := range result.Hits { - assert.Equal(t, int64(0), data[v.ID].ProjectBoardID) + assert.Equal(t, int64(0), data[v.ID].ProjectColumnID) } assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { - return v.ProjectBoardID == 0 + return v.ProjectColumnID == 0 }), result.Total) }, }, @@ -720,7 +720,7 @@ func generateDefaultIndexerData() []*internal.IndexerData { NoLabel: len(labelIDs) == 0, MilestoneID: issueIndex % 4, ProjectID: issueIndex % 5, - ProjectBoardID: issueIndex % 6, + ProjectColumnID: issueIndex % 6, PosterID: id%10 + 1, // PosterID should not be 0 AssigneeID: issueIndex % 10, MentionIDs: mentionIDs, diff --git a/modules/indexer/issues/meilisearch/meilisearch.go b/modules/indexer/issues/meilisearch/meilisearch.go index 8a7cec6cba..9332319339 100644 --- a/modules/indexer/issues/meilisearch/meilisearch.go +++ b/modules/indexer/issues/meilisearch/meilisearch.go @@ -174,8 +174,8 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( if options.ProjectID.Has() { query.And(inner_meilisearch.NewFilterEq("project_id", options.ProjectID.Value())) } - if options.ProjectBoardID.Has() { - query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectBoardID.Value())) + if options.ProjectColumnID.Has() { + query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectColumnID.Value())) } if options.PosterID.Has() { diff --git a/modules/indexer/issues/util.go b/modules/indexer/issues/util.go index 9861c808dc..e752ae6f24 100644 --- a/modules/indexer/issues/util.go +++ b/modules/indexer/issues/util.go @@ -105,7 +105,7 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD NoLabel: len(labels) == 0, MilestoneID: issue.MilestoneID, ProjectID: projectID, - ProjectBoardID: issue.ProjectBoardID(ctx), + ProjectColumnID: issue.ProjectColumnID(ctx), PosterID: issue.PosterID, AssigneeID: issue.AssigneeID, MentionIDs: mentionIDs, diff --git a/modules/metrics/collector.go b/modules/metrics/collector.go index 1bf8f58b93..230260ff94 100755 --- a/modules/metrics/collector.go +++ b/modules/metrics/collector.go @@ -36,7 +36,7 @@ type Collector struct { Oauths *prometheus.Desc Organizations *prometheus.Desc Projects *prometheus.Desc - ProjectBoards *prometheus.Desc + ProjectColumns *prometheus.Desc PublicKeys *prometheus.Desc Releases *prometheus.Desc Repositories *prometheus.Desc @@ -146,9 +146,9 @@ func NewCollector() Collector { "Number of projects", nil, nil, ), - ProjectBoards: prometheus.NewDesc( - namespace+"projects_boards", - "Number of project boards", + ProjectColumns: prometheus.NewDesc( + namespace+"projects_boards", // TODO: change the key name will affect the consume's result history + "Number of project columns", nil, nil, ), PublicKeys: prometheus.NewDesc( @@ -219,7 +219,7 @@ func (c Collector) Describe(ch chan<- *prometheus.Desc) { ch <- c.Oauths ch <- c.Organizations ch <- c.Projects - ch <- c.ProjectBoards + ch <- c.ProjectColumns ch <- c.PublicKeys ch <- c.Releases ch <- c.Repositories @@ -336,9 +336,9 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) { float64(stats.Counter.Project), ) ch <- prometheus.MustNewConstMetric( - c.ProjectBoards, + c.ProjectColumns, prometheus.GaugeValue, - float64(stats.Counter.ProjectBoard), + float64(stats.Counter.ProjectColumn), ) ch <- prometheus.MustNewConstMetric( c.PublicKeys, diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 3f3b7143ad..1232ad8d5f 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1237,7 +1237,7 @@ tag = Tag tags = Tags issues = Issues pulls = Pull requests -project_board = Projects +project = Projects packages = Packages actions = Actions release = Release diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index e6f3a19625..705a412347 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -34,7 +34,7 @@ const ( // MustEnableProjects check if projects are enabled in settings func MustEnableProjects(ctx *context.Context) { if unit.TypeProjects.UnitGlobalDisabled() { - ctx.NotFound("EnableKanbanBoard", nil) + ctx.NotFound("EnableProjects", nil) return } } @@ -42,7 +42,7 @@ func MustEnableProjects(ctx *context.Context) { // Projects renders the home page of projects func Projects(ctx *context.Context) { shared_user.PrepareContextForProfileBigAvatar(ctx) - ctx.Data["Title"] = ctx.Tr("repo.project_board") + ctx.Data["Title"] = ctx.Tr("repo.projects") sortType := ctx.FormTrim("sort") @@ -139,7 +139,7 @@ func canWriteProjects(ctx *context.Context) bool { // RenderNewProject render creating a project page func RenderNewProject(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.projects.new") - ctx.Data["BoardTypes"] = project_model.GetBoardConfig() + ctx.Data["TemplateConfigs"] = project_model.GetTemplateConfigs() ctx.Data["CardTypes"] = project_model.GetCardConfig() ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) ctx.Data["PageIsViewProjects"] = true @@ -168,12 +168,12 @@ func NewProjectPost(ctx *context.Context) { } newProject := project_model.Project{ - OwnerID: ctx.ContextUser.ID, - Title: form.Title, - Description: form.Content, - CreatorID: ctx.Doer.ID, - BoardType: form.BoardType, - CardType: form.CardType, + OwnerID: ctx.ContextUser.ID, + Title: form.Title, + Description: form.Content, + CreatorID: ctx.Doer.ID, + TemplateType: form.TemplateType, + CardType: form.CardType, } if ctx.ContextUser.IsOrganization() { @@ -314,7 +314,7 @@ func EditProjectPost(ctx *context.Context) { } } -// ViewProject renders the project board for a project +// ViewProject renders the project with board view for a project func ViewProject(ctx *context.Context) { project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) if err != nil { @@ -326,15 +326,15 @@ func ViewProject(ctx *context.Context) { return } - boards, err := project.GetBoards(ctx) + columns, err := project.GetColumns(ctx) if err != nil { - ctx.ServerError("GetProjectBoards", err) + ctx.ServerError("GetProjectColumns", err) return } - issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards) + issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns) if err != nil { - ctx.ServerError("LoadIssuesOfBoards", err) + ctx.ServerError("LoadIssuesOfColumns", err) return } @@ -377,7 +377,7 @@ func ViewProject(ctx *context.Context) { ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) ctx.Data["Project"] = project ctx.Data["IssuesMap"] = issuesMap - ctx.Data["Columns"] = boards // TODO: rename boards to columns in backend + ctx.Data["Columns"] = columns shared_user.RenderUserHeader(ctx) err = shared_user.LoadHeaderCount(ctx) @@ -389,8 +389,8 @@ func ViewProject(ctx *context.Context) { ctx.HTML(http.StatusOK, tplProjectsView) } -// DeleteProjectBoard allows for the deletion of a project board -func DeleteProjectBoard(ctx *context.Context) { +// DeleteProjectColumn allows for the deletion of a project column +func DeleteProjectColumn(ctx *context.Context) { if ctx.Doer == nil { ctx.JSON(http.StatusForbidden, map[string]string{ "message": "Only signed in users are allowed to perform this action.", @@ -404,36 +404,36 @@ func DeleteProjectBoard(ctx *context.Context) { return } - pb, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) + pb, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID")) if err != nil { - ctx.ServerError("GetProjectBoard", err) + ctx.ServerError("GetProjectColumn", err) return } if pb.ProjectID != ctx.ParamsInt64(":id") { ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ - "message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", pb.ID, project.ID), + "message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", pb.ID, project.ID), }) return } if project.OwnerID != ctx.ContextUser.ID { ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ - "message": fmt.Sprintf("ProjectBoard[%d] is not in Owner[%d] as expected", pb.ID, ctx.ContextUser.ID), + "message": fmt.Sprintf("ProjectColumn[%d] is not in Owner[%d] as expected", pb.ID, ctx.ContextUser.ID), }) return } - if err := project_model.DeleteBoardByID(ctx, ctx.ParamsInt64(":boardID")); err != nil { - ctx.ServerError("DeleteProjectBoardByID", err) + if err := project_model.DeleteColumnByID(ctx, ctx.ParamsInt64(":columnID")); err != nil { + ctx.ServerError("DeleteProjectColumnByID", err) return } ctx.JSONOK() } -// AddBoardToProjectPost allows a new board to be added to a project. -func AddBoardToProjectPost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.EditProjectBoardForm) +// AddColumnToProjectPost allows a new column to be added to a project. +func AddColumnToProjectPost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.EditProjectColumnForm) project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) if err != nil { @@ -441,21 +441,21 @@ func AddBoardToProjectPost(ctx *context.Context) { return } - if err := project_model.NewBoard(ctx, &project_model.Board{ + if err := project_model.NewColumn(ctx, &project_model.Column{ ProjectID: project.ID, Title: form.Title, Color: form.Color, CreatorID: ctx.Doer.ID, }); err != nil { - ctx.ServerError("NewProjectBoard", err) + ctx.ServerError("NewProjectColumn", err) return } ctx.JSONOK() } -// CheckProjectBoardChangePermissions check permission -func CheckProjectBoardChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Board) { +// CheckProjectColumnChangePermissions check permission +func CheckProjectColumnChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Column) { if ctx.Doer == nil { ctx.JSON(http.StatusForbidden, map[string]string{ "message": "Only signed in users are allowed to perform this action.", @@ -469,62 +469,60 @@ func CheckProjectBoardChangePermissions(ctx *context.Context) (*project_model.Pr return nil, nil } - board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) + column, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID")) if err != nil { - ctx.ServerError("GetProjectBoard", err) + ctx.ServerError("GetProjectColumn", err) return nil, nil } - if board.ProjectID != ctx.ParamsInt64(":id") { + if column.ProjectID != ctx.ParamsInt64(":id") { ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ - "message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", board.ID, project.ID), + "message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", column.ID, project.ID), }) return nil, nil } if project.OwnerID != ctx.ContextUser.ID { ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ - "message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", board.ID, project.ID), + "message": fmt.Sprintf("ProjectColumn[%d] is not in Repository[%d] as expected", column.ID, project.ID), }) return nil, nil } - return project, board + return project, column } -// EditProjectBoard allows a project board's to be updated -func EditProjectBoard(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.EditProjectBoardForm) - _, board := CheckProjectBoardChangePermissions(ctx) +// EditProjectColumn allows a project column's to be updated +func EditProjectColumn(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.EditProjectColumnForm) + _, column := CheckProjectColumnChangePermissions(ctx) if ctx.Written() { return } if form.Title != "" { - board.Title = form.Title + column.Title = form.Title } - - board.Color = form.Color - + column.Color = form.Color if form.Sorting != 0 { - board.Sorting = form.Sorting + column.Sorting = form.Sorting } - if err := project_model.UpdateBoard(ctx, board); err != nil { - ctx.ServerError("UpdateProjectBoard", err) + if err := project_model.UpdateColumn(ctx, column); err != nil { + ctx.ServerError("UpdateProjectColumn", err) return } ctx.JSONOK() } -// SetDefaultProjectBoard set default board for uncategorized issues/pulls -func SetDefaultProjectBoard(ctx *context.Context) { - project, board := CheckProjectBoardChangePermissions(ctx) +// SetDefaultProjectColumn set default column for uncategorized issues/pulls +func SetDefaultProjectColumn(ctx *context.Context) { + project, column := CheckProjectColumnChangePermissions(ctx) if ctx.Written() { return } - if err := project_model.SetDefaultBoard(ctx, project.ID, board.ID); err != nil { - ctx.ServerError("SetDefaultBoard", err) + if err := project_model.SetDefaultColumn(ctx, project.ID, column.ID); err != nil { + ctx.ServerError("SetDefaultColumn", err) return } @@ -550,14 +548,14 @@ func MoveIssues(ctx *context.Context) { return } - board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) + column, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID")) if err != nil { - ctx.NotFoundOrServerError("GetProjectBoard", project_model.IsErrProjectBoardNotExist, err) + ctx.NotFoundOrServerError("GetProjectColumn", project_model.IsErrProjectColumnNotExist, err) return } - if board.ProjectID != project.ID { - ctx.NotFound("BoardNotInProject", nil) + if column.ProjectID != project.ID { + ctx.NotFound("ColumnNotInProject", nil) return } @@ -602,8 +600,8 @@ func MoveIssues(ctx *context.Context) { } } - if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs); err != nil { - ctx.ServerError("MoveIssuesOnProjectBoard", err) + if err = project_model.MoveIssuesOnProjectColumn(ctx, column, sortedIssueIDs); err != nil { + ctx.ServerError("MoveIssuesOnProjectColumn", err) return } diff --git a/routers/web/org/projects_test.go b/routers/web/org/projects_test.go index f4ccfe1c06..ab419cc878 100644 --- a/routers/web/org/projects_test.go +++ b/routers/web/org/projects_test.go @@ -13,16 +13,16 @@ import ( "github.com/stretchr/testify/assert" ) -func TestCheckProjectBoardChangePermissions(t *testing.T) { +func TestCheckProjectColumnChangePermissions(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/-/projects/4/4") contexttest.LoadUser(t, ctx, 2) ctx.ContextUser = ctx.Doer // user2 ctx.SetParams(":id", "4") - ctx.SetParams(":boardID", "4") + ctx.SetParams(":columnID", "4") - project, board := org.CheckProjectBoardChangePermissions(ctx) + project, column := org.CheckProjectColumnChangePermissions(ctx) assert.NotNil(t, project) - assert.NotNil(t, board) + assert.NotNil(t, column) assert.False(t, ctx.Written()) } diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 350e577ede..358554d2ae 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -2822,12 +2822,12 @@ func ListIssues(ctx *context.Context) { Page: ctx.FormInt("page"), PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), }, - Keyword: keyword, - RepoIDs: []int64{ctx.Repo.Repository.ID}, - IsPull: isPull, - IsClosed: isClosed, - ProjectBoardID: projectID, - SortBy: issue_indexer.SortByCreatedDesc, + Keyword: keyword, + RepoIDs: []int64{ctx.Repo.Repository.ID}, + IsPull: isPull, + IsClosed: isClosed, + ProjectID: projectID, + SortBy: issue_indexer.SortByCreatedDesc, } if since != 0 { searchOpt.UpdatedAfterUnix = optional.Some(since) diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 934cf8873b..878b7ee699 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -36,7 +36,7 @@ const ( // MustEnableProjects check if projects are enabled in settings func MustEnableProjects(ctx *context.Context) { if unit.TypeProjects.UnitGlobalDisabled() { - ctx.NotFound("EnableKanbanBoard", nil) + ctx.NotFound("EnableRepoProjects", nil) return } @@ -50,7 +50,7 @@ func MustEnableProjects(ctx *context.Context) { // Projects renders the home page of projects func Projects(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("repo.project_board") + ctx.Data["Title"] = ctx.Tr("repo.projects") sortType := ctx.FormTrim("sort") @@ -131,7 +131,7 @@ func Projects(ctx *context.Context) { // RenderNewProject render creating a project page func RenderNewProject(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.projects.new") - ctx.Data["BoardTypes"] = project_model.GetBoardConfig() + ctx.Data["TemplateConfigs"] = project_model.GetTemplateConfigs() ctx.Data["CardTypes"] = project_model.GetCardConfig() ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) ctx.Data["CancelLink"] = ctx.Repo.Repository.Link() + "/projects" @@ -149,13 +149,13 @@ func NewProjectPost(ctx *context.Context) { } if err := project_model.NewProject(ctx, &project_model.Project{ - RepoID: ctx.Repo.Repository.ID, - Title: form.Title, - Description: form.Content, - CreatorID: ctx.Doer.ID, - BoardType: form.BoardType, - CardType: form.CardType, - Type: project_model.TypeRepository, + RepoID: ctx.Repo.Repository.ID, + Title: form.Title, + Description: form.Content, + CreatorID: ctx.Doer.ID, + TemplateType: form.TemplateType, + CardType: form.CardType, + Type: project_model.TypeRepository, }); err != nil { ctx.ServerError("NewProject", err) return @@ -288,7 +288,7 @@ func EditProjectPost(ctx *context.Context) { } } -// ViewProject renders the project board for a project +// ViewProject renders the project with board view func ViewProject(ctx *context.Context) { project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) if err != nil { @@ -304,15 +304,15 @@ func ViewProject(ctx *context.Context) { return } - boards, err := project.GetBoards(ctx) + columns, err := project.GetColumns(ctx) if err != nil { - ctx.ServerError("GetProjectBoards", err) + ctx.ServerError("GetProjectColumns", err) return } - issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards) + issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns) if err != nil { - ctx.ServerError("LoadIssuesOfBoards", err) + ctx.ServerError("LoadIssuesOfColumns", err) return } @@ -367,7 +367,7 @@ func ViewProject(ctx *context.Context) { ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) ctx.Data["Project"] = project ctx.Data["IssuesMap"] = issuesMap - ctx.Data["Columns"] = boards // TODO: rename boards to columns in backend + ctx.Data["Columns"] = columns ctx.HTML(http.StatusOK, tplProjectsView) } @@ -405,8 +405,8 @@ func UpdateIssueProject(ctx *context.Context) { ctx.JSONOK() } -// DeleteProjectBoard allows for the deletion of a project board -func DeleteProjectBoard(ctx *context.Context) { +// DeleteProjectColumn allows for the deletion of a project column +func DeleteProjectColumn(ctx *context.Context) { if ctx.Doer == nil { ctx.JSON(http.StatusForbidden, map[string]string{ "message": "Only signed in users are allowed to perform this action.", @@ -431,36 +431,36 @@ func DeleteProjectBoard(ctx *context.Context) { return } - pb, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) + pb, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID")) if err != nil { - ctx.ServerError("GetProjectBoard", err) + ctx.ServerError("GetProjectColumn", err) return } if pb.ProjectID != ctx.ParamsInt64(":id") { ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ - "message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", pb.ID, project.ID), + "message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", pb.ID, project.ID), }) return } if project.RepoID != ctx.Repo.Repository.ID { ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ - "message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", pb.ID, ctx.Repo.Repository.ID), + "message": fmt.Sprintf("ProjectColumn[%d] is not in Repository[%d] as expected", pb.ID, ctx.Repo.Repository.ID), }) return } - if err := project_model.DeleteBoardByID(ctx, ctx.ParamsInt64(":boardID")); err != nil { - ctx.ServerError("DeleteProjectBoardByID", err) + if err := project_model.DeleteColumnByID(ctx, ctx.ParamsInt64(":columnID")); err != nil { + ctx.ServerError("DeleteProjectColumnByID", err) return } ctx.JSONOK() } -// AddBoardToProjectPost allows a new board to be added to a project. -func AddBoardToProjectPost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.EditProjectBoardForm) +// AddColumnToProjectPost allows a new column to be added to a project. +func AddColumnToProjectPost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.EditProjectColumnForm) if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { ctx.JSON(http.StatusForbidden, map[string]string{ "message": "Only authorized users are allowed to perform this action.", @@ -478,20 +478,20 @@ func AddBoardToProjectPost(ctx *context.Context) { return } - if err := project_model.NewBoard(ctx, &project_model.Board{ + if err := project_model.NewColumn(ctx, &project_model.Column{ ProjectID: project.ID, Title: form.Title, Color: form.Color, CreatorID: ctx.Doer.ID, }); err != nil { - ctx.ServerError("NewProjectBoard", err) + ctx.ServerError("NewProjectColumn", err) return } ctx.JSONOK() } -func checkProjectBoardChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Board) { +func checkProjectColumnChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Column) { if ctx.Doer == nil { ctx.JSON(http.StatusForbidden, map[string]string{ "message": "Only signed in users are allowed to perform this action.", @@ -516,62 +516,60 @@ func checkProjectBoardChangePermissions(ctx *context.Context) (*project_model.Pr return nil, nil } - board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) + column, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID")) if err != nil { - ctx.ServerError("GetProjectBoard", err) + ctx.ServerError("GetProjectColumn", err) return nil, nil } - if board.ProjectID != ctx.ParamsInt64(":id") { + if column.ProjectID != ctx.ParamsInt64(":id") { ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ - "message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", board.ID, project.ID), + "message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", column.ID, project.ID), }) return nil, nil } if project.RepoID != ctx.Repo.Repository.ID { ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ - "message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", board.ID, ctx.Repo.Repository.ID), + "message": fmt.Sprintf("ProjectColumn[%d] is not in Repository[%d] as expected", column.ID, ctx.Repo.Repository.ID), }) return nil, nil } - return project, board + return project, column } -// EditProjectBoard allows a project board's to be updated -func EditProjectBoard(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.EditProjectBoardForm) - _, board := checkProjectBoardChangePermissions(ctx) +// EditProjectColumn allows a project column's to be updated +func EditProjectColumn(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.EditProjectColumnForm) + _, column := checkProjectColumnChangePermissions(ctx) if ctx.Written() { return } if form.Title != "" { - board.Title = form.Title + column.Title = form.Title } - - board.Color = form.Color - + column.Color = form.Color if form.Sorting != 0 { - board.Sorting = form.Sorting + column.Sorting = form.Sorting } - if err := project_model.UpdateBoard(ctx, board); err != nil { - ctx.ServerError("UpdateProjectBoard", err) + if err := project_model.UpdateColumn(ctx, column); err != nil { + ctx.ServerError("UpdateProjectColumn", err) return } ctx.JSONOK() } -// SetDefaultProjectBoard set default board for uncategorized issues/pulls -func SetDefaultProjectBoard(ctx *context.Context) { - project, board := checkProjectBoardChangePermissions(ctx) +// SetDefaultProjectColumn set default column for uncategorized issues/pulls +func SetDefaultProjectColumn(ctx *context.Context) { + project, column := checkProjectColumnChangePermissions(ctx) if ctx.Written() { return } - if err := project_model.SetDefaultBoard(ctx, project.ID, board.ID); err != nil { - ctx.ServerError("SetDefaultBoard", err) + if err := project_model.SetDefaultColumn(ctx, project.ID, column.ID); err != nil { + ctx.ServerError("SetDefaultColumn", err) return } @@ -608,18 +606,18 @@ func MoveIssues(ctx *context.Context) { return } - board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) + column, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID")) if err != nil { - if project_model.IsErrProjectBoardNotExist(err) { - ctx.NotFound("ProjectBoardNotExist", nil) + if project_model.IsErrProjectColumnNotExist(err) { + ctx.NotFound("ProjectColumnNotExist", nil) } else { - ctx.ServerError("GetProjectBoard", err) + ctx.ServerError("GetProjectColumn", err) } return } - if board.ProjectID != project.ID { - ctx.NotFound("BoardNotInProject", nil) + if column.ProjectID != project.ID { + ctx.NotFound("ColumnNotInProject", nil) return } @@ -663,8 +661,8 @@ func MoveIssues(ctx *context.Context) { } } - if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs); err != nil { - ctx.ServerError("MoveIssuesOnProjectBoard", err) + if err = project_model.MoveIssuesOnProjectColumn(ctx, column, sortedIssueIDs); err != nil { + ctx.ServerError("MoveIssuesOnProjectColumn", err) return } diff --git a/routers/web/repo/projects_test.go b/routers/web/repo/projects_test.go index 479f8c55a2..d61230a57e 100644 --- a/routers/web/repo/projects_test.go +++ b/routers/web/repo/projects_test.go @@ -12,16 +12,16 @@ import ( "github.com/stretchr/testify/assert" ) -func TestCheckProjectBoardChangePermissions(t *testing.T) { +func TestCheckProjectColumnChangePermissions(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1/projects/1/2") contexttest.LoadUser(t, ctx, 2) contexttest.LoadRepo(t, ctx, 1) ctx.SetParams(":id", "1") - ctx.SetParams(":boardID", "2") + ctx.SetParams(":columnID", "2") - project, board := checkProjectBoardChangePermissions(ctx) + project, column := checkProjectColumnChangePermissions(ctx) assert.NotNil(t, project) - assert.NotNil(t, board) + assert.NotNil(t, column) assert.False(t, ctx.Written()) } diff --git a/routers/web/web.go b/routers/web/web.go index a65994c246..34bfb8b60a 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -978,7 +978,7 @@ func registerRoutes(m *web.Route) { m.Get("/new", org.RenderNewProject) m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost) m.Group("/{id}", func() { - m.Post("", web.Bind(forms.EditProjectBoardForm{}), org.AddBoardToProjectPost) + m.Post("", web.Bind(forms.EditProjectColumnForm{}), org.AddColumnToProjectPost) m.Post("/move", project.MoveColumns) m.Post("/delete", org.DeleteProject) @@ -986,10 +986,10 @@ func registerRoutes(m *web.Route) { m.Post("/edit", web.Bind(forms.CreateProjectForm{}), org.EditProjectPost) m.Post("/{action:open|close}", org.ChangeProjectStatus) - m.Group("/{boardID}", func() { - m.Put("", web.Bind(forms.EditProjectBoardForm{}), org.EditProjectBoard) - m.Delete("", org.DeleteProjectBoard) - m.Post("/default", org.SetDefaultProjectBoard) + m.Group("/{columnID}", func() { + m.Put("", web.Bind(forms.EditProjectColumnForm{}), org.EditProjectColumn) + m.Delete("", org.DeleteProjectColumn) + m.Post("/default", org.SetDefaultProjectColumn) m.Post("/move", org.MoveIssues) }) @@ -1352,7 +1352,7 @@ func registerRoutes(m *web.Route) { m.Get("/new", repo.RenderNewProject) m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost) m.Group("/{id}", func() { - m.Post("", web.Bind(forms.EditProjectBoardForm{}), repo.AddBoardToProjectPost) + m.Post("", web.Bind(forms.EditProjectColumnForm{}), repo.AddColumnToProjectPost) m.Post("/move", project.MoveColumns) m.Post("/delete", repo.DeleteProject) @@ -1360,10 +1360,10 @@ func registerRoutes(m *web.Route) { m.Post("/edit", web.Bind(forms.CreateProjectForm{}), repo.EditProjectPost) m.Post("/{action:open|close}", repo.ChangeProjectStatus) - m.Group("/{boardID}", func() { - m.Put("", web.Bind(forms.EditProjectBoardForm{}), repo.EditProjectBoard) - m.Delete("", repo.DeleteProjectBoard) - m.Post("/default", repo.SetDefaultProjectBoard) + m.Group("/{columnID}", func() { + m.Put("", web.Bind(forms.EditProjectColumnForm{}), repo.EditProjectColumn) + m.Delete("", repo.DeleteProjectColumn) + m.Post("/default", repo.SetDefaultProjectColumn) m.Post("/move", repo.MoveIssues) }) diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index e826d179ed..0fd8965df9 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -370,45 +370,21 @@ func (i IssueLockForm) HasValidReason() bool { return false } -// __________ __ __ -// \______ \_______ ____ |__| ____ _____/ |_ ______ -// | ___/\_ __ \/ _ \ | |/ __ \_/ ___\ __\/ ___/ -// | | | | \( <_> ) | \ ___/\ \___| | \___ \ -// |____| |__| \____/\__| |\___ >\___ >__| /____ > -// \______| \/ \/ \/ - // CreateProjectForm form for creating a project type CreateProjectForm struct { - Title string `binding:"Required;MaxSize(100)"` - Content string - BoardType project_model.BoardType - CardType project_model.CardType + Title string `binding:"Required;MaxSize(100)"` + Content string + TemplateType project_model.TemplateType + CardType project_model.CardType } -// UserCreateProjectForm is a from for creating an individual or organization -// form. -type UserCreateProjectForm struct { - Title string `binding:"Required;MaxSize(100)"` - Content string - BoardType project_model.BoardType - CardType project_model.CardType - UID int64 `binding:"Required"` -} - -// EditProjectBoardForm is a form for editing a project board -type EditProjectBoardForm struct { +// EditProjectColumnForm is a form for editing a project column +type EditProjectColumnForm struct { Title string `binding:"Required;MaxSize(100)"` Sorting int8 Color string `binding:"MaxSize(7)"` } -// _____ .__.__ __ -// / \ |__| | ____ _______/ |_ ____ ____ ____ -// / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \ -// / Y \ | |_\ ___/ \___ \ | | ( <_> ) | \ ___/ -// \____|__ /__|____/\___ >____ > |__| \____/|___| /\___ > -// \/ \/ \/ \/ \/ - // CreateMilestoneForm form for creating milestone type CreateMilestoneForm struct { Title string `binding:"Required;MaxSize(50)"` @@ -422,13 +398,6 @@ func (f *CreateMilestoneForm) Validate(req *http.Request, errs binding.Errors) b return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } -// .____ ___. .__ -// | | _____ \_ |__ ____ | | -// | | \__ \ | __ \_/ __ \| | -// | |___ / __ \| \_\ \ ___/| |__ -// |_______ (____ /___ /\___ >____/ -// \/ \/ \/ \/ - // CreateLabelForm form for creating label type CreateLabelForm struct { ID int64 @@ -456,13 +425,6 @@ func (f *InitializeLabelsForm) Validate(req *http.Request, errs binding.Errors) return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } -// __________ .__ .__ __________ __ -// \______ \__ __| | | | \______ \ ____ ________ __ ____ _______/ |_ -// | ___/ | \ | | | | _// __ \/ ____/ | \_/ __ \ / ___/\ __\ -// | | | | / |_| |__ | | \ ___< <_| | | /\ ___/ \___ \ | | -// |____| |____/|____/____/ |____|_ /\___ >__ |____/ \___ >____ > |__| -// \/ \/ |__| \/ \/ - // MergePullRequestForm form for merging Pull Request // swagger:model MergePullRequestOption type MergePullRequestForm struct { diff --git a/services/forms/user_form_hidden_comments.go b/services/forms/user_form_hidden_comments.go index c21fddf478..b9677c1800 100644 --- a/services/forms/user_form_hidden_comments.go +++ b/services/forms/user_form_hidden_comments.go @@ -65,7 +65,7 @@ var hiddenCommentTypeGroups = hiddenCommentTypeGroupsType{ }, "project": { /*30*/ issues_model.CommentTypeProject, - /*31*/ issues_model.CommentTypeProjectBoard, + /*31*/ issues_model.CommentTypeProjectColumn, }, "issue_ref": { /*33*/ issues_model.CommentTypeChangeIssueRef, diff --git a/templates/projects/new.tmpl b/templates/projects/new.tmpl index 92ee36c1c4..bd173b54bc 100644 --- a/templates/projects/new.tmpl +++ b/templates/projects/new.tmpl @@ -25,11 +25,11 @@
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index 21017415c1..82e7679cd2 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -131,7 +131,7 @@ {{if and (not .UnitProjectsGlobalDisabled) (.Permission.CanRead $.UnitTypeProjects)}} - {{svg "octicon-project"}} {{ctx.Locale.Tr "repo.project_board"}} + {{svg "octicon-project"}} {{ctx.Locale.Tr "repo.project"}} {{if .Repository.NumOpenProjects}} {{CountFmt .Repository.NumOpenProjects}} {{end}} diff --git a/templates/repo/issue/filter_actions.tmpl b/templates/repo/issue/filter_actions.tmpl index d559f335b7..a341448bcc 100644 --- a/templates/repo/issue/filter_actions.tmpl +++ b/templates/repo/issue/filter_actions.tmpl @@ -71,7 +71,7 @@