diff --git a/MAINTAINERS b/MAINTAINERS index f66973576d342..f920f71f63e83 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -44,7 +44,7 @@ Jimmy Praet (@jpraet) Leon Hofmeister (@delvh) Wim (@42wim) Jason Song (@wolfogre) -Yarden Shoham (@yardenshoham) +Yarden Shoham (@yardenshoham) Yu Tian (@Zettat123) Eddie Yang <576951401@qq.com> (@yp05327) Dong Ge (@sillyguodong) diff --git a/assets/go-licenses.json b/assets/go-licenses.json index ee158292746a3..593d6af5c1168 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -704,6 +704,11 @@ "path": "github.com/matttproud/golang_protobuf_extensions/pbutil/LICENSE", "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, + { + "name": "github.com/meilisearch/meilisearch-go", + "path": "github.com/meilisearch/meilisearch-go/LICENSE", + "licenseText": "MIT License\n\nCopyright (c) 2020-2022 Meili SAS\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + }, { "name": "github.com/mholt/acmez", "path": "github.com/mholt/acmez/LICENSE", @@ -929,6 +934,16 @@ "path": "github.com/urfave/cli/LICENSE", "licenseText": "MIT License\n\nCopyright (c) 2023 Jeremy Saenz \u0026 Contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" }, + { + "name": "github.com/valyala/bytebufferpool", + "path": "github.com/valyala/bytebufferpool/LICENSE", + "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2016 Aliaksandr Valialkin, VertaMedia\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n" + }, + { + "name": "github.com/valyala/fasthttp", + "path": "github.com/valyala/fasthttp/LICENSE", + "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015-present Aliaksandr Valialkin, VertaMedia, Kirill Danshin, Erik Dubbelboer, FastHTTP Authors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" + }, { "name": "github.com/valyala/fastjson", "path": "github.com/valyala/fastjson/LICENSE", diff --git a/build/generate-emoji.go b/build/generate-emoji.go index 60350336c1e01..09bdeb6808283 100644 --- a/build/generate-emoji.go +++ b/build/generate-emoji.go @@ -60,13 +60,13 @@ func main() { // generate data buf, err := generate() if err != nil { - log.Fatal(err) + log.Fatalf("generate err: %v", err) } // write err = os.WriteFile(*flagOut, buf, 0o644) if err != nil { - log.Fatal(err) + log.Fatalf("WriteFile err: %v", err) } } diff --git a/cmd/hook.go b/cmd/hook.go index a62ffdde5fe67..9605fcb3318ff 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -6,9 +6,9 @@ package cmd import ( "bufio" "bytes" + "context" "fmt" "io" - "net/http" "os" "strconv" "strings" @@ -167,11 +167,11 @@ func runHookPreReceive(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setup("hooks/pre-receive.log", c.Bool("debug")) + setup(ctx, c.Bool("debug")) if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 { if setting.OnlyAllowPushIfGiteaEnvironmentSet { - return fail(`Rejecting changes as Gitea environment not set. + return fail(ctx, `Rejecting changes as Gitea environment not set. If you are pushing over SSH you must push with a key managed by Gitea or set your environment appropriately.`, "") } @@ -257,14 +257,9 @@ Gitea or set your environment appropriately.`, "") hookOptions.OldCommitIDs = oldCommitIDs hookOptions.NewCommitIDs = newCommitIDs hookOptions.RefFullNames = refFullNames - statusCode, msg := private.HookPreReceive(ctx, username, reponame, hookOptions) - switch statusCode { - case http.StatusOK: - // no-op - case http.StatusInternalServerError: - return fail("Internal Server Error", msg) - default: - return fail(msg, "") + extra := private.HookPreReceive(ctx, username, reponame, hookOptions) + if extra.HasError() { + return fail(ctx, extra.UserMsg, "HookPreReceive(batch) failed: %v", extra.Error) } count = 0 lastline = 0 @@ -285,12 +280,9 @@ Gitea or set your environment appropriately.`, "") fmt.Fprintf(out, " Checking %d references\n", count) - statusCode, msg := private.HookPreReceive(ctx, username, reponame, hookOptions) - switch statusCode { - case http.StatusInternalServerError: - return fail("Internal Server Error", msg) - case http.StatusForbidden: - return fail(msg, "") + extra := private.HookPreReceive(ctx, username, reponame, hookOptions) + if extra.HasError() { + return fail(ctx, extra.UserMsg, "HookPreReceive(last) failed: %v", extra.Error) } } else if lastline > 0 { fmt.Fprintf(out, "\n") @@ -309,7 +301,7 @@ func runHookPostReceive(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setup("hooks/post-receive.log", c.Bool("debug")) + setup(ctx, c.Bool("debug")) // First of all run update-server-info no matter what if _, _, err := git.NewCommand(ctx, "update-server-info").RunStdString(nil); err != nil { @@ -323,7 +315,7 @@ func runHookPostReceive(c *cli.Context) error { if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 { if setting.OnlyAllowPushIfGiteaEnvironmentSet { - return fail(`Rejecting changes as Gitea environment not set. + return fail(ctx, `Rejecting changes as Gitea environment not set. If you are pushing over SSH you must push with a key managed by Gitea or set your environment appropriately.`, "") } @@ -394,11 +386,11 @@ Gitea or set your environment appropriately.`, "") hookOptions.OldCommitIDs = oldCommitIDs hookOptions.NewCommitIDs = newCommitIDs hookOptions.RefFullNames = refFullNames - resp, err := private.HookPostReceive(ctx, repoUser, repoName, hookOptions) - if resp == nil { + resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions) + if extra.HasError() { _ = dWriter.Close() hookPrintResults(results) - return fail("Internal Server Error", err) + return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error) } wasEmpty = wasEmpty || resp.RepoWasEmpty results = append(results, resp.Results...) @@ -409,9 +401,9 @@ Gitea or set your environment appropriately.`, "") if count == 0 { if wasEmpty && masterPushed { // We need to tell the repo to reset the default branch to master - err := private.SetDefaultBranch(ctx, repoUser, repoName, "master") - if err != nil { - return fail("Internal Server Error", "SetDefaultBranch failed with Error: %v", err) + extra := private.SetDefaultBranch(ctx, repoUser, repoName, "master") + if extra.HasError() { + return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error) } } fmt.Fprintf(out, "Processed %d references in total\n", total) @@ -427,11 +419,11 @@ Gitea or set your environment appropriately.`, "") fmt.Fprintf(out, " Processing %d references\n", count) - resp, err := private.HookPostReceive(ctx, repoUser, repoName, hookOptions) + resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions) if resp == nil { _ = dWriter.Close() hookPrintResults(results) - return fail("Internal Server Error", err) + return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error) } wasEmpty = wasEmpty || resp.RepoWasEmpty results = append(results, resp.Results...) @@ -440,9 +432,9 @@ Gitea or set your environment appropriately.`, "") if wasEmpty && masterPushed { // We need to tell the repo to reset the default branch to master - err := private.SetDefaultBranch(ctx, repoUser, repoName, "master") - if err != nil { - return fail("Internal Server Error", "SetDefaultBranch failed with Error: %v", err) + extra := private.SetDefaultBranch(ctx, repoUser, repoName, "master") + if extra.HasError() { + return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error) } } _ = dWriter.Close() @@ -485,22 +477,22 @@ func pushOptions() map[string]string { } func runHookProcReceive(c *cli.Context) error { - setup("hooks/proc-receive.log", c.Bool("debug")) + ctx, cancel := installSignals() + defer cancel() + + setup(ctx, c.Bool("debug")) if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 { if setting.OnlyAllowPushIfGiteaEnvironmentSet { - return fail(`Rejecting changes as Gitea environment not set. + return fail(ctx, `Rejecting changes as Gitea environment not set. If you are pushing over SSH you must push with a key managed by Gitea or set your environment appropriately.`, "") } return nil } - ctx, cancel := installSignals() - defer cancel() - if git.CheckGitVersionAtLeast("2.29") != nil { - return fail("Internal Server Error", "git not support proc-receive.") + return fail(ctx, "No proc-receive support", "current git version doesn't support proc-receive.") } reader := bufio.NewReader(os.Stdin) @@ -515,7 +507,7 @@ Gitea or set your environment appropriately.`, "") // H: PKT-LINE(version=1\0push-options...) // H: flush-pkt - rs, err := readPktLine(reader, pktLineTypeData) + rs, err := readPktLine(ctx, reader, pktLineTypeData) if err != nil { return err } @@ -530,19 +522,19 @@ Gitea or set your environment appropriately.`, "") index := bytes.IndexByte(rs.Data, byte(0)) if index >= len(rs.Data) { - return fail("Internal Server Error", "pkt-line: format error "+fmt.Sprint(rs.Data)) + return fail(ctx, "Protocol: format error", "pkt-line: format error "+fmt.Sprint(rs.Data)) } if index < 0 { if len(rs.Data) == 10 && rs.Data[9] == '\n' { index = 9 } else { - return fail("Internal Server Error", "pkt-line: format error "+fmt.Sprint(rs.Data)) + return fail(ctx, "Protocol: format error", "pkt-line: format error "+fmt.Sprint(rs.Data)) } } if string(rs.Data[0:index]) != VersionHead { - return fail("Internal Server Error", "Received unsupported version: %s", string(rs.Data[0:index])) + return fail(ctx, "Protocol: version error", "Received unsupported version: %s", string(rs.Data[0:index])) } requestOptions = strings.Split(string(rs.Data[index+1:]), " ") @@ -555,17 +547,17 @@ Gitea or set your environment appropriately.`, "") } response = append(response, '\n') - _, err = readPktLine(reader, pktLineTypeFlush) + _, err = readPktLine(ctx, reader, pktLineTypeFlush) if err != nil { return err } - err = writeDataPktLine(os.Stdout, response) + err = writeDataPktLine(ctx, os.Stdout, response) if err != nil { return err } - err = writeFlushPktLine(os.Stdout) + err = writeFlushPktLine(ctx, os.Stdout) if err != nil { return err } @@ -588,7 +580,7 @@ Gitea or set your environment appropriately.`, "") for { // note: pktLineTypeUnknow means pktLineTypeFlush and pktLineTypeData all allowed - rs, err = readPktLine(reader, pktLineTypeUnknow) + rs, err = readPktLine(ctx, reader, pktLineTypeUnknow) if err != nil { return err } @@ -609,7 +601,7 @@ Gitea or set your environment appropriately.`, "") if hasPushOptions { for { - rs, err = readPktLine(reader, pktLineTypeUnknow) + rs, err = readPktLine(ctx, reader, pktLineTypeUnknow) if err != nil { return err } @@ -626,9 +618,9 @@ Gitea or set your environment appropriately.`, "") } // 3. run hook - resp, err := private.HookProcReceive(ctx, repoUser, repoName, hookOptions) - if err != nil { - return fail("Internal Server Error", "run proc-receive hook failed :%v", err) + resp, extra := private.HookProcReceive(ctx, repoUser, repoName, hookOptions) + if extra.HasError() { + return fail(ctx, extra.UserMsg, "HookProcReceive failed: %v", extra.Error) } // 4. response result to service @@ -649,7 +641,7 @@ Gitea or set your environment appropriately.`, "") for _, rs := range resp.Results { if len(rs.Err) > 0 { - err = writeDataPktLine(os.Stdout, []byte("ng "+rs.OriginalRef+" "+rs.Err)) + err = writeDataPktLine(ctx, os.Stdout, []byte("ng "+rs.OriginalRef+" "+rs.Err)) if err != nil { return err } @@ -657,43 +649,43 @@ Gitea or set your environment appropriately.`, "") } if rs.IsNotMatched { - err = writeDataPktLine(os.Stdout, []byte("ok "+rs.OriginalRef)) + err = writeDataPktLine(ctx, os.Stdout, []byte("ok "+rs.OriginalRef)) if err != nil { return err } - err = writeDataPktLine(os.Stdout, []byte("option fall-through")) + err = writeDataPktLine(ctx, os.Stdout, []byte("option fall-through")) if err != nil { return err } continue } - err = writeDataPktLine(os.Stdout, []byte("ok "+rs.OriginalRef)) + err = writeDataPktLine(ctx, os.Stdout, []byte("ok "+rs.OriginalRef)) if err != nil { return err } - err = writeDataPktLine(os.Stdout, []byte("option refname "+rs.Ref)) + err = writeDataPktLine(ctx, os.Stdout, []byte("option refname "+rs.Ref)) if err != nil { return err } if rs.OldOID != git.EmptySHA { - err = writeDataPktLine(os.Stdout, []byte("option old-oid "+rs.OldOID)) + err = writeDataPktLine(ctx, os.Stdout, []byte("option old-oid "+rs.OldOID)) if err != nil { return err } } - err = writeDataPktLine(os.Stdout, []byte("option new-oid "+rs.NewOID)) + err = writeDataPktLine(ctx, os.Stdout, []byte("option new-oid "+rs.NewOID)) if err != nil { return err } if rs.IsForcePush { - err = writeDataPktLine(os.Stdout, []byte("option forced-update")) + err = writeDataPktLine(ctx, os.Stdout, []byte("option forced-update")) if err != nil { return err } } } - err = writeFlushPktLine(os.Stdout) + err = writeFlushPktLine(ctx, os.Stdout) return err } @@ -718,7 +710,7 @@ type gitPktLine struct { Data []byte } -func readPktLine(in *bufio.Reader, requestType pktLineType) (*gitPktLine, error) { +func readPktLine(ctx context.Context, in *bufio.Reader, requestType pktLineType) (*gitPktLine, error) { var ( err error r *gitPktLine @@ -729,33 +721,33 @@ func readPktLine(in *bufio.Reader, requestType pktLineType) (*gitPktLine, error) for i := 0; i < 4; i++ { lengthBytes[i], err = in.ReadByte() if err != nil { - return nil, fail("Internal Server Error", "Pkt-Line: read stdin failed : %v", err) + return nil, fail(ctx, "Protocol: stdin error", "Pkt-Line: read stdin failed : %v", err) } } r = new(gitPktLine) r.Length, err = strconv.ParseUint(string(lengthBytes), 16, 32) if err != nil { - return nil, fail("Internal Server Error", "Pkt-Line format is wrong :%v", err) + return nil, fail(ctx, "Protocol: format parse error", "Pkt-Line format is wrong :%v", err) } if r.Length == 0 { if requestType == pktLineTypeData { - return nil, fail("Internal Server Error", "Pkt-Line format is wrong") + return nil, fail(ctx, "Protocol: format data error", "Pkt-Line format is wrong") } r.Type = pktLineTypeFlush return r, nil } if r.Length <= 4 || r.Length > 65520 || requestType == pktLineTypeFlush { - return nil, fail("Internal Server Error", "Pkt-Line format is wrong") + return nil, fail(ctx, "Protocol: format length error", "Pkt-Line format is wrong") } r.Data = make([]byte, r.Length-4) for i := range r.Data { r.Data[i], err = in.ReadByte() if err != nil { - return nil, fail("Internal Server Error", "Pkt-Line: read stdin failed : %v", err) + return nil, fail(ctx, "Protocol: data error", "Pkt-Line: read stdin failed : %v", err) } } @@ -764,19 +756,15 @@ func readPktLine(in *bufio.Reader, requestType pktLineType) (*gitPktLine, error) return r, nil } -func writeFlushPktLine(out io.Writer) error { +func writeFlushPktLine(ctx context.Context, out io.Writer) error { l, err := out.Write([]byte("0000")) - if err != nil { - return fail("Internal Server Error", "Pkt-Line response failed: %v", err) + if err != nil || l != 4 { + return fail(ctx, "Protocol: write error", "Pkt-Line response failed: %v", err) } - if l != 4 { - return fail("Internal Server Error", "Pkt-Line response failed: %v", err) - } - return nil } -func writeDataPktLine(out io.Writer, data []byte) error { +func writeDataPktLine(ctx context.Context, out io.Writer, data []byte) error { hexchar := []byte("0123456789abcdef") hex := func(n uint64) byte { return hexchar[(n)&15] @@ -790,19 +778,13 @@ func writeDataPktLine(out io.Writer, data []byte) error { tmp[3] = hex(length) lr, err := out.Write(tmp) - if err != nil { - return fail("Internal Server Error", "Pkt-Line response failed: %v", err) - } - if lr != 4 { - return fail("Internal Server Error", "Pkt-Line response failed: %v", err) + if err != nil || lr != 4 { + return fail(ctx, "Protocol: write error", "Pkt-Line response failed: %v", err) } lr, err = out.Write(data) - if err != nil { - return fail("Internal Server Error", "Pkt-Line response failed: %v", err) - } - if int(length-4) != lr { - return fail("Internal Server Error", "Pkt-Line response failed: %v", err) + if err != nil || int(length-4) != lr { + return fail(ctx, "Protocol: write error", "Pkt-Line response failed: %v", err) } return nil diff --git a/cmd/hook_test.go b/cmd/hook_test.go index fe1f072a6f794..91f24ff2b4466 100644 --- a/cmd/hook_test.go +++ b/cmd/hook_test.go @@ -6,6 +6,7 @@ package cmd import ( "bufio" "bytes" + "context" "strings" "testing" @@ -14,27 +15,28 @@ import ( func TestPktLine(t *testing.T) { // test read + ctx := context.Background() s := strings.NewReader("0000") r := bufio.NewReader(s) - result, err := readPktLine(r, pktLineTypeFlush) + result, err := readPktLine(ctx, r, pktLineTypeFlush) assert.NoError(t, err) assert.Equal(t, pktLineTypeFlush, result.Type) s = strings.NewReader("0006a\n") r = bufio.NewReader(s) - result, err = readPktLine(r, pktLineTypeData) + result, err = readPktLine(ctx, r, pktLineTypeData) assert.NoError(t, err) assert.Equal(t, pktLineTypeData, result.Type) assert.Equal(t, []byte("a\n"), result.Data) // test write w := bytes.NewBuffer([]byte{}) - err = writeFlushPktLine(w) + err = writeFlushPktLine(ctx, w) assert.NoError(t, err) assert.Equal(t, []byte("0000"), w.Bytes()) w.Reset() - err = writeDataPktLine(w, []byte("a\nb")) + err = writeDataPktLine(ctx, w, []byte("a\nb")) assert.NoError(t, err) assert.Equal(t, []byte("0007a\nb"), w.Bytes()) } diff --git a/cmd/keys.go b/cmd/keys.go index 74dc1cc68c6c3..deb94fca5d6ab 100644 --- a/cmd/keys.go +++ b/cmd/keys.go @@ -64,11 +64,12 @@ func runKeys(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setup("keys.log", false) + setup(ctx, false) - authorizedString, err := private.AuthorizedPublicKeyByContent(ctx, content) - if err != nil { - return err + authorizedString, extra := private.AuthorizedPublicKeyByContent(ctx, content) + // do not use handleCliResponseExtra or cli.NewExitError, if it exists immediately, it breaks some tests like Test_CmdKeys + if extra.Error != nil { + return extra.Error } fmt.Println(strings.TrimSpace(authorizedString)) return nil diff --git a/cmd/mailer.go b/cmd/mailer.go index d05fee12bca72..50ba4b474110b 100644 --- a/cmd/mailer.go +++ b/cmd/mailer.go @@ -5,7 +5,6 @@ package cmd import ( "fmt" - "net/http" "code.gitea.io/gitea/modules/private" "code.gitea.io/gitea/modules/setting" @@ -43,13 +42,10 @@ func runSendMail(c *cli.Context) error { } } - status, message := private.SendEmail(ctx, subject, body, nil) - if status != http.StatusOK { - fmt.Printf("error: %s\n", message) - return nil + respText, extra := private.SendEmail(ctx, subject, body, nil) + if extra.HasError() { + return handleCliResponseExtra(extra) } - - fmt.Printf("Success: %s\n", message) - + _, _ = fmt.Printf("Sent %s email(s) to all users\n", respText) return nil } diff --git a/cmd/manager.go b/cmd/manager.go index cdfe509075f3b..3f1e2231903ae 100644 --- a/cmd/manager.go +++ b/cmd/manager.go @@ -4,8 +4,6 @@ package cmd import ( - "fmt" - "net/http" "os" "time" @@ -103,57 +101,34 @@ func runShutdown(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setup("manager", c.Bool("debug")) - statusCode, msg := private.Shutdown(ctx) - switch statusCode { - case http.StatusInternalServerError: - return fail("InternalServerError", msg) - } - - fmt.Fprintln(os.Stdout, msg) - return nil + setup(ctx, c.Bool("debug")) + extra := private.Shutdown(ctx) + return handleCliResponseExtra(extra) } func runRestart(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setup("manager", c.Bool("debug")) - statusCode, msg := private.Restart(ctx) - switch statusCode { - case http.StatusInternalServerError: - return fail("InternalServerError", msg) - } - - fmt.Fprintln(os.Stdout, msg) - return nil + setup(ctx, c.Bool("debug")) + extra := private.Restart(ctx) + return handleCliResponseExtra(extra) } func runFlushQueues(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setup("manager", c.Bool("debug")) - statusCode, msg := private.FlushQueues(ctx, c.Duration("timeout"), c.Bool("non-blocking")) - switch statusCode { - case http.StatusInternalServerError: - return fail("InternalServerError", msg) - } - - fmt.Fprintln(os.Stdout, msg) - return nil + setup(ctx, c.Bool("debug")) + extra := private.FlushQueues(ctx, c.Duration("timeout"), c.Bool("non-blocking")) + return handleCliResponseExtra(extra) } func runProcesses(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setup("manager", c.Bool("debug")) - statusCode, msg := private.Processes(ctx, os.Stdout, c.Bool("flat"), c.Bool("no-system"), c.Bool("stacktraces"), c.Bool("json"), c.String("cancel")) - switch statusCode { - case http.StatusInternalServerError: - return fail("InternalServerError", msg) - } - - return nil + setup(ctx, c.Bool("debug")) + extra := private.Processes(ctx, os.Stdout, c.Bool("flat"), c.Bool("no-system"), c.Bool("stacktraces"), c.Bool("json"), c.String("cancel")) + return handleCliResponseExtra(extra) } diff --git a/cmd/manager_logging.go b/cmd/manager_logging.go index d49675ce87093..914210d370372 100644 --- a/cmd/manager_logging.go +++ b/cmd/manager_logging.go @@ -5,7 +5,6 @@ package cmd import ( "fmt" - "net/http" "os" "code.gitea.io/gitea/modules/log" @@ -191,27 +190,25 @@ var ( ) func runRemoveLogger(c *cli.Context) error { - setup("manager", c.Bool("debug")) + ctx, cancel := installSignals() + defer cancel() + + setup(ctx, c.Bool("debug")) group := c.String("group") if len(group) == 0 { group = log.DEFAULT } name := c.Args().First() - ctx, cancel := installSignals() - defer cancel() - - statusCode, msg := private.RemoveLogger(ctx, group, name) - switch statusCode { - case http.StatusInternalServerError: - return fail("InternalServerError", msg) - } - fmt.Fprintln(os.Stdout, msg) - return nil + extra := private.RemoveLogger(ctx, group, name) + return handleCliResponseExtra(extra) } func runAddSMTPLogger(c *cli.Context) error { - setup("manager", c.Bool("debug")) + ctx, cancel := installSignals() + defer cancel() + + setup(ctx, c.Bool("debug")) vals := map[string]interface{}{} mode := "smtp" if c.IsSet("host") { @@ -242,7 +239,10 @@ func runAddSMTPLogger(c *cli.Context) error { } func runAddConnLogger(c *cli.Context) error { - setup("manager", c.Bool("debug")) + ctx, cancel := installSignals() + defer cancel() + + setup(ctx, c.Bool("debug")) vals := map[string]interface{}{} mode := "conn" vals["net"] = "tcp" @@ -269,7 +269,10 @@ func runAddConnLogger(c *cli.Context) error { } func runAddFileLogger(c *cli.Context) error { - setup("manager", c.Bool("debug")) + ctx, cancel := installSignals() + defer cancel() + + setup(ctx, c.Bool("debug")) vals := map[string]interface{}{} mode := "file" if c.IsSet("filename") { @@ -299,7 +302,10 @@ func runAddFileLogger(c *cli.Context) error { } func runAddConsoleLogger(c *cli.Context) error { - setup("manager", c.Bool("debug")) + ctx, cancel := installSignals() + defer cancel() + + setup(ctx, c.Bool("debug")) vals := map[string]interface{}{} mode := "console" if c.IsSet("stderr") && c.Bool("stderr") { @@ -338,28 +344,17 @@ func commonAddLogger(c *cli.Context, mode string, vals map[string]interface{}) e ctx, cancel := installSignals() defer cancel() - statusCode, msg := private.AddLogger(ctx, group, name, mode, vals) - switch statusCode { - case http.StatusInternalServerError: - return fail("InternalServerError", msg) - } - - fmt.Fprintln(os.Stdout, msg) - return nil + extra := private.AddLogger(ctx, group, name, mode, vals) + return handleCliResponseExtra(extra) } func runPauseLogging(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setup("manager", c.Bool("debug")) - statusCode, msg := private.PauseLogging(ctx) - switch statusCode { - case http.StatusInternalServerError: - return fail("InternalServerError", msg) - } - - fmt.Fprintln(os.Stdout, msg) + setup(ctx, c.Bool("debug")) + userMsg := private.PauseLogging(ctx) + _, _ = fmt.Fprintln(os.Stdout, userMsg) return nil } @@ -367,14 +362,9 @@ func runResumeLogging(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setup("manager", c.Bool("debug")) - statusCode, msg := private.ResumeLogging(ctx) - switch statusCode { - case http.StatusInternalServerError: - return fail("InternalServerError", msg) - } - - fmt.Fprintln(os.Stdout, msg) + setup(ctx, c.Bool("debug")) + userMsg := private.ResumeLogging(ctx) + _, _ = fmt.Fprintln(os.Stdout, userMsg) return nil } @@ -382,28 +372,17 @@ func runReleaseReopenLogging(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setup("manager", c.Bool("debug")) - statusCode, msg := private.ReleaseReopenLogging(ctx) - switch statusCode { - case http.StatusInternalServerError: - return fail("InternalServerError", msg) - } - - fmt.Fprintln(os.Stdout, msg) + setup(ctx, c.Bool("debug")) + userMsg := private.ReleaseReopenLogging(ctx) + _, _ = fmt.Fprintln(os.Stdout, userMsg) return nil } func runSetLogSQL(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setup("manager", c.Bool("debug")) - - statusCode, msg := private.SetLogSQL(ctx, !c.Bool("off")) - switch statusCode { - case http.StatusInternalServerError: - return fail("InternalServerError", msg) - } + setup(ctx, c.Bool("debug")) - fmt.Fprintln(os.Stdout, msg) - return nil + extra := private.SetLogSQL(ctx, !c.Bool("off")) + return handleCliResponseExtra(extra) } diff --git a/cmd/migrate_storage.go b/cmd/migrate_storage.go index dfa7212e5b0ca..9798913705f2d 100644 --- a/cmd/migrate_storage.go +++ b/cmd/migrate_storage.go @@ -72,12 +72,21 @@ var CmdMigrateStorage = cli.Command{ cli.StringFlag{ Name: "minio-base-path", Value: "", - Usage: "Minio storage basepath on the bucket", + Usage: "Minio storage base path on the bucket", }, cli.BoolFlag{ Name: "minio-use-ssl", Usage: "Enable SSL for minio", }, + cli.BoolFlag{ + Name: "minio-insecure-skip-verify", + Usage: "Skip SSL verification", + }, + cli.StringFlag{ + Name: "minio-checksum-algorithm", + Value: "", + Usage: "Minio checksum algorithm (default/md5)", + }, }, } @@ -168,13 +177,15 @@ func runMigrateStorage(ctx *cli.Context) error { dstStorage, err = storage.NewMinioStorage( stdCtx, storage.MinioStorageConfig{ - Endpoint: ctx.String("minio-endpoint"), - AccessKeyID: ctx.String("minio-access-key-id"), - SecretAccessKey: ctx.String("minio-secret-access-key"), - Bucket: ctx.String("minio-bucket"), - Location: ctx.String("minio-location"), - BasePath: ctx.String("minio-base-path"), - UseSSL: ctx.Bool("minio-use-ssl"), + Endpoint: ctx.String("minio-endpoint"), + AccessKeyID: ctx.String("minio-access-key-id"), + SecretAccessKey: ctx.String("minio-secret-access-key"), + Bucket: ctx.String("minio-bucket"), + Location: ctx.String("minio-location"), + BasePath: ctx.String("minio-base-path"), + UseSSL: ctx.Bool("minio-use-ssl"), + InsecureSkipVerify: ctx.Bool("minio-insecure-skip-verify"), + ChecksumAlgorithm: ctx.String("minio-checksum-algorithm"), }) default: return fmt.Errorf("unsupported storage type: %s", ctx.String("storage")) diff --git a/cmd/restore_repo.go b/cmd/restore_repo.go index c7dff41966892..887b59bba9e14 100644 --- a/cmd/restore_repo.go +++ b/cmd/restore_repo.go @@ -4,11 +4,8 @@ package cmd import ( - "errors" - "net/http" "strings" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/private" "code.gitea.io/gitea/modules/setting" @@ -60,7 +57,7 @@ func runRestoreRepository(c *cli.Context) error { if s := c.String("units"); s != "" { units = strings.Split(s, ",") } - statusCode, errStr := private.RestoreRepo( + extra := private.RestoreRepo( ctx, c.String("repo_dir"), c.String("owner_name"), @@ -68,10 +65,5 @@ func runRestoreRepository(c *cli.Context) error { units, c.Bool("validation"), ) - if statusCode == http.StatusOK { - return nil - } - - log.Fatal("Failed to restore repository: %v", errStr) - return errors.New(errStr) + return handleCliResponseExtra(extra) } diff --git a/cmd/serv.go b/cmd/serv.go index d7510845a5beb..72eb6370711e4 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -7,7 +7,6 @@ package cmd import ( "context" "fmt" - "net/http" "net/url" "os" "os/exec" @@ -16,6 +15,7 @@ import ( "strconv" "strings" "time" + "unicode" asymkey_model "code.gitea.io/gitea/models/asymkey" git_model "code.gitea.io/gitea/models/git" @@ -55,7 +55,7 @@ var CmdServ = cli.Command{ }, } -func setup(logPath string, debug bool) { +func setup(ctx context.Context, debug bool) { _ = log.DelLogger("console") if debug { _ = log.NewLogger(1000, "console", "console", `{"level":"trace","stacktracelevel":"NONE","stderr":true}`) @@ -72,15 +72,15 @@ func setup(logPath string, debug bool) { // `[repository]` `ROOT` is a relative path and $GITEA_WORK_DIR isn't passed to the SSH connection. if _, err := os.Stat(setting.RepoRootPath); err != nil { if os.IsNotExist(err) { - _ = fail("Incorrect configuration, no repository directory.", "Directory `[repository].ROOT` %q was not found, please check if $GITEA_WORK_DIR is passed to the SSH connection or make `[repository].ROOT` an absolute value.", setting.RepoRootPath) + _ = fail(ctx, "Incorrect configuration, no repository directory.", "Directory `[repository].ROOT` %q was not found, please check if $GITEA_WORK_DIR is passed to the SSH connection or make `[repository].ROOT` an absolute value.", setting.RepoRootPath) } else { - _ = fail("Incorrect configuration, repository directory is inaccessible", "Directory `[repository].ROOT` %q is inaccessible. err: %v", setting.RepoRootPath, err) + _ = fail(ctx, "Incorrect configuration, repository directory is inaccessible", "Directory `[repository].ROOT` %q is inaccessible. err: %v", setting.RepoRootPath, err) } return } if err := git.InitSimple(context.Background()); err != nil { - _ = fail("Failed to init git", "Failed to init git, err: %v", err) + _ = fail(ctx, "Failed to init git", "Failed to init git, err: %v", err) } } @@ -94,24 +94,46 @@ var ( alphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`) ) -func fail(userMessage, logMessage string, args ...interface{}) error { +// fail prints message to stdout, it's mainly used for git serv and git hook commands. +// The output will be passed to git client and shown to user. +func fail(ctx context.Context, userMessage, logMsgFmt string, args ...interface{}) error { + if userMessage == "" { + userMessage = "Internal Server Error (no specific error)" + } + // There appears to be a chance to cause a zombie process and failure to read the Exit status // if nothing is outputted on stdout. _, _ = fmt.Fprintln(os.Stdout, "") _, _ = fmt.Fprintln(os.Stderr, "Gitea:", userMessage) - if len(logMessage) > 0 { + if logMsgFmt != "" { + logMsg := fmt.Sprintf(logMsgFmt, args...) if !setting.IsProd { - _, _ = fmt.Fprintf(os.Stderr, logMessage+"\n", args...) + _, _ = fmt.Fprintln(os.Stderr, "Gitea:", logMsg) + } + if userMessage != "" { + if unicode.IsPunct(rune(userMessage[len(userMessage)-1])) { + logMsg = userMessage + " " + logMsg + } else { + logMsg = userMessage + ". " + logMsg + } } + _ = private.SSHLog(ctx, true, logMsg) } - ctx, cancel := installSignals() - defer cancel() + return cli.NewExitError("", 1) +} - if len(logMessage) > 0 { - _ = private.SSHLog(ctx, true, fmt.Sprintf(logMessage+": ", args...)) +// handleCliResponseExtra handles the extra response from the cli sub-commands +// If there is a user message it will be printed to stdout +// If the command failed it will return an error (the error will be printed by cli framework) +func handleCliResponseExtra(extra private.ResponseExtra) error { + if extra.UserMsg != "" { + _, _ = fmt.Fprintln(os.Stdout, extra.UserMsg) } - return cli.NewExitError("", 1) + if extra.HasError() { + return cli.NewExitError(extra.Error, 1) + } + return nil } func runServ(c *cli.Context) error { @@ -119,7 +141,7 @@ func runServ(c *cli.Context) error { defer cancel() // FIXME: This needs to internationalised - setup("serv.log", c.Bool("debug")) + setup(ctx, c.Bool("debug")) if setting.SSH.Disabled { println("Gitea: SSH has been disabled") @@ -135,18 +157,18 @@ func runServ(c *cli.Context) error { keys := strings.Split(c.Args()[0], "-") if len(keys) != 2 || keys[0] != "key" { - return fail("Key ID format error", "Invalid key argument: %s", c.Args()[0]) + return fail(ctx, "Key ID format error", "Invalid key argument: %s", c.Args()[0]) } keyID, err := strconv.ParseInt(keys[1], 10, 64) if err != nil { - return fail("Key ID format error", "Invalid key argument: %s", c.Args()[1]) + return fail(ctx, "Key ID parsing error", "Invalid key argument: %s", c.Args()[1]) } cmd := os.Getenv("SSH_ORIGINAL_COMMAND") if len(cmd) == 0 { key, user, err := private.ServNoCommand(ctx, keyID) if err != nil { - return fail("Internal error", "Failed to check provided key: %v", err) + return fail(ctx, "Key check failed", "Failed to check provided key: %v", err) } switch key.Type { case asymkey_model.KeyTypeDeploy: @@ -164,7 +186,7 @@ func runServ(c *cli.Context) error { words, err := shellquote.Split(cmd) if err != nil { - return fail("Error parsing arguments", "Failed to parse arguments: %v", err) + return fail(ctx, "Error parsing arguments", "Failed to parse arguments: %v", err) } if len(words) < 2 { @@ -175,7 +197,7 @@ func runServ(c *cli.Context) error { return nil } } - return fail("Too few arguments", "Too few arguments in cmd: %s", cmd) + return fail(ctx, "Too few arguments", "Too few arguments in cmd: %s", cmd) } verb := words[0] @@ -187,7 +209,7 @@ func runServ(c *cli.Context) error { var lfsVerb string if verb == lfsAuthenticateVerb { if !setting.LFS.StartServer { - return fail("Unknown git command", "LFS authentication request over SSH denied, LFS support is disabled") + return fail(ctx, "Unknown git command", "LFS authentication request over SSH denied, LFS support is disabled") } if len(words) > 2 { @@ -200,37 +222,37 @@ func runServ(c *cli.Context) error { rr := strings.SplitN(repoPath, "/", 2) if len(rr) != 2 { - return fail("Invalid repository path", "Invalid repository path: %v", repoPath) + return fail(ctx, "Invalid repository path", "Invalid repository path: %v", repoPath) } username := strings.ToLower(rr[0]) reponame := strings.ToLower(strings.TrimSuffix(rr[1], ".git")) if alphaDashDotPattern.MatchString(reponame) { - return fail("Invalid repo name", "Invalid repo name: %s", reponame) + return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame) } if c.Bool("enable-pprof") { if err := os.MkdirAll(setting.PprofDataPath, os.ModePerm); err != nil { - return fail("Error while trying to create PPROF_DATA_PATH", "Error while trying to create PPROF_DATA_PATH: %v", err) + return fail(ctx, "Error while trying to create PPROF_DATA_PATH", "Error while trying to create PPROF_DATA_PATH: %v", err) } stopCPUProfiler, err := pprof.DumpCPUProfileForUsername(setting.PprofDataPath, username) if err != nil { - return fail("Internal Server Error", "Unable to start CPU profile: %v", err) + return fail(ctx, "Unable to start CPU profiler", "Unable to start CPU profile: %v", err) } defer func() { stopCPUProfiler() err := pprof.DumpMemProfileForUsername(setting.PprofDataPath, username) if err != nil { - _ = fail("Internal Server Error", "Unable to dump Mem Profile: %v", err) + _ = fail(ctx, "Unable to dump Mem profile", "Unable to dump Mem Profile: %v", err) } }() } requestedMode, has := allowedCommands[verb] if !has { - return fail("Unknown git command", "Unknown git command %s", verb) + return fail(ctx, "Unknown git command", "Unknown git command %s", verb) } if verb == lfsAuthenticateVerb { @@ -239,20 +261,13 @@ func runServ(c *cli.Context) error { } else if lfsVerb == "download" { requestedMode = perm.AccessModeRead } else { - return fail("Unknown LFS verb", "Unknown lfs verb %s", lfsVerb) + return fail(ctx, "Unknown LFS verb", "Unknown lfs verb %s", lfsVerb) } } - results, err := private.ServCommand(ctx, keyID, username, reponame, requestedMode, verb, lfsVerb) - if err != nil { - if private.IsErrServCommand(err) { - errServCommand := err.(private.ErrServCommand) - if errServCommand.StatusCode != http.StatusInternalServerError { - return fail("Unauthorized", "%s", errServCommand.Error()) - } - return fail("Internal Server Error", "%s", errServCommand.Error()) - } - return fail("Internal Server Error", "%s", err.Error()) + results, extra := private.ServCommand(ctx, keyID, username, reponame, requestedMode, verb, lfsVerb) + if extra.HasError() { + return fail(ctx, extra.UserMsg, "ServCommand failed: %s", extra.Error) } // LFS token authentication @@ -274,7 +289,7 @@ func runServ(c *cli.Context) error { // Sign and get the complete encoded token as a string using the secret tokenString, err := token.SignedString(setting.LFS.JWTSecretBytes) if err != nil { - return fail("Internal error", "Failed to sign JWT token: %v", err) + return fail(ctx, "Failed to sign JWT Token", "Failed to sign JWT token: %v", err) } tokenAuthentication := &git_model.LFSTokenResponse{ @@ -286,7 +301,7 @@ func runServ(c *cli.Context) error { enc := json.NewEncoder(os.Stdout) err = enc.Encode(tokenAuthentication) if err != nil { - return fail("Internal error", "Failed to encode LFS json response: %v", err) + return fail(ctx, "Failed to encode LFS json response", "Failed to encode LFS json response: %v", err) } return nil } @@ -332,13 +347,13 @@ func runServ(c *cli.Context) error { gitcmd.Env = append(gitcmd.Env, git.CommonCmdServEnvs()...) if err = gitcmd.Run(); err != nil { - return fail("Internal error", "Failed to execute git command: %v", err) + return fail(ctx, "Failed to execute git command", "Failed to execute git command: %v", err) } // Update user key activity. if results.KeyID > 0 { if err = private.UpdatePublicKeyInRepo(ctx, results.KeyID, results.RepoID); err != nil { - return fail("Internal error", "UpdatePublicKeyInRepo: %v", err) + return fail(ctx, "Failed to update public key", "UpdatePublicKeyInRepo: %v", err) } } diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index d1cfcd70e5850..5e0d16b042cc7 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -583,15 +583,15 @@ ROUTER = console ;; * In request Header: X-Request-ID: test-id-123 ;; * Configuration in app.ini: REQUEST_ID_HEADERS = X-Request-ID ;; * Print in log: 127.0.0.1:58384 - - [14/Feb/2023:16:33:51 +0800] "test-id-123" -;; -;; If you configure more than one in the .ini file, it will match in the order of configuration, +;; +;; If you configure more than one in the .ini file, it will match in the order of configuration, ;; and the first match will be finally printed in the log. ;; * E.g: ;; * In reuqest Header: X-Trace-ID: trace-id-1q2w3e4r ;; * Configuration in app.ini: REQUEST_ID_HEADERS = X-Request-ID, X-Trace-ID, X-Req-ID ;; * Print in log: 127.0.0.1:58384 - - [14/Feb/2023:16:33:51 +0800] "trace-id-1q2w3e4r" ;; -;; REQUEST_ID_HEADERS = +;; REQUEST_ID_HEADERS = ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; @@ -1357,13 +1357,13 @@ ROUTER = console ;; Issue Indexer settings ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; -;; Issue indexer type, currently support: bleve, db or elasticsearch, default is bleve +;; Issue indexer type, currently support: bleve, db, elasticsearch or meilisearch default is bleve ;ISSUE_INDEXER_TYPE = bleve ;; ;; Issue indexer storage path, available when ISSUE_INDEXER_TYPE is bleve ;ISSUE_INDEXER_PATH = indexers/issues.bleve ; Relative paths will be made absolute against _`AppWorkPath`_. ;; -;; Issue indexer connection string, available when ISSUE_INDEXER_TYPE is elasticsearch +;; Issue indexer connection string, available when ISSUE_INDEXER_TYPE is elasticsearch or meilisearch ;ISSUE_INDEXER_CONN_STR = http://elastic:changeme@localhost:9200 ;; ;; Issue indexer name, available when ISSUE_INDEXER_TYPE is elasticsearch @@ -1886,6 +1886,9 @@ ROUTER = console ;; ;; Minio skip SSL verification available when STORAGE_TYPE is `minio` ;MINIO_INSECURE_SKIP_VERIFY = false +;; +;; Minio checksum algorithm: default (for MinIO or AWS S3) or md5 (for Cloudflare or Backblaze) +;MINIO_CHECKSUM_ALGORITHM = default ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/doc/administration/config-cheat-sheet.en-us.md b/docs/content/doc/administration/config-cheat-sheet.en-us.md index 7f31a27f15066..b62ec6f3cafdf 100644 --- a/docs/content/doc/administration/config-cheat-sheet.en-us.md +++ b/docs/content/doc/administration/config-cheat-sheet.en-us.md @@ -456,8 +456,8 @@ relation to port exhaustion. ## Indexer (`indexer`) -- `ISSUE_INDEXER_TYPE`: **bleve**: Issue indexer type, currently supported: `bleve`, `db` or `elasticsearch`. -- `ISSUE_INDEXER_CONN_STR`: ****: Issue indexer connection string, available when ISSUE_INDEXER_TYPE is elasticsearch. i.e. http://elastic:changeme@localhost:9200 +- `ISSUE_INDEXER_TYPE`: **bleve**: Issue indexer type, currently supported: `bleve`, `db`, `elasticsearch` or `meilisearch`. +- `ISSUE_INDEXER_CONN_STR`: ****: Issue indexer connection string, available when ISSUE_INDEXER_TYPE is elasticsearch, or meilisearch. i.e. http://elastic:changeme@localhost:9200 - `ISSUE_INDEXER_NAME`: **gitea_issues**: Issue indexer name, available when ISSUE_INDEXER_TYPE is elasticsearch - `ISSUE_INDEXER_PATH`: **indexers/issues.bleve**: Index file used for issue search; available when ISSUE_INDEXER_TYPE is bleve and elasticsearch. Relative paths will be made absolute against _`AppWorkPath`_. - The next 4 configuration values are deprecated and should be set in `queue.issue_indexer` however are kept for backwards compatibility: @@ -855,6 +855,7 @@ Default templates for project boards: - `MINIO_BASE_PATH`: **attachments/**: Minio base path on the bucket only available when STORAGE_TYPE is `minio` - `MINIO_USE_SSL`: **false**: Minio enabled ssl only available when STORAGE_TYPE is `minio` - `MINIO_INSECURE_SKIP_VERIFY`: **false**: Minio skip SSL verification available when STORAGE_TYPE is `minio` +- `MINIO_CHECKSUM_ALGORITHM`: **default**: Minio checksum algorithm: `default` (for MinIO or AWS S3) or `md5` (for Cloudflare or Backblaze) ## Log (`log`) diff --git a/docs/content/doc/usage/issue-pull-request-templates.en-us.md b/docs/content/doc/usage/issue-pull-request-templates.en-us.md index 0cf63625b60d3..f36037956a5cb 100644 --- a/docs/content/doc/usage/issue-pull-request-templates.en-us.md +++ b/docs/content/doc/usage/issue-pull-request-templates.en-us.md @@ -50,6 +50,17 @@ Possible file names for issue templates: - `.github/issue_template.yaml` - `.github/issue_template.yml` +Possible file names for issue config: + +- `.gitea/ISSUE_TEMPLATE/config.yaml` +- `.gitea/ISSUE_TEMPLATE/config.yml` +- `.gitea/issue_template/config.yaml` +- `.gitea/issue_template/config.yml` +- `.github/ISSUE_TEMPLATE/config.yaml` +- `.github/ISSUE_TEMPLATE/config.yml` +- `.github/issue_template/config.yaml` +- `.github/issue_template/config.yml` + Possible file names for PR templates: - `PULL_REQUEST_TEMPLATE.md` @@ -267,3 +278,30 @@ For each value in the options array, you can set the following keys. |----------|------------------------------------------------------------------------------------------------------------------------------------------|----------|---------|---------|---------| | label | The identifier for the option, which is displayed in the form. Markdown is supported for bold or italic text formatting, and hyperlinks. | Required | String | - | - | | required | Prevents form submission until element is completed. | Optional | Boolean | false | - | + +## Syntax for issue config + +This is a example for a issue config file + +```yaml +blank_issues_enabled: true +contact_links: + - name: Gitea + url: https://gitea.io + about: Visit the Gitea Website +``` + +### Possible Options + +| Key | Description | Type | Default | +|----------------------|-------------------------------------------------------------------------------------------------------|--------------------|----------------| +| blank_issues_enabled | If set to false, the User is forced to use a Template | Boolean | true | +| contact_links | Custom Links to show in the Choose Box | Contact Link Array | Empty Array | + +### Contact Link + +| Key | Description | Type | Required | +|----------------------|-------------------------------------------------------------------------------------------------------|---------|----------| +| name | the name of your link | String | true | +| url | The URL of your Link | String | true | +| about | A short description of your Link | String | true | diff --git a/go.mod b/go.mod index 925c7a9aec3bd..ba7ab27c193e7 100644 --- a/go.mod +++ b/go.mod @@ -73,6 +73,7 @@ require ( github.com/markbates/goth v1.76.0 github.com/mattn/go-isatty v0.0.17 github.com/mattn/go-sqlite3 v1.14.16 + github.com/meilisearch/meilisearch-go v0.23.0 github.com/mholt/archiver/v3 v3.5.1 github.com/microcosm-cc/bluemonday v1.0.22 github.com/minio/minio-go/v7 v7.0.49 @@ -218,7 +219,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect - github.com/mholt/acmez v1.1.0 // indirect + github.com/mholt/acmez v1.0.4 // indirect github.com/miekg/dns v1.1.50 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect @@ -259,6 +260,8 @@ require ( github.com/toqueteos/webbrowser v1.2.0 // indirect github.com/ulikunitz/xz v0.5.11 // indirect github.com/unknwon/com v1.0.1 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.44.0 // indirect github.com/valyala/fastjson v1.6.4 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect diff --git a/go.sum b/go.sum index 191ed7124b088..0faea93e2273e 100644 --- a/go.sum +++ b/go.sum @@ -127,6 +127,7 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= @@ -151,6 +152,7 @@ github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZw github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -496,6 +498,7 @@ github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14/go.mod h1:jPoNZLWDAqA5N3 github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 h1:UjoPNDAQ5JPCjlxoJd6K8ALZqSDDhk2ymieAZOVaDg0= github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85/go.mod h1:fR6z1Ie6rtF7kl/vBYMfgD5/G5B1blui7z426/sj2DU= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= @@ -771,6 +774,9 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= @@ -862,8 +868,10 @@ github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwp github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mholt/acmez v1.1.0 h1:IQ9CGHKOHokorxnffsqDvmmE30mDenO1lptYZ1AYkHY= -github.com/mholt/acmez v1.1.0/go.mod h1:zwo5+fbLLTowAX8o8ETfQzbDtwGEXnPhkmGdKIP+bgs= +github.com/meilisearch/meilisearch-go v0.23.0 h1:CuqB+/NyEJKXF2SovTetAZW7lX+nSH+QTqbgSH6bv+Q= +github.com/meilisearch/meilisearch-go v0.23.0/go.mod h1:sAPJgywANHUCFUo/spCQ8SoP6sJhmfIKFWIXu7Dd5GQ= +github.com/mholt/acmez v1.0.4 h1:N3cE4Pek+dSolbsofIkAYz6H1d3pE+2G0os7QHslf80= +github.com/mholt/acmez v1.0.4/go.mod h1:qFGLZ4u+ehWINeJZjzPlsnjJBCPAADWTcIqE/7DAYQY= github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= github.com/microcosm-cc/bluemonday v1.0.22 h1:p2tT7RNzRdCi0qmwxG+HbqD6ILkmwter1ZwVZn1oTxA= @@ -1181,9 +1189,15 @@ github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8= github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.37.1-0.20220607072126-8a320890c08d/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= +github.com/valyala/fasthttp v1.44.0 h1:R+gLUhldIsfg1HokMuQjdQ5bh9nuXHPIfvkYUu9eR5Q= +github.com/valyala/fasthttp v1.44.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY= github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= @@ -1249,18 +1263,22 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -1383,8 +1401,10 @@ golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= @@ -1492,6 +1512,7 @@ golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1591,6 +1612,7 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= diff --git a/models/issues/dependency.go b/models/issues/dependency.go index bd39824369a93..4dc5a4aec791f 100644 --- a/models/issues/dependency.go +++ b/models/issues/dependency.go @@ -134,7 +134,7 @@ func CreateIssueDependency(user *user_model.User, issue, dep *Issue) error { } defer committer.Close() - // Check if it aleready exists + // Check if it already exists exists, err := issueDepExists(ctx, issue.ID, dep.ID) if err != nil { return err diff --git a/models/issues/issue.go b/models/issues/issue.go index edd74261ecfa5..64b0edd3e7bac 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -189,7 +189,7 @@ func (issue *Issue) IsOverdue() bool { // LoadRepo loads issue's repository func (issue *Issue) LoadRepo(ctx context.Context) (err error) { - if issue.Repo == nil { + if issue.Repo == nil && issue.RepoID != 0 { issue.Repo, err = repo_model.GetRepositoryByID(ctx, issue.RepoID) if err != nil { return fmt.Errorf("getRepositoryByID [%d]: %w", issue.RepoID, err) @@ -223,7 +223,7 @@ func (issue *Issue) GetPullRequest() (pr *PullRequest, err error) { // LoadLabels loads labels func (issue *Issue) LoadLabels(ctx context.Context) (err error) { - if issue.Labels == nil { + if issue.Labels == nil && issue.ID != 0 { issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID) if err != nil { return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err) @@ -234,7 +234,7 @@ func (issue *Issue) LoadLabels(ctx context.Context) (err error) { // LoadPoster loads poster func (issue *Issue) LoadPoster(ctx context.Context) (err error) { - if issue.Poster == nil { + if issue.Poster == nil && issue.PosterID != 0 { issue.Poster, err = user_model.GetPossibleUserByID(ctx, issue.PosterID) if err != nil { issue.PosterID = -1 @@ -252,7 +252,7 @@ func (issue *Issue) LoadPoster(ctx context.Context) (err error) { // LoadPullRequest loads pull request info func (issue *Issue) LoadPullRequest(ctx context.Context) (err error) { if issue.IsPull { - if issue.PullRequest == nil { + if issue.PullRequest == nil && issue.ID != 0 { issue.PullRequest, err = GetPullRequestByIssueID(ctx, issue.ID) if err != nil { if IsErrPullRequestNotExist(err) { @@ -261,7 +261,9 @@ func (issue *Issue) LoadPullRequest(ctx context.Context) (err error) { return fmt.Errorf("getPullRequestByIssueID [%d]: %w", issue.ID, err) } } - issue.PullRequest.Issue = issue + if issue.PullRequest != nil { + issue.PullRequest.Issue = issue + } } return nil } @@ -2128,15 +2130,18 @@ func (issue *Issue) GetParticipantIDsByIssue(ctx context.Context) ([]int64, erro } // BlockedByDependencies finds all Dependencies an issue is blocked by -func (issue *Issue) BlockedByDependencies(ctx context.Context) (issueDeps []*DependencyInfo, err error) { - err = db.GetEngine(ctx). +func (issue *Issue) BlockedByDependencies(ctx context.Context, opts db.ListOptions) (issueDeps []*DependencyInfo, err error) { + sess := db.GetEngine(ctx). Table("issue"). Join("INNER", "repository", "repository.id = issue.repo_id"). Join("INNER", "issue_dependency", "issue_dependency.dependency_id = issue.id"). Where("issue_id = ?", issue.ID). // sort by repo id then created date, with the issues of the same repo at the beginning of the list - OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID). - Find(&issueDeps) + OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID) + if opts.Page != 0 { + sess = db.SetSessionPagination(sess, &opts) + } + err = sess.Find(&issueDeps) for _, depInfo := range issueDeps { depInfo.Issue.Repo = &depInfo.Repository diff --git a/models/repo/repo.go b/models/repo/repo.go index dcffb63fd1970..3653dae015d6b 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -658,6 +658,49 @@ func GetRepositoryByName(ownerID int64, name string) (*Repository, error) { return repo, err } +// getRepositoryURLPathSegments returns segments (owner, reponame) extracted from a url +func getRepositoryURLPathSegments(repoURL string) []string { + if strings.HasPrefix(repoURL, setting.AppURL) { + return strings.Split(strings.TrimPrefix(repoURL, setting.AppURL), "/") + } + + sshURLVariants := [4]string{ + setting.SSH.Domain + ":", + setting.SSH.User + "@" + setting.SSH.Domain + ":", + "git+ssh://" + setting.SSH.Domain + "/", + "git+ssh://" + setting.SSH.User + "@" + setting.SSH.Domain + "/", + } + + for _, sshURL := range sshURLVariants { + if strings.HasPrefix(repoURL, sshURL) { + return strings.Split(strings.TrimPrefix(repoURL, sshURL), "/") + } + } + + return nil +} + +// GetRepositoryByURL returns the repository by given url +func GetRepositoryByURL(ctx context.Context, repoURL string) (*Repository, error) { + // possible urls for git: + // https://my.domain/sub-path//.git + // https://my.domain/sub-path// + // git+ssh://user@my.domain//.git + // git+ssh://user@my.domain// + // user@my.domain:/.git + // user@my.domain:/ + + pathSegments := getRepositoryURLPathSegments(repoURL) + + if len(pathSegments) != 2 { + return nil, fmt.Errorf("unknown or malformed repository URL") + } + + ownerName := pathSegments[0] + repoName := strings.TrimSuffix(pathSegments[1], ".git") + return GetRepositoryByOwnerAndName(ctx, ownerName, repoName) +} + // GetRepositoryByID returns the repository by given id if exists. func GetRepositoryByID(ctx context.Context, id int64) (*Repository, error) { repo := new(Repository) diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go index fb473151eb34b..92a58ea3f9b21 100644 --- a/models/repo/repo_test.go +++ b/models/repo/repo_test.go @@ -124,3 +124,65 @@ func TestMetas(t *testing.T) { assert.Equal(t, "user3", metas["org"]) assert.Equal(t, ",owners,team1,", metas["teams"]) } + +func TestGetRepositoryByURL(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + t.Run("InvalidPath", func(t *testing.T) { + repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, "something") + + assert.Nil(t, repo) + assert.Error(t, err) + }) + + t.Run("ValidHttpURL", func(t *testing.T) { + test := func(t *testing.T, url string) { + repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, url) + + assert.NotNil(t, repo) + assert.NoError(t, err) + + assert.Equal(t, repo.ID, int64(2)) + assert.Equal(t, repo.OwnerID, int64(2)) + } + + test(t, "https://try.gitea.io/user2/repo2") + test(t, "https://try.gitea.io/user2/repo2.git") + }) + + t.Run("ValidGitSshURL", func(t *testing.T) { + test := func(t *testing.T, url string) { + repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, url) + + assert.NotNil(t, repo) + assert.NoError(t, err) + + assert.Equal(t, repo.ID, int64(2)) + assert.Equal(t, repo.OwnerID, int64(2)) + } + + test(t, "git+ssh://sshuser@try.gitea.io/user2/repo2") + test(t, "git+ssh://sshuser@try.gitea.io/user2/repo2.git") + + test(t, "git+ssh://try.gitea.io/user2/repo2") + test(t, "git+ssh://try.gitea.io/user2/repo2.git") + }) + + t.Run("ValidImplicitSshURL", func(t *testing.T) { + test := func(t *testing.T, url string) { + repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, url) + + assert.NotNil(t, repo) + assert.NoError(t, err) + + assert.Equal(t, repo.ID, int64(2)) + assert.Equal(t, repo.OwnerID, int64(2)) + } + + test(t, "sshuser@try.gitea.io:user2/repo2") + test(t, "sshuser@try.gitea.io:user2/repo2.git") + + test(t, "try.gitea.io:user2/repo2") + test(t, "try.gitea.io:user2/repo2.git") + }) +} diff --git a/modules/context/repo.go b/modules/context/repo.go index b83caf4e4b529..820e756fbdb11 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -8,6 +8,7 @@ import ( "context" "fmt" "html" + "io" "net/http" "net/url" "path" @@ -33,6 +34,7 @@ import ( asymkey_service "code.gitea.io/gitea/services/asymkey" "github.com/editorconfig/editorconfig-core-go/v2" + "gopkg.in/yaml.v3" ) // IssueTemplateDirCandidates issue templates directory @@ -47,6 +49,13 @@ var IssueTemplateDirCandidates = []string{ ".gitlab/issue_template", } +var IssueConfigCandidates = []string{ + ".gitea/ISSUE_TEMPLATE/config", + ".gitea/issue_template/config", + ".github/ISSUE_TEMPLATE/config", + ".github/issue_template/config", +} + // PullRequest contains information to make a pull request type PullRequest struct { BaseRepo *repo_model.Repository @@ -1088,3 +1097,108 @@ func (ctx *Context) IssueTemplatesErrorsFromDefaultBranch() ([]*api.IssueTemplat } return issueTemplates, invalidFiles } + +func GetDefaultIssueConfig() api.IssueConfig { + return api.IssueConfig{ + BlankIssuesEnabled: true, + ContactLinks: make([]api.IssueConfigContactLink, 0), + } +} + +// GetIssueConfig loads the given issue config file. +// It never returns a nil config. +func (r *Repository) GetIssueConfig(path string, commit *git.Commit) (api.IssueConfig, error) { + if r.GitRepo == nil { + return GetDefaultIssueConfig(), nil + } + + var err error + + treeEntry, err := commit.GetTreeEntryByPath(path) + if err != nil { + return GetDefaultIssueConfig(), err + } + + reader, err := treeEntry.Blob().DataAsync() + if err != nil { + log.Debug("DataAsync: %v", err) + return GetDefaultIssueConfig(), nil + } + + defer reader.Close() + + configContent, err := io.ReadAll(reader) + if err != nil { + return GetDefaultIssueConfig(), err + } + + issueConfig := api.IssueConfig{} + if err := yaml.Unmarshal(configContent, &issueConfig); err != nil { + return GetDefaultIssueConfig(), err + } + + for pos, link := range issueConfig.ContactLinks { + if link.Name == "" { + return GetDefaultIssueConfig(), fmt.Errorf("contact_link at position %d is missing name key", pos+1) + } + + if link.URL == "" { + return GetDefaultIssueConfig(), fmt.Errorf("contact_link at position %d is missing url key", pos+1) + } + + if link.About == "" { + return GetDefaultIssueConfig(), fmt.Errorf("contact_link at position %d is missing about key", pos+1) + } + + _, err = url.ParseRequestURI(link.URL) + if err != nil { + return GetDefaultIssueConfig(), fmt.Errorf("%s is not a valid URL", link.URL) + } + } + + return issueConfig, nil +} + +// IssueConfigFromDefaultBranch returns the issue config for this repo. +// It never returns a nil config. +func (ctx *Context) IssueConfigFromDefaultBranch() (api.IssueConfig, error) { + if ctx.Repo.Repository.IsEmpty { + return GetDefaultIssueConfig(), nil + } + + commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) + if err != nil { + return GetDefaultIssueConfig(), err + } + + for _, configName := range IssueConfigCandidates { + if _, err := commit.GetTreeEntryByPath(configName + ".yaml"); err == nil { + return ctx.Repo.GetIssueConfig(configName+".yaml", commit) + } + + if _, err := commit.GetTreeEntryByPath(configName + ".yml"); err == nil { + return ctx.Repo.GetIssueConfig(configName+".yml", commit) + } + } + + return GetDefaultIssueConfig(), nil +} + +// IsIssueConfig returns if the given path is a issue config file. +func (r *Repository) IsIssueConfig(path string) bool { + for _, configName := range IssueConfigCandidates { + if path == configName+".yaml" || path == configName+".yml" { + return true + } + } + return false +} + +func (ctx *Context) HasIssueTemplatesOrContactLinks() bool { + if len(ctx.IssueTemplatesFromDefaultBranch()) > 0 { + return true + } + + issueConfig, _ := ctx.IssueConfigFromDefaultBranch() + return len(issueConfig.ContactLinks) > 0 +} diff --git a/modules/httplib/httplib.go b/modules/httplib/httplib.go index a1984400d61d9..e904d77e14977 100644 --- a/modules/httplib/httplib.go +++ b/modules/httplib/httplib.go @@ -8,6 +8,7 @@ import ( "bytes" "context" "crypto/tls" + "fmt" "io" "net" "net/http" @@ -68,6 +69,11 @@ func (r *Request) SetTimeout(connectTimeout, readWriteTimeout time.Duration) *Re return r } +func (r *Request) SetReadWriteTimeout(readWriteTimeout time.Duration) *Request { + r.setting.ReadWriteTimeout = readWriteTimeout + return r +} + // SetTLSClientConfig sets tls connection configurations if visiting https url. func (r *Request) SetTLSClientConfig(config *tls.Config) *Request { r.setting.TLSClientConfig = config @@ -138,11 +144,11 @@ func (r *Request) getResponse() (*http.Response, error) { r.Body(paramBody) } - url, err := url.Parse(r.url) + var err error + r.req.URL, err = url.Parse(r.url) if err != nil { return nil, err } - r.req.URL = url trans := r.setting.Transport if trans == nil { @@ -194,3 +200,7 @@ func TimeoutDialer(cTimeout time.Duration) func(ctx context.Context, net, addr s return conn, nil } } + +func (r *Request) GoString() string { + return fmt.Sprintf("%s %s", r.req.Method, r.url) +} diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go index 55d3c7bc0914a..47a8b10794ed6 100644 --- a/modules/indexer/issues/indexer.go +++ b/modules/indexer/issues/indexer.go @@ -107,7 +107,7 @@ func InitIssueIndexer(syncReindex bool) { // Create the Queue switch setting.Indexer.IssueType { - case "bleve", "elasticsearch": + case "bleve", "elasticsearch", "meilisearch": handler := func(data ...queue.Data) []queue.Data { indexer := holder.get() if indexer == nil { @@ -220,6 +220,21 @@ func InitIssueIndexer(syncReindex bool) { issueIndexer := &DBIndexer{} holder.set(issueIndexer) graceful.GetManager().RunAtTerminate(finished) + case "meilisearch": + graceful.GetManager().RunWithShutdownFns(func(_, atTerminate func(func())) { + pprof.SetGoroutineLabels(ctx) + issueIndexer, err := NewMeilisearchIndexer(setting.Indexer.IssueConnStr, setting.Indexer.IssueConnAuth, setting.Indexer.IssueIndexerName) + if err != nil { + log.Fatal("Unable to initialize Meilisearch Issue Indexer at connection: %s Error: %v", setting.Indexer.IssueConnStr, err) + } + exist, err := issueIndexer.Init() + if err != nil { + log.Fatal("Unable to issueIndexer.Init with connection %s Error: %v", setting.Indexer.IssueConnStr, err) + } + populate = !exist + holder.set(issueIndexer) + atTerminate(finished) + }) default: holder.cancel() log.Fatal("Unknown issue indexer type: %s", setting.Indexer.IssueType) diff --git a/modules/indexer/issues/meilisearch.go b/modules/indexer/issues/meilisearch.go new file mode 100644 index 0000000000000..5c45236e66006 --- /dev/null +++ b/modules/indexer/issues/meilisearch.go @@ -0,0 +1,183 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package issues + +import ( + "context" + "strconv" + "sync" + "time" + + "github.com/meilisearch/meilisearch-go" +) + +var _ Indexer = &MeilisearchIndexer{} + +// MeilisearchIndexer implements Indexer interface +type MeilisearchIndexer struct { + client *meilisearch.Client + indexerName string + available bool + availabilityCallback func(bool) + stopTimer chan struct{} + lock sync.RWMutex +} + +// MeilisearchIndexer creates a new meilisearch indexer +func NewMeilisearchIndexer(url, apiKey, indexerName string) (*MeilisearchIndexer, error) { + client := meilisearch.NewClient(meilisearch.ClientConfig{ + Host: url, + APIKey: apiKey, + }) + + indexer := &MeilisearchIndexer{ + client: client, + indexerName: indexerName, + available: true, + stopTimer: make(chan struct{}), + } + + ticker := time.NewTicker(10 * time.Second) + go func() { + for { + select { + case <-ticker.C: + indexer.checkAvailability() + case <-indexer.stopTimer: + ticker.Stop() + return + } + } + }() + + return indexer, nil +} + +// Init will initialize the indexer +func (b *MeilisearchIndexer) Init() (bool, error) { + _, err := b.client.GetIndex(b.indexerName) + if err == nil { + return true, nil + } + _, err = b.client.CreateIndex(&meilisearch.IndexConfig{ + Uid: b.indexerName, + PrimaryKey: "id", + }) + if err != nil { + return false, b.checkError(err) + } + + _, err = b.client.Index(b.indexerName).UpdateFilterableAttributes(&[]string{"repo_id"}) + return false, b.checkError(err) +} + +// SetAvailabilityChangeCallback sets callback that will be triggered when availability changes +func (b *MeilisearchIndexer) SetAvailabilityChangeCallback(callback func(bool)) { + b.lock.Lock() + defer b.lock.Unlock() + b.availabilityCallback = callback +} + +// Ping checks if meilisearch is available +func (b *MeilisearchIndexer) Ping() bool { + b.lock.RLock() + defer b.lock.RUnlock() + return b.available +} + +// Index will save the index data +func (b *MeilisearchIndexer) Index(issues []*IndexerData) error { + if len(issues) == 0 { + return nil + } + for _, issue := range issues { + _, err := b.client.Index(b.indexerName).AddDocuments(issue) + if err != nil { + return b.checkError(err) + } + } + // TODO: bulk send index data + return nil +} + +// Delete deletes indexes by ids +func (b *MeilisearchIndexer) Delete(ids ...int64) error { + if len(ids) == 0 { + return nil + } + + for _, id := range ids { + _, err := b.client.Index(b.indexerName).DeleteDocument(strconv.FormatInt(id, 10)) + if err != nil { + return b.checkError(err) + } + } + // TODO: bulk send deletes + return nil +} + +// Search searches for issues by given conditions. +// Returns the matching issue IDs +func (b *MeilisearchIndexer) Search(ctx context.Context, keyword string, repoIDs []int64, limit, start int) (*SearchResult, error) { + filter := make([][]string, 0, len(repoIDs)) + for _, repoID := range repoIDs { + filter = append(filter, []string{"repo_id = " + strconv.FormatInt(repoID, 10)}) + } + searchRes, err := b.client.Index(b.indexerName).Search(keyword, &meilisearch.SearchRequest{ + Filter: filter, + Limit: int64(limit), + Offset: int64(start), + }) + if err != nil { + return nil, b.checkError(err) + } + + hits := make([]Match, 0, len(searchRes.Hits)) + for _, hit := range searchRes.Hits { + hits = append(hits, Match{ + ID: int64(hit.(map[string]interface{})["id"].(float64)), + }) + } + return &SearchResult{ + Total: searchRes.TotalHits, + Hits: hits, + }, nil +} + +// Close implements indexer +func (b *MeilisearchIndexer) Close() { + select { + case <-b.stopTimer: + default: + close(b.stopTimer) + } +} + +func (b *MeilisearchIndexer) checkError(err error) error { + return err +} + +func (b *MeilisearchIndexer) checkAvailability() { + _, err := b.client.Health() + if err != nil { + b.setAvailability(false) + return + } + b.setAvailability(true) +} + +func (b *MeilisearchIndexer) setAvailability(available bool) { + b.lock.Lock() + defer b.lock.Unlock() + + if b.available == available { + return + } + + b.available = available + if b.availabilityCallback != nil { + // Call the callback from within the lock to ensure that the ordering remains correct + b.availabilityCallback(b.available) + } +} diff --git a/modules/lfs/content_store.go b/modules/lfs/content_store.go index a4ae21bfd65c6..53fac4ab858bb 100644 --- a/modules/lfs/content_store.go +++ b/modules/lfs/content_store.go @@ -60,15 +60,22 @@ func (s *ContentStore) Put(pointer Pointer, r io.Reader) error { return err } - // This shouldn't happen but it is sensible to test - if written != pointer.Size { - if err := s.Delete(p); err != nil { - log.Error("Cleaning the LFS OID[%s] failed: %v", pointer.Oid, err) + // check again whether there is any error during the Save operation + // because some errors might be ignored by the Reader's caller + if wrappedRd.lastError != nil && !errors.Is(wrappedRd.lastError, io.EOF) { + err = wrappedRd.lastError + } else if written != pointer.Size { + err = ErrSizeMismatch + } + + // if the upload failed, try to delete the file + if err != nil { + if errDel := s.Delete(p); errDel != nil { + log.Error("Cleaning the LFS OID[%s] failed: %v", pointer.Oid, errDel) } - return ErrSizeMismatch } - return nil + return err } // Exists returns true if the object exists in the content store. @@ -109,6 +116,17 @@ type hashingReader struct { expectedSize int64 hash hash.Hash expectedHash string + lastError error +} + +// recordError records the last error during the Save operation +// Some callers of the Reader doesn't respect the returned "err" +// For example, MinIO's Put will ignore errors if the written size could equal to expected size +// So we must remember the error by ourselves, +// and later check again whether ErrSizeMismatch or ErrHashMismatch occurs during the Save operation +func (r *hashingReader) recordError(err error) error { + r.lastError = err + return err } func (r *hashingReader) Read(b []byte) (int, error) { @@ -118,22 +136,22 @@ func (r *hashingReader) Read(b []byte) (int, error) { r.currentSize += int64(n) wn, werr := r.hash.Write(b[:n]) if wn != n || werr != nil { - return n, werr + return n, r.recordError(werr) } } - if err != nil && err == io.EOF { + if errors.Is(err, io.EOF) || r.currentSize >= r.expectedSize { if r.currentSize != r.expectedSize { - return n, ErrSizeMismatch + return n, r.recordError(ErrSizeMismatch) } shaStr := hex.EncodeToString(r.hash.Sum(nil)) if shaStr != r.expectedHash { - return n, ErrHashMismatch + return n, r.recordError(ErrHashMismatch) } } - return n, err + return n, r.recordError(err) } func newHashingReader(expectedSize int64, expectedHash string, reader io.Reader) *hashingReader { diff --git a/modules/private/hook.go b/modules/private/hook.go index 9533eaae5966a..0563e4d80a65a 100644 --- a/modules/private/hook.go +++ b/modules/private/hook.go @@ -5,14 +5,11 @@ package private import ( "context" - "errors" "fmt" - "net/http" "net/url" "strconv" "time" - "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" ) @@ -99,126 +96,46 @@ type HookProcReceiveRefResult struct { } // HookPreReceive check whether the provided commits are allowed -func HookPreReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (int, string) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s", - url.PathEscape(ownerName), - url.PathEscape(repoName), - ) - req := newInternalRequest(ctx, reqURL, "POST") - req = req.Header("Content-Type", "application/json") - jsonBytes, _ := json.Marshal(opts) - req.Body(jsonBytes) - req.SetTimeout(60*time.Second, time.Duration(60+len(opts.OldCommitIDs))*time.Second) - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return resp.StatusCode, decodeJSONError(resp).Err - } - - return http.StatusOK, "" +func HookPreReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) ResponseExtra { + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName)) + req := newInternalRequest(ctx, reqURL, "POST", opts) + req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second) + _, extra := requestJSONResp(req, &responseText{}) + return extra } // HookPostReceive updates services and users -func HookPostReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookPostReceiveResult, string) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/post-receive/%s/%s", - url.PathEscape(ownerName), - url.PathEscape(repoName), - ) - - req := newInternalRequest(ctx, reqURL, "POST") - req = req.Header("Content-Type", "application/json") - req.SetTimeout(60*time.Second, time.Duration(60+len(opts.OldCommitIDs))*time.Second) - jsonBytes, _ := json.Marshal(opts) - req.Body(jsonBytes) - resp, err := req.Response() - if err != nil { - return nil, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, decodeJSONError(resp).Err - } - res := &HookPostReceiveResult{} - _ = json.NewDecoder(resp.Body).Decode(res) - - return res, "" +func HookPostReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookPostReceiveResult, ResponseExtra) { + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/post-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName)) + req := newInternalRequest(ctx, reqURL, "POST", opts) + req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second) + return requestJSONResp(req, &HookPostReceiveResult{}) } // HookProcReceive proc-receive hook -func HookProcReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookProcReceiveResult, error) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/proc-receive/%s/%s", - url.PathEscape(ownerName), - url.PathEscape(repoName), - ) - - req := newInternalRequest(ctx, reqURL, "POST") - req = req.Header("Content-Type", "application/json") - req.SetTimeout(60*time.Second, time.Duration(60+len(opts.OldCommitIDs))*time.Second) - jsonBytes, _ := json.Marshal(opts) - req.Body(jsonBytes) - resp, err := req.Response() - if err != nil { - return nil, fmt.Errorf("Unable to contact gitea: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, errors.New(decodeJSONError(resp).Err) - } - res := &HookProcReceiveResult{} - _ = json.NewDecoder(resp.Body).Decode(res) +func HookProcReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookProcReceiveResult, ResponseExtra) { + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/proc-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName)) - return res, nil + req := newInternalRequest(ctx, reqURL, "POST", opts) + req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second) + return requestJSONResp(req, &HookProcReceiveResult{}) } // SetDefaultBranch will set the default branch to the provided branch for the provided repository -func SetDefaultBranch(ctx context.Context, ownerName, repoName, branch string) error { +func SetDefaultBranch(ctx context.Context, ownerName, repoName, branch string) ResponseExtra { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/set-default-branch/%s/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName), url.PathEscape(branch), ) req := newInternalRequest(ctx, reqURL, "POST") - req = req.Header("Content-Type", "application/json") - - req.SetTimeout(60*time.Second, 60*time.Second) - resp, err := req.Response() - if err != nil { - return fmt.Errorf("Unable to contact gitea: %w", err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("Error returned from gitea: %v", decodeJSONError(resp).Err) - } - return nil + return requestJSONUserMsg(req, "") } // SSHLog sends ssh error log response func SSHLog(ctx context.Context, isErr bool, msg string) error { reqURL := setting.LocalURL + "api/internal/ssh/log" - req := newInternalRequest(ctx, reqURL, "POST") - req = req.Header("Content-Type", "application/json") - - jsonBytes, _ := json.Marshal(&SSHLogOption{ - IsError: isErr, - Message: msg, - }) - req.Body(jsonBytes) - - req.SetTimeout(60*time.Second, 60*time.Second) - resp, err := req.Response() - if err != nil { - return fmt.Errorf("unable to contact gitea: %w", err) - } - - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("Error returned from gitea: %v", decodeJSONError(resp).Err) - } - return nil + req := newInternalRequest(ctx, reqURL, "POST", &SSHLogOption{IsError: isErr, Message: msg}) + _, extra := requestJSONResp(req, &responseText{}) + return extra.Error } diff --git a/modules/private/internal.go b/modules/private/internal.go index a8b62fdde72bb..9c330a24a865b 100644 --- a/modules/private/internal.go +++ b/modules/private/internal.go @@ -11,6 +11,7 @@ import ( "net/http" "os" "strings" + "time" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/json" @@ -19,29 +20,10 @@ import ( "code.gitea.io/gitea/modules/setting" ) -func newRequest(ctx context.Context, url, method, sourceIP string) *httplib.Request { - if setting.InternalToken == "" { - log.Fatal(`The INTERNAL_TOKEN setting is missing from the configuration file: %q. -Ensure you are running in the correct environment or set the correct configuration file with -c.`, setting.CustomConf) - } - return httplib.NewRequest(url, method). - SetContext(ctx). - Header("X-Real-IP", sourceIP). - Header("Authorization", fmt.Sprintf("Bearer %s", setting.InternalToken)) -} - -// Response internal request response +// Response is used for internal request response (for user message and error message) type Response struct { - Err string `json:"err"` -} - -func decodeJSONError(resp *http.Response) *Response { - var res Response - err := json.NewDecoder(resp.Body).Decode(&res) - if err != nil { - res.Err = err.Error() - } - return &res + Err string `json:"err,omitempty"` // server-side error log message, it won't be exposed to end users + UserMsg string `json:"user_msg,omitempty"` // meaningful error message for end users, it will be shown in git client's output. } func getClientIP() string { @@ -52,11 +34,21 @@ func getClientIP() string { return strings.Fields(sshConnEnv)[0] } -func newInternalRequest(ctx context.Context, url, method string) *httplib.Request { - req := newRequest(ctx, url, method, getClientIP()).SetTLSClientConfig(&tls.Config{ - InsecureSkipVerify: true, - ServerName: setting.Domain, - }) +func newInternalRequest(ctx context.Context, url, method string, body ...any) *httplib.Request { + if setting.InternalToken == "" { + log.Fatal(`The INTERNAL_TOKEN setting is missing from the configuration file: %q. +Ensure you are running in the correct environment or set the correct configuration file with -c.`, setting.CustomConf) + } + + req := httplib.NewRequest(url, method). + SetContext(ctx). + Header("X-Real-IP", getClientIP()). + Header("Authorization", fmt.Sprintf("Bearer %s", setting.InternalToken)). + SetTLSClientConfig(&tls.Config{ + InsecureSkipVerify: true, + ServerName: setting.Domain, + }) + if setting.Protocol == setting.HTTPUnix { req.SetTransport(&http.Transport{ DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { @@ -90,5 +82,15 @@ func newInternalRequest(ctx context.Context, url, method string) *httplib.Reques }, }) } + + if len(body) == 1 { + req.Header("Content-Type", "application/json") + jsonBytes, _ := json.Marshal(body[0]) + req.Body(jsonBytes) + } else if len(body) > 1 { + log.Fatal("Too many arguments for newInternalRequest") + } + + req.SetTimeout(10*time.Second, 60*time.Second) return req } diff --git a/modules/private/key.go b/modules/private/key.go index f09d6de2bf890..6f7cd877962ce 100644 --- a/modules/private/key.go +++ b/modules/private/key.go @@ -6,8 +6,6 @@ package private import ( "context" "fmt" - "io" - "net/http" "code.gitea.io/gitea/modules/setting" ) @@ -16,39 +14,18 @@ import ( func UpdatePublicKeyInRepo(ctx context.Context, keyID, repoID int64) error { // Ask for running deliver hook and test pull request tasks. reqURL := setting.LocalURL + fmt.Sprintf("api/internal/ssh/%d/update/%d", keyID, repoID) - resp, err := newInternalRequest(ctx, reqURL, "POST").Response() - if err != nil { - return err - } - - defer resp.Body.Close() - - // All 2XX status codes are accepted and others will return an error - if resp.StatusCode/100 != 2 { - return fmt.Errorf("Failed to update public key: %s", decodeJSONError(resp).Err) - } - return nil + req := newInternalRequest(ctx, reqURL, "POST") + _, extra := requestJSONResp(req, &responseText{}) + return extra.Error } // AuthorizedPublicKeyByContent searches content as prefix (leak e-mail part) // and returns public key found. -func AuthorizedPublicKeyByContent(ctx context.Context, content string) (string, error) { +func AuthorizedPublicKeyByContent(ctx context.Context, content string) (string, ResponseExtra) { // Ask for running deliver hook and test pull request tasks. reqURL := setting.LocalURL + "api/internal/ssh/authorized_keys" req := newInternalRequest(ctx, reqURL, "POST") req.Param("content", content) - resp, err := req.Response() - if err != nil { - return "", err - } - - defer resp.Body.Close() - - // All 2XX status codes are accepted and others will return an error - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("Failed to update public key: %s", decodeJSONError(resp).Err) - } - bs, err := io.ReadAll(resp.Body) - - return string(bs), err + resp, extra := requestJSONResp(req, &responseText{}) + return resp.Text, extra } diff --git a/modules/private/mail.go b/modules/private/mail.go index 6eb7c2acd0a49..82216b346b711 100644 --- a/modules/private/mail.go +++ b/modules/private/mail.go @@ -5,11 +5,7 @@ package private import ( "context" - "fmt" - "io" - "net/http" - "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" ) @@ -21,38 +17,18 @@ type Email struct { } // SendEmail calls the internal SendEmail function -// // It accepts a list of usernames. // If DB contains these users it will send the email to them. -// -// If to list == nil its supposed to send an email to every -// user present in DB -func SendEmail(ctx context.Context, subject, message string, to []string) (int, string) { +// If to list == nil, it's supposed to send emails to every user present in DB +func SendEmail(ctx context.Context, subject, message string, to []string) (string, ResponseExtra) { reqURL := setting.LocalURL + "api/internal/mail/send" - req := newInternalRequest(ctx, reqURL, "POST") - req = req.Header("Content-Type", "application/json") - jsonBytes, _ := json.Marshal(Email{ + req := newInternalRequest(ctx, reqURL, "POST", Email{ Subject: subject, Message: message, To: to, }) - req.Body(jsonBytes) - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Response body error: %v", err.Error()) - } - - users := fmt.Sprintf("%d", len(to)) - if len(to) == 0 { - users = "all" - } - - return http.StatusOK, fmt.Sprintf("Sent %s email(s) to %s users", body, users) + resp, extra := requestJSONResp(req, &responseText{}) + return resp.Text, extra } diff --git a/modules/private/manager.go b/modules/private/manager.go index bbf470cd7ad30..5853db34e4b74 100644 --- a/modules/private/manager.go +++ b/modules/private/manager.go @@ -12,44 +12,21 @@ import ( "strconv" "time" - "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" ) // Shutdown calls the internal shutdown function -func Shutdown(ctx context.Context) (int, string) { +func Shutdown(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/shutdown" - req := newInternalRequest(ctx, reqURL, "POST") - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return resp.StatusCode, decodeJSONError(resp).Err - } - - return http.StatusOK, "Shutting down" + return requestJSONUserMsg(req, "Shutting down") } // Restart calls the internal restart function -func Restart(ctx context.Context) (int, string) { +func Restart(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/restart" - req := newInternalRequest(ctx, reqURL, "POST") - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return resp.StatusCode, decodeJSONError(resp).Err - } - - return http.StatusOK, "Restarting" + return requestJSONUserMsg(req, "Restarting") } // FlushOptions represents the options for the flush call @@ -59,102 +36,41 @@ type FlushOptions struct { } // FlushQueues calls the internal flush-queues function -func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) (int, string) { +func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/flush-queues" - - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequest(ctx, reqURL, "POST", FlushOptions{Timeout: timeout, NonBlocking: nonBlocking}) if timeout > 0 { - req.SetTimeout(timeout+10*time.Second, timeout+10*time.Second) - } - req = req.Header("Content-Type", "application/json") - jsonBytes, _ := json.Marshal(FlushOptions{ - Timeout: timeout, - NonBlocking: nonBlocking, - }) - req.Body(jsonBytes) - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return resp.StatusCode, decodeJSONError(resp).Err + req.SetReadWriteTimeout(timeout + 10*time.Second) } - - return http.StatusOK, "Flushed" + return requestJSONUserMsg(req, "Flushed") } // PauseLogging pauses logging -func PauseLogging(ctx context.Context) (int, string) { +func PauseLogging(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/pause-logging" - req := newInternalRequest(ctx, reqURL, "POST") - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return resp.StatusCode, decodeJSONError(resp).Err - } - - return http.StatusOK, "Logging Paused" + return requestJSONUserMsg(req, "Logging Paused") } // ResumeLogging resumes logging -func ResumeLogging(ctx context.Context) (int, string) { +func ResumeLogging(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/resume-logging" - req := newInternalRequest(ctx, reqURL, "POST") - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return resp.StatusCode, decodeJSONError(resp).Err - } - - return http.StatusOK, "Logging Restarted" + return requestJSONUserMsg(req, "Logging Restarted") } // ReleaseReopenLogging releases and reopens logging files -func ReleaseReopenLogging(ctx context.Context) (int, string) { +func ReleaseReopenLogging(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/release-and-reopen-logging" - req := newInternalRequest(ctx, reqURL, "POST") - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return resp.StatusCode, decodeJSONError(resp).Err - } - - return http.StatusOK, "Logging Restarted" + return requestJSONUserMsg(req, "Logging Restarted") } // SetLogSQL sets database logging -func SetLogSQL(ctx context.Context, on bool) (int, string) { +func SetLogSQL(ctx context.Context, on bool) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/set-log-sql?on=" + strconv.FormatBool(on) - req := newInternalRequest(ctx, reqURL, "POST") - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return resp.StatusCode, decodeJSONError(resp).Err - } - - return http.StatusOK, "Log SQL setting set" + return requestJSONUserMsg(req, "Log SQL setting set") } // LoggerOptions represents the options for the add logger call @@ -166,67 +82,32 @@ type LoggerOptions struct { } // AddLogger adds a logger -func AddLogger(ctx context.Context, group, name, mode string, config map[string]interface{}) (int, string) { +func AddLogger(ctx context.Context, group, name, mode string, config map[string]interface{}) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/add-logger" - - req := newInternalRequest(ctx, reqURL, "POST") - req = req.Header("Content-Type", "application/json") - jsonBytes, _ := json.Marshal(LoggerOptions{ + req := newInternalRequest(ctx, reqURL, "POST", LoggerOptions{ Group: group, Name: name, Mode: mode, Config: config, }) - req.Body(jsonBytes) - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return resp.StatusCode, decodeJSONError(resp).Err - } - - return http.StatusOK, "Added" + return requestJSONUserMsg(req, "Added") } // RemoveLogger removes a logger -func RemoveLogger(ctx context.Context, group, name string) (int, string) { +func RemoveLogger(ctx context.Context, group, name string) ResponseExtra { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/remove-logger/%s/%s", url.PathEscape(group), url.PathEscape(name)) - req := newInternalRequest(ctx, reqURL, "POST") - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return resp.StatusCode, decodeJSONError(resp).Err - } - - return http.StatusOK, "Removed" + return requestJSONUserMsg(req, "Removed") } // Processes return the current processes from this gitea instance -func Processes(ctx context.Context, out io.Writer, flat, noSystem, stacktraces, json bool, cancel string) (int, string) { +func Processes(ctx context.Context, out io.Writer, flat, noSystem, stacktraces, json bool, cancel string) ResponseExtra { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/processes?flat=%t&no-system=%t&stacktraces=%t&json=%t&cancel-pid=%s", flat, noSystem, stacktraces, json, url.QueryEscape(cancel)) req := newInternalRequest(ctx, reqURL, "GET") - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return resp.StatusCode, decodeJSONError(resp).Err - } - - _, err = io.Copy(out, resp.Body) - if err != nil { - return http.StatusInternalServerError, err.Error() + callback := func(resp *http.Response, extra *ResponseExtra) { + _, extra.Error = io.Copy(out, resp.Body) } - return http.StatusOK, "" + _, extra := requestJSONResp(req, &callback) + return extra } diff --git a/modules/private/request.go b/modules/private/request.go new file mode 100644 index 0000000000000..3eb8c92c1ac02 --- /dev/null +++ b/modules/private/request.go @@ -0,0 +1,135 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package private + +import ( + "fmt" + "io" + "net/http" + "unicode" + + "code.gitea.io/gitea/modules/httplib" + "code.gitea.io/gitea/modules/json" +) + +// responseText is used to get the response as text, instead of parsing it as JSON. +type responseText struct { + Text string +} + +// ResponseExtra contains extra information about the response, especially for error responses. +type ResponseExtra struct { + StatusCode int + UserMsg string + Error error +} + +type responseCallback func(resp *http.Response, extra *ResponseExtra) + +func (re *ResponseExtra) HasError() bool { + return re.Error != nil +} + +type responseError struct { + statusCode int + errorString string +} + +func (re responseError) Error() string { + if re.errorString == "" { + return fmt.Sprintf("internal API error response, status=%d", re.statusCode) + } + return fmt.Sprintf("internal API error response, status=%d, err=%s", re.statusCode, re.errorString) +} + +// requestJSONUserMsg sends a request to the gitea server and then parses the response. +// If the status code is not 2xx, or any error occurs, the ResponseExtra.Error field is guaranteed to be non-nil, +// and the ResponseExtra.UserMsg field will be set to a message for the end user. +// +// * If the "res" is a struct pointer, the response will be parsed as JSON +// * If the "res" is responseText pointer, the response will be stored as text in it +// * If the "res" is responseCallback pointer, the callback function should set the ResponseExtra fields accordingly +func requestJSONResp[T any](req *httplib.Request, res *T) (ret *T, extra ResponseExtra) { + resp, err := req.Response() + if err != nil { + extra.UserMsg = "Internal Server Connection Error" + extra.Error = fmt.Errorf("unable to contact gitea %q: %w", req.GoString(), err) + return nil, extra + } + defer resp.Body.Close() + + extra.StatusCode = resp.StatusCode + + // if the status code is not 2xx, try to parse the error response + if resp.StatusCode/100 != 2 { + var respErr Response + if err := json.NewDecoder(resp.Body).Decode(&respErr); err != nil { + extra.UserMsg = "Internal Server Error Decoding Failed" + extra.Error = fmt.Errorf("unable to decode error response %q: %w", req.GoString(), err) + return nil, extra + } + extra.UserMsg = respErr.UserMsg + if extra.UserMsg == "" { + extra.UserMsg = "Internal Server Error (no message for end users)" + } + extra.Error = responseError{statusCode: resp.StatusCode, errorString: respErr.Err} + return res, extra + } + + // now, the StatusCode must be 2xx + var v any = res + if respText, ok := v.(*responseText); ok { + // get the whole response as a text string + bs, err := io.ReadAll(resp.Body) + if err != nil { + extra.UserMsg = "Internal Server Response Reading Failed" + extra.Error = fmt.Errorf("unable to read response %q: %w", req.GoString(), err) + return nil, extra + } + respText.Text = string(bs) + return res, extra + } else if callback, ok := v.(*responseCallback); ok { + // pass the response to callback, and let the callback update the ResponseExtra + extra.StatusCode = resp.StatusCode + (*callback)(resp, &extra) + return nil, extra + } else if err := json.NewDecoder(resp.Body).Decode(res); err != nil { + // decode the response into the given struct + extra.UserMsg = "Internal Server Response Decoding Failed" + extra.Error = fmt.Errorf("unable to decode response %q: %w", req.GoString(), err) + return nil, extra + } + + if respMsg, ok := v.(*Response); ok { + // if the "res" is Response structure, try to get the UserMsg from it and update the ResponseExtra + extra.UserMsg = respMsg.UserMsg + if respMsg.Err != "" { + // usually this shouldn't happen, because the StatusCode is 2xx, there should be no error. + // but we still handle the "err" response, in case some people return error messages by status code 200. + extra.Error = responseError{statusCode: resp.StatusCode, errorString: respMsg.Err} + } + } + + return res, extra +} + +// requestJSONUserMsg sends a request to the gitea server and then parses the response as private.Response +// If the request succeeds, the successMsg will be used as part of ResponseExtra.UserMsg. +func requestJSONUserMsg(req *httplib.Request, successMsg string) ResponseExtra { + resp, extra := requestJSONResp(req, &Response{}) + if extra.HasError() { + return extra + } + if resp.UserMsg == "" { + extra.UserMsg = successMsg // if UserMsg is empty, then use successMsg as userMsg + } else if successMsg != "" { + // else, now UserMsg is not empty, if successMsg is not empty, then append successMsg to UserMsg + if unicode.IsPunct(rune(extra.UserMsg[len(extra.UserMsg)-1])) { + extra.UserMsg = extra.UserMsg + " " + successMsg + } else { + extra.UserMsg = extra.UserMsg + ". " + successMsg + } + } + return extra +} diff --git a/modules/private/restore_repo.go b/modules/private/restore_repo.go index f40d914a7bb57..34d0f5d4828bb 100644 --- a/modules/private/restore_repo.go +++ b/modules/private/restore_repo.go @@ -6,11 +6,8 @@ package private import ( "context" "fmt" - "io" - "net/http" "time" - "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" ) @@ -24,39 +21,16 @@ type RestoreParams struct { } // RestoreRepo calls the internal RestoreRepo function -func RestoreRepo(ctx context.Context, repoDir, ownerName, repoName string, units []string, validation bool) (int, string) { +func RestoreRepo(ctx context.Context, repoDir, ownerName, repoName string, units []string, validation bool) ResponseExtra { reqURL := setting.LocalURL + "api/internal/restore_repo" - req := newInternalRequest(ctx, reqURL, "POST") - req.SetTimeout(3*time.Second, 0) // since the request will spend much time, don't timeout - req = req.Header("Content-Type", "application/json") - jsonBytes, _ := json.Marshal(RestoreParams{ + req := newInternalRequest(ctx, reqURL, "POST", RestoreParams{ RepoDir: repoDir, OwnerName: ownerName, RepoName: repoName, Units: units, Validation: validation, }) - req.Body(jsonBytes) - resp, err := req.Response() - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v, could you confirm it's running?", err.Error()) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - ret := struct { - Err string `json:"err"` - }{} - body, err := io.ReadAll(resp.Body) - if err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Response body error: %v", err.Error()) - } - if err := json.Unmarshal(body, &ret); err != nil { - return http.StatusInternalServerError, fmt.Sprintf("Response body Unmarshal error: %v", err.Error()) - } - return http.StatusInternalServerError, ret.Err - } - - return http.StatusOK, fmt.Sprintf("Restore repo %s/%s successfully", ownerName, repoName) + req.SetTimeout(3*time.Second, 0) // since the request will spend much time, don't timeout + return requestJSONUserMsg(req, fmt.Sprintf("Restore repo %s/%s successfully", ownerName, repoName)) } diff --git a/modules/private/serv.go b/modules/private/serv.go index c176e1ddfc844..480a44695496d 100644 --- a/modules/private/serv.go +++ b/modules/private/serv.go @@ -6,13 +6,11 @@ package private import ( "context" "fmt" - "net/http" "net/url" asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/perm" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" ) @@ -24,20 +22,11 @@ type KeyAndOwner struct { // ServNoCommand returns information about the provided key func ServNoCommand(ctx context.Context, keyID int64) (*asymkey_model.PublicKey, *user_model.User, error) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/none/%d", - keyID) - resp, err := newInternalRequest(ctx, reqURL, "GET").Response() - if err != nil { - return nil, nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, nil, fmt.Errorf("%s", decodeJSONError(resp).Err) - } - - var keyAndOwner KeyAndOwner - if err := json.NewDecoder(resp.Body).Decode(&keyAndOwner); err != nil { - return nil, nil, err + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/none/%d", keyID) + req := newInternalRequest(ctx, reqURL, "GET") + keyAndOwner, extra := requestJSONResp(req, &KeyAndOwner{}) + if extra.HasError() { + return nil, nil, extra.Error } return keyAndOwner.Key, keyAndOwner.Owner, nil } @@ -56,53 +45,19 @@ type ServCommandResults struct { RepoID int64 } -// ErrServCommand is an error returned from ServCommmand. -type ErrServCommand struct { - Results ServCommandResults - Err string - StatusCode int -} - -func (err ErrServCommand) Error() string { - return err.Err -} - -// IsErrServCommand checks if an error is a ErrServCommand. -func IsErrServCommand(err error) bool { - _, ok := err.(ErrServCommand) - return ok -} - // ServCommand preps for a serv call -func ServCommand(ctx context.Context, keyID int64, ownerName, repoName string, mode perm.AccessMode, verbs ...string) (*ServCommandResults, error) { +func ServCommand(ctx context.Context, keyID int64, ownerName, repoName string, mode perm.AccessMode, verbs ...string) (*ServCommandResults, ResponseExtra) { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/command/%d/%s/%s?mode=%d", keyID, url.PathEscape(ownerName), url.PathEscape(repoName), - mode) + mode, + ) for _, verb := range verbs { if verb != "" { reqURL += fmt.Sprintf("&verb=%s", url.QueryEscape(verb)) } } - - resp, err := newInternalRequest(ctx, reqURL, "GET").Response() - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - var errServCommand ErrServCommand - if err := json.NewDecoder(resp.Body).Decode(&errServCommand); err != nil { - return nil, err - } - errServCommand.StatusCode = resp.StatusCode - return nil, errServCommand - } - var results ServCommandResults - if err := json.NewDecoder(resp.Body).Decode(&results); err != nil { - return nil, err - } - return &results, nil + req := newInternalRequest(ctx, reqURL, "GET") + return requestJSONResp(req, &ServCommandResults{}) } diff --git a/modules/setting/indexer.go b/modules/setting/indexer.go index 5b10018eb7b21..8aee8596ded79 100644 --- a/modules/setting/indexer.go +++ b/modules/setting/indexer.go @@ -4,6 +4,7 @@ package setting import ( + "net/url" "path/filepath" "strings" "time" @@ -18,6 +19,7 @@ var Indexer = struct { IssueType string IssuePath string IssueConnStr string + IssueConnAuth string IssueIndexerName string StartupTimeout time.Duration @@ -34,6 +36,7 @@ var Indexer = struct { IssueType: "bleve", IssuePath: "indexers/issues.bleve", IssueConnStr: "", + IssueConnAuth: "", IssueIndexerName: "gitea_issues", RepoIndexerEnabled: false, @@ -53,6 +56,18 @@ func loadIndexerFrom(rootCfg ConfigProvider) { Indexer.IssuePath = filepath.ToSlash(filepath.Join(AppWorkPath, Indexer.IssuePath)) } Indexer.IssueConnStr = sec.Key("ISSUE_INDEXER_CONN_STR").MustString(Indexer.IssueConnStr) + + if Indexer.IssueType == "meilisearch" { + u, err := url.Parse(Indexer.IssueConnStr) + if err != nil { + log.Warn("Failed to parse ISSUE_INDEXER_CONN_STR: %v", err) + u = &url.URL{} + } + Indexer.IssueConnAuth, _ = u.User.Password() + u.User = nil + Indexer.IssueConnStr = u.String() + } + Indexer.IssueIndexerName = sec.Key("ISSUE_INDEXER_NAME").MustString(Indexer.IssueIndexerName) // The following settings are deprecated and can be overridden by settings in [queue] or [queue.issue_indexer] diff --git a/modules/setting/storage.go b/modules/setting/storage.go index 4d401614e4929..50d4c8439e2b1 100644 --- a/modules/setting/storage.go +++ b/modules/setting/storage.go @@ -42,6 +42,7 @@ func getStorage(rootCfg ConfigProvider, name, typ string, targetSec *ini.Section sec.Key("MINIO_LOCATION").MustString("us-east-1") sec.Key("MINIO_USE_SSL").MustBool(false) sec.Key("MINIO_INSECURE_SKIP_VERIFY").MustBool(false) + sec.Key("MINIO_CHECKSUM_ALGORITHM").MustString("default") if targetSec == nil { targetSec, _ = rootCfg.NewSection(name) diff --git a/modules/storage/minio.go b/modules/storage/minio.go index 5c67dbf26a298..250f17827ff5b 100644 --- a/modules/storage/minio.go +++ b/modules/storage/minio.go @@ -6,6 +6,7 @@ package storage import ( "context" "crypto/tls" + "fmt" "io" "net/http" "net/url" @@ -53,10 +54,12 @@ type MinioStorageConfig struct { BasePath string `ini:"MINIO_BASE_PATH"` UseSSL bool `ini:"MINIO_USE_SSL"` InsecureSkipVerify bool `ini:"MINIO_INSECURE_SKIP_VERIFY"` + ChecksumAlgorithm string `ini:"MINIO_CHECKSUM_ALGORITHM"` } // MinioStorage returns a minio bucket storage type MinioStorage struct { + cfg *MinioStorageConfig ctx context.Context client *minio.Client bucket string @@ -91,6 +94,10 @@ func NewMinioStorage(ctx context.Context, cfg interface{}) (ObjectStorage, error } config := configInterface.(MinioStorageConfig) + if config.ChecksumAlgorithm != "" && config.ChecksumAlgorithm != "default" && config.ChecksumAlgorithm != "md5" { + return nil, fmt.Errorf("invalid minio checksum algorithm: %s", config.ChecksumAlgorithm) + } + log.Info("Creating Minio storage at %s:%s with base path %s", config.Endpoint, config.Bucket, config.BasePath) minioClient, err := minio.New(config.Endpoint, &minio.Options{ @@ -113,6 +120,7 @@ func NewMinioStorage(ctx context.Context, cfg interface{}) (ObjectStorage, error } return &MinioStorage{ + cfg: &config, ctx: ctx, client: minioClient, bucket: config.Bucket, @@ -124,7 +132,7 @@ func (m *MinioStorage) buildMinioPath(p string) string { return util.PathJoinRelX(m.basePath, p) } -// Open open a file +// Open opens a file func (m *MinioStorage) Open(path string) (Object, error) { opts := minio.GetObjectOptions{} object, err := m.client.GetObject(m.ctx, m.bucket, m.buildMinioPath(path), opts) @@ -134,7 +142,7 @@ func (m *MinioStorage) Open(path string) (Object, error) { return &minioObject{object}, nil } -// Save save a file to minio +// Save saves a file to minio func (m *MinioStorage) Save(path string, r io.Reader, size int64) (int64, error) { uploadInfo, err := m.client.PutObject( m.ctx, @@ -142,7 +150,14 @@ func (m *MinioStorage) Save(path string, r io.Reader, size int64) (int64, error) m.buildMinioPath(path), r, size, - minio.PutObjectOptions{ContentType: "application/octet-stream"}, + minio.PutObjectOptions{ + ContentType: "application/octet-stream", + // some storages like: + // * https://developers.cloudflare.com/r2/api/s3/api/ + // * https://www.backblaze.com/b2/docs/s3_compatible_api.html + // do not support "x-amz-checksum-algorithm" header, so use legacy MD5 checksum + SendContentMd5: m.cfg.ChecksumAlgorithm == "md5", + }, ) if err != nil { return 0, convertMinioErr(err) diff --git a/modules/structs/issue.go b/modules/structs/issue.go index 48e4e0e7e3695..04e169df84197 100644 --- a/modules/structs/issue.go +++ b/modules/structs/issue.go @@ -190,6 +190,22 @@ func (l *IssueTemplateLabels) UnmarshalYAML(value *yaml.Node) error { return fmt.Errorf("line %d: cannot unmarshal %s into IssueTemplateLabels", value.Line, value.ShortTag()) } +type IssueConfigContactLink struct { + Name string `json:"name" yaml:"name"` + URL string `json:"url" yaml:"url"` + About string `json:"about" yaml:"about"` +} + +type IssueConfig struct { + BlankIssuesEnabled bool `json:"blank_issues_enabled" yaml:"blank_issues_enabled"` + ContactLinks []IssueConfigContactLink `json:"contact_links" yaml:"contact_links"` +} + +type IssueConfigValidation struct { + Valid bool `json:"valid"` + Message string `json:"message"` +} + // IssueTemplateType defines issue template type type IssueTemplateType string @@ -211,3 +227,11 @@ func (it IssueTemplate) Type() IssueTemplateType { } return "" } + +// IssueMeta basic issue information +// swagger:model +type IssueMeta struct { + Index int64 `json:"index"` + Owner string `json:"owner"` + Name string `json:"repo"` +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index d09ea26942b4d..3f47af826e54b 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1272,10 +1272,12 @@ issues.new.no_assignees = No Assignees issues.new.no_reviewers = No reviewers issues.new.add_reviewer_title = Request review issues.choose.get_started = Get Started +issues.choose.open_external_link = Open issues.choose.blank = Default issues.choose.blank_about = Create an issue from default template. issues.choose.ignore_invalid_templates = Invalid templates have been ignored issues.choose.invalid_templates = %v invalid template(s) found +issues.choose.invalid_config = The issue config contains errors: issues.no_ref = No Branch/Tag Specified issues.create = Create Issue issues.new_label = New Label @@ -1489,6 +1491,9 @@ issues.due_date_invalid = "The due date is invalid or out of range. Please use t issues.dependency.title = Dependencies issues.dependency.issue_no_dependencies = No dependencies set. issues.dependency.pr_no_dependencies = No dependencies set. +issues.dependency.no_permission_1 = "You do not have permission to read %d dependency" +issues.dependency.no_permission_n = "You do not have permission to read %d dependencies" +issues.dependency.no_permission.can_remove = "You do not have permission to read this dependency but can remove this dependency" issues.dependency.add = Add dependency… issues.dependency.cancel = Cancel issues.dependency.remove = Remove @@ -3360,6 +3365,7 @@ runners.status.idle = Idle runners.status.active = Active runners.status.offline = Offline runners.version = Version +runners.reset_registration_token_success = Runner registration token reset successfully runs.all_workflows = All Workflows runs.open_tab = %d Open diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 9e227b3b94680..ac2aa573027bb 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -84,6 +84,7 @@ add=追加 add_all=すべて追加 remove=除去 remove_all=すべて除去 +remove_label_str=アイテム「%s」を削除 edit=編集 enabled=有効 @@ -218,6 +219,7 @@ openid_signup_popup=OpenIDベースでのユーザーのセルフ登録を有効 enable_captcha=登録時のCAPTCHAを有効にする enable_captcha_popup=ユーザーのセルフ登録時にCAPTCHAを必須にします。 require_sign_in_view=ページ閲覧にサインインが必要 +require_sign_in_view_popup=ページアクセスをサインイン済みユーザーに限定します。 訪問者はサインインページと登録ページだけ見ることができます。 admin_setting_desc=管理者アカウントの作成は任意です。 最初に登録したユーザーは自動的に管理者になります。 admin_title=管理者アカウントの設定 admin_name=管理者ユーザー名 @@ -247,6 +249,7 @@ no_reply_address=メールを隠すときのドメイン no_reply_address_helper=メールアドレスを隠しているユーザーに使用するドメイン名。 例えば 'noreply.example.org' と設定した場合、ユーザー名 'joe' はGitに 'joe@noreply.example.org' としてログインすることになります。 password_algorithm=パスワードハッシュアルゴリズム invalid_password_algorithm=無効なパスワードハッシュアルゴリズム +password_algorithm_helper=パスワードハッシュアルゴリズムを設定します。 アルゴリズムにより動作要件と強度が異なります。 argon2アルゴリズムはかなり安全ですが、多くのメモリを使用するため小さなシステムには適さない場合があります。 enable_update_checker=アップデートチェッカーを有効にする enable_update_checker_helper=gitea.ioに接続して定期的に新しいバージョンのリリースを確認します。 @@ -518,8 +521,14 @@ invalid_ssh_key=SSHキーが確認できません: %s invalid_gpg_key=GPGキーが確認できません: %s invalid_ssh_principal=無効なプリンシパル: %s must_use_public_key=あなたが提供したキーは秘密鍵です。秘密鍵をどこにもアップロードしないでください。代わりに公開鍵を使用してください。 +unable_verify_ssh_key=SSHキーが確認できません。間違いが無いか、よく確認してください。 auth_failed=認証に失敗しました: %v +still_own_repo=あなたのアカウントは1つ以上のリポジトリを所有しています。 先にそれらを削除するか移転してください。 +still_has_org=あなたのアカウントは1つ以上の組織に参加しています。 先にそれらから脱退してください。 +still_own_packages=あなたのアカウントは1つ以上のパッケージを所有しています。 先にそれらを削除してください。 +org_still_own_repo=組織はまだ1つ以上のリポジトリを所有しています。 先にそれらを削除するか移転してください。 +org_still_own_packages=組織はまだ1つ以上のパッケージを所有しています。 先にそれらを削除してください。 target_branch_not_exist=ターゲットのブランチが存在していません。 @@ -984,6 +993,7 @@ migrate.github_token_desc=GitHub APIにはレート制限があります。移 migrate.clone_local_path=、またはローカルサーバー上のパス migrate.permission_denied=ローカルリポジトリをインポートする権限がありません。 migrate.permission_denied_blocked=許可されていないホストからインポートできません。管理者に問い合わせて、ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS の設定を確認してください。 +migrate.invalid_local_path=ローカルパスが無効です。 存在しないかディレクトリではありません。 migrate.invalid_lfs_endpoint=LFS エンドポイントが無効です。 migrate.failed=移行に失敗しました: %v migrate.migrate_items_options=追加の項目を移行するにはアクセストークンが必要です @@ -1165,6 +1175,7 @@ commits.commits=コミット commits.no_commits=共通のコミットはありません。 '%s' と '%s' の履歴はすべて異なっています。 commits.nothing_to_compare=二つのブランチは同じ内容です。 commits.search=コミットの検索… +commits.search.tooltip=`キーワード "author:"、"committer:"、"after:"、"before:" を付けて指定できます。 例 "revert author:Alice before:2019-01-13"` commits.find=検索 commits.search_all=すべてのブランチ commits.author=作成者 @@ -1923,6 +1934,7 @@ settings.trust_model.collaborator.long=共同作業者: 共同作業者による settings.trust_model.collaborator.desc=このリポジトリの共同作業者による正常な署名は、(署名がコミッターのものかどうかにかかわらず)「信頼済み」とみなします。 署名が共同作業者ではないコミッターのものであれば「信頼不可」、それ以外は「不一致」となります。 settings.trust_model.committer=コミッター settings.trust_model.committer.long=コミッター: コミッターによる署名を信頼します (これはGitHub方式であり、Giteaの署名が付いたコミットはコミッターがGitea自身であることが強制されます) +settings.trust_model.committer.desc=正常な署名は、コミッターに一致する場合のみ「信頼済み」とみなし、それ以外は「不一致」となります。 Giteaは署名付きでコミットすることが強制され、本来のコミッターはコミットの最後に Co-authored-by: と Co-committed-by: で追加されます。 Giteaのデフォルト鍵はデータベース内のユーザー1人とマッチしなければなりません。 settings.trust_model.collaboratorcommitter=共同作業者+コミッター settings.trust_model.collaboratorcommitter.long=共同作業者+コミッター: コミッターと一致する共同作業者による署名を信頼します settings.trust_model.collaboratorcommitter.desc=このリポジトリの共同作業者による正常な署名は、コミッターと一致する場合に「信頼済み」とみなします。 それ以外の正常な署名のうち、コミッターに一致するものは「信頼不可」、他は「不一致」となります。 Giteaが署名付きコミットのコミッターであることが強制され、本来のコミッターはコミットの最後に Co-Authored-By: と Co-Committed-By: で追加されます。 Giteaのデフォルト鍵はデータベース内のユーザー1人とマッチしなければなりません。 @@ -2464,6 +2476,7 @@ teams.remove_all_repos_title=チームリポジトリをすべて除去 teams.remove_all_repos_desc=チームからすべてのリポジトリを除去します。 teams.add_all_repos_title=すべてのリポジトリを追加 teams.add_all_repos_desc=組織のすべてのリポジトリをチームに追加します。 +teams.add_nonexistent_repo=追加しようとしているリポジトリは存在しません。 先にリポジトリを作成してください。 teams.add_duplicate_users=ユーザーは既にチームのメンバーです。 teams.repos.none=このチームがアクセスできるリポジトリはありません。 teams.members.none=このチームにはメンバーがいません。 @@ -2493,6 +2506,7 @@ first_page=最初 last_page=最後 total=合計: %d +dashboard.new_version_hint=Gitea %s が入手可能になりました。 現在実行しているのは %s です。 詳細は ブログ を確認してください。 dashboard.statistic=サマリー dashboard.operations=メンテナンス操作 dashboard.system_status=システム状況 @@ -2612,6 +2626,7 @@ users.still_own_repo=このユーザーはまだ1つ以上のリポジトリを users.still_has_org=このユーザーは組織のメンバーになっています。 先に組織からこのユーザーを削除してください。 users.purge=ユーザーを抹消 users.purge_help=強制的にユーザーとそのユーザーが所有していたリポジトリ、組織、パッケージを削除します。コメントもすべて削除します。 +users.still_own_packages=このユーザーはまだ1つ以上のパッケージを所有しています。先にそれらのパッケージを削除してください。 users.deletion_success=ユーザーアカウントを削除しました。 users.reset_2fa=2要素認証をリセット users.list_status_filter.menu_text=フィルター diff --git a/routers/api/packages/npm/npm.go b/routers/api/packages/npm/npm.go index 0d25f173e922b..51b34d3e2721e 100644 --- a/routers/api/packages/npm/npm.go +++ b/routers/api/packages/npm/npm.go @@ -13,6 +13,9 @@ import ( "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" + access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/context" packages_module "code.gitea.io/gitea/modules/packages" npm_module "code.gitea.io/gitea/modules/packages/npm" @@ -166,6 +169,26 @@ func UploadPackage(ctx *context.Context) { return } + repo, err := repo_model.GetRepositoryByURL(ctx, npmPackage.Metadata.Repository.URL) + if err == nil { + canWrite := repo.OwnerID == ctx.Doer.ID + + if !canWrite { + perms, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + canWrite = perms.CanWrite(unit.TypePackages) + } + + if !canWrite { + apiError(ctx, http.StatusForbidden, "no permission to upload this package") + return + } + } + buf, err := packages_module.CreateHashedBufferFromReader(bytes.NewReader(npmPackage.Data), 32*1024*1024) if err != nil { apiError(ctx, http.StatusInternalServerError, err) @@ -217,6 +240,13 @@ func UploadPackage(ctx *context.Context) { } } + if repo != nil { + if err := packages_model.SetRepositoryLink(ctx, pv.PackageID, repo.ID); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + } + ctx.Status(http.StatusCreated) } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 8fd824640ff7f..8b13f5492c148 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1026,6 +1026,14 @@ func Routes(ctx gocontext.Context) *web.Route { Patch(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueAttachment). Delete(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, repo.DeleteIssueAttachment) }, mustEnableAttachments) + m.Combo("/dependencies"). + Get(repo.GetIssueDependencies). + Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.IssueMeta{}), repo.CreateIssueDependency). + Delete(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.IssueMeta{}), repo.RemoveIssueDependency) + m.Combo("/blocks"). + Get(repo.GetIssueBlocks). + Post(reqToken(auth_model.AccessTokenScopeRepo), bind(api.IssueMeta{}), repo.CreateIssueBlocking). + Delete(reqToken(auth_model.AccessTokenScopeRepo), bind(api.IssueMeta{}), repo.RemoveIssueBlocking) }) }, mustEnableIssuesOrPulls) m.Group("/labels", func() { @@ -1161,6 +1169,8 @@ func Routes(ctx gocontext.Context) *web.Route { }, reqAdmin()) }, reqAnyRepoReader()) m.Get("/issue_templates", context.ReferencesGitRepo(), repo.GetIssueTemplates) + m.Get("/issue_config", context.ReferencesGitRepo(), repo.GetIssueConfig) + m.Get("/issue_config/validate", context.ReferencesGitRepo(), repo.ValidateIssueConfig) m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages) }, repoAssignment()) }) diff --git a/routers/api/v1/repo/issue_dependency.go b/routers/api/v1/repo/issue_dependency.go new file mode 100644 index 0000000000000..8a57ad581e281 --- /dev/null +++ b/routers/api/v1/repo/issue_dependency.go @@ -0,0 +1,598 @@ +// Copyright 2016 The Gogs Authors. All rights reserved. +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/convert" +) + +// GetIssueDependencies list an issue's dependencies +func GetIssueDependencies(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/issues/{index}/dependencies issue issueListIssueDependencies + // --- + // summary: List an issue's dependencies, i.e all issues that block this issue. + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: index + // in: path + // description: index of the issue + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/IssueList" + + // If this issue's repository does not enable dependencies then there can be no dependencies by default + if !ctx.Repo.Repository.IsDependenciesEnabled(ctx) { + ctx.NotFound() + return + } + + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + if err != nil { + if issues_model.IsErrIssueNotExist(err) { + ctx.NotFound("IsErrIssueNotExist", err) + } else { + ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + } + return + } + + // 1. We must be able to read this issue + if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) { + ctx.NotFound() + return + } + + page := ctx.FormInt("page") + if page <= 1 { + page = 1 + } + limit := ctx.FormInt("limit") + if limit == 0 { + limit = setting.API.DefaultPagingNum + } else if limit > setting.API.MaxResponseItems { + limit = setting.API.MaxResponseItems + } + + canWrite := ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) + + blockerIssues := make([]*issues_model.Issue, 0, limit) + + // 2. Get the issues this issue depends on, i.e. the `<#b>`: ` <- <#b>` + blockersInfo, err := issue.BlockedByDependencies(ctx, db.ListOptions{ + Page: page, + PageSize: limit, + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "BlockedByDependencies", err) + return + } + + var lastRepoID int64 + var lastPerm access_model.Permission + for _, blocker := range blockersInfo { + // Get the permissions for this repository + perm := lastPerm + if lastRepoID != blocker.Repository.ID { + if blocker.Repository.ID == ctx.Repo.Repository.ID { + perm = ctx.Repo.Permission + } else { + var err error + perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer) + if err != nil { + ctx.ServerError("GetUserRepoPermission", err) + return + } + } + lastRepoID = blocker.Repository.ID + } + + // check permission + if !perm.CanReadIssuesOrPulls(blocker.Issue.IsPull) { + if !canWrite { + hiddenBlocker := &issues_model.DependencyInfo{ + Issue: issues_model.Issue{ + Title: "HIDDEN", + }, + } + blocker = hiddenBlocker + } else { + confidentialBlocker := &issues_model.DependencyInfo{ + Issue: issues_model.Issue{ + RepoID: blocker.Issue.RepoID, + Index: blocker.Index, + Title: blocker.Title, + IsClosed: blocker.IsClosed, + IsPull: blocker.IsPull, + }, + Repository: repo_model.Repository{ + ID: blocker.Issue.Repo.ID, + Name: blocker.Issue.Repo.Name, + OwnerName: blocker.Issue.Repo.OwnerName, + }, + } + confidentialBlocker.Issue.Repo = &confidentialBlocker.Repository + blocker = confidentialBlocker + } + } + blockerIssues = append(blockerIssues, &blocker.Issue) + } + + ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, blockerIssues)) +} + +// CreateIssueDependency create a new issue dependencies +func CreateIssueDependency(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/issues/{index}/dependencies issue issueCreateIssueDependencies + // --- + // summary: Make the issue in the url depend on the issue in the form. + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: index + // in: path + // description: index of the issue + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/IssueMeta" + // responses: + // "201": + // "$ref": "#/responses/Issue" + // "404": + // description: the issue does not exist + + // We want to make <:index> depend on
, i.e. <:index> is the target + target := getParamsIssue(ctx) + if ctx.Written() { + return + } + + // and represents the dependency + form := web.GetForm(ctx).(*api.IssueMeta) + dependency := getFormIssue(ctx, form) + if ctx.Written() { + return + } + + dependencyPerm := getPermissionForRepo(ctx, target.Repo) + if ctx.Written() { + return + } + + createIssueDependency(ctx, target, dependency, ctx.Repo.Permission, *dependencyPerm) + if ctx.Written() { + return + } + + ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, target)) +} + +// RemoveIssueDependency remove an issue dependency +func RemoveIssueDependency(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/dependencies issue issueRemoveIssueDependencies + // --- + // summary: Remove an issue dependency + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: index + // in: path + // description: index of the issue + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/IssueMeta" + // responses: + // "200": + // "$ref": "#/responses/Issue" + + // We want to make <:index> depend on , i.e. <:index> is the target + target := getParamsIssue(ctx) + if ctx.Written() { + return + } + + // and represents the dependency + form := web.GetForm(ctx).(*api.IssueMeta) + dependency := getFormIssue(ctx, form) + if ctx.Written() { + return + } + + dependencyPerm := getPermissionForRepo(ctx, target.Repo) + if ctx.Written() { + return + } + + removeIssueDependency(ctx, target, dependency, ctx.Repo.Permission, *dependencyPerm) + if ctx.Written() { + return + } + + ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, target)) +} + +// GetIssueBlocks list issues that are blocked by this issue +func GetIssueBlocks(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/issues/{index}/blocks issue issueListBlocks + // --- + // summary: List issues that are blocked by this issue + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: index + // in: path + // description: index of the issue + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/IssueList" + + // We need to list the issues that DEPEND on this issue not the other way round + // Therefore whether dependencies are enabled or not in this repository is potentially irrelevant. + + issue := getParamsIssue(ctx) + if ctx.Written() { + return + } + + if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) { + ctx.NotFound() + return + } + + page := ctx.FormInt("page") + if page <= 1 { + page = 1 + } + limit := ctx.FormInt("limit") + if limit <= 1 { + limit = setting.API.DefaultPagingNum + } + + skip := (page - 1) * limit + max := page * limit + + deps, err := issue.BlockingDependencies(ctx) + if err != nil { + ctx.Error(http.StatusInternalServerError, "BlockingDependencies", err) + return + } + + var lastRepoID int64 + var lastPerm access_model.Permission + + var issues []*issues_model.Issue + for i, depMeta := range deps { + if i < skip || i >= max { + continue + } + + // Get the permissions for this repository + perm := lastPerm + if lastRepoID != depMeta.Repository.ID { + if depMeta.Repository.ID == ctx.Repo.Repository.ID { + perm = ctx.Repo.Permission + } else { + var err error + perm, err = access_model.GetUserRepoPermission(ctx, &depMeta.Repository, ctx.Doer) + if err != nil { + ctx.ServerError("GetUserRepoPermission", err) + return + } + } + lastRepoID = depMeta.Repository.ID + } + + if !perm.CanReadIssuesOrPulls(depMeta.Issue.IsPull) { + continue + } + + depMeta.Issue.Repo = &depMeta.Repository + issues = append(issues, &depMeta.Issue) + } + + ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, issues)) +} + +// CreateIssueBlocking block the issue given in the body by the issue in path +func CreateIssueBlocking(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/issues/{index}/blocks issue issueCreateIssueBlocking + // --- + // summary: Block the issue given in the body by the issue in path + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: index + // in: path + // description: index of the issue + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/IssueMeta" + // responses: + // "201": + // "$ref": "#/responses/Issue" + // "404": + // description: the issue does not exist + + dependency := getParamsIssue(ctx) + if ctx.Written() { + return + } + + form := web.GetForm(ctx).(*api.IssueMeta) + target := getFormIssue(ctx, form) + if ctx.Written() { + return + } + + targetPerm := getPermissionForRepo(ctx, target.Repo) + if ctx.Written() { + return + } + + createIssueDependency(ctx, target, dependency, *targetPerm, ctx.Repo.Permission) + if ctx.Written() { + return + } + + ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, dependency)) +} + +// RemoveIssueBlocking unblock the issue given in the body by the issue in path +func RemoveIssueBlocking(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/blocks issue issueRemoveIssueBlocking + // --- + // summary: Unblock the issue given in the body by the issue in path + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: index + // in: path + // description: index of the issue + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/IssueMeta" + // responses: + // "200": + // "$ref": "#/responses/Issue" + + dependency := getParamsIssue(ctx) + if ctx.Written() { + return + } + + form := web.GetForm(ctx).(*api.IssueMeta) + target := getFormIssue(ctx, form) + if ctx.Written() { + return + } + + targetPerm := getPermissionForRepo(ctx, target.Repo) + if ctx.Written() { + return + } + + removeIssueDependency(ctx, target, dependency, *targetPerm, ctx.Repo.Permission) + if ctx.Written() { + return + } + + ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, dependency)) +} + +func getParamsIssue(ctx *context.APIContext) *issues_model.Issue { + issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + if err != nil { + if issues_model.IsErrIssueNotExist(err) { + ctx.NotFound("IsErrIssueNotExist", err) + } else { + ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + } + return nil + } + issue.Repo = ctx.Repo.Repository + return issue +} + +func getFormIssue(ctx *context.APIContext, form *api.IssueMeta) *issues_model.Issue { + var repo *repo_model.Repository + if form.Owner != ctx.Repo.Repository.OwnerName || form.Name != ctx.Repo.Repository.Name { + if !setting.Service.AllowCrossRepositoryDependencies { + ctx.JSON(http.StatusBadRequest, "CrossRepositoryDependencies not enabled") + return nil + } + var err error + repo, err = repo_model.GetRepositoryByOwnerAndName(ctx, form.Owner, form.Name) + if err != nil { + if repo_model.IsErrRepoNotExist(err) { + ctx.NotFound("IsErrRepoNotExist", err) + } else { + ctx.Error(http.StatusInternalServerError, "GetRepositoryByOwnerAndName", err) + } + return nil + } + } else { + repo = ctx.Repo.Repository + } + + issue, err := issues_model.GetIssueByIndex(repo.ID, form.Index) + if err != nil { + if issues_model.IsErrIssueNotExist(err) { + ctx.NotFound("IsErrIssueNotExist", err) + } else { + ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + } + return nil + } + issue.Repo = repo + return issue +} + +func getPermissionForRepo(ctx *context.APIContext, repo *repo_model.Repository) *access_model.Permission { + if repo.ID == ctx.Repo.Repository.ID { + return &ctx.Repo.Permission + } + + perm, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) + return nil + } + + return &perm +} + +func createIssueDependency(ctx *context.APIContext, target, dependency *issues_model.Issue, targetPerm, dependencyPerm access_model.Permission) { + if target.Repo.IsArchived || !target.Repo.IsDependenciesEnabled(ctx) { + // The target's repository doesn't have dependencies enabled + ctx.NotFound() + return + } + + if !targetPerm.CanWriteIssuesOrPulls(target.IsPull) { + // We can't write to the target + ctx.NotFound() + return + } + + if !dependencyPerm.CanReadIssuesOrPulls(dependency.IsPull) { + // We can't read the dependency + ctx.NotFound() + return + } + + err := issues_model.CreateIssueDependency(ctx.Doer, target, dependency) + if err != nil { + ctx.Error(http.StatusInternalServerError, "CreateIssueDependency", err) + return + } +} + +func removeIssueDependency(ctx *context.APIContext, target, dependency *issues_model.Issue, targetPerm, dependencyPerm access_model.Permission) { + if target.Repo.IsArchived || !target.Repo.IsDependenciesEnabled(ctx) { + // The target's repository doesn't have dependencies enabled + ctx.NotFound() + return + } + + if !targetPerm.CanWriteIssuesOrPulls(target.IsPull) { + // We can't write to the target + ctx.NotFound() + return + } + + if !dependencyPerm.CanReadIssuesOrPulls(dependency.IsPull) { + // We can't read the dependency + ctx.NotFound() + return + } + + err := issues_model.RemoveIssueDependency(ctx.Doer, target, dependency, issues_model.DependencyTypeBlockedBy) + if err != nil { + ctx.Error(http.StatusInternalServerError, "CreateIssueDependency", err) + return + } +} diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 4f43b10259726..60e71495e87b8 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -1144,3 +1144,58 @@ func GetIssueTemplates(ctx *context.APIContext) { ctx.JSON(http.StatusOK, ctx.IssueTemplatesFromDefaultBranch()) } + +// GetIssueConfig returns the issue config for a repo +func GetIssueConfig(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/issue_config repository repoGetIssueConfig + // --- + // summary: Returns the issue config for a repo + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/RepoIssueConfig" + issueConfig, _ := ctx.IssueConfigFromDefaultBranch() + ctx.JSON(http.StatusOK, issueConfig) +} + +// ValidateIssueConfig returns validation errors for the issue config +func ValidateIssueConfig(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/issue_config/validate repository repoValidateIssueConfig + // --- + // summary: Returns the validation information for a issue config + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/RepoIssueConfigValidation" + _, err := ctx.IssueConfigFromDefaultBranch() + + if err == nil { + ctx.JSON(http.StatusOK, api.IssueConfigValidation{Valid: true, Message: ""}) + } else { + ctx.JSON(http.StatusOK, api.IssueConfigValidation{Valid: false, Message: err.Error()}) + } +} diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 1ddc93c383d64..09bb1d18f3ae0 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -41,6 +41,8 @@ type swaggerParameterBodies struct { CreateIssueCommentOption api.CreateIssueCommentOption // in:body EditIssueCommentOption api.EditIssueCommentOption + // in:body + IssueMeta api.IssueMeta // in:body IssueLabelsOption api.IssueLabelsOption diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index bd867213a62da..e0418e99dc44f 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -386,3 +386,17 @@ type swaggerRepoCollaboratorPermission struct { // in:body Body api.RepoCollaboratorPermission `json:"body"` } + +// RepoIssueConfig +// swagger:response RepoIssueConfig +type swaggerRepoIssueConfig struct { + // in:body + Body api.IssueConfig `json:"body"` +} + +// RepoIssueConfigValidation +// swagger:response RepoIssueConfigValidation +type swaggerRepoIssueConfigValidation struct { + // in:body + Body api.IssueConfigValidation `json:"body"` +} diff --git a/routers/private/default_branch.go b/routers/private/default_branch.go index 268ebbe4431ba..b15d6ba33a4b7 100644 --- a/routers/private/default_branch.go +++ b/routers/private/default_branch.go @@ -1,7 +1,6 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -// Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead. package private import ( @@ -14,19 +13,6 @@ import ( "code.gitea.io/gitea/modules/private" ) -// ________ _____ .__ __ -// \______ \ _____/ ____\____ __ __| |_/ |_ -// | | \_/ __ \ __\\__ \ | | \ |\ __\ -// | ` \ ___/| | / __ \| | / |_| | -// /_______ /\___ >__| (____ /____/|____/__| -// \/ \/ \/ -// __________ .__ -// \______ \____________ ____ ____ | |__ -// | | _/\_ __ \__ \ / \_/ ___\| | \ -// | | \ | | \// __ \| | \ \___| Y \ -// |______ / |__| (____ /___| /\___ >___| / -// \/ \/ \/ \/ \/ - // SetDefaultBranch updates the default branch func SetDefaultBranch(ctx *gitea_context.PrivateContext) { ownerName := ctx.Params(":owner") diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go index 75de47bdc4b26..cfe20be106cf7 100644 --- a/routers/private/hook_post_receive.go +++ b/routers/private/hook_post_receive.go @@ -1,7 +1,6 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -// Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead. package private import ( diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go index c711fc947714f..63b4a8622e129 100644 --- a/routers/private/hook_pre_receive.go +++ b/routers/private/hook_pre_receive.go @@ -1,7 +1,6 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -// Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead. package private import ( @@ -69,8 +68,8 @@ func (ctx *preReceiveContext) AssertCanWriteCode() bool { if ctx.Written() { return false } - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": "User permission denied for writing.", + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: "User permission denied for writing.", }) return false } @@ -95,8 +94,8 @@ func (ctx *preReceiveContext) AssertCreatePullRequest() bool { if ctx.Written() { return false } - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": "User permission denied for creating pull-request.", + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: "User permission denied for creating pull-request.", }) return false } @@ -151,7 +150,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN if branchName == repo.DefaultBranch && newCommitID == git.EmptySHA { log.Warn("Forbidden: Branch: %s is the default branch in %-v and cannot be deleted", branchName, repo) ctx.JSON(http.StatusForbidden, private.Response{ - Err: fmt.Sprintf("branch %s is the default branch and cannot be deleted", branchName), + UserMsg: fmt.Sprintf("branch %s is the default branch and cannot be deleted", branchName), }) return } @@ -179,7 +178,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN if newCommitID == git.EmptySHA { log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo) ctx.JSON(http.StatusForbidden, private.Response{ - Err: fmt.Sprintf("branch %s is protected from deletion", branchName), + UserMsg: fmt.Sprintf("branch %s is protected from deletion", branchName), }) return } @@ -196,7 +195,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN } else if len(output) > 0 { log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo) ctx.JSON(http.StatusForbidden, private.Response{ - Err: fmt.Sprintf("branch %s is protected from force push", branchName), + UserMsg: fmt.Sprintf("branch %s is protected from force push", branchName), }) return @@ -217,7 +216,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN unverifiedCommit := err.(*errUnverifiedCommit).sha log.Warn("Forbidden: Branch: %s in %-v is protected from unverified commit %s", branchName, repo, unverifiedCommit) ctx.JSON(http.StatusForbidden, private.Response{ - Err: fmt.Sprintf("branch %s is protected from unverified commit %s", branchName, unverifiedCommit), + UserMsg: fmt.Sprintf("branch %s is protected from unverified commit %s", branchName, unverifiedCommit), }) return } @@ -272,7 +271,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN if changedProtectedfiles { log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath) ctx.JSON(http.StatusForbidden, private.Response{ - Err: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), + UserMsg: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), }) return } @@ -297,7 +296,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN // Or we're simply not able to push to this protected branch log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v", ctx.opts.UserID, branchName, repo) ctx.JSON(http.StatusForbidden, private.Response{ - Err: fmt.Sprintf("Not allowed to push to protected branch %s", branchName), + UserMsg: fmt.Sprintf("Not allowed to push to protected branch %s", branchName), }) return } @@ -333,7 +332,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN if !allowedMerge { log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v and is not allowed to merge pr #%d", ctx.opts.UserID, branchName, repo, pr.Index) ctx.JSON(http.StatusForbidden, private.Response{ - Err: fmt.Sprintf("Not allowed to push to protected branch %s", branchName), + UserMsg: fmt.Sprintf("Not allowed to push to protected branch %s", branchName), }) return } @@ -347,7 +346,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN if changedProtectedfiles { log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath) ctx.JSON(http.StatusForbidden, private.Response{ - Err: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), + UserMsg: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), }) return } @@ -357,7 +356,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN if models.IsErrDisallowedToMerge(err) { log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", ctx.opts.UserID, branchName, repo, pr.Index, err.Error()) ctx.JSON(http.StatusForbidden, private.Response{ - Err: fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, ctx.opts.PullRequestID, err.Error()), + UserMsg: fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, ctx.opts.PullRequestID, err.Error()), }) return } @@ -400,7 +399,7 @@ func preReceiveTag(ctx *preReceiveContext, oldCommitID, newCommitID, refFullName if !isAllowed { log.Warn("Forbidden: Tag %s in %-v is protected", tagName, ctx.Repo.Repository) ctx.JSON(http.StatusForbidden, private.Response{ - Err: fmt.Sprintf("Tag %s is protected", tagName), + UserMsg: fmt.Sprintf("Tag %s is protected", tagName), }) return } @@ -412,15 +411,15 @@ func preReceivePullRequest(ctx *preReceiveContext, oldCommitID, newCommitID, ref } if ctx.Repo.Repository.IsEmpty { - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": "Can't create pull request for an empty repository.", + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: "Can't create pull request for an empty repository.", }) return } if ctx.opts.IsWiki { - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": "Pull requests are not supported on the wiki.", + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: "Pull requests are not supported on the wiki.", }) return } @@ -443,7 +442,7 @@ func preReceivePullRequest(ctx *preReceiveContext, oldCommitID, newCommitID, ref if !baseBranchExist { ctx.JSON(http.StatusForbidden, private.Response{ - Err: fmt.Sprintf("Unexpected ref: %s", refFullName), + UserMsg: fmt.Sprintf("Unexpected ref: %s", refFullName), }) return } diff --git a/routers/private/hook_proc_receive.go b/routers/private/hook_proc_receive.go index 05921e6f5841b..5577120770604 100644 --- a/routers/private/hook_proc_receive.go +++ b/routers/private/hook_proc_receive.go @@ -1,7 +1,6 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -// Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead. package private import ( @@ -30,8 +29,8 @@ func HookProcReceive(ctx *gitea_context.PrivateContext) { ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error()) } else { log.Error(err.Error()) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "Err": err.Error(), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) } diff --git a/routers/private/hook_verification.go b/routers/private/hook_verification.go index 8ccde4f3d7549..caf3874ec3030 100644 --- a/routers/private/hook_verification.go +++ b/routers/private/hook_verification.go @@ -1,7 +1,6 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -// Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead. package private import ( @@ -16,19 +15,6 @@ import ( "code.gitea.io/gitea/modules/log" ) -// _________ .__ __ -// \_ ___ \ ____ _____ _____ |__|/ |_ -// / \ \/ / _ \ / \ / \| \ __\ -// \ \___( <_> ) Y Y \ Y Y \ || | -// \______ /\____/|__|_| /__|_| /__||__| -// \/ \/ \/ -// ____ ____ .__ _____.__ __ .__ -// \ \ / /___________|__|/ ____\__| ____ _____ _/ |_|__| ____ ____ -// \ Y // __ \_ __ \ \ __\| |/ ___\\__ \\ __\ |/ _ \ / \ -// \ /\ ___/| | \/ || | | \ \___ / __ \| | | ( <_> ) | \ -// \___/ \___ >__| |__||__| |__|\___ >____ /__| |__|\____/|___| / -// \/ \/ \/ \/ -// // This file contains commit verification functions for refs passed across in hooks func verifyCommits(oldCommitID, newCommitID string, repo *git.Repository, env []string) error { diff --git a/routers/private/internal.go b/routers/private/internal.go index 306e4ffb0040f..4acede33705d6 100644 --- a/routers/private/internal.go +++ b/routers/private/internal.go @@ -1,7 +1,7 @@ // Copyright 2017 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -// Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead. +// Package private contains all internal routes. The package name "internal" isn't usable because Golang reserves it for disabling cross-package usage. package private import ( diff --git a/routers/private/internal_repo.go b/routers/private/internal_repo.go index bd8db0a1854dc..5e7e82b03c961 100644 --- a/routers/private/internal_repo.go +++ b/routers/private/internal_repo.go @@ -1,7 +1,6 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -// Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead. package private import ( @@ -13,23 +12,10 @@ import ( gitea_context "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/private" ) -// __________ -// \______ \ ____ ______ ____ -// | _// __ \\____ \ / _ \ -// | | \ ___/| |_> > <_> ) -// |____|_ /\___ > __/ \____/ -// \/ \/|__| -// _____ .__ __ -// / _ \ ______ _____|__| ____ ____ _____ ____ _____/ |_ -// / /_\ \ / ___// ___/ |/ ___\ / \ / \_/ __ \ / \ __\ -// / | \\___ \ \___ \| / /_/ > | \ Y Y \ ___/| | \ | -// \____|__ /____ >____ >__\___ /|___| /__|_| /\___ >___| /__| -// \/ \/ \/ /_____/ \/ \/ \/ \/ - -// This file contains common functions relating to setting the Repository for the -// internal routes +// This file contains common functions relating to setting the Repository for the internal routes // RepoAssignment assigns the repository and gitrepository to the private context func RepoAssignment(ctx *gitea_context.PrivateContext) context.CancelFunc { @@ -45,8 +31,8 @@ func RepoAssignment(ctx *gitea_context.PrivateContext) context.CancelFunc { gitRepo, err := git.OpenRepository(ctx, repo.RepoPath()) if err != nil { log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "Err": fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err), }) return nil } @@ -71,8 +57,8 @@ func loadRepository(ctx *gitea_context.PrivateContext, ownerName, repoName strin repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName) if err != nil { log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "Err": fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err), }) return nil } diff --git a/routers/private/key.go b/routers/private/key.go index b536019dd7a01..a13b4c12ae3ce 100644 --- a/routers/private/key.go +++ b/routers/private/key.go @@ -1,7 +1,6 @@ // Copyright 2018 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -// Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead. package private import ( diff --git a/routers/private/manager.go b/routers/private/manager.go index a56fe9d12349a..38ad83326fe10 100644 --- a/routers/private/manager.go +++ b/routers/private/manager.go @@ -31,14 +31,14 @@ func FlushQueues(ctx *context.PrivateContext) { } }() ctx.JSON(http.StatusAccepted, private.Response{ - Err: "Flushing", + UserMsg: "Flushing", }) return } err := queue.GetManager().FlushAll(ctx, opts.Timeout) if err != nil { ctx.JSON(http.StatusRequestTimeout, private.Response{ - Err: fmt.Sprintf("%v", err), + UserMsg: fmt.Sprintf("%v", err), }) } ctx.PlainText(http.StatusOK, "success") diff --git a/routers/private/manager_windows.go b/routers/private/manager_windows.go index b5382c7d91611..bd3c3c30d06a3 100644 --- a/routers/private/manager_windows.go +++ b/routers/private/manager_windows.go @@ -16,7 +16,7 @@ import ( // Restart is not implemented for Windows based servers as they can't fork func Restart(ctx *context.PrivateContext) { ctx.JSON(http.StatusNotImplemented, private.Response{ - Err: "windows servers cannot be gracefully restarted - shutdown and restart manually", + UserMsg: "windows servers cannot be gracefully restarted - shutdown and restart manually", }) } diff --git a/routers/private/serv.go b/routers/private/serv.go index 23ac011cf5a69..b1efc58800752 100644 --- a/routers/private/serv.go +++ b/routers/private/serv.go @@ -1,7 +1,6 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -// Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead. package private import ( @@ -29,7 +28,7 @@ func ServNoCommand(ctx *context.PrivateContext) { keyID := ctx.ParamsInt64(":keyid") if keyID <= 0 { ctx.JSON(http.StatusBadRequest, private.Response{ - Err: fmt.Sprintf("Bad key id: %d", keyID), + UserMsg: fmt.Sprintf("Bad key id: %d", keyID), }) } results := private.KeyAndOwner{} @@ -38,7 +37,7 @@ func ServNoCommand(ctx *context.PrivateContext) { if err != nil { if asymkey_model.IsErrKeyNotExist(err) { ctx.JSON(http.StatusUnauthorized, private.Response{ - Err: fmt.Sprintf("Cannot find key: %d", keyID), + UserMsg: fmt.Sprintf("Cannot find key: %d", keyID), }) return } @@ -55,7 +54,7 @@ func ServNoCommand(ctx *context.PrivateContext) { if err != nil { if user_model.IsErrUserNotExist(err) { ctx.JSON(http.StatusUnauthorized, private.Response{ - Err: fmt.Sprintf("Cannot find owner with id: %d for key: %d", key.OwnerID, keyID), + UserMsg: fmt.Sprintf("Cannot find owner with id: %d for key: %d", key.OwnerID, keyID), }) return } @@ -67,7 +66,7 @@ func ServNoCommand(ctx *context.PrivateContext) { } if !user.IsActive || user.ProhibitLogin { ctx.JSON(http.StatusForbidden, private.Response{ - Err: "Your account is disabled.", + UserMsg: "Your account is disabled.", }) return } @@ -113,23 +112,20 @@ func ServCommand(ctx *context.PrivateContext) { if user_model.IsErrUserNotExist(err) { // User is fetching/cloning a non-existent repository log.Warn("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr()) - ctx.JSON(http.StatusNotFound, private.ErrServCommand{ - Results: results, - Err: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), + ctx.JSON(http.StatusNotFound, private.Response{ + UserMsg: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), }) return } log.Error("Unable to get repository owner: %s/%s Error: %v", results.OwnerName, results.RepoName, err) - ctx.JSON(http.StatusForbidden, private.ErrServCommand{ - Results: results, - Err: fmt.Sprintf("Unable to get repository owner: %s/%s %v", results.OwnerName, results.RepoName, err), + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: fmt.Sprintf("Unable to get repository owner: %s/%s %v", results.OwnerName, results.RepoName, err), }) return } if !owner.IsOrganization() && !owner.IsActive { - ctx.JSON(http.StatusForbidden, private.ErrServCommand{ - Results: results, - Err: "Repository cannot be accessed, you could retry it later", + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: "Repository cannot be accessed, you could retry it later", }) return } @@ -144,18 +140,16 @@ func ServCommand(ctx *context.PrivateContext) { if verb == "git-upload-pack" { // User is fetching/cloning a non-existent repository log.Warn("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr()) - ctx.JSON(http.StatusNotFound, private.ErrServCommand{ - Results: results, - Err: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), + ctx.JSON(http.StatusNotFound, private.Response{ + UserMsg: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), }) return } } } else { log.Error("Unable to get repository: %s/%s Error: %v", results.OwnerName, results.RepoName, err) - ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ - Results: results, - Err: fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err), }) return } @@ -167,26 +161,23 @@ func ServCommand(ctx *context.PrivateContext) { results.RepoID = repo.ID if repo.IsBeingCreated() { - ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ - Results: results, - Err: "Repository is being created, you could retry after it finished", + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: "Repository is being created, you could retry after it finished", }) return } if repo.IsBroken() { - ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ - Results: results, - Err: "Repository is in a broken state", + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: "Repository is in a broken state", }) return } // We can shortcut at this point if the repo is a mirror if mode > perm.AccessModeRead && repo.IsMirror { - ctx.JSON(http.StatusForbidden, private.ErrServCommand{ - Results: results, - Err: fmt.Sprintf("Mirror Repository %s/%s is read-only", results.OwnerName, results.RepoName), + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: fmt.Sprintf("Mirror Repository %s/%s is read-only", results.OwnerName, results.RepoName), }) return } @@ -196,16 +187,14 @@ func ServCommand(ctx *context.PrivateContext) { key, err := asymkey_model.GetPublicKeyByID(keyID) if err != nil { if asymkey_model.IsErrKeyNotExist(err) { - ctx.JSON(http.StatusNotFound, private.ErrServCommand{ - Results: results, - Err: fmt.Sprintf("Cannot find key: %d", keyID), + ctx.JSON(http.StatusNotFound, private.Response{ + UserMsg: fmt.Sprintf("Cannot find key: %d", keyID), }) return } log.Error("Unable to get public key: %d Error: %v", keyID, err) - ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ - Results: results, - Err: fmt.Sprintf("Unable to get key: %d Error: %v", keyID, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get key: %d Error: %v", keyID, err), }) return } @@ -215,9 +204,8 @@ func ServCommand(ctx *context.PrivateContext) { // If repo doesn't exist, deploy key doesn't make sense if !repoExist && key.Type == asymkey_model.KeyTypeDeploy { - ctx.JSON(http.StatusNotFound, private.ErrServCommand{ - Results: results, - Err: fmt.Sprintf("Cannot find repository %s/%s", results.OwnerName, results.RepoName), + ctx.JSON(http.StatusNotFound, private.Response{ + UserMsg: fmt.Sprintf("Cannot find repository %s/%s", results.OwnerName, results.RepoName), }) return } @@ -232,16 +220,14 @@ func ServCommand(ctx *context.PrivateContext) { deployKey, err = asymkey_model.GetDeployKeyByRepo(ctx, key.ID, repo.ID) if err != nil { if asymkey_model.IsErrDeployKeyNotExist(err) { - ctx.JSON(http.StatusNotFound, private.ErrServCommand{ - Results: results, - Err: fmt.Sprintf("Public (Deploy) Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName), + ctx.JSON(http.StatusNotFound, private.Response{ + UserMsg: fmt.Sprintf("Public (Deploy) Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName), }) return } log.Error("Unable to get deploy for public (deploy) key: %d in %-v Error: %v", key.ID, repo, err) - ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ - Results: results, - Err: fmt.Sprintf("Unable to get Deploy Key for Public Key: %d:%s in %s/%s.", key.ID, key.Name, results.OwnerName, results.RepoName), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get Deploy Key for Public Key: %d:%s in %s/%s.", key.ID, key.Name, results.OwnerName, results.RepoName), }) return } @@ -262,23 +248,21 @@ func ServCommand(ctx *context.PrivateContext) { user, err = user_model.GetUserByID(ctx, key.OwnerID) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.JSON(http.StatusUnauthorized, private.ErrServCommand{ - Results: results, - Err: fmt.Sprintf("Public Key: %d:%s owner %d does not exist.", key.ID, key.Name, key.OwnerID), + ctx.JSON(http.StatusUnauthorized, private.Response{ + UserMsg: fmt.Sprintf("Public Key: %d:%s owner %d does not exist.", key.ID, key.Name, key.OwnerID), }) return } log.Error("Unable to get owner: %d for public key: %d:%s Error: %v", key.OwnerID, key.ID, key.Name, err) - ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ - Results: results, - Err: fmt.Sprintf("Unable to get Owner: %d for Deploy Key: %d:%s in %s/%s.", key.OwnerID, key.ID, key.Name, ownerName, repoName), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get Owner: %d for Deploy Key: %d:%s in %s/%s.", key.OwnerID, key.ID, key.Name, ownerName, repoName), }) return } if !user.IsActive || user.ProhibitLogin { ctx.JSON(http.StatusForbidden, private.Response{ - Err: "Your account is disabled.", + UserMsg: "Your account is disabled.", }) return } @@ -291,9 +275,8 @@ func ServCommand(ctx *context.PrivateContext) { // Don't allow pushing if the repo is archived if repoExist && mode > perm.AccessModeRead && repo.IsArchived { - ctx.JSON(http.StatusUnauthorized, private.ErrServCommand{ - Results: results, - Err: fmt.Sprintf("Repo: %s/%s is archived.", results.OwnerName, results.RepoName), + ctx.JSON(http.StatusUnauthorized, private.Response{ + UserMsg: fmt.Sprintf("Repo: %s/%s is archived.", results.OwnerName, results.RepoName), }) return } @@ -307,9 +290,8 @@ func ServCommand(ctx *context.PrivateContext) { setting.Service.RequireSignInView) { if key.Type == asymkey_model.KeyTypeDeploy { if deployKey.Mode < mode { - ctx.JSON(http.StatusUnauthorized, private.ErrServCommand{ - Results: results, - Err: fmt.Sprintf("Deploy Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName), + ctx.JSON(http.StatusUnauthorized, private.Response{ + UserMsg: fmt.Sprintf("Deploy Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName), }) return } @@ -322,9 +304,8 @@ func ServCommand(ctx *context.PrivateContext) { perm, err := access_model.GetUserRepoPermission(ctx, repo, user) if err != nil { log.Error("Unable to get permissions for %-v with key %d in %-v Error: %v", user, key.ID, repo, err) - ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ - Results: results, - Err: fmt.Sprintf("Unable to get permissions for user %d:%s with key %d in %s/%s Error: %v", user.ID, user.Name, key.ID, results.OwnerName, results.RepoName, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get permissions for user %d:%s with key %d in %s/%s Error: %v", user.ID, user.Name, key.ID, results.OwnerName, results.RepoName, err), }) return } @@ -333,9 +314,8 @@ func ServCommand(ctx *context.PrivateContext) { if userMode < mode { log.Warn("Failed authentication attempt for %s with key %s (not authorized to %s %s/%s) from %s", user.Name, key.Name, modeString, ownerName, repoName, ctx.RemoteAddr()) - ctx.JSON(http.StatusUnauthorized, private.ErrServCommand{ - Results: results, - Err: fmt.Sprintf("User: %d:%s with Key: %d:%s is not authorized to %s %s/%s.", user.ID, user.Name, key.ID, key.Name, modeString, ownerName, repoName), + ctx.JSON(http.StatusUnauthorized, private.Response{ + UserMsg: fmt.Sprintf("User: %d:%s with Key: %d:%s is not authorized to %s %s/%s.", user.ID, user.Name, key.ID, key.Name, modeString, ownerName, repoName), }) return } @@ -346,24 +326,21 @@ func ServCommand(ctx *context.PrivateContext) { if !repoExist { owner, err := user_model.GetUserByName(ctx, ownerName) if err != nil { - ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ - Results: results, - Err: fmt.Sprintf("Unable to get owner: %s %v", results.OwnerName, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get owner: %s %v", results.OwnerName, err), }) return } if owner.IsOrganization() && !setting.Repository.EnablePushCreateOrg { - ctx.JSON(http.StatusForbidden, private.ErrServCommand{ - Results: results, - Err: "Push to create is not enabled for organizations.", + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: "Push to create is not enabled for organizations.", }) return } if !owner.IsOrganization() && !setting.Repository.EnablePushCreateUser { - ctx.JSON(http.StatusForbidden, private.ErrServCommand{ - Results: results, - Err: "Push to create is not enabled for users.", + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: "Push to create is not enabled for users.", }) return } @@ -371,9 +348,8 @@ func ServCommand(ctx *context.PrivateContext) { repo, err = repo_service.PushCreateRepo(ctx, user, owner, results.RepoName) if err != nil { log.Error("pushCreateRepo: %v", err) - ctx.JSON(http.StatusNotFound, private.ErrServCommand{ - Results: results, - Err: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), + ctx.JSON(http.StatusNotFound, private.Response{ + UserMsg: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), }) return } @@ -384,16 +360,14 @@ func ServCommand(ctx *context.PrivateContext) { // Ensure the wiki is enabled before we allow access to it if _, err := repo.GetUnit(ctx, unit.TypeWiki); err != nil { if repo_model.IsErrUnitTypeNotExist(err) { - ctx.JSON(http.StatusForbidden, private.ErrServCommand{ - Results: results, - Err: "repository wiki is disabled", + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: "repository wiki is disabled", }) return } log.Error("Failed to get the wiki unit in %-v Error: %v", repo, err) - ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ - Results: results, - Err: fmt.Sprintf("Failed to get the wiki unit in %s/%s Error: %v", ownerName, repoName, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Failed to get the wiki unit in %s/%s Error: %v", ownerName, repoName, err), }) return } @@ -401,9 +375,8 @@ func ServCommand(ctx *context.PrivateContext) { // Finally if we're trying to touch the wiki we should init it if err = wiki_service.InitWiki(ctx, repo); err != nil { log.Error("Failed to initialize the wiki in %-v Error: %v", repo, err) - ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ - Results: results, - Err: fmt.Sprintf("Failed to initialize the wiki in %s/%s Error: %v", ownerName, repoName, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Failed to initialize the wiki in %s/%s Error: %v", ownerName, repoName, err), }) return } diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 3715320f10c84..612222598f2fa 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -435,7 +435,7 @@ func Issues(ctx *context.Context) { } ctx.Data["Title"] = ctx.Tr("repo.issues") ctx.Data["PageIsIssueList"] = true - ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0 + ctx.Data["NewIssueChooseTemplate"] = ctx.HasIssueTemplatesOrContactLinks() } issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), util.OptionalBoolOf(isPullList)) @@ -848,7 +848,7 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles func NewIssue(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.issues.new") ctx.Data["PageIsIssueList"] = true - ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0 + ctx.Data["NewIssueChooseTemplate"] = ctx.HasIssueTemplatesOrContactLinks() ctx.Data["RequireTribute"] = true ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes title := ctx.FormString("title") @@ -946,12 +946,16 @@ func NewIssueChooseTemplate(ctx *context.Context) { ctx.Flash.Warning(renderErrorOfTemplates(ctx, errs), true) } - if len(issueTemplates) == 0 { + if !ctx.HasIssueTemplatesOrContactLinks() { // The "issues/new" and "issues/new/choose" share the same query parameters "project" and "milestone", if no template here, just redirect to the "issues/new" page with these parameters. ctx.Redirect(fmt.Sprintf("%s/issues/new?%s", ctx.Repo.Repository.Link(), ctx.Req.URL.RawQuery), http.StatusSeeOther) return } + issueConfig, err := ctx.IssueConfigFromDefaultBranch() + ctx.Data["IssueConfig"] = issueConfig + ctx.Data["IssueConfigError"] = err // ctx.Flash.Err makes problems here + ctx.Data["milestone"] = ctx.FormInt64("milestone") ctx.Data["project"] = ctx.FormInt64("project") @@ -1086,7 +1090,7 @@ func NewIssuePost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CreateIssueForm) ctx.Data["Title"] = ctx.Tr("repo.issues.new") ctx.Data["PageIsIssueList"] = true - ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0 + ctx.Data["NewIssueChooseTemplate"] = ctx.HasIssueTemplatesOrContactLinks() ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled upload.AddUploadContext(ctx, "comment") @@ -1280,7 +1284,7 @@ func ViewIssue(ctx *context.Context) { return } ctx.Data["PageIsIssueList"] = true - ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0 + ctx.Data["NewIssueChooseTemplate"] = ctx.HasIssueTemplatesOrContactLinks() } if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) { @@ -1812,17 +1816,27 @@ func ViewIssue(ctx *context.Context) { } // Get Dependencies - ctx.Data["BlockedByDependencies"], err = issue.BlockedByDependencies(ctx) + blockedBy, err := issue.BlockedByDependencies(ctx, db.ListOptions{}) if err != nil { ctx.ServerError("BlockedByDependencies", err) return } - ctx.Data["BlockingDependencies"], err = issue.BlockingDependencies(ctx) + ctx.Data["BlockedByDependencies"], ctx.Data["BlockedByDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blockedBy) + if ctx.Written() { + return + } + + blocking, err := issue.BlockingDependencies(ctx) if err != nil { ctx.ServerError("BlockingDependencies", err) return } + ctx.Data["BlockingDependencies"], ctx.Data["BlockingByDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blocking) + if ctx.Written() { + return + } + ctx.Data["Participants"] = participants ctx.Data["NumParticipants"] = len(participants) ctx.Data["Issue"] = issue @@ -1851,6 +1865,48 @@ func ViewIssue(ctx *context.Context) { ctx.HTML(http.StatusOK, tplIssueView) } +func checkBlockedByIssues(ctx *context.Context, blockers []*issues_model.DependencyInfo) (canRead, notPermitted []*issues_model.DependencyInfo) { + var lastRepoID int64 + var lastPerm access_model.Permission + for i, blocker := range blockers { + // Get the permissions for this repository + perm := lastPerm + if lastRepoID != blocker.Repository.ID { + if blocker.Repository.ID == ctx.Repo.Repository.ID { + perm = ctx.Repo.Permission + } else { + var err error + perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer) + if err != nil { + ctx.ServerError("GetUserRepoPermission", err) + return + } + } + lastRepoID = blocker.Repository.ID + } + + // check permission + if !perm.CanReadIssuesOrPulls(blocker.Issue.IsPull) { + blockers[len(notPermitted)], blockers[i] = blocker, blockers[len(notPermitted)] + notPermitted = blockers[:len(notPermitted)+1] + } + } + blockers = blockers[len(notPermitted):] + sortDependencyInfo(blockers) + sortDependencyInfo(notPermitted) + + return blockers, notPermitted +} + +func sortDependencyInfo(blockers []*issues_model.DependencyInfo) { + sort.Slice(blockers, func(i, j int) bool { + if blockers[i].RepoID == blockers[j].RepoID { + return blockers[i].Issue.CreatedUnix < blockers[j].Issue.CreatedUnix + } + return blockers[i].RepoID < blockers[j].RepoID + }) +} + // GetActionIssue will return the issue which is used in the context. func GetActionIssue(ctx *context.Context) *issues_model.Issue { issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) diff --git a/routers/web/repo/issue_dependency.go b/routers/web/repo/issue_dependency.go index 365d9609d65cc..d3af319c711a6 100644 --- a/routers/web/repo/issue_dependency.go +++ b/routers/web/repo/issue_dependency.go @@ -7,6 +7,7 @@ import ( "net/http" issues_model "code.gitea.io/gitea/models/issues" + access_model "code.gitea.io/gitea/models/perm/access" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" ) @@ -44,9 +45,25 @@ func AddDependency(ctx *context.Context) { } // Check if both issues are in the same repo if cross repository dependencies is not enabled - if issue.RepoID != dep.RepoID && !setting.Service.AllowCrossRepositoryDependencies { - ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_not_same_repo")) - return + if issue.RepoID != dep.RepoID { + if !setting.Service.AllowCrossRepositoryDependencies { + ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_not_same_repo")) + return + } + if err := dep.LoadRepo(ctx); err != nil { + ctx.ServerError("loadRepo", err) + return + } + // Can ctx.Doer read issues in the dep repo? + depRepoPerm, err := access_model.GetUserRepoPermission(ctx, dep.Repo, ctx.Doer) + if err != nil { + ctx.ServerError("GetUserRepoPermission", err) + return + } + if !depRepoPerm.CanReadIssuesOrPulls(dep.IsPull) { + // you can't see this dependency + return + } } // Check if issue and dependency is the same diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 4d49ab6359522..ce60d91150281 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -348,6 +348,9 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st if ctx.Repo.TreePath == ".editorconfig" { _, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit) ctx.Data["FileError"] = editorconfigErr + } else if ctx.Repo.IsIssueConfig(ctx.Repo.TreePath) { + _, issueConfigErr := ctx.Repo.GetIssueConfig(ctx.Repo.TreePath, ctx.Repo.Commit) + ctx.Data["FileError"] = issueConfigErr } isDisplayingSource := ctx.FormString("display") == "source" diff --git a/services/convert/issue.go b/services/convert/issue.go index e79fcfcccb7bb..6d31a123bd9fc 100644 --- a/services/convert/issue.go +++ b/services/convert/issue.go @@ -32,21 +32,15 @@ func ToAPIIssue(ctx context.Context, issue *issues_model.Issue) *api.Issue { if err := issue.LoadRepo(ctx); err != nil { return &api.Issue{} } - if err := issue.Repo.LoadOwner(ctx); err != nil { - return &api.Issue{} - } apiIssue := &api.Issue{ ID: issue.ID, - URL: issue.APIURL(), - HTMLURL: issue.HTMLURL(), Index: issue.Index, Poster: ToUser(ctx, issue.Poster, nil), Title: issue.Title, Body: issue.Content, Attachments: ToAttachments(issue.Attachments), Ref: issue.Ref, - Labels: ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner), State: issue.State(), IsLocked: issue.IsLocked, Comments: issue.NumComments, @@ -54,11 +48,19 @@ func ToAPIIssue(ctx context.Context, issue *issues_model.Issue) *api.Issue { Updated: issue.UpdatedUnix.AsTime(), } - apiIssue.Repo = &api.RepositoryMeta{ - ID: issue.Repo.ID, - Name: issue.Repo.Name, - Owner: issue.Repo.OwnerName, - FullName: issue.Repo.FullName(), + if issue.Repo != nil { + if err := issue.Repo.LoadOwner(ctx); err != nil { + return &api.Issue{} + } + apiIssue.URL = issue.APIURL() + apiIssue.HTMLURL = issue.HTMLURL() + apiIssue.Labels = ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner) + apiIssue.Repo = &api.RepositoryMeta{ + ID: issue.Repo.ID, + Name: issue.Repo.Name, + Owner: issue.Repo.OwnerName, + FullName: issue.Repo.FullName(), + } } if issue.ClosedUnix != 0 { @@ -85,11 +87,13 @@ func ToAPIIssue(ctx context.Context, issue *issues_model.Issue) *api.Issue { if err := issue.LoadPullRequest(ctx); err != nil { return &api.Issue{} } - apiIssue.PullRequest = &api.PullRequestMeta{ - HasMerged: issue.PullRequest.HasMerged, - } - if issue.PullRequest.HasMerged { - apiIssue.PullRequest.Merged = issue.PullRequest.MergedUnix.AsTimePtr() + if issue.PullRequest != nil { + apiIssue.PullRequest = &api.PullRequestMeta{ + HasMerged: issue.PullRequest.HasMerged, + } + if issue.PullRequest.HasMerged { + apiIssue.PullRequest.Merged = issue.PullRequest.MergedUnix.AsTimePtr() + } } } if issue.DeadlineUnix != 0 { diff --git a/templates/package/content/generic.tmpl b/templates/package/content/generic.tmpl index eae4d670f9c6f..2674a155d33ce 100644 --- a/templates/package/content/generic.tmpl +++ b/templates/package/content/generic.tmpl @@ -5,9 +5,9 @@

-					{{- range .PackageDescriptor.Files -}}
-curl 
- {{- end -}} +{{- range .PackageDescriptor.Files -}} +curl +{{end -}}
diff --git a/templates/repo/issue/choose.tmpl b/templates/repo/issue/choose.tmpl index 688e98bfc6ed2..b5316454ba1e1 100644 --- a/templates/repo/issue/choose.tmpl +++ b/templates/repo/issue/choose.tmpl @@ -20,17 +20,40 @@
{{end}} -
-
-
- {{.locale.Tr "repo.issues.choose.blank"}} -
{{.locale.Tr "repo.issues.choose.blank_about"}} + {{range .IssueConfig.ContactLinks}} +
+
+
+ {{.Name | RenderEmojiPlain}} +
{{.About | RenderEmojiPlain}} +
+
- + {{end}} + {{if .IssueConfig.BlankIssuesEnabled}} +
+
+
+ {{.locale.Tr "repo.issues.choose.blank"}} +
{{.locale.Tr "repo.issues.choose.blank_about"}} +
+
-
+ {{end}} + {{- if .IssueConfigError}}{{/* normal warning flash makes problems here*/}} +
+
+
{{.locale.Tr "repo.issues.choose.invalid_config"}}
+ {{.IssueConfigError}}
+
+
+ {{end}}
{{template "base/footer" .}} diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index 573bc24c8248a..21e88ecd3b61f 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -420,7 +420,7 @@
- {{if (and (not .BlockedByDependencies) (not .BlockingDependencies))}} + {{if (and (not .BlockedByDependencies) (not .BlockedByDependenciesNotPermitted) (not .BlockingDependencies) (not .BlockingDependenciesNotPermitted))}} {{.locale.Tr "repo.issues.dependency.title"}}

@@ -432,7 +432,7 @@

{{end}} - {{if .BlockingDependencies}} + {{if or .BlockingDependencies .BlockingDependenciesNotPermitted}} {{.locale.Tr "repo.issues.dependency.blocks_short"}} @@ -456,10 +456,15 @@
{{end}} + {{if .BlockingDependenciesNotPermitted}} +
+ {{$.locale.TrN (len .BlockingDependenciesNotPermitted) "repo.issues.dependency.no_permission_1" "repo.issues.dependency.no_permission_n" (len .BlockingDependenciesNotPermitted)}} +
+ {{end}} {{end}} - {{if .BlockedByDependencies}} + {{if or .BlockedByDependencies .BlockedByDependenciesNotPermitted}} {{.locale.Tr "repo.issues.dependency.blocked_by_short"}} @@ -483,6 +488,34 @@ {{end}} + {{if $.CanCreateIssueDependencies}} + {{range .BlockedByDependenciesNotPermitted}} +
+
+
+ {{svg "octicon-lock" 16}} + + #{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}} + +
+
+ {{.Repository.OwnerName}}/{{.Repository.Name}} +
+
+
+ {{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}} + + {{svg "octicon-trash" 16}} + + {{end}} +
+
+ {{end}} + {{else if .BlockedByDependenciesNotPermitted}} +
+ {{$.locale.TrN (len .BlockedByDependenciesNotPermitted) "repo.issues.dependency.no_permission_1" "repo.issues.dependency.no_permission_n" (len .BlockedByDependenciesNotPermitted)}} +
+ {{end}} {{end}} diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl index 5e5716fa573c5..3dc3deb3954c7 100644 --- a/templates/repo/release/list.tmpl +++ b/templates/repo/release/list.tmpl @@ -39,6 +39,9 @@
{{if $.Permission.CanRead $.UnitTypeCode}} + {{if .CreatedUnix}} + {{svg "octicon-clock" 16 "gt-mr-2"}}{{TimeSinceUnix .CreatedUnix $.locale}} + {{end}} {{svg "octicon-git-commit" 16 "gt-mr-2"}}{{ShortSha .Sha1}} {{if not $.DisableDownloadSourceArchives}} {{svg "octicon-file-zip" 16 "gt-mr-2"}}ZIP diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 0f7e60c598bf8..fe3f31b13d5d6 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -5013,6 +5013,72 @@ } } }, + "/repos/{owner}/{repo}/issue_config": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Returns the issue config for a repo", + "operationId": "repoGetIssueConfig", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/RepoIssueConfig" + } + } + } + }, + "/repos/{owner}/{repo}/issue_config/validate": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Returns the validation information for a issue config", + "operationId": "repoValidateIssueConfig", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/RepoIssueConfigValidation" + } + } + } + }, "/repos/{owner}/{repo}/issue_templates": { "get": { "produces": [ @@ -6256,6 +6322,151 @@ } } }, + "/repos/{owner}/{repo}/issues/{index}/blocks": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "issue" + ], + "summary": "List issues that are blocked by this issue", + "operationId": "issueListBlocks", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "index of the issue", + "name": "index", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/IssueList" + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "issue" + ], + "summary": "Block the issue given in the body by the issue in path", + "operationId": "issueCreateIssueBlocking", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "index of the issue", + "name": "index", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/IssueMeta" + } + } + ], + "responses": { + "201": { + "$ref": "#/responses/Issue" + }, + "404": { + "description": "the issue does not exist" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "issue" + ], + "summary": "Unblock the issue given in the body by the issue in path", + "operationId": "issueRemoveIssueBlocking", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "index of the issue", + "name": "index", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/IssueMeta" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/Issue" + } + } + } + }, "/repos/{owner}/{repo}/issues/{index}/comments": { "get": { "produces": [ @@ -6538,6 +6749,151 @@ } } }, + "/repos/{owner}/{repo}/issues/{index}/dependencies": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "issue" + ], + "summary": "List an issue's dependencies, i.e all issues that block this issue.", + "operationId": "issueListIssueDependencies", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "index of the issue", + "name": "index", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/IssueList" + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "issue" + ], + "summary": "Make the issue in the url depend on the issue in the form.", + "operationId": "issueCreateIssueDependencies", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "index of the issue", + "name": "index", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/IssueMeta" + } + } + ], + "responses": { + "201": { + "$ref": "#/responses/Issue" + }, + "404": { + "description": "the issue does not exist" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "issue" + ], + "summary": "Remove an issue dependency", + "operationId": "issueRemoveIssueDependencies", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "index of the issue", + "name": "index", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/IssueMeta" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/Issue" + } + } + } + }, "/repos/{owner}/{repo}/issues/{index}/labels": { "get": { "produces": [ @@ -17875,6 +18231,55 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "IssueConfig": { + "type": "object", + "properties": { + "blank_issues_enabled": { + "type": "boolean", + "x-go-name": "BlankIssuesEnabled" + }, + "contact_links": { + "type": "array", + "items": { + "$ref": "#/definitions/IssueConfigContactLink" + }, + "x-go-name": "ContactLinks" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, + "IssueConfigContactLink": { + "type": "object", + "properties": { + "about": { + "type": "string", + "x-go-name": "About" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "url": { + "type": "string", + "x-go-name": "URL" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, + "IssueConfigValidation": { + "type": "object", + "properties": { + "message": { + "type": "string", + "x-go-name": "Message" + }, + "valid": { + "type": "boolean", + "x-go-name": "Valid" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "IssueDeadline": { "description": "IssueDeadline represents an issue deadline", "type": "object", @@ -17932,6 +18337,26 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "IssueMeta": { + "description": "IssueMeta basic issue information", + "type": "object", + "properties": { + "index": { + "type": "integer", + "format": "int64", + "x-go-name": "Index" + }, + "owner": { + "type": "string", + "x-go-name": "Owner" + }, + "repo": { + "type": "string", + "x-go-name": "Name" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "IssueTemplate": { "description": "IssueTemplate represents an issue template for a repository", "type": "object", @@ -21134,6 +21559,18 @@ "$ref": "#/definitions/RepoCollaboratorPermission" } }, + "RepoIssueConfig": { + "description": "RepoIssueConfig", + "schema": { + "$ref": "#/definitions/IssueConfig" + } + }, + "RepoIssueConfigValidation": { + "description": "RepoIssueConfigValidation", + "schema": { + "$ref": "#/definitions/IssueConfigValidation" + } + }, "Repository": { "description": "Repository", "schema": { diff --git a/tests/integration/api_issue_config_test.go b/tests/integration/api_issue_config_test.go new file mode 100644 index 0000000000000..b9b3765c4e565 --- /dev/null +++ b/tests/integration/api_issue_config_test.go @@ -0,0 +1,52 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "testing" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestAPIReposGetDefaultIssueConfig(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issue_config", owner.Name, repo.Name) + req := NewRequest(t, "GET", urlStr) + resp := MakeRequest(t, req, http.StatusOK) + + var issueConfig api.IssueConfig + DecodeJSON(t, resp, &issueConfig) + + assert.True(t, issueConfig.BlankIssuesEnabled) + assert.Equal(t, issueConfig.ContactLinks, make([]api.IssueConfigContactLink, 0)) +} + +func TestAPIReposValidateDefaultIssueConfig(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issue_config/validate", owner.Name, repo.Name) + req := NewRequest(t, "GET", urlStr) + resp := MakeRequest(t, req, http.StatusOK) + + var issueConfigValidation api.IssueConfigValidation + DecodeJSON(t, resp, &issueConfigValidation) + + assert.True(t, issueConfigValidation.Valid) + assert.Equal(t, issueConfigValidation.Message, "") +} diff --git a/tests/integration/api_private_serv_test.go b/tests/integration/api_private_serv_test.go index d26935f446270..8beec62382c97 100644 --- a/tests/integration/api_private_serv_test.go +++ b/tests/integration/api_private_serv_test.go @@ -43,8 +43,8 @@ func TestAPIPrivateServ(t *testing.T) { defer cancel() // Can push to a repo we own - results, err := private.ServCommand(ctx, 1, "user2", "repo1", perm.AccessModeWrite, "git-upload-pack", "") - assert.NoError(t, err) + results, extra := private.ServCommand(ctx, 1, "user2", "repo1", perm.AccessModeWrite, "git-upload-pack", "") + assert.NoError(t, extra.Error) assert.False(t, results.IsWiki) assert.Zero(t, results.DeployKeyID) assert.Equal(t, int64(1), results.KeyID) @@ -56,18 +56,18 @@ func TestAPIPrivateServ(t *testing.T) { assert.Equal(t, int64(1), results.RepoID) // Cannot push to a private repo we're not associated with - results, err = private.ServCommand(ctx, 1, "user15", "big_test_private_1", perm.AccessModeWrite, "git-upload-pack", "") - assert.Error(t, err) + results, extra = private.ServCommand(ctx, 1, "user15", "big_test_private_1", perm.AccessModeWrite, "git-upload-pack", "") + assert.Error(t, extra.Error) assert.Empty(t, results) // Cannot pull from a private repo we're not associated with - results, err = private.ServCommand(ctx, 1, "user15", "big_test_private_1", perm.AccessModeRead, "git-upload-pack", "") - assert.Error(t, err) + results, extra = private.ServCommand(ctx, 1, "user15", "big_test_private_1", perm.AccessModeRead, "git-upload-pack", "") + assert.Error(t, extra.Error) assert.Empty(t, results) // Can pull from a public repo we're not associated with - results, err = private.ServCommand(ctx, 1, "user15", "big_test_public_1", perm.AccessModeRead, "git-upload-pack", "") - assert.NoError(t, err) + results, extra = private.ServCommand(ctx, 1, "user15", "big_test_public_1", perm.AccessModeRead, "git-upload-pack", "") + assert.NoError(t, extra.Error) assert.False(t, results.IsWiki) assert.Zero(t, results.DeployKeyID) assert.Equal(t, int64(1), results.KeyID) @@ -79,8 +79,8 @@ func TestAPIPrivateServ(t *testing.T) { assert.Equal(t, int64(17), results.RepoID) // Cannot push to a public repo we're not associated with - results, err = private.ServCommand(ctx, 1, "user15", "big_test_public_1", perm.AccessModeWrite, "git-upload-pack", "") - assert.Error(t, err) + results, extra = private.ServCommand(ctx, 1, "user15", "big_test_public_1", perm.AccessModeWrite, "git-upload-pack", "") + assert.Error(t, extra.Error) assert.Empty(t, results) // Add reading deploy key @@ -88,8 +88,8 @@ func TestAPIPrivateServ(t *testing.T) { assert.NoError(t, err) // Can pull from repo we're a deploy key for - results, err = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_1", perm.AccessModeRead, "git-upload-pack", "") - assert.NoError(t, err) + results, extra = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_1", perm.AccessModeRead, "git-upload-pack", "") + assert.NoError(t, extra.Error) assert.False(t, results.IsWiki) assert.NotZero(t, results.DeployKeyID) assert.Equal(t, deployKey.KeyID, results.KeyID) @@ -101,18 +101,18 @@ func TestAPIPrivateServ(t *testing.T) { assert.Equal(t, int64(19), results.RepoID) // Cannot push to a private repo with reading key - results, err = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_1", perm.AccessModeWrite, "git-upload-pack", "") - assert.Error(t, err) + results, extra = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_1", perm.AccessModeWrite, "git-upload-pack", "") + assert.Error(t, extra.Error) assert.Empty(t, results) // Cannot pull from a private repo we're not associated with - results, err = private.ServCommand(ctx, deployKey.ID, "user15", "big_test_private_2", perm.AccessModeRead, "git-upload-pack", "") - assert.Error(t, err) + results, extra = private.ServCommand(ctx, deployKey.ID, "user15", "big_test_private_2", perm.AccessModeRead, "git-upload-pack", "") + assert.Error(t, extra.Error) assert.Empty(t, results) // Cannot pull from a public repo we're not associated with - results, err = private.ServCommand(ctx, deployKey.ID, "user15", "big_test_public_1", perm.AccessModeRead, "git-upload-pack", "") - assert.Error(t, err) + results, extra = private.ServCommand(ctx, deployKey.ID, "user15", "big_test_public_1", perm.AccessModeRead, "git-upload-pack", "") + assert.Error(t, extra.Error) assert.Empty(t, results) // Add writing deploy key @@ -120,13 +120,13 @@ func TestAPIPrivateServ(t *testing.T) { assert.NoError(t, err) // Cannot push to a private repo with reading key - results, err = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_1", perm.AccessModeWrite, "git-upload-pack", "") - assert.Error(t, err) + results, extra = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_1", perm.AccessModeWrite, "git-upload-pack", "") + assert.Error(t, extra.Error) assert.Empty(t, results) // Can pull from repo we're a writing deploy key for - results, err = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_2", perm.AccessModeRead, "git-upload-pack", "") - assert.NoError(t, err) + results, extra = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_2", perm.AccessModeRead, "git-upload-pack", "") + assert.NoError(t, extra.Error) assert.False(t, results.IsWiki) assert.NotZero(t, results.DeployKeyID) assert.Equal(t, deployKey.KeyID, results.KeyID) @@ -138,8 +138,8 @@ func TestAPIPrivateServ(t *testing.T) { assert.Equal(t, int64(20), results.RepoID) // Can push to repo we're a writing deploy key for - results, err = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_2", perm.AccessModeWrite, "git-upload-pack", "") - assert.NoError(t, err) + results, extra = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_2", perm.AccessModeWrite, "git-upload-pack", "") + assert.NoError(t, extra.Error) assert.False(t, results.IsWiki) assert.NotZero(t, results.DeployKeyID) assert.Equal(t, deployKey.KeyID, results.KeyID) diff --git a/tests/pgsql.ini.tmpl b/tests/pgsql.ini.tmpl index fbfbae7c68130..15349aa4c217c 100644 --- a/tests/pgsql.ini.tmpl +++ b/tests/pgsql.ini.tmpl @@ -125,6 +125,7 @@ MINIO_SECRET_ACCESS_KEY = 12345678 MINIO_BUCKET = gitea MINIO_LOCATION = us-east-1 MINIO_USE_SSL = false +MINIO_CHECKSUM_ALGORITHM = md5 [packages] ENABLED = true diff --git a/web_src/css/base.css b/web_src/css/base.css index d9d93da0c3f85..3e25a47a01254 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -2399,9 +2399,107 @@ a.ui.label:hover { .ui.basic.labels .primary.label, .ui.ui.ui.basic.primary.label { - background: transparent !important; - border-color: var(--color-primary) !important; - color: var(--color-primary) !important; + background: transparent; + border-color: var(--color-primary); + color: var(--color-primary); +} + +.ui.basic.labels .secondary.label, +.ui.ui.ui.basic.secondary.label { + background: transparent; + border-color: var(--color-secondary); + color: var(--color-secondary); +} + +.ui.basic.labels .orange.label, +.ui.ui.ui.basic.orange.label { + background: transparent; + border-color: var(--color-orange); + color: var(--color-orange); +} + +.ui.basic.labels .green.label, +.ui.ui.ui.basic.green.label { + background: transparent; + border-color: var(--color-green); + color: var(--color-green); +} + +.ui.basic.labels .olive.label, +.ui.ui.ui.basic.olive.label { + background: transparent; + border-color: var(--color-olive); + color: var(--color-olive); +} + +.ui.basic.labels .teal.label, +.ui.ui.ui.basic.teal.label { + background: transparent; + border-color: var(--color-teal); + color: var(--color-teal); +} + +.ui.basic.labels .blue.label, +.ui.ui.ui.basic.blue.label { + background: transparent; + border-color: var(--color-blue); + color: var(--color-blue); +} + +.ui.basic.labels .violet.label, +.ui.ui.ui.basic.violet.label { + background: transparent; + border-color: var(--color-violet); + color: var(--color-violet); +} + +.ui.basic.labels .purple.label, +.ui.ui.ui.basic.purple.label { + background: transparent; + border-color: var(--color-purple); + color: var(--color-purple); +} + +.ui.basic.labels .pink.label, +.ui.ui.ui.basic.pink.label { + background: transparent; + border-color: var(--color-pink); + color: var(--color-pink); +} + +.ui.basic.labels .red.label, +.ui.ui.ui.basic.red.label { + background: transparent; + border-color: var(--color-red); + color: var(--color-red); +} + +.ui.basic.labels .brown.label, +.ui.ui.ui.basic.brown.label { + background: transparent; + border-color: var(--color-brown); + color: var(--color-brown); +} + +.ui.basic.labels .yellow.label, +.ui.ui.ui.basic.yellow.label { + background: transparent; + border-color: var(--color-yellow); + color: var(--color-yellow); +} + +.ui.basic.labels .grey.label, +.ui.ui.ui.basic.grey.label { + background: transparent; + border-color: var(--color-grey); + color: var(--color-grey); +} + +.ui.basic.labels .black.label, +.ui.ui.ui.basic.black.label { + background: transparent; + border-color: var(--color-black); + color: var(--color-black); } .ui.basic.labels .label,