mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-13 14:30:25 +00:00
Compare commits
212 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e51a55b63 | ||
|
|
67790f5736 | ||
|
|
2f452741b7 | ||
|
|
61db35dd89 | ||
|
|
9b76ac6cec | ||
|
|
6aad12aa86 | ||
|
|
ff90aded6b | ||
|
|
9cfbae08dc | ||
|
|
7b4ea980b2 | ||
|
|
b83d564113 | ||
|
|
ec098a93dd | ||
|
|
446cdcce91 | ||
|
|
9097718160 | ||
|
|
9b27191393 | ||
|
|
099cbe5569 | ||
|
|
b451d2869d | ||
|
|
91e3c36c69 | ||
|
|
865b590bb5 | ||
|
|
db4b2f9802 | ||
|
|
feabaab4bd | ||
|
|
f154efcd58 | ||
|
|
63b891453b | ||
|
|
622267e547 | ||
|
|
9fca6dd456 | ||
|
|
1c190a8701 | ||
|
|
1dca7d4142 | ||
|
|
663aa50eec | ||
|
|
8c8947f4a5 | ||
|
|
a58ab0e680 | ||
|
|
20dc6c5a67 | ||
|
|
f488689c47 | ||
|
|
acb7311cfd | ||
|
|
ba5ffc0e88 | ||
|
|
5ab5ec29b2 | ||
|
|
37973790dc | ||
|
|
b50073eb0d | ||
|
|
e43c55b5b1 | ||
|
|
d5c4db0428 | ||
|
|
afee520971 | ||
|
|
dddf1f01cc | ||
|
|
6a91063191 | ||
|
|
3a39cf85aa | ||
|
|
54a561d72c | ||
|
|
42827e182b | ||
|
|
f8992cc825 | ||
|
|
e60149d2d6 | ||
|
|
dea74f0f09 | ||
|
|
e14257e958 | ||
|
|
7657bbfcb2 | ||
|
|
90d42a6337 | ||
|
|
5972b319b3 | ||
|
|
84f236c43d | ||
|
|
08b510f47b | ||
|
|
28dbbf44cf | ||
|
|
7e73f047ba | ||
|
|
52406dc6ea | ||
|
|
7b274ac6d2 | ||
|
|
97844fa8a8 | ||
|
|
69a85a673f | ||
|
|
09c95df7a4 | ||
|
|
bc402472e8 | ||
|
|
dd2f8a1352 | ||
|
|
7f374db798 | ||
|
|
d99b6d7864 | ||
|
|
babc109b26 | ||
|
|
582970afe0 | ||
|
|
c72df2f09f | ||
|
|
03faf5f9d1 | ||
|
|
afb5825b73 | ||
|
|
1bcc7d77bb | ||
|
|
d6969e4332 | ||
|
|
a50eb47e0b | ||
|
|
171b05c946 | ||
|
|
1985eb17fa | ||
|
|
63ec90b0ef | ||
|
|
a5951616e8 | ||
|
|
8e155cf0e2 | ||
|
|
e3b332fb0e | ||
|
|
bd89c7f92b | ||
|
|
328a0b4b21 | ||
|
|
1808b48a48 | ||
|
|
9074bf666c | ||
|
|
9bbfb89d5c | ||
|
|
e5b73ec69d | ||
|
|
b3fc543af4 | ||
|
|
86a6b30504 | ||
|
|
b8a8f672ef | ||
|
|
0d9a8e3fa2 | ||
|
|
dafda51fda | ||
|
|
b17a1bc8b3 | ||
|
|
237b883128 | ||
|
|
a1b1a63549 | ||
|
|
5c5ed7b0b7 | ||
|
|
aab1813acb | ||
|
|
305e059573 | ||
|
|
4dce932690 | ||
|
|
59b7f80f00 | ||
|
|
be97e31f8b | ||
|
|
156abe7db8 | ||
|
|
f73db56f00 | ||
|
|
5374af95dc | ||
|
|
e1e3f6eefa | ||
|
|
73bd2e922a | ||
|
|
8bbfcfc213 | ||
|
|
5d8d019cc5 | ||
|
|
190853568f | ||
|
|
e777854be0 | ||
|
|
79e5e20bea | ||
|
|
a040ef4b0d | ||
|
|
3de4b351a2 | ||
|
|
d21ef060c2 | ||
|
|
af3091d35b | ||
|
|
d45e6fb777 | ||
|
|
a5e342d827 | ||
|
|
9639b006c3 | ||
|
|
e7b4962823 | ||
|
|
2578672d09 | ||
|
|
474e80c2f1 | ||
|
|
b4b2b367d8 | ||
|
|
b8eb046a3e | ||
|
|
3dc674489d | ||
|
|
c39dd1f1fd | ||
|
|
31323ed721 | ||
|
|
e86730289b | ||
|
|
3564f5a876 | ||
|
|
b0034a4a02 | ||
|
|
b81aa4d080 | ||
|
|
cfea452aa9 | ||
|
|
094eb89b5e | ||
|
|
86b6553f3a | ||
|
|
0dc2bed2dd | ||
|
|
c0d11a365f | ||
|
|
f71e5ac323 | ||
|
|
c7841d13bc | ||
|
|
1a8509e027 | ||
|
|
9e431fea79 | ||
|
|
066a1da33c | ||
|
|
87c29c00f6 | ||
|
|
15c66fab30 | ||
|
|
c2a810bf5c | ||
|
|
5a98225cb7 | ||
|
|
3e70494f65 | ||
|
|
08b361ca29 | ||
|
|
956e1e8aac | ||
|
|
52abe2680d | ||
|
|
a4f6c07306 | ||
|
|
ca07340093 | ||
|
|
178f31ac28 | ||
|
|
c72fd88d35 | ||
|
|
16484c72ec | ||
|
|
006d9c060e | ||
|
|
f067db8f8e | ||
|
|
d3c6ab538e | ||
|
|
eed84d72b6 | ||
|
|
ff4f2bcf07 | ||
|
|
dc0d4fb3ad | ||
|
|
673eccf51f | ||
|
|
ed87ecd17f | ||
|
|
e9f0e96a27 | ||
|
|
55df95b1be | ||
|
|
aa8fa7f7e9 | ||
|
|
a821eb9e0f | ||
|
|
bdef19f62b | ||
|
|
f13147a019 | ||
|
|
1ca805933f | ||
|
|
da267ab00e | ||
|
|
fe55ddcdaf | ||
|
|
738ec94b8f | ||
|
|
a4cb898335 | ||
|
|
4a30f59e6e | ||
|
|
723fa1c966 | ||
|
|
661028623c | ||
|
|
5a54ce0fbc | ||
|
|
5bb61cf6c2 | ||
|
|
5d7953def4 | ||
|
|
5816106de5 | ||
|
|
c5601e9399 | ||
|
|
1d15e243e4 | ||
|
|
97220d1ce9 | ||
|
|
44a5cd3b7a | ||
|
|
6a9fb3dbbc | ||
|
|
fe07c90636 | ||
|
|
4215476cee | ||
|
|
e837350319 | ||
|
|
bc6c0b610b | ||
|
|
b067d0df6e | ||
|
|
c5bfe77873 | ||
|
|
b2241c3939 | ||
|
|
a4396782b5 | ||
|
|
55cff9cfb4 | ||
|
|
5ffe4e54e1 | ||
|
|
9147665e2c | ||
|
|
5395eea338 | ||
|
|
973bc33a5f | ||
|
|
722ea4179c | ||
|
|
9ebdc09939 | ||
|
|
308810cdd1 | ||
|
|
ecdb814dd8 | ||
|
|
64d8854222 | ||
|
|
a4e8594643 | ||
|
|
43c35fb3d3 | ||
|
|
50104993d2 | ||
|
|
fdf641ddce | ||
|
|
6bcdfd6efb | ||
|
|
7bc4f4976e | ||
|
|
52da8aab7e | ||
|
|
49625aac60 | ||
|
|
fa8d75b3e5 | ||
|
|
1d1e0ced3e | ||
|
|
e153e21177 | ||
|
|
e286457990 | ||
|
|
0a6a6d351d |
4014 changed files with 66878 additions and 212473 deletions
|
|
@ -11,7 +11,7 @@ include_file = ["main.go"]
|
|||
include_dir = ["cmd", "models", "modules", "options", "routers", "services"]
|
||||
exclude_dir = [
|
||||
"models/fixtures",
|
||||
"models/gitea_migrations/fixtures",
|
||||
"models/migrations/fixtures",
|
||||
"modules/avatar/identicon/testdata",
|
||||
"modules/avatar/testdata",
|
||||
"modules/git/tests",
|
||||
|
|
|
|||
|
|
@ -18,25 +18,22 @@ forgejo.org/models/auth
|
|||
|
||||
forgejo.org/models/db
|
||||
TruncateBeans
|
||||
TruncateBeansCascade
|
||||
InTransaction
|
||||
DumpTables
|
||||
GetTableNames
|
||||
extendBeansForCascade
|
||||
IsErrNameActivityPubInvalid
|
||||
|
||||
forgejo.org/models/dbfs
|
||||
file.renameTo
|
||||
Create
|
||||
Rename
|
||||
|
||||
forgejo.org/models/forgefed
|
||||
GetFederationHost
|
||||
|
||||
forgejo.org/models/forgejo/semver
|
||||
GetVersion
|
||||
SetVersionString
|
||||
SetVersion
|
||||
|
||||
forgejo.org/models/forgejo_migrations
|
||||
resetMigrations
|
||||
|
||||
forgejo.org/models/git
|
||||
RemoveDeletedBranchByID
|
||||
|
||||
|
|
@ -50,8 +47,10 @@ forgejo.org/models/organization
|
|||
forgejo.org/models/perm/access
|
||||
GetRepoWriters
|
||||
|
||||
forgejo.org/models/repo
|
||||
WatchRepoMode
|
||||
|
||||
forgejo.org/models/user
|
||||
IsErrUserWrongType
|
||||
IsErrExternalLoginUserAlreadyExist
|
||||
IsErrExternalLoginUserNotExist
|
||||
NewFederatedUser
|
||||
|
|
@ -77,6 +76,7 @@ forgejo.org/modules/base
|
|||
SetupGiteaRoot
|
||||
|
||||
forgejo.org/modules/cache
|
||||
GetInt
|
||||
WithNoCacheContext
|
||||
RemoveContextData
|
||||
|
||||
|
|
@ -90,7 +90,6 @@ forgejo.org/modules/forgefed
|
|||
NewForgeUndoLike
|
||||
ForgeUndoLike.UnmarshalJSON
|
||||
ForgeUndoLike.Validate
|
||||
NewPersonIDFromModel
|
||||
GetItemByType
|
||||
JSONUnmarshalerFn
|
||||
NotEmpty
|
||||
|
|
@ -103,7 +102,6 @@ forgejo.org/modules/git
|
|||
AddChangesWithArgs
|
||||
CommitChanges
|
||||
CommitChangesWithArgs
|
||||
IsErrMoreThanOne
|
||||
SetUpdateHook
|
||||
openRepositoryWithDefaultContext
|
||||
ToEntryMode
|
||||
|
|
@ -132,9 +130,6 @@ forgejo.org/modules/json
|
|||
StdJSON.Indent
|
||||
|
||||
forgejo.org/modules/log
|
||||
eventWriterBuffer.Close
|
||||
eventWriterBuffer.Write
|
||||
eventWriterBuffer.GetString
|
||||
NewEventWriterBuffer
|
||||
|
||||
forgejo.org/modules/markup
|
||||
|
|
@ -193,14 +188,10 @@ forgejo.org/modules/translation
|
|||
MockLocale.Tr
|
||||
MockLocale.TrN
|
||||
MockLocale.TrPluralString
|
||||
MockLocale.TrPluralStringAllForms
|
||||
MockLocale.TrSize
|
||||
MockLocale.HasKey
|
||||
MockLocale.PrettyNumber
|
||||
|
||||
forgejo.org/modules/translation/localeiter
|
||||
IterateMessagesContent
|
||||
|
||||
forgejo.org/modules/util
|
||||
OptionalArg
|
||||
|
||||
|
|
@ -219,28 +210,24 @@ forgejo.org/modules/zstd
|
|||
Writer.Write
|
||||
Writer.Close
|
||||
|
||||
forgejo.org/routers/web
|
||||
NotFound
|
||||
|
||||
forgejo.org/routers/web/org
|
||||
MustEnableProjects
|
||||
|
||||
forgejo.org/services/context
|
||||
GetPrivateContext
|
||||
|
||||
forgejo.org/services/notify
|
||||
UnregisterNotifier
|
||||
|
||||
forgejo.org/services/repository
|
||||
IsErrForkAlreadyExist
|
||||
|
||||
forgejo.org/services/repository/files
|
||||
ContentType.String
|
||||
RepoFileOptionMode
|
||||
|
||||
forgejo.org/services/repository/gitgraph
|
||||
Parser.Reset
|
||||
|
||||
forgejo.org/services/stats
|
||||
Flush
|
||||
|
||||
forgejo.org/services/webhook
|
||||
NewNotifier
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
{
|
||||
"name": "forgejo-dev",
|
||||
"image": "mcr.microsoft.com/devcontainers/go:1.26-trixie",
|
||||
"name": "Gitea DevContainer",
|
||||
"image": "mcr.microsoft.com/devcontainers/go:1.24-bullseye",
|
||||
"features": {
|
||||
// installs nodejs into container
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "24"
|
||||
"version": "20"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/git-lfs:1.2.3": {},
|
||||
"ghcr.io/devcontainers-contrib/features/poetry:2": {},
|
||||
"ghcr.io/devcontainers/features/python:1": {
|
||||
"version": "3.12"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/git-lfs:1.2.5": {},
|
||||
"ghcr.io/warrenbuckley/codespace-features/sqlite:1": {}
|
||||
},
|
||||
"customizations": {
|
||||
|
|
|
|||
|
|
@ -37,9 +37,13 @@ coverage.all
|
|||
coverage/
|
||||
cpu.out
|
||||
|
||||
/modules/migration/bindata.go
|
||||
/modules/migration/bindata.go.hash
|
||||
/modules/options/bindata.go
|
||||
/modules/options/bindata.go.hash
|
||||
/modules/public/bindata.go
|
||||
/modules/public/bindata.go.hash
|
||||
/modules/templates/bindata.go
|
||||
/modules/templates/bindata.go.hash
|
||||
|
||||
*.db
|
||||
|
|
|
|||
|
|
@ -12,9 +12,6 @@ insert_final_newline = true
|
|||
[{*.{go,tmpl,html},Makefile,go.mod}]
|
||||
indent_style = tab
|
||||
|
||||
[go.*]
|
||||
indent_style = tab
|
||||
|
||||
[templates/custom/*.tmpl]
|
||||
insert_final_newline = false
|
||||
|
||||
|
|
@ -30,6 +27,7 @@ insert_final_newline = false
|
|||
[options/locale/locale_*.ini]
|
||||
insert_final_newline = false
|
||||
|
||||
# Weblate is configured to use one tab for indention
|
||||
# Weblate JSON output defaults to four spaces
|
||||
[options/locale_next/locale_*.json]
|
||||
indent_style = tab
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
|
|
|||
|
|
@ -1,107 +0,0 @@
|
|||
name: "Step 1: Report a problem or need"
|
||||
description: Please start here and describe your situation.
|
||||
title: "problem: "
|
||||
labels: ["problem", "impact/unknown"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**NOTE: If your issue is a security concern, please email <security@forgejo.org> ([security.txt](https://forgejo.org/.well-known/security.txt)) instead of opening a public issue.**
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
- Please speak English, as this is the language all maintainers can speak and write.
|
||||
- Be civil, and follow the [Forgejo Code of Conduct](https://codeberg.org/forgejo/code-of-conduct).
|
||||
- Take a moment to [check if a similar problem has already been discussed in the past.](https://codeberg.org/forgejo/forgejo/issues?q=&type=all&labels=78137). Feel free to add your own experience there, if applicable.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### New workflow
|
||||
|
||||
We are currently experimenting with a new workflow to manage issues and better understand your problems and needs. This is step 1 of the workflow: Please try to focus on explaining the problem you are facing, which could be a bug in the code, a moment of confusion, or a need that you have.
|
||||
|
||||
We do not expect anything from Forgejo users after creating a problem report, but we appreciate if you stay responsive to further questions. If you want, you can also participate in a discussion for solutions.
|
||||
|
||||
Forgejo contributors will review your report, try to understand how important it is to you and other Forgejo users, and suggest potential solutions. In a next step, solutions can be documented and implemented.
|
||||
|
||||
If you want to learn more about the background of our workflow, feel invited to read and participate in [the discussion that led to the current approach](https://codeberg.org/forgejo/discussions/issues/415). We are looking forward to your feedback.
|
||||
- type: dropdown
|
||||
id: can-reproduce
|
||||
attributes:
|
||||
label: Does your problem still exist on the latest Forgejo version?
|
||||
description: |
|
||||
Please try reproducing your problem at https://dev.next.forgejo.org or a local development version of Forgejo.
|
||||
If you check that your problem is not already addressed by a recent change, this will save all volunteers involved a lot of time.
|
||||
options:
|
||||
- "Yes, the problem still exists (tested on a next instance)"
|
||||
- "Yes, the problem still exists (tested locally with the latest development version)"
|
||||
- "Unknown, I can't try it for some reason (please explain)"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: About your usage of Forgejo
|
||||
description: |
|
||||
Please provide a brief description of your usage of Forgejo. There is no clear guideline on how much you need to share here. You can be brief, but we value the insights you provide us to better understand your use case. Thank you very much!
|
||||
<details><summary>Further instructions</summary>
|
||||
|
||||
* When reporting problems with certain functionality, you should include related information. Examples:
|
||||
* When reporting an issue with setting up an identity provider, it is useful to know if you use Forgejo in a 10-users non-profit / start up, or if you are talking about a school / university with several thousands of users.
|
||||
* When describing confusion, it will be relevant to know your skill level and background ("New, but used a similar product", "In my role as a project manager ..."), so we know for which target audience we need to design the functionality.
|
||||
* When reporting workflow issues or needs, it will be useful to know how large your project is, how many team members you have, which skill level we are talking about etc. For example, we might choose a different design depending on whether a feature is only for professional developers or for hobby coders.
|
||||
* If you want, we always appreciate generic information about your Forgejo usage to help us understand your usage and make better decisions. For example:
|
||||
* Your personal relation to Forgejo and user role ("I'm new to Forgejo, but used a comparable product called …", "In my role as a designer, …").
|
||||
* If you already explained your usage of Forgejo elsewhere (e.g. in another issue or in a user research repository), feel free to just drop a link.
|
||||
|
||||
</details>
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Problem description
|
||||
description: |
|
||||
Please describe your problem as a first-hand experience. Try to focus on the problem. You do not have to find a solution, you can leave this to us.
|
||||
<details><summary>Further instructions</summary>
|
||||
|
||||
* Start by explaining what you want to achieve ("I wanted to find an issue to work on").
|
||||
* Try to include steps that you took and that resulted in the current situation. ("I opened the issue tracker, clicked on …, then …").
|
||||
* If there were moments of confusion, please describe them. ("There was a button saying … and I was not sure if it would do what I expect").
|
||||
* If you want to do something and don't know how or if it is possible, explain the goal ("I would like to know if there are issues that meet the following criteria …").
|
||||
|
||||
</details>
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: workarounds
|
||||
attributes:
|
||||
label: Potential workarounds
|
||||
description: |
|
||||
If you found a solution to your problem, even if it is not great, please share your experiences with it.
|
||||
<details><summary>Further instructions</summary>
|
||||
|
||||
* Start by explaining what you tried and how it worked out ("I used X and it gives me the results, but it takes a lot of time to click through the UI").
|
||||
* What are the major problems with your workaround(s)? ("It takes long to get there", "It looks very ugly")
|
||||
|
||||
</details>
|
||||
- type: input
|
||||
id: forgejo-ver
|
||||
attributes:
|
||||
label: Forgejo Version
|
||||
description: Forgejo version (or commit reference) your instance is running or that you used to reproduce the bug on Forgejo Next.
|
||||
- type: textarea
|
||||
id: versions
|
||||
attributes:
|
||||
label: Other details about your environment (software names and versions)
|
||||
description: |
|
||||
Please include details to help us understand your problem: browser engine and version (for UI issues), operating system and version running Forgejo, database engine and version, deployment methods and relevant third-party packages (e.g. renderers, identity providers)
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Solutions
|
||||
|
||||
*Accepted solutions to address this problem will go here*
|
||||
visible: [content]
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
name: "Step 2: Enhancement"
|
||||
description: "[Advanced users only] Suggest a solution to one or multiple problems that have already been reported (see step 1)."
|
||||
title: "enh: "
|
||||
labels: ["enhancement/feature"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
- Please speak English, as this is the language all maintainers can speak and write.
|
||||
- Be civil, and follow the [Forgejo Code of Conduct](https://codeberg.org/forgejo/code-of-conduct).
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### New workflow
|
||||
|
||||
We are currently experimenting with a new workflow to manage issues and better understand your problems and needs. This is step 2 of the workflow, which is intended for Forgejo contributors: If you just want to raise a problem or need you have, please refer to step 1.
|
||||
|
||||
This step allows to document a solution to one or multiple problems. It allows developers to focus on actionable implementation tasks without the clutter of previous discussions or triaging work.
|
||||
|
||||
If you want to learn more about the background of our workflow, feel invited to read and participate in [the discussion that led to the current approach](https://codeberg.org/forgejo/discussions/issues/415). We are looking forward to your feedback.
|
||||
- type: textarea
|
||||
id: problems
|
||||
attributes:
|
||||
label: Existing problems this enhancement addresses
|
||||
description: Only list the issue numbers of the **existing** problems that your proposal addresses. **Do not add new descriptions.** If you haven't previously described a problem, please [complete step one of the workflow](https://codeberg.org/forgejo/forgejo/issues/new?template=.forgejo%2fissue_template%2fproblem.yaml) and describe the problem you'd like to solve first.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Enhancement description
|
||||
description: Describe the changes you suggest for Forgejo and explain how they address the problems.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: details
|
||||
attributes:
|
||||
label: Details and notes
|
||||
description: Feel free to supply additional information like technical considerations, link to alternative solutions, UI mockups etc.
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
name: 🦋 Bug Report (web interface / frontend)
|
||||
description: "[Advanced users only] Something doesn't look quite as it should? Report it here!"
|
||||
description: Something doesn't look quite as it should? Report it here!
|
||||
title: "bug: "
|
||||
labels: ["bug/new-report", "forgejo/ui"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**NOTE: If your issue is a security concern, please email <security@forgejo.org> ([security.txt](https://forgejo.org/.well-known/security.txt)) instead of opening a public issue.**
|
||||
**NOTE: If your issue is a security concern, please email <security@forgejo.org> (GPG: `A4676E79`) instead of opening a public issue.**
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
|
|
@ -23,9 +23,8 @@ body:
|
|||
It is running the latest development branch and will confirm the problem is not already fixed.
|
||||
If you can reproduce it, provide a URL in the description.
|
||||
options:
|
||||
- "Yes, I've linked the repository below"
|
||||
- "No, I've tried it and the problem is not present there"
|
||||
- "No, I can't try it on the test instance for some reason"
|
||||
- "Yes"
|
||||
- "No"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
name: 🐛 Bug Report (server / backend)
|
||||
description: "[Advanced users only] Found something you weren't expecting? Report it here!"
|
||||
description: Found something you weren't expecting? Report it here!
|
||||
title: "bug: "
|
||||
labels: bug/new-report
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**NOTE: If your issue is a security concern, please email <security@forgejo.org> ([security.txt](https://forgejo.org/.well-known/security.txt)) instead of opening a public issue.**
|
||||
**NOTE: If your issue is a security concern, please email <security@forgejo.org> (GPG: `A4676E79`) instead of opening a public issue.**
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
|
|
@ -23,9 +23,8 @@ body:
|
|||
It is running the latest development branch and will confirm the problem is not already fixed.
|
||||
If you can reproduce it, provide a URL in the description.
|
||||
options:
|
||||
- "Yes, I've linked the repository below"
|
||||
- "No, I've tried it and the problem is not present there"
|
||||
- "No, I can't try it on the test instance for some reason"
|
||||
- "Yes"
|
||||
- "No"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 🔓 Security Reports
|
||||
url: mailto:security@forgejo.org
|
||||
|
|
|
|||
31
.forgejo/issue_template/feature-request.yaml
Normal file
31
.forgejo/issue_template/feature-request.yaml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
name: 💡 Feature Request
|
||||
description: Got an idea for a feature that Forgejo doesn't have yet? Suggest it here!
|
||||
title: "feat: "
|
||||
labels: ["enhancement/feature"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
- Please speak English, as this is the language all maintainers can speak and write.
|
||||
- Be as clear and concise as possible. A very verbose request is harder to interpret in a concrete way.
|
||||
- Be civil, and follow the [Forgejo Code of Conduct](https://codeberg.org/forgejo/code-of-conduct).
|
||||
- Please make sure you are using the latest release of Forgejo and take a moment to [check that your feature hasn't already been suggested](https://codeberg.org/forgejo/forgejo/issues?q=&type=all&labels=78139).
|
||||
- type: textarea
|
||||
id: needs-benefits
|
||||
attributes:
|
||||
label: Needs and benefits
|
||||
description: As concisely as possible, describe the benefits your feature request will provide or the problems it will try to solve.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Feature Description
|
||||
description: As concisely as possible, describe the feature you would like to see added or the changes you would like to see made to Forgejo.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If you can, provide screenshots of an implementation on another site, e.g. GitHub.
|
||||
|
|
@ -10,22 +10,13 @@ labels:
|
|||
|
||||
## Checklist
|
||||
|
||||
The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. All work and communication must conform to Forgejo's [AI Agreement](https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md). There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).
|
||||
The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).
|
||||
|
||||
### Tests for Go changes
|
||||
|
||||
(can be removed for JavaScript changes)
|
||||
### Tests
|
||||
|
||||
- I added test coverage for Go changes...
|
||||
- [ ] in their respective `*_test.go` for unit tests.
|
||||
- [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
|
||||
- I ran...
|
||||
- [ ] `make pr-go` before pushing
|
||||
|
||||
### Tests for JavaScript changes
|
||||
|
||||
(can be removed for Go changes)
|
||||
|
||||
- I added test coverage for JavaScript changes...
|
||||
- [ ] in `web_src/js/*.test.js` if it can be unit tested.
|
||||
- [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)).
|
||||
|
|
@ -37,9 +28,6 @@ The [contributor guide](https://forgejo.org/docs/next/contributor/) contains inf
|
|||
|
||||
### Release notes
|
||||
|
||||
- [ ] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change.
|
||||
- [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change.
|
||||
|
||||
*The decision if the pull request will be shown in the release notes is up to the mergers / release team.*
|
||||
|
||||
The content of the `release-notes/<pull request number>.md` file will serve as the basis for the release notes. If the file does not exist, the title of the pull request will be used instead.
|
||||
- [ ] I do not want this change to show in the release notes.
|
||||
- [ ] I want the title to show in the release notes with a link to this pull request.
|
||||
- [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title.
|
||||
|
|
|
|||
2
.forgejo/testdata/build-release/Dockerfile
vendored
2
.forgejo/testdata/build-release/Dockerfile
vendored
|
|
@ -1,4 +1,4 @@
|
|||
FROM data.forgejo.org/oci/alpine:3.23
|
||||
FROM data.forgejo.org/oci/alpine:3.22
|
||||
ARG RELEASE_VERSION=unkown
|
||||
LABEL maintainer="contact@forgejo.org" \
|
||||
org.opencontainers.image.version="${RELEASE_VERSION}"
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ runs:
|
|||
- name: install packages
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get -q install --allow-downgrades -qq -y ${PACKAGES}
|
||||
apt-get -q install -qq -y ${PACKAGES}
|
||||
env:
|
||||
PACKAGES: ${{inputs.packages}}
|
||||
- name: remove temporary package list to prevent using it in other steps
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ runs:
|
|||
steps:
|
||||
- run: |
|
||||
su forgejo -c 'make deps-backend'
|
||||
- uses: https://data.forgejo.org/actions/cache@v5
|
||||
- uses: https://data.forgejo.org/actions/cache@v4
|
||||
id: cache-backend
|
||||
with:
|
||||
path: ${{github.workspace}}/gitea
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
#
|
||||
# Install the minimal version of Git supported by Forgejo
|
||||
#
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: install git and git-lfs
|
||||
run: |
|
||||
set -x
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
apt-get update -qq
|
||||
apt-get -q install -y -qq curl ca-certificates
|
||||
|
||||
curl -sS -o /tmp/git-man.deb https://archive.ubuntu.com/ubuntu/pool/main/g/git/git-man_2.34.1-1ubuntu1_all.deb
|
||||
curl -sS -o /tmp/git.deb https://archive.ubuntu.com/ubuntu/pool/main/g/git/git_2.34.1-1ubuntu1_amd64.deb
|
||||
curl -sS -o /tmp/git-lfs.deb https://archive.ubuntu.com/ubuntu/pool/universe/g/git-lfs/git-lfs_3.0.2-1_amd64.deb
|
||||
|
||||
apt-get -q install --allow-downgrades -y -qq /tmp/git-man.deb
|
||||
apt-get -q install --allow-downgrades -y -qq /tmp/git.deb
|
||||
apt-get -q install --allow-downgrades -y -qq /tmp/git-lfs.deb
|
||||
|
|
@ -17,7 +17,7 @@ runs:
|
|||
apt-get -q install -qq -y zstd
|
||||
|
||||
- name: "Set up Go using setup-go"
|
||||
uses: https://data.forgejo.org/actions/setup-go@v6
|
||||
uses: https://data.forgejo.org/actions/setup-go@v5
|
||||
id: go-version
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
|
|
@ -50,7 +50,7 @@ runs:
|
|||
|
||||
- name: "Restore Go dependencies from cache or mark for later caching"
|
||||
id: cache-deps
|
||||
uses: https://data.forgejo.org/actions/cache@v5
|
||||
uses: https://data.forgejo.org/actions/cache@v4
|
||||
with:
|
||||
key: setup-cache-go-deps-${{ runner.os }}-${{ inputs.username }}-${{ steps.go-version.outputs.go_version }}-${{ hashFiles('go.sum', 'go.mod', 'Makefile') }}
|
||||
restore-keys: |
|
||||
|
|
|
|||
|
|
@ -40,14 +40,14 @@ jobs:
|
|||
)
|
||||
runs-on: docker
|
||||
container:
|
||||
image: 'data.forgejo.org/oci/node:24-bookworm'
|
||||
image: 'data.forgejo.org/oci/node:22-bookworm'
|
||||
steps:
|
||||
- name: event info
|
||||
run: |
|
||||
cat <<'EOF'
|
||||
${{ toJSON(github) }}
|
||||
EOF
|
||||
- uses: https://data.forgejo.org/actions/git-backporting@v4.9.1
|
||||
- uses: https://data.forgejo.org/actions/git-backporting@v4.8.4
|
||||
with:
|
||||
target-branch-pattern: "^backport/(?<target>(v.*))$"
|
||||
strategy: ort
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
name: Integration tests for the release process
|
||||
enable-email-notifications: true
|
||||
|
||||
env:
|
||||
FORGEJO_VERSION: 11.0.14 # renovate: datasource=docker depName=data.forgejo.org/forgejo/forgejo
|
||||
|
||||
on:
|
||||
push:
|
||||
|
|
@ -29,14 +25,14 @@ jobs:
|
|||
if: vars.ROLE == 'forgejo-coding'
|
||||
runs-on: lxc-bookworm
|
||||
steps:
|
||||
- uses: https://data.forgejo.org/actions/checkout@v6
|
||||
- uses: https://data.forgejo.org/actions/checkout@v4
|
||||
|
||||
- id: forgejo
|
||||
uses: https://data.forgejo.org/actions/setup-forgejo@v3.1.11
|
||||
uses: https://data.forgejo.org/actions/setup-forgejo@v3.1.7
|
||||
with:
|
||||
user: root
|
||||
password: admin1234
|
||||
image-version: ${{ env.FORGEJO_VERSION }}
|
||||
image-version: 1.21
|
||||
lxc-ip-prefix: 10.0.9
|
||||
|
||||
- name: publish the forgejo release
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ jobs:
|
|||
# root is used for testing, allow it
|
||||
if: vars.ROLE == 'forgejo-integration' || github.repository_owner == 'root'
|
||||
steps:
|
||||
- uses: https://data.forgejo.org/actions/checkout@v6
|
||||
- uses: https://data.forgejo.org/actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
|
@ -43,11 +43,11 @@ jobs:
|
|||
repository="${{ github.repository }}"
|
||||
echo "value=${repository##*/}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: https://data.forgejo.org/actions/setup-node@v6
|
||||
- uses: https://data.forgejo.org/actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- uses: https://data.forgejo.org/actions/setup-go@v6
|
||||
- uses: https://data.forgejo.org/actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ jobs:
|
|||
|
||||
- name: cache node_modules
|
||||
id: node
|
||||
uses: https://data.forgejo.org/actions/cache@v5
|
||||
uses: https://data.forgejo.org/actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
|
|
@ -227,14 +227,11 @@ jobs:
|
|||
curl -sS -X DELETE $url/api/v1/repos/forgejo-experimental/forgejo/releases/tags/$tag > /dev/null
|
||||
curl -sS -X DELETE $url/api/v1/repos/forgejo-experimental/forgejo/tags/$tag > /dev/null
|
||||
fi
|
||||
# actions/checkout@v6 sets http.https://codeberg.org/.extraheader with the automatic token. Get rid of it so
|
||||
# it does not prevent using the token that has write permissions. As of @v6, it is stored in
|
||||
# $RUNNER_TEMP/git-credentials-(uuid).config and included in the repo via
|
||||
# includeif.gitdir:...=$RUNNER_TEMP/git-credentials-(uuid).config. Since we don't need these credentials
|
||||
# anymore we can just remove the generated config file.
|
||||
rm -f $RUNNER_TEMP/git-credentials-*
|
||||
# actions/checkout@v3 sets http.https://codeberg.org/.extraheader with the automatic token.
|
||||
# Get rid of it so it does not prevent using the token that has write permissions
|
||||
git config --local --unset http.https://codeberg.org/.extraheader
|
||||
if test -f .git/shallow ; then
|
||||
echo "unexpected .git/shallow file is present"
|
||||
echo "unexptected .git/shallow file is present"
|
||||
echo "it suggests a checkout --depth X was used which may prevent pushing the commit"
|
||||
echo "it happens when actions/checkout is called without depth: 0"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -35,9 +35,9 @@ jobs:
|
|||
)
|
||||
runs-on: docker
|
||||
container:
|
||||
image: data.forgejo.org/oci/node:24-bookworm
|
||||
image: data.forgejo.org/oci/node:22-bookworm
|
||||
steps:
|
||||
- uses: https://data.forgejo.org/actions/checkout@v6
|
||||
- uses: https://data.forgejo.org/actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: '0'
|
||||
show-progress: 'false'
|
||||
|
|
@ -53,5 +53,5 @@ jobs:
|
|||
destination-repo: forgejo/end-to-end
|
||||
destination-branch: main
|
||||
destination-token: ${{ secrets.END_TO_END_CASCADING_PR_DESTINATION }}
|
||||
close: true
|
||||
close-merge: true
|
||||
update: .forgejo/cascading-pr-end-to-end
|
||||
|
|
|
|||
|
|
@ -1,89 +0,0 @@
|
|||
name: coverage
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
repository:
|
||||
description: 'repository'
|
||||
type: string
|
||||
ref:
|
||||
description: 'ref'
|
||||
type: string
|
||||
unit-tests-env:
|
||||
description: 'COVERAGE_TEST_PACKAGES=forgejo.org/modules/actions'
|
||||
type: string
|
||||
integration-tests-env:
|
||||
description: 'COVERAGE_TEST_ARGS=-run=TestAPIListRepoComments'
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
all:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: 'data.forgejo.org/oci/ci:1'
|
||||
options: --tmpfs /tmp:exec,noatime
|
||||
services:
|
||||
elasticsearch:
|
||||
image: data.forgejo.org/oci/bitnami/elasticsearch:7
|
||||
options: --tmpfs /bitnami/elasticsearch/data
|
||||
env:
|
||||
discovery.type: single-node
|
||||
ES_JAVA_OPTS: "-Xms512m -Xmx512m"
|
||||
minio:
|
||||
image: data.forgejo.org/oci/bitnami/minio:2024.8.17
|
||||
options: >-
|
||||
--hostname gitea.minio --tmpfs /bitnami/minio/data:noatime
|
||||
env:
|
||||
MINIO_DOMAIN: minio
|
||||
MINIO_ROOT_USER: 123456
|
||||
MINIO_ROOT_PASSWORD: 12345678
|
||||
mysql:
|
||||
image: 'data.forgejo.org/oci/bitnami/mysql:8.4'
|
||||
env:
|
||||
ALLOW_EMPTY_PASSWORD: yes
|
||||
MYSQL_DATABASE: testgitea
|
||||
#
|
||||
# See also https://codeberg.org/forgejo/forgejo/issues/976
|
||||
#
|
||||
MYSQL_EXTRA_FLAGS: --innodb-adaptive-flushing=OFF --innodb-buffer-pool-size=4G --innodb-log-buffer-size=128M --innodb-flush-log-at-trx-commit=0 --innodb-flush-log-at-timeout=30 --innodb-flush-method=nosync --innodb-fsync-threshold=1000000000 --disable-log-bin
|
||||
options: --tmpfs /bitnami/mysql/data:noatime
|
||||
ldap:
|
||||
image: data.forgejo.org/oci/forgejo-test-openldap:latest
|
||||
pgsql:
|
||||
image: data.forgejo.org/oci/bitnami/postgresql:16
|
||||
env:
|
||||
POSTGRESQL_DATABASE: test
|
||||
POSTGRESQL_PASSWORD: postgres
|
||||
POSTGRESQL_FSYNC: off
|
||||
POSTGRESQL_EXTRA_FLAGS: -c full_page_writes=off
|
||||
options: --tmpfs /bitnami/postgresql
|
||||
cacher:
|
||||
image: registry.redict.io/redict:7.3.6-scratch
|
||||
options: --tmpfs /data:noatime
|
||||
steps:
|
||||
- uses: https://data.forgejo.org/actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ inputs.repository }}
|
||||
ref: ${{ inputs.ref }}
|
||||
- uses: ./.forgejo/workflows-composite/setup-env
|
||||
- name: install git >= 2.42
|
||||
uses: ./.forgejo/workflows-composite/apt-install-from
|
||||
with:
|
||||
packages: git
|
||||
- uses: ./.forgejo/workflows-composite/build-backend
|
||||
- run: |
|
||||
su forgejo -c '${{ inputs.unit-tests-env }} make coverage-run'
|
||||
su forgejo -c '${{ inputs.integration-tests-env }} make coverage-run-pgsql'
|
||||
su forgejo -c '${{ inputs.integration-tests-env }} make coverage-run-mysql'
|
||||
su forgejo -c '${{ inputs.integration-tests-env }} make coverage-run-sqlite'
|
||||
su forgejo -c 'make coverage-merge'
|
||||
timeout-minutes: 180
|
||||
env:
|
||||
TEST_ELASTICSEARCH_URL: http://elasticsearch:9200
|
||||
TEST_MINIO_ENDPOINT: minio:9000
|
||||
TEST_LDAP: 1
|
||||
TEST_REDIS_SERVER: cacher:6379
|
||||
- uses: https://data.forgejo.org/forgejo/upload-artifact@v5
|
||||
with:
|
||||
name: coverage
|
||||
path: ${{ forge.workspace }}/coverage/merged
|
||||
|
|
@ -9,7 +9,7 @@ jobs:
|
|||
if: vars.ROLE == 'forgejo-integration'
|
||||
runs-on: docker
|
||||
container:
|
||||
image: 'data.forgejo.org/oci/node:24-bookworm'
|
||||
image: 'data.forgejo.org/oci/node:22-bookworm'
|
||||
steps:
|
||||
|
||||
- name: apt install curl jq
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright 2025 The Forgejo Authors
|
||||
# Copyright 2024 The Forgejo Authors
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
name: requirements
|
||||
|
|
@ -13,11 +13,10 @@ on:
|
|||
|
||||
jobs:
|
||||
merge-conditions:
|
||||
if: >
|
||||
vars.ROLE == 'forgejo-coding' && forge.event.pull_request.head.repo.full_name != 'forgejo-cascading-pr/forgejo'
|
||||
if: vars.ROLE == 'forgejo-coding'
|
||||
runs-on: docker
|
||||
container:
|
||||
image: 'data.forgejo.org/oci/node:24-bookworm'
|
||||
image: 'data.forgejo.org/oci/node:22-bookworm'
|
||||
steps:
|
||||
- name: Debug output
|
||||
run: |
|
||||
|
|
@ -27,18 +26,18 @@ jobs:
|
|||
- name: Missing test label
|
||||
if: >
|
||||
!(
|
||||
contains(toJSON(forge.event.pull_request.labels), 'test/present')
|
||||
|| contains(toJSON(forge.event.pull_request.labels), 'test/not-needed')
|
||||
|| contains(toJSON(forge.event.pull_request.labels), 'test/manual')
|
||||
contains(toJSON(github.event.pull_request.labels), 'test/present')
|
||||
|| contains(toJSON(github.event.pull_request.labels), 'test/not-needed')
|
||||
|| contains(toJSON(github.event.pull_request.labels), 'test/manual')
|
||||
)
|
||||
run: |
|
||||
echo "A team member must set the label to either 'present', 'not-needed' or 'manual'."
|
||||
echo "Test label must be set to either 'present', 'not-needed' or 'manual'."
|
||||
exit 1
|
||||
- name: Missing manual test instructions
|
||||
if: >
|
||||
(
|
||||
contains(toJSON(forge.event.pull_request.labels), 'test/manual')
|
||||
&& !contains(toJSON(forge.event.pull_request.body), '# Test')
|
||||
contains(toJSON(github.event.pull_request.labels), 'test/manual')
|
||||
&& !contains(toJSON(github.event.pull_request.body), '# Test')
|
||||
)
|
||||
run: |
|
||||
echo "Manual test label is set. The PR description needs to contain test steps introduced by a heading like:"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
if: ${{ secrets.MIRROR_TOKEN != '' }}
|
||||
runs-on: docker
|
||||
container:
|
||||
image: 'data.forgejo.org/oci/node:24-bookworm'
|
||||
image: 'data.forgejo.org/oci/node:22-bookworm'
|
||||
steps:
|
||||
- name: git push {v*/,}forgejo
|
||||
run: |
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ jobs:
|
|||
runs-on: lxc-bookworm
|
||||
if: vars.DOER != '' && vars.FORGEJO != '' && vars.TO_OWNER != '' && vars.FROM_OWNER != '' && secrets.TOKEN != ''
|
||||
steps:
|
||||
- uses: https://data.forgejo.org/actions/checkout@v6
|
||||
- uses: https://data.forgejo.org/actions/checkout@v4
|
||||
|
||||
- name: copy & sign
|
||||
uses: https://data.forgejo.org/forgejo/forgejo-build-publish/publish@v5.6.0
|
||||
|
|
|
|||
|
|
@ -4,35 +4,30 @@ on:
|
|||
schedule:
|
||||
- cron: '@daily'
|
||||
|
||||
env:
|
||||
RNA_WORKDIR: /srv/rna
|
||||
RNA_VERSION: v1.7.0 # renovate: datasource=forgejo-releases depName=forgejo/release-notes-assistant registryUrl=https://code.forgejo.org
|
||||
|
||||
jobs:
|
||||
release-notes:
|
||||
if: vars.ROLE == 'forgejo-coding'
|
||||
runs-on: docker
|
||||
container:
|
||||
image: 'data.forgejo.org/oci/ci:1'
|
||||
image: 'data.forgejo.org/oci/node:22-bookworm'
|
||||
steps:
|
||||
- uses: https://data.forgejo.org/actions/checkout@v6
|
||||
- uses: https://data.forgejo.org/actions/checkout@v4
|
||||
|
||||
- uses: https://data.forgejo.org/actions/cache@v5
|
||||
- uses: https://data.forgejo.org/actions/setup-go@v5
|
||||
with:
|
||||
key: rna-${{ env.RNA_VERSION }}
|
||||
path: ${{ env.RNA_WORKDIR }}
|
||||
go-version-file: "go.mod"
|
||||
cache: false
|
||||
|
||||
- name: install release-notes-assistant
|
||||
- name: apt install jq
|
||||
run: |
|
||||
set -x
|
||||
wget -O /usr/local/bin/rna https://code.forgejo.org/forgejo/release-notes-assistant/releases/download/${{ env.RNA_VERSION}}/release-notes-assistant
|
||||
chmod +x /usr/local/bin/rna
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get -q install -y -qq jq
|
||||
|
||||
- name: update open milestones
|
||||
run: |
|
||||
set -x
|
||||
mkdir -p ${{ env.RNA_WORKDIR }}
|
||||
curl -sS $FORGEJO_SERVER_URL/api/v1/repos/$FORGEJO_REPOSITORY/milestones?state=open | jq -r '.[] | .title' | while read forgejo version ; do
|
||||
curl -sS $GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/milestones?state=open | jq -r '.[] | .title' | while read forgejo version ; do
|
||||
milestone="$forgejo $version"
|
||||
rna --workdir ${{ env.RNA_WORKDIR }} --config .release-notes-assistant.yaml --storage milestone --storage-location "$milestone" --forgejo-url $FORGEJO_SERVER_URL --repository $FORGEJO_REPOSITORY --token ${{ secrets.RELEASE_NOTES_ASSISTANT_TOKEN }} release $version
|
||||
go run code.forgejo.org/forgejo/release-notes-assistant@v1.1.1 --config .release-notes-assistant.yaml --storage milestone --storage-location "$milestone" --forgejo-url $GITHUB_SERVER_URL --repository $GITHUB_REPOSITORY --token ${{ secrets.RELEASE_NOTES_ASSISTANT_TOKEN }} release $version
|
||||
done
|
||||
|
|
|
|||
|
|
@ -7,17 +7,14 @@ on:
|
|||
- synchronize
|
||||
- labeled
|
||||
|
||||
env:
|
||||
RNA_VERSION: v1.7.0 # renovate: datasource=forgejo-releases depName=forgejo/release-notes-assistant registryUrl=https://code.forgejo.org
|
||||
|
||||
jobs:
|
||||
release-notes:
|
||||
if: ( vars.ROLE == 'forgejo-coding' ) && contains(github.event.pull_request.labels.*.name, 'worth a release-note')
|
||||
runs-on: docker
|
||||
container:
|
||||
image: 'data.forgejo.org/oci/ci:1'
|
||||
image: 'data.forgejo.org/oci/node:22-bookworm'
|
||||
steps:
|
||||
- uses: https://data.forgejo.org/actions/checkout@v6
|
||||
- uses: https://data.forgejo.org/actions/checkout@v4
|
||||
|
||||
- name: event
|
||||
run: |
|
||||
|
|
@ -28,12 +25,17 @@ jobs:
|
|||
${{ toJSON(github.event) }}
|
||||
EOF
|
||||
|
||||
- name: install release-notes-assistant
|
||||
- uses: https://data.forgejo.org/actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache: false
|
||||
|
||||
- name: apt install jq
|
||||
run: |
|
||||
set -x
|
||||
wget -O /usr/local/bin/rna https://code.forgejo.org/forgejo/release-notes-assistant/releases/download/${{ env.RNA_VERSION}}/release-notes-assistant
|
||||
chmod +x /usr/local/bin/rna
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get -q install -y -qq jq
|
||||
|
||||
- name: release-notes-assistant preview
|
||||
run: |
|
||||
rna --config .release-notes-assistant.yaml --storage pr --storage-location ${{ github.event.pull_request.number }} --forgejo-url $GITHUB_SERVER_URL --repository $GITHUB_REPOSITORY --token ${{ secrets.RELEASE_NOTES_ASSISTANT_TOKEN }} preview ${{ github.event.pull_request.number }}
|
||||
go run code.forgejo.org/forgejo/release-notes-assistant@v1.1.1 --config .release-notes-assistant.yaml --storage pr --storage-location ${{ github.event.pull_request.number }} --forgejo-url $GITHUB_SERVER_URL --repository $GITHUB_REPOSITORY --token ${{ secrets.RELEASE_NOTES_ASSISTANT_TOKEN }} preview ${{ github.event.pull_request.number }}
|
||||
|
|
|
|||
74
.forgejo/workflows/renovate.yml
Normal file
74
.forgejo/workflows/renovate.yml
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
#
|
||||
# Runs every 2 hours, but Renovate is limited to create new PR before 4am.
|
||||
# See renovate.json for more settings.
|
||||
# Automerge is enabled for Renovate PR's but need to be approved before.
|
||||
#
|
||||
name: renovate
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- renovate/** # self-test updates
|
||||
paths:
|
||||
- .forgejo/workflows/renovate.yml
|
||||
schedule:
|
||||
- cron: '0 0/2 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
RENOVATE_DRY_RUN: ${{ (github.event_name != 'schedule' && github.ref_name != github.event.repository.default_branch) && 'full' || '' }}
|
||||
RENOVATE_REPOSITORIES: ${{ github.repository }}
|
||||
# fix because 10.0.0-58-7e1df53+gitea-1.22.0 < 10.0.0 for semver
|
||||
# and codeberg api returns such versions from `git describe --tags`
|
||||
RENOVATE_X_PLATFORM_VERSION: 10.0.0+gitea-1.22.0
|
||||
|
||||
jobs:
|
||||
renovate:
|
||||
if: vars.ROLE == 'forgejo-coding' && secrets.RENOVATE_TOKEN != ''
|
||||
|
||||
runs-on: docker
|
||||
container:
|
||||
image: data.forgejo.org/renovate/renovate:39.212.0
|
||||
|
||||
steps:
|
||||
- name: Load renovate repo cache
|
||||
uses: https://data.forgejo.org/actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
|
||||
with:
|
||||
path: |
|
||||
.tmp/cache/renovate/repository
|
||||
.tmp/cache/renovate/renovate-cache-sqlite
|
||||
.tmp/osv
|
||||
key: repo-cache-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
repo-cache-
|
||||
|
||||
- name: Run renovate
|
||||
run: renovate
|
||||
env:
|
||||
GITHUB_COM_TOKEN: ${{ secrets.RENOVATE_GITHUB_COM_TOKEN }}
|
||||
LOG_LEVEL: debug
|
||||
RENOVATE_BASE_DIR: ${{ github.workspace }}/.tmp
|
||||
RENOVATE_ENDPOINT: ${{ github.server_url }}
|
||||
RENOVATE_PLATFORM: gitea
|
||||
RENOVATE_REPOSITORY_CACHE: 'enabled'
|
||||
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
||||
RENOVATE_GIT_AUTHOR: 'Renovate Bot <forgejo-renovate-action@forgejo.org>'
|
||||
|
||||
RENOVATE_X_SQLITE_PACKAGE_CACHE: true
|
||||
|
||||
GIT_AUTHOR_NAME: 'Renovate Bot'
|
||||
GIT_AUTHOR_EMAIL: 'forgejo-renovate-action@forgejo.org'
|
||||
GIT_COMMITTER_NAME: 'Renovate Bot'
|
||||
GIT_COMMITTER_EMAIL: 'forgejo-renovate-action@forgejo.org'
|
||||
|
||||
OSV_OFFLINE_ROOT_DIR: ${{ github.workspace }}/.tmp/osv
|
||||
|
||||
- name: Save renovate repo cache
|
||||
if: always() && env.RENOVATE_DRY_RUN != 'full'
|
||||
uses: https://data.forgejo.org/actions/cache/save@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
|
||||
with:
|
||||
path: |
|
||||
.tmp/cache/renovate/repository
|
||||
.tmp/cache/renovate/renovate-cache-sqlite
|
||||
.tmp/osv
|
||||
key: repo-cache-${{ github.run_id }}
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
#
|
||||
# Additional integration tests designed to run once a day when
|
||||
# `mirror.yml` pushes to https://codeberg.org/forgejo-integration/forgejo
|
||||
# and send a notification via email to the contact email of the
|
||||
# organization should they fail.
|
||||
#
|
||||
# For debug purposes:
|
||||
#
|
||||
# - uncomment [on].pull_request
|
||||
# - swap 'forgejo-integration' and 'forgejo-coding'
|
||||
# - open a pull request at https://codeberg.org/forgejo/forgejo and fix things
|
||||
# - swap 'forgejo-integration' and 'forgejo-coding'
|
||||
# - comment [on].pull_request
|
||||
#
|
||||
|
||||
name: testing-integration
|
||||
|
||||
on:
|
||||
# pull_request:
|
||||
push:
|
||||
tags: 'v[0-9]+.[0-9]+.*'
|
||||
branches:
|
||||
- 'forgejo'
|
||||
- 'v*/forgejo'
|
||||
|
||||
enable-email-notifications: true
|
||||
|
||||
jobs:
|
||||
test-unit:
|
||||
# if: vars.ROLE == 'forgejo-coding'
|
||||
if: vars.ROLE == 'forgejo-integration'
|
||||
runs-on: docker
|
||||
container:
|
||||
image: 'data.forgejo.org/oci/node:24-trixie'
|
||||
options: --tmpfs /tmp:exec,noatime
|
||||
steps:
|
||||
- uses: https://data.forgejo.org/actions/checkout@v6
|
||||
- uses: ./.forgejo/workflows-composite/setup-env
|
||||
- name: install git 2.34.1 and git-lfs 3.0.2
|
||||
uses: ./.forgejo/workflows-composite/install-minimum-git-version
|
||||
- uses: ./.forgejo/workflows-composite/build-backend
|
||||
- run: |
|
||||
su forgejo -c 'make test-backend test-check'
|
||||
timeout-minutes: 120
|
||||
env:
|
||||
RACE_ENABLED: 'true'
|
||||
TAGS: bindata
|
||||
test-sqlite:
|
||||
# if: vars.ROLE == 'forgejo-coding'
|
||||
if: vars.ROLE == 'forgejo-integration'
|
||||
runs-on: docker
|
||||
container:
|
||||
image: 'data.forgejo.org/oci/node:24-trixie'
|
||||
options: --tmpfs /tmp:exec,noatime
|
||||
steps:
|
||||
- uses: https://data.forgejo.org/actions/checkout@v6
|
||||
- uses: ./.forgejo/workflows-composite/setup-env
|
||||
- name: install git 2.34.1 and git-lfs 3.0.2
|
||||
uses: ./.forgejo/workflows-composite/install-minimum-git-version
|
||||
- uses: ./.forgejo/workflows-composite/build-backend
|
||||
- run: |
|
||||
su forgejo -c 'make test-sqlite-migration test-sqlite'
|
||||
timeout-minutes: 120
|
||||
env:
|
||||
TAGS: sqlite sqlite_unlock_notify
|
||||
RACE_ENABLED: true
|
||||
TEST_TAGS: sqlite sqlite_unlock_notify
|
||||
USE_REPO_TEST_DIR: 1
|
||||
test-mariadb:
|
||||
# if: vars.ROLE == 'forgejo-coding'
|
||||
if: vars.ROLE == 'forgejo-integration'
|
||||
runs-on: docker
|
||||
name: ${{ format('test-mariadb (v{0})', matrix.version) }}
|
||||
strategy:
|
||||
matrix:
|
||||
version: ['10.6', '11.8']
|
||||
container:
|
||||
image: 'data.forgejo.org/oci/node:24-trixie'
|
||||
options: --tmpfs /tmp:exec,noatime
|
||||
services:
|
||||
mysql:
|
||||
image: ${{ format('data.forgejo.org/oci/mariadb:{0}', matrix.version) }}
|
||||
env:
|
||||
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: yes
|
||||
MARIADB_DATABASE: testgitea
|
||||
options: --tmpfs /var/lib/mysql:noatime
|
||||
steps:
|
||||
- uses: https://data.forgejo.org/actions/checkout@v6
|
||||
- uses: ./.forgejo/workflows-composite/setup-env
|
||||
- name: install dependencies
|
||||
run: apt-get update -qq && apt-get -q install -qq -y git-lfs
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
- uses: ./.forgejo/workflows-composite/build-backend
|
||||
- run: |
|
||||
su forgejo -c 'make test-mysql-migration test-mysql'
|
||||
timeout-minutes: 120
|
||||
env:
|
||||
USE_REPO_TEST_DIR: 1
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
name: testing
|
||||
enable-email-notifications: true
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
|
@ -14,7 +13,7 @@ jobs:
|
|||
if: vars.ROLE == 'forgejo-coding' || vars.ROLE == 'forgejo-testing'
|
||||
runs-on: docker
|
||||
container:
|
||||
image: 'data.forgejo.org/oci/node:24-trixie'
|
||||
image: 'data.forgejo.org/oci/node:22-trixie'
|
||||
options: --tmpfs /tmp:exec,noatime
|
||||
steps:
|
||||
- name: event info
|
||||
|
|
@ -22,35 +21,23 @@ jobs:
|
|||
cat <<'EOF'
|
||||
${{ toJSON(github) }}
|
||||
EOF
|
||||
- uses: https://data.forgejo.org/actions/checkout@v6
|
||||
- uses: https://data.forgejo.org/actions/checkout@v4
|
||||
- uses: ./.forgejo/workflows-composite/setup-env
|
||||
# DO NOT add checks here, but rather in the makefile
|
||||
- run: su forgejo -c './tools/cimake.sh pr-go'
|
||||
# this will re-run the backend target also contained in pr-go, but
|
||||
# a re-build is insignificant
|
||||
- run: su forgejo -c 'make deps-backend deps-tools'
|
||||
- run: su forgejo -c 'make --always-make -j$(nproc) lint-backend tidy-check swagger-check lint-swagger fmt-check swagger-validate' # ensure the "go-licenses" make target runs
|
||||
- uses: ./.forgejo/workflows-composite/build-backend
|
||||
frontend-checks:
|
||||
if: vars.ROLE == 'forgejo-coding' || vars.ROLE == 'forgejo-testing'
|
||||
runs-on: docker
|
||||
container:
|
||||
image: 'data.forgejo.org/oci/node:24-trixie'
|
||||
image: 'data.forgejo.org/oci/node:22-trixie'
|
||||
options: --tmpfs /tmp:exec,noatime
|
||||
steps:
|
||||
- uses: https://data.forgejo.org/actions/checkout@v6
|
||||
|
||||
- uses: https://data.forgejo.org/actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
|
||||
- uses: https://data.forgejo.org/actions/checkout@v4
|
||||
- run: make deps-frontend
|
||||
- run: make lint-frontend
|
||||
- run: make checks-frontend
|
||||
- name: make test-frontend-coverage
|
||||
run: |
|
||||
# Usage of `dayjs` can be impacted by local system timezone and can be sensitive to DST differences; since
|
||||
# frontend tests are very short they're run twice with varying DST rules to reduce regression risk.
|
||||
TZ=Europe/Berlin make test-frontend-coverage
|
||||
TZ=America/Edmonton make test-frontend-coverage
|
||||
- run: make test-frontend-coverage
|
||||
- run: make frontend
|
||||
- name: Install zstd for cache saving
|
||||
# works around https://github.com/actions/cache/issues/1169, because the
|
||||
|
|
@ -58,8 +45,8 @@ jobs:
|
|||
run: |
|
||||
apt-get update -qq
|
||||
apt-get -q install -qq -y zstd
|
||||
- name: 'Cache frontend build for playwright testing'
|
||||
uses: https://data.forgejo.org/actions/cache/save@v5
|
||||
- name: "Cache frontend build for playwright testing"
|
||||
uses: https://data.forgejo.org/actions/cache/save@v4
|
||||
with:
|
||||
path: ${{github.workspace}}/public/assets
|
||||
key: frontend-build-${{ github.sha }}
|
||||
|
|
@ -68,7 +55,7 @@ jobs:
|
|||
runs-on: docker
|
||||
needs: [backend-checks, frontend-checks]
|
||||
container:
|
||||
image: 'data.forgejo.org/oci/node:24-trixie'
|
||||
image: 'data.forgejo.org/oci/node:22-trixie'
|
||||
options: --tmpfs /tmp:exec,noatime
|
||||
services:
|
||||
elasticsearch:
|
||||
|
|
@ -76,7 +63,7 @@ jobs:
|
|||
options: --tmpfs /bitnami/elasticsearch/data
|
||||
env:
|
||||
discovery.type: single-node
|
||||
ES_JAVA_OPTS: '-Xms512m -Xmx512m'
|
||||
ES_JAVA_OPTS: "-Xms512m -Xmx512m"
|
||||
minio:
|
||||
image: data.forgejo.org/oci/bitnami/minio:2024.8.17
|
||||
options: >-
|
||||
|
|
@ -86,7 +73,7 @@ jobs:
|
|||
MINIO_ROOT_USER: 123456
|
||||
MINIO_ROOT_PASSWORD: 12345678
|
||||
steps:
|
||||
- uses: https://data.forgejo.org/actions/checkout@v6
|
||||
- uses: https://data.forgejo.org/actions/checkout@v4
|
||||
- uses: ./.forgejo/workflows-composite/setup-env
|
||||
- name: test release-notes-assistant.sh
|
||||
run: |
|
||||
|
|
@ -109,29 +96,24 @@ jobs:
|
|||
image: 'data.forgejo.org/oci/playwright:latest'
|
||||
options: --tmpfs /tmp:exec,noatime
|
||||
steps:
|
||||
- uses: https://data.forgejo.org/actions/checkout@v6
|
||||
- uses: https://data.forgejo.org/actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 20
|
||||
- uses: ./.forgejo/workflows-composite/setup-env
|
||||
- name: 'Restore frontend build'
|
||||
uses: https://data.forgejo.org/actions/cache/restore@v5
|
||||
- name: "Restore frontend build"
|
||||
uses: https://data.forgejo.org/actions/cache/restore@v4
|
||||
id: cache-frontend
|
||||
with:
|
||||
path: ${{github.workspace}}/public/assets
|
||||
key: frontend-build-${{ github.sha }}
|
||||
- name: 'Build frontend (if not cached)'
|
||||
- name: "Build frontend (if not cached)"
|
||||
if: steps.cache-frontend.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
su forgejo -c 'make deps-frontend frontend'
|
||||
- uses: ./.forgejo/workflows-composite/build-backend
|
||||
- name: Decide to run all tests
|
||||
id: run-all
|
||||
if: contains(github.event.pull_request.labels.*.name, 'run-all-playwright-tests') || contains(github.event.pull_request.title, 'playwright')
|
||||
run: |
|
||||
echo "all=1" >> "$GITHUB_OUTPUT"
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: https://data.forgejo.org/tj-actions/changed-files@v47
|
||||
uses: https://data.forgejo.org/tj-actions/changed-files@v45
|
||||
with:
|
||||
separator: '\n'
|
||||
- run: |
|
||||
|
|
@ -141,10 +123,9 @@ jobs:
|
|||
USE_REPO_TEST_DIR: 1
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||
CHANGED_FILES: ${{steps.changed-files.outputs.all_changed_files}}
|
||||
RUN_ALL: ${{steps.run-all.all}}
|
||||
- name: Upload test artifacts on failure
|
||||
if: failure()
|
||||
uses: https://data.forgejo.org/forgejo/upload-artifact@v5
|
||||
uses: https://data.forgejo.org/forgejo/upload-artifact@v4
|
||||
with:
|
||||
name: test-artifacts.zip
|
||||
path: tests/e2e/test-artifacts/
|
||||
|
|
@ -154,7 +135,7 @@ jobs:
|
|||
runs-on: docker
|
||||
needs: [backend-checks, frontend-checks, test-unit]
|
||||
container:
|
||||
image: 'data.forgejo.org/oci/node:24-trixie'
|
||||
image: 'data.forgejo.org/oci/node:22-trixie'
|
||||
options: --tmpfs /tmp:exec,noatime
|
||||
name: ${{ format('test-remote-cacher ({0})', matrix.cacher.name) }}
|
||||
strategy:
|
||||
|
|
@ -176,10 +157,8 @@ jobs:
|
|||
cacher:
|
||||
image: ${{ matrix.cacher.image }}
|
||||
options: ${{ matrix.cacher.options }}
|
||||
env:
|
||||
ALLOW_EMPTY_PASSWORD: 'yes' # redis & valkey will immediately shutdown with no defined password unless overridden
|
||||
steps:
|
||||
- uses: https://data.forgejo.org/actions/checkout@v6
|
||||
- uses: https://data.forgejo.org/actions/checkout@v4
|
||||
- uses: ./.forgejo/workflows-composite/setup-env
|
||||
- uses: ./.forgejo/workflows-composite/build-backend
|
||||
- run: |
|
||||
|
|
@ -188,13 +167,13 @@ jobs:
|
|||
env:
|
||||
RACE_ENABLED: 'true'
|
||||
TAGS: bindata
|
||||
TEST_REDIS_SERVER: cacher:6379
|
||||
TEST_REDIS_SERVER: cacher:${{ matrix.cacher.port }}
|
||||
test-mysql:
|
||||
if: vars.ROLE == 'forgejo-coding' || vars.ROLE == 'forgejo-testing'
|
||||
runs-on: docker
|
||||
needs: [backend-checks, frontend-checks]
|
||||
container:
|
||||
image: 'data.forgejo.org/oci/node:24-trixie'
|
||||
image: 'data.forgejo.org/oci/node:22-trixie'
|
||||
options: --tmpfs /tmp:exec,noatime
|
||||
services:
|
||||
mysql:
|
||||
|
|
@ -208,7 +187,7 @@ jobs:
|
|||
MYSQL_EXTRA_FLAGS: --innodb-adaptive-flushing=OFF --innodb-buffer-pool-size=4G --innodb-log-buffer-size=128M --innodb-flush-log-at-trx-commit=0 --innodb-flush-log-at-timeout=30 --innodb-flush-method=nosync --innodb-fsync-threshold=1000000000 --disable-log-bin
|
||||
options: --tmpfs /bitnami/mysql/data:noatime
|
||||
steps:
|
||||
- uses: https://data.forgejo.org/actions/checkout@v6
|
||||
- uses: https://data.forgejo.org/actions/checkout@v4
|
||||
- uses: ./.forgejo/workflows-composite/setup-env
|
||||
- name: install dependencies
|
||||
run: apt-get update -qq && apt-get -q install -qq -y git-lfs
|
||||
|
|
@ -225,7 +204,7 @@ jobs:
|
|||
runs-on: docker
|
||||
needs: [backend-checks, frontend-checks]
|
||||
container:
|
||||
image: 'data.forgejo.org/oci/node:24-trixie'
|
||||
image: 'data.forgejo.org/oci/node:22-trixie'
|
||||
options: --tmpfs /tmp:exec,noatime
|
||||
services:
|
||||
minio:
|
||||
|
|
@ -246,7 +225,7 @@ jobs:
|
|||
POSTGRESQL_EXTRA_FLAGS: -c full_page_writes=off
|
||||
options: --tmpfs /bitnami/postgresql
|
||||
steps:
|
||||
- uses: https://data.forgejo.org/actions/checkout@v6
|
||||
- uses: https://data.forgejo.org/actions/checkout@v4
|
||||
- uses: ./.forgejo/workflows-composite/setup-env
|
||||
- name: install dependencies
|
||||
run: apt-get update -qq && apt-get -q install -qq -y git-lfs
|
||||
|
|
@ -265,10 +244,10 @@ jobs:
|
|||
runs-on: docker
|
||||
needs: [backend-checks, frontend-checks]
|
||||
container:
|
||||
image: 'data.forgejo.org/oci/node:24-trixie'
|
||||
image: 'data.forgejo.org/oci/node:22-trixie'
|
||||
options: --tmpfs /tmp:exec,noatime
|
||||
steps:
|
||||
- uses: https://data.forgejo.org/actions/checkout@v6
|
||||
- uses: https://data.forgejo.org/actions/checkout@v4
|
||||
- uses: ./.forgejo/workflows-composite/setup-env
|
||||
- name: install dependencies
|
||||
run: apt-get update -qq && apt-get -q install -qq -y git-lfs
|
||||
|
|
@ -293,23 +272,10 @@ jobs:
|
|||
- test-remote-cacher
|
||||
- test-unit
|
||||
container:
|
||||
image: 'data.forgejo.org/oci/node:24-trixie'
|
||||
image: 'data.forgejo.org/oci/node:22-trixie'
|
||||
options: --tmpfs /tmp:exec,noatime
|
||||
steps:
|
||||
- uses: https://data.forgejo.org/actions/checkout@v6
|
||||
- uses: https://data.forgejo.org/actions/checkout@v4
|
||||
- uses: ./.forgejo/workflows-composite/setup-env
|
||||
- run: su forgejo -c 'make deps-backend deps-tools'
|
||||
- run: su forgejo -c 'make security-check'
|
||||
semgrep:
|
||||
if: vars.ROLE == 'forgejo-coding' || vars.ROLE == 'forgejo-testing'
|
||||
name: semgrep/ci
|
||||
runs-on: docker
|
||||
container:
|
||||
image: 'data.forgejo.org/oci/semgrep:latest'
|
||||
steps:
|
||||
- run: apk add nodejs # required for actions/checkout
|
||||
- uses: https://data.forgejo.org/actions/checkout@v6
|
||||
- name: self-check semgrep rules
|
||||
run: semgrep --test .semgrep/tests/ --config .semgrep/config/
|
||||
- name: semgrep ci
|
||||
run: semgrep ci --config .semgrep/config/ --metrics=off
|
||||
|
|
|
|||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -37,8 +37,6 @@ _testmain.go
|
|||
|
||||
*coverage.out
|
||||
coverage.all
|
||||
coverage.html
|
||||
coverage.html.gz
|
||||
coverage/
|
||||
cpu.out
|
||||
|
||||
|
|
@ -55,8 +53,6 @@ cpu.out
|
|||
*.log
|
||||
*.log.*.gz
|
||||
|
||||
/build/lint-locale/lint-locale
|
||||
/build/lint-locale-usage/lint-locale-usage
|
||||
/gitea
|
||||
/gitea-vet
|
||||
/debug
|
||||
|
|
@ -107,7 +103,6 @@ cpu.out
|
|||
/.air
|
||||
/.go-licenses
|
||||
/.cur-deadcode-out
|
||||
/.deadcode.diff
|
||||
|
||||
# Files and folders that were previously generated
|
||||
/public/assets/img/webpack
|
||||
|
|
@ -134,4 +129,3 @@ prime/
|
|||
|
||||
# Manpage
|
||||
/man
|
||||
tests/integration/api_activitypub_person_inbox_useractivity_test.go
|
||||
|
|
|
|||
519
.golangci.yml
519
.golangci.yml
|
|
@ -1,10 +1,7 @@
|
|||
---
|
||||
version: "2"
|
||||
output:
|
||||
sort-order:
|
||||
- file
|
||||
linters:
|
||||
default: none
|
||||
enable-all: false
|
||||
disable-all: true
|
||||
fast: false
|
||||
enable:
|
||||
- bidichk
|
||||
- depguard
|
||||
|
|
@ -12,412 +9,142 @@ linters:
|
|||
- errcheck
|
||||
- forbidigo
|
||||
- gocritic
|
||||
- gofmt
|
||||
- gofumpt
|
||||
- gosimple
|
||||
- govet
|
||||
- importas
|
||||
- ineffassign
|
||||
- modernize
|
||||
- nakedret
|
||||
- nolintlint
|
||||
- revive
|
||||
- staticcheck
|
||||
- stylecheck
|
||||
- testifylint
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- unparam
|
||||
- usetesting
|
||||
- wastedassign
|
||||
- nilnil
|
||||
settings:
|
||||
depguard:
|
||||
rules:
|
||||
main:
|
||||
deny:
|
||||
- pkg: encoding/json
|
||||
desc: use gitea's modules/json instead of encoding/json
|
||||
- pkg: golang.org/x/exp
|
||||
desc: it's experimental and unreliable
|
||||
- pkg: forgejo.org/modules/git/internal
|
||||
desc: do not use the internal package, use AddXxx function instead
|
||||
- pkg: gopkg.in/ini.v1
|
||||
desc: do not use the ini package, use gitea's config system instead
|
||||
- pkg: github.com/minio/sha256-simd
|
||||
desc: use crypto/sha256 instead, see https://codeberg.org/forgejo/forgejo/pulls/1528
|
||||
- pkg: github.com/go-git/go-git
|
||||
desc: use forgejo.org/modules/git instead, see https://codeberg.org/forgejo/forgejo/pulls/4941
|
||||
- pkg: gopkg.in/yaml.v3
|
||||
desc: use go.yaml.in/yaml instead, see https://codeberg.org/forgejo/forgejo/pulls/8956
|
||||
migration-isolation:
|
||||
list-mode: lax
|
||||
files:
|
||||
- "**/models/forgejo_migrations/**"
|
||||
deny:
|
||||
- pkg: "forgejo.org/models"
|
||||
desc: >
|
||||
Migrations must not import application models. Application models will be the most recent schema for
|
||||
Forgejo, while migrations will be operating against the database schema that existed when they were
|
||||
authored.
|
||||
- pkg: "forgejo.org/services"
|
||||
desc: >
|
||||
Migrations must not import application services. Application services will reference application
|
||||
models which will use the most recent schema for Forgejo, while migrations will be operating against the
|
||||
database schema that existed when they were authored.
|
||||
allow:
|
||||
- "forgejo.org/models/db"
|
||||
- "forgejo.org/models/gitea_migrations/base"
|
||||
- "forgejo.org/models/gitea_migrations/test"
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- ifElseChain
|
||||
importas:
|
||||
alias:
|
||||
# Specific overrides that would violate the default rules further below.
|
||||
- pkg: forgejo.org/models/db
|
||||
alias: ""
|
||||
- pkg: forgejo.org/models/organization
|
||||
alias: org_model
|
||||
|
||||
- pkg: forgejo.org/services/context
|
||||
alias: app_context
|
||||
- pkg: forgejo.org/services/doctor
|
||||
alias: doctor
|
||||
- pkg: forgejo.org/services/repository
|
||||
alias: repo_service
|
||||
run:
|
||||
timeout: 10m
|
||||
|
||||
# Make sure that we follow a consistent naming for model and service aliases.
|
||||
- pkg: 'forgejo.org/(model|service)s/(\w+)'
|
||||
alias: '${2}_${1}'
|
||||
output:
|
||||
sort-results: true
|
||||
sort-order: [file]
|
||||
show-stats: true
|
||||
|
||||
revive:
|
||||
severity: error
|
||||
rules:
|
||||
- name: atomic
|
||||
- name: bare-return
|
||||
- name: blank-imports
|
||||
- name: constant-logical-expr
|
||||
- name: context-as-argument
|
||||
- name: context-keys-type
|
||||
- name: dot-imports
|
||||
- name: duplicated-imports
|
||||
- name: empty-lines
|
||||
- name: error-naming
|
||||
- name: error-return
|
||||
- name: error-strings
|
||||
- name: errorf
|
||||
- name: exported
|
||||
- name: identical-branches
|
||||
- name: if-return
|
||||
- name: increment-decrement
|
||||
- name: indent-error-flow
|
||||
- name: modifies-value-receiver
|
||||
- name: package-comments
|
||||
- name: range
|
||||
- name: receiver-naming
|
||||
- name: redefines-builtin-id
|
||||
- name: string-of-int
|
||||
- name: superfluous-else
|
||||
- name: time-naming
|
||||
- name: unconditional-recursion
|
||||
- name: unexported-return
|
||||
- name: unreachable-code
|
||||
- name: var-declaration
|
||||
- name: var-naming
|
||||
arguments:
|
||||
- []
|
||||
- []
|
||||
- - skip-package-name-checks: true
|
||||
- name: redefines-builtin-id
|
||||
disabled: true
|
||||
staticcheck:
|
||||
checks:
|
||||
- all
|
||||
testifylint:
|
||||
disable:
|
||||
- error-is-as
|
||||
- go-require
|
||||
nilnil:
|
||||
only-two: false
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
linters-settings:
|
||||
stylecheck:
|
||||
checks: ["all", "-ST1005", "-ST1003"]
|
||||
nakedret:
|
||||
max-func-lines: 0
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- ifElseChain
|
||||
revive:
|
||||
severity: error
|
||||
rules:
|
||||
- linters:
|
||||
- nolintlint
|
||||
path: models/db/sql_postgres_with_schema.go
|
||||
- linters:
|
||||
- dupl
|
||||
- errcheck
|
||||
- gocyclo
|
||||
- gosec
|
||||
- staticcheck
|
||||
- unparam
|
||||
path: _test\.go
|
||||
- linters:
|
||||
- dupl
|
||||
- errcheck
|
||||
- gocyclo
|
||||
- gosec
|
||||
path: models/gitea_migrations/v
|
||||
- linters:
|
||||
- forbidigo
|
||||
path: cmd
|
||||
- linters:
|
||||
- dupl
|
||||
text: (?i)webhook
|
||||
- linters:
|
||||
- gocritic
|
||||
text: (?i)`ID' should not be capitalized
|
||||
- linters:
|
||||
- deadcode
|
||||
- unused
|
||||
text: (?i)swagger
|
||||
- linters:
|
||||
- staticcheck
|
||||
text: (?i)argument x is overwritten before first use
|
||||
- linters:
|
||||
- gocritic
|
||||
text: '(?i)commentFormatting: put a space between `//` and comment text'
|
||||
- linters:
|
||||
- gocritic
|
||||
text: '(?i)exitAfterDefer:'
|
||||
- linters:
|
||||
- staticcheck
|
||||
text: "(ST1005|ST1003|QF1001):"
|
||||
- name: atomic
|
||||
- name: bare-return
|
||||
- name: blank-imports
|
||||
- name: constant-logical-expr
|
||||
- name: context-as-argument
|
||||
- name: context-keys-type
|
||||
- name: dot-imports
|
||||
- name: duplicated-imports
|
||||
- name: empty-lines
|
||||
- name: error-naming
|
||||
- name: error-return
|
||||
- name: error-strings
|
||||
- name: errorf
|
||||
- name: exported
|
||||
- name: identical-branches
|
||||
- name: if-return
|
||||
- name: increment-decrement
|
||||
- name: indent-error-flow
|
||||
- name: modifies-value-receiver
|
||||
- name: package-comments
|
||||
- name: range
|
||||
- name: receiver-naming
|
||||
- name: redefines-builtin-id
|
||||
- name: string-of-int
|
||||
- name: superfluous-else
|
||||
- name: time-naming
|
||||
- name: unconditional-recursion
|
||||
- name: unexported-return
|
||||
- name: unreachable-code
|
||||
- name: var-declaration
|
||||
- name: var-naming
|
||||
- name: redefines-builtin-id
|
||||
disabled: true
|
||||
gofumpt:
|
||||
extra-rules: true
|
||||
depguard:
|
||||
rules:
|
||||
main:
|
||||
deny:
|
||||
- pkg: encoding/json
|
||||
desc: use gitea's modules/json instead of encoding/json
|
||||
- pkg: github.com/unknwon/com
|
||||
desc: use gitea's util and replacements
|
||||
- pkg: io/ioutil
|
||||
desc: use os or io instead
|
||||
- pkg: golang.org/x/exp
|
||||
desc: it's experimental and unreliable
|
||||
- pkg: forgejo.org/modules/git/internal
|
||||
desc: do not use the internal package, use AddXxx function instead
|
||||
- pkg: gopkg.in/ini.v1
|
||||
desc: do not use the ini package, use gitea's config system instead
|
||||
- pkg: github.com/minio/sha256-simd
|
||||
desc: use crypto/sha256 instead, see https://codeberg.org/forgejo/forgejo/pulls/1528
|
||||
testifylint:
|
||||
disable:
|
||||
- go-require
|
||||
|
||||
# TODO: eventually remove this section entirely
|
||||
- path: cmd/admin_auth_ldap_test.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: cmd/admin_auth_oauth_test.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: cmd/admin_auth_pam_test.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: cmd/cmd.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: cmd/forgejo/actions.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/actions/run.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/actions/task.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/activities/action_list.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/asymkey/gpg_key_object_verification.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/auth/oauth2.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/db/collation.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/dbfs/dbfile.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/forgejo_migrations_legacy/v32.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/forgejo_migrations_legacy/v32_test.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/db/context.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/git/branch_list.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/git/lfs_lock.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/git/protected_branch.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/git/protected_tag.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/issues/issue.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/issues/issue_xref.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/issues/review.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/organization/org_user.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/quota/rule.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/repo/archiver.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/repo/fork.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/repo/topic.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/user/email_address.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/user/list.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/user/user.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: models/repo/repo.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: modules/git/commit.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: modules/git/foreachref/parser.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: modules/git/last_commit_cache.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: modules/git/log_name_status.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: modules/graceful/net_unix.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: modules/indexer/internal/bleve/util.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: modules/indexer/issues/util.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: modules/optional/serialization.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: modules/setting/storage.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: routers/api/packages/chef/auth.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: routers/api/packages/container/auth.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: routers/api/packages/nuget/auth.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: routers/api/packages/swift/swift.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: routers/web/auth/oauth.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: routers/web/repo/compare.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: routers/web/repo/release.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: routers/web/repo/setting/runners.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: routers/web/repo/setting/secrets.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: routers/web/repo/setting/variables.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: services/actions/context.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: services/actions/task.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: services/actions/trust.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: services/contexttest/context_tests.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: services/gitdiff/csv.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: services/issue/assignee.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: routers/api/packages/conan/auth.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: services/issue/commit.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: services/issue/issue.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: services/migrations/onedev.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: services/packages/cargo/index.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: services/pull/check.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: services/pull/comment.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: services/pull/merge.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: services/pull/review.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: services/remote/promote.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: services/repository/archiver/archiver.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: services/repository/generate_repo_commit.go
|
||||
linters:
|
||||
- nilnil
|
||||
- path: services/repository/repository.go
|
||||
linters:
|
||||
- nilnil
|
||||
paths:
|
||||
- node_modules
|
||||
- public
|
||||
- web_src
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- gofumpt
|
||||
settings:
|
||||
gofumpt:
|
||||
extra-rules: true
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- node_modules
|
||||
- public
|
||||
- web_src
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
exclude-dirs: [node_modules, public, web_src]
|
||||
exclude-case-sensitive: true
|
||||
exclude-rules:
|
||||
- path: models/db/sql_postgres_with_schema.go
|
||||
linters:
|
||||
- nolintlint
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- gocyclo
|
||||
- errcheck
|
||||
- dupl
|
||||
- gosec
|
||||
- unparam
|
||||
- staticcheck
|
||||
- path: models/migrations/v
|
||||
linters:
|
||||
- gocyclo
|
||||
- errcheck
|
||||
- dupl
|
||||
- gosec
|
||||
- path: cmd
|
||||
linters:
|
||||
- forbidigo
|
||||
- text: "webhook"
|
||||
linters:
|
||||
- dupl
|
||||
- text: "`ID' should not be capitalized"
|
||||
linters:
|
||||
- gocritic
|
||||
- text: "swagger"
|
||||
linters:
|
||||
- unused
|
||||
- deadcode
|
||||
- text: "argument x is overwritten before first use"
|
||||
linters:
|
||||
- staticcheck
|
||||
- text: "commentFormatting: put a space between `//` and comment text"
|
||||
linters:
|
||||
- gocritic
|
||||
- text: "exitAfterDefer:"
|
||||
linters:
|
||||
- gocritic
|
||||
|
|
|
|||
17
.mockery.yml
17
.mockery.yml
|
|
@ -1,17 +0,0 @@
|
|||
formatter: gofmt
|
||||
template: testify
|
||||
packages:
|
||||
forgejo.org/modules/nosql:
|
||||
config:
|
||||
filename: mocks.go # make mocks public so that external packages can use
|
||||
forgejo.org/services/auth/method:
|
||||
forgejo.org/services/authz:
|
||||
config:
|
||||
filename: authorization_reducer_mock.go # make mocks public so that external packages can use
|
||||
code.forgejo.org/go-chi/cache:
|
||||
interfaces:
|
||||
Cache:
|
||||
config:
|
||||
pkgname: cache
|
||||
dir: modules/cache
|
||||
filename: mocks.go # make mocks public, not `_test.go`, so that external packages can mock caching
|
||||
|
|
@ -1 +1 @@
|
|||
24.15.0
|
||||
22.21.1
|
||||
|
|
@ -5,10 +5,8 @@ branch-find-version: 'v(?P<version>\d+\.\d+)/forgejo'
|
|||
branch-to-version: '${version}.0'
|
||||
branch-from-version: 'v%[1]d.%[2]d/forgejo'
|
||||
tag-from-version: 'v%[1]d.%[2]d.%[3]d'
|
||||
supported-release-count: 3
|
||||
branch-known:
|
||||
# replace with v15 when v11 becomes EOL
|
||||
- 'v11.0/forgejo'
|
||||
- 'v7.0/forgejo'
|
||||
cleanup-line: 'sed -Ee "s/^(feat|fix):\s*//g" -e "s/^\[WIP\] //" -e "s/^WIP: //" -e "s;\[(UI|BUG|FEAT|v.*?/forgejo)\]\s*;;g"'
|
||||
render-header: |
|
||||
|
||||
|
|
|
|||
|
|
@ -1,111 +0,0 @@
|
|||
rules:
|
||||
- id: forgejo-api-use-resource-SearchRepoOptions
|
||||
patterns:
|
||||
- pattern: |
|
||||
repo_model.SearchRepoOptions{...}
|
||||
- pattern-not: |
|
||||
repo_model.SearchRepoOptions{
|
||||
...,
|
||||
AuthorizationReducer: ctx.Reducer,
|
||||
...
|
||||
}
|
||||
languages:
|
||||
- go
|
||||
message: >
|
||||
SearchRepoOptions does not take into account fine-grained access token limitations. Include the
|
||||
AuthorizationReducer field.
|
||||
severity: ERROR
|
||||
paths:
|
||||
include:
|
||||
- "/routers/api/**/*.go"
|
||||
|
||||
- id: forgejo-api-use-resource-SearchRepoOptions
|
||||
patterns:
|
||||
- pattern: |
|
||||
organization.SearchTeamRepoOptions{...}
|
||||
- pattern-not: |
|
||||
organization.SearchTeamRepoOptions{
|
||||
...,
|
||||
AuthorizationReducer: ctx.Reducer,
|
||||
...
|
||||
}
|
||||
languages:
|
||||
- go
|
||||
message: >
|
||||
SearchTeamRepoOptions does not take into account fine-grained access token limitations. Include the
|
||||
AuthorizationReducer field.
|
||||
severity: ERROR
|
||||
paths:
|
||||
include:
|
||||
- "/routers/api/**/*.go"
|
||||
|
||||
- id: forgejo-api-use-resource-GetUserRepoPermission
|
||||
patterns:
|
||||
- pattern: |
|
||||
$X.GetUserRepoPermission($CTX, $REPO, $DOER)
|
||||
- metavariable-type:
|
||||
metavariable: $CTX
|
||||
types:
|
||||
- "*context.APIContext"
|
||||
languages:
|
||||
- go
|
||||
message: >
|
||||
GetUserRepoPermission does not take into account fine-grained access token limitations. Use
|
||||
GetUserRepoPermissionWithReducer.
|
||||
fix: |
|
||||
$X.GetUserRepoPermissionWithReducer($CTX, $REPO, $DOER, $CTX.Reducer)
|
||||
severity: ERROR
|
||||
|
||||
- id: forgejo-api-suspicious-GetUserRepoPermission
|
||||
patterns:
|
||||
- pattern: $X.GetUserRepoPermission($CTX, $REPO, $DOER)
|
||||
- pattern-not: # don't match if identical to forgejo-api-use-resource-GetUserRepoPermission
|
||||
patterns:
|
||||
- pattern: |
|
||||
$X.GetUserRepoPermission($CTX, $REPO, $DOER)
|
||||
- metavariable-type:
|
||||
metavariable: $CTX
|
||||
types:
|
||||
- "*context.APIContext"
|
||||
languages:
|
||||
- go
|
||||
message: >
|
||||
API code is accessing GetUserRepoPermission which does not take into account fine-grained access token
|
||||
limitations. Should this use GetUserRepoPermissionWithReducer?
|
||||
severity: ERROR
|
||||
paths:
|
||||
include:
|
||||
- "/routers/api/**/*.go"
|
||||
|
||||
- id: forgejo-api-direct-IsAdmin-check
|
||||
patterns:
|
||||
- pattern: |
|
||||
ctx.Doer.IsAdmin
|
||||
languages:
|
||||
- go
|
||||
message: |
|
||||
ctx.Doer.IsAdmin does not take into account limited API access tokens. Use ctx.IsUserSiteAdmin() instead.
|
||||
fix: |
|
||||
ctx.IsUserSiteAdmin()
|
||||
severity: ERROR
|
||||
paths:
|
||||
include:
|
||||
- "/routers/api/**/*.go"
|
||||
|
||||
- id: forgejo-api-direct-repo-Admin-check
|
||||
patterns:
|
||||
- pattern: |
|
||||
ctx.Repo.IsAdmin()
|
||||
- pattern: |
|
||||
ctx.Repo.IsOwner()
|
||||
languages:
|
||||
- go
|
||||
message: |
|
||||
ctx.Repo.IsAdmin/IsOwner() does not take into account limited API access tokens. Use ctx.IsUserRepoAdmin() instead.
|
||||
fix: |
|
||||
ctx.IsUserRepoAdmin()
|
||||
severity: ERROR
|
||||
paths:
|
||||
include:
|
||||
- "/routers/api/**/*.go"
|
||||
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
rules:
|
||||
- id: forgejo-switch-empty-case
|
||||
pattern-either:
|
||||
- pattern: |-
|
||||
switch $_ {
|
||||
case $_:
|
||||
}
|
||||
- patterns:
|
||||
- pattern: |-
|
||||
switch {
|
||||
case $_:
|
||||
}
|
||||
languages:
|
||||
- go
|
||||
severity: ERROR
|
||||
message: >
|
||||
switch has a case block with no content. This is treated as "break" by Go, but developers may confuse it for
|
||||
"fallthrough". To fix this error, disambiguate by using "break" or "fallthrough".
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
rules:
|
||||
- id: forgejo-logic-suspicious-OwnerID-check
|
||||
pattern: |-
|
||||
$X.OwnerID > 0
|
||||
languages:
|
||||
- go
|
||||
severity: ERROR
|
||||
message: >
|
||||
Many resources like comments or runners cannot only be owned by regular users, which have positive IDs, but also
|
||||
by predefined system users like Ghost or Forgejo Actions that have negative IDs. In those cases, ownership checks
|
||||
should only exclude 0: `OwnerID != 0`.
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
rules:
|
||||
- id: xorm-sync-missing-ignore-drop-indices
|
||||
patterns:
|
||||
- pattern-either:
|
||||
- pattern: |
|
||||
$X.Sync(...)
|
||||
- pattern: |
|
||||
$X.SyncWithOptions($OPTS, ...)
|
||||
- pattern-not: |
|
||||
$X.SyncWithOptions(xorm.SyncOptions{..., IgnoreDropIndices: true, ...}, ...)
|
||||
- metavariable-type:
|
||||
metavariable: $X
|
||||
types:
|
||||
- "*xorm.Engine"
|
||||
- "*xorm.Session"
|
||||
paths:
|
||||
exclude:
|
||||
- /models/gitea_migrations/**/*.go
|
||||
- /models/forgejo_migrations_legacy/**/*.go
|
||||
languages:
|
||||
- go
|
||||
message: |
|
||||
xorm Sync operation may drop indices if used on an incomplete bean definition for an existing table. Use SyncWithOptions with IgnoreDropIndices: true instead.
|
||||
severity: ERROR
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
// Copyright 2016 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2020 The Gitea Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forgejo.org/models/db"
|
||||
access_model "forgejo.org/models/perm/access"
|
||||
repo_model "forgejo.org/models/repo"
|
||||
api "forgejo.org/modules/structs"
|
||||
"forgejo.org/routers/api/v1/utils"
|
||||
"forgejo.org/services/context"
|
||||
"forgejo.org/services/convert"
|
||||
)
|
||||
|
||||
// ListForks list a repository's forks
|
||||
func ListForks(ctx *context.APIContext) {
|
||||
forks, total, err := repo_model.GetForks(ctx, ctx.Repo.Repository, ctx.Doer, utils.GetListOptions(ctx))
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetForks", err)
|
||||
return
|
||||
}
|
||||
apiForks := make([]*api.Repository, len(forks))
|
||||
for i, fork := range forks {
|
||||
// ruleid:forgejo-api-use-resource-GetUserRepoPermission
|
||||
permission, err := access_model.GetUserRepoPermissionWithReducer(ctx, fork, ctx.Doer, ctx.Reducer)
|
||||
// ok:forgejo-api-use-resource-GetUserRepoPermission
|
||||
permission, err := access_model.GetUserRepoPermissionWithReducer(ctx, fork, ctx.Doer, ctx.Reducer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
|
||||
return
|
||||
}
|
||||
apiForks[i] = convert.ToRepo(ctx, fork, permission)
|
||||
}
|
||||
}
|
||||
|
||||
// getStarredRepos returns the repos that the user with the specified userID has
|
||||
// starred
|
||||
func getStarredRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, error) {
|
||||
starredRepos, err := repo_model.GetStarredRepos(ctx, user.ID, private, listOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repos := make([]*api.Repository, len(starredRepos))
|
||||
for i, starred := range starredRepos {
|
||||
// ruleid:forgejo-api-suspicious-GetUserRepoPermission
|
||||
permission, err := access_model.GetUserRepoPermission(ctx, starred, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repos[i] = convert.ToRepo(ctx, starred, permission)
|
||||
}
|
||||
return repos, nil
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
// Copyright 2016 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2020 The Gitea Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forgejo.org/models/db"
|
||||
access_model "forgejo.org/models/perm/access"
|
||||
repo_model "forgejo.org/models/repo"
|
||||
api "forgejo.org/modules/structs"
|
||||
"forgejo.org/routers/api/v1/utils"
|
||||
"forgejo.org/services/context"
|
||||
"forgejo.org/services/convert"
|
||||
)
|
||||
|
||||
// ListForks list a repository's forks
|
||||
func ListForks(ctx *context.APIContext) {
|
||||
forks, total, err := repo_model.GetForks(ctx, ctx.Repo.Repository, ctx.Doer, utils.GetListOptions(ctx))
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetForks", err)
|
||||
return
|
||||
}
|
||||
apiForks := make([]*api.Repository, len(forks))
|
||||
for i, fork := range forks {
|
||||
// ruleid:forgejo-api-use-resource-GetUserRepoPermission
|
||||
permission, err := access_model.GetUserRepoPermission(ctx, fork, ctx.Doer)
|
||||
// ok:forgejo-api-use-resource-GetUserRepoPermission
|
||||
permission, err := access_model.GetUserRepoPermissionWithReducer(ctx, fork, ctx.Doer, ctx.Reducer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
|
||||
return
|
||||
}
|
||||
apiForks[i] = convert.ToRepo(ctx, fork, permission)
|
||||
}
|
||||
}
|
||||
|
||||
// getStarredRepos returns the repos that the user with the specified userID has
|
||||
// starred
|
||||
func getStarredRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, error) {
|
||||
starredRepos, err := repo_model.GetStarredRepos(ctx, user.ID, private, listOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repos := make([]*api.Repository, len(starredRepos))
|
||||
for i, starred := range starredRepos {
|
||||
// ruleid:forgejo-api-suspicious-GetUserRepoPermission
|
||||
permission, err := access_model.GetUserRepoPermission(ctx, starred, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repos[i] = convert.ToRepo(ctx, starred, permission)
|
||||
}
|
||||
return repos, nil
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
_ "net/http/pprof" // Used for debugging if enabled and a web server is running
|
||||
|
||||
"forgejo.org/modules/setting"
|
||||
)
|
||||
|
||||
func setPortEmptyCaseBad(port string) error {
|
||||
setting.AppURL = strings.Replace(setting.AppURL, setting.HTTPPort, port, 1)
|
||||
setting.HTTPPort = port
|
||||
|
||||
// ruleid:forgejo-switch-empty-case
|
||||
switch setting.Protocol {
|
||||
case setting.HTTPUnix:
|
||||
case setting.FCGI:
|
||||
case setting.FCGIUnix:
|
||||
default:
|
||||
defaultLocalURL := string(setting.Protocol) + "://"
|
||||
}
|
||||
|
||||
// ok:forgejo-switch-empty-case
|
||||
switch setting.Protocol {
|
||||
case setting.HTTPUnix:
|
||||
break
|
||||
case setting.FCGI:
|
||||
break
|
||||
case setting.FCGIUnix:
|
||||
break
|
||||
default:
|
||||
defaultLocalURL := string(setting.Protocol) + "://"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import "xorm.io/builder"
|
||||
|
||||
type FindRunJobOptions struct {
|
||||
RepoID int64
|
||||
OwnerID int64
|
||||
}
|
||||
|
||||
func (opts FindRunJobOptions) Bad() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.RepoID > 0 {
|
||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||
}
|
||||
// ruleid:forgejo-logic-suspicious-OwnerID-check
|
||||
if opts.OwnerID > 0 {
|
||||
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
func (opts FindRunJobOptions) Good() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.RepoID > 0 {
|
||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||
}
|
||||
// ok:forgejo-logic-suspicious-OwnerID-check
|
||||
if opts.OwnerID != 0 {
|
||||
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package forgejo_migrations
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
|
||||
"forgejo.org/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type ActionUser struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UserID int64 `xorm:"INDEX UNIQUE(action_user_index) REFERENCES(user, id)"`
|
||||
RepoID int64 `xorm:"INDEX UNIQUE(action_user_index) REFERENCES(repository, id)"`
|
||||
|
||||
TrustedWithPullRequests bool
|
||||
|
||||
LastAccess timeutil.TimeStamp `xorm:"INDEX"`
|
||||
}
|
||||
|
||||
func testSyncBad1(x *xorm.Engine) error {
|
||||
// ruleid:xorm-sync-missing-ignore-drop-indices
|
||||
return x.Sync(new(ActionUser))
|
||||
}
|
||||
|
||||
func testSyncBad2(x *xorm.Engine) error {
|
||||
// ruleid:xorm-sync-missing-ignore-drop-indices
|
||||
_, err = x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: false}, bean)
|
||||
return err
|
||||
}
|
||||
|
||||
func testSyncGood1(x *xorm.Engine) error {
|
||||
// ok:xorm-sync-missing-ignore-drop-indices
|
||||
_, err = x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, bean)
|
||||
return err
|
||||
}
|
||||
|
||||
func testSyncGood2(x *fs.File) error {
|
||||
// ok:xorm-sync-missing-ignore-drop-indices
|
||||
_, err = x.Sync()
|
||||
return err
|
||||
}
|
||||
17
BSDmakefile
17
BSDmakefile
|
|
@ -36,6 +36,10 @@ GARGS = "--no-print-directory"
|
|||
JARG = -j$(.MAKE.JOBS)
|
||||
.endif
|
||||
|
||||
# bmake prefers out-of-source builds and tries to cd into ./obj (among others)
|
||||
# where possible. GNU Make doesn't, so override that value.
|
||||
.OBJDIR: ./
|
||||
|
||||
# The GNU convention is to use the lowercased `prefix` variable/macro to
|
||||
# specify the installation directory. Humor them.
|
||||
GPREFIX =
|
||||
|
|
@ -44,12 +48,11 @@ GPREFIX =
|
|||
.endif
|
||||
|
||||
.BEGIN: .SILENT
|
||||
which $(GMAKE) >/dev/null || (printf "Error: GNU Make is required!\n\n" 1>&2 && false)
|
||||
which $(GMAKE) || (printf "Error: GNU Make is required!\n\n" 1>&2 && false)
|
||||
|
||||
.PHONY: EMPTY
|
||||
EMPTY: .SILENT
|
||||
$(GMAKE) $(GPREFIX) $(GARGS) $(JARG)
|
||||
.PHONY: FRC
|
||||
$(.TARGETS): FRC
|
||||
$(GMAKE) $(GPREFIX) $(GARGS) $(.TARGETS:S,.DONE,,) $(JARG)
|
||||
|
||||
.PHONY: $(.TARGETS)
|
||||
$(.TARGETS): .SILENT
|
||||
$(GMAKE) $(GPREFIX) $(GARGS) $(JARG) $@
|
||||
.DONE .DEFAULT: .SILENT
|
||||
$(GMAKE) $(GPREFIX) $(GARGS) $(.TARGETS:S,.DONE,,) $(JARG)
|
||||
|
|
|
|||
25
CODEOWNERS
25
CODEOWNERS
|
|
@ -9,11 +9,10 @@
|
|||
# Files related to frontend development.
|
||||
|
||||
# Javascript and CSS code.
|
||||
web_src/.* @beowulf @gusted
|
||||
web_src/css/.* @0ko
|
||||
web_src/.* @caesar @crystal @gusted
|
||||
|
||||
# HTML templates used by the backend.
|
||||
templates/.* @beowulf @gusted
|
||||
templates/.* @caesar @crystal @gusted
|
||||
## the issue sidebar was touched by fnetx
|
||||
templates/repo/issue/view_content/sidebar.* @fnetx
|
||||
|
||||
|
|
@ -38,25 +37,5 @@ routers/.* @gusted
|
|||
options/locale/.* @0ko
|
||||
options/locale_next/.* @0ko
|
||||
|
||||
# lint-locale-usage
|
||||
build/lint-locale-usage/.* @fogti
|
||||
models/unit/.* @fogti
|
||||
services/migrations/lint-locale-usage/.* @fogti
|
||||
|
||||
# Personal interest
|
||||
.*/webhook.* @oliverpool
|
||||
|
||||
# Often work with, and on, the API
|
||||
modules/structs/.* @Cyborus
|
||||
routers/api/v1/.* @Cyborus
|
||||
routers/api/forgejo/.* @Cyborus
|
||||
tests/integration/api_.* @Cyborus
|
||||
|
||||
# Federation code, requires care to be taken with regards to interoperability
|
||||
# and backwards compatibility due to the way signatures work.
|
||||
services/federation/.* @famfo @0xllx0
|
||||
modules/forgefed/.* @famfo @0xllx0
|
||||
models/forgefed/.* @famfo @0xllx0
|
||||
routers/api/v1/activitypub/.* @famfo @0xllx0
|
||||
tests/integration/api_activitypub_.* @famfo @0xllx0
|
||||
tests/integration/activitypub_.* @famfo @0xllx0
|
||||
|
|
|
|||
10
Dockerfile
10
Dockerfile
|
|
@ -1,9 +1,9 @@
|
|||
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/xx AS xx
|
||||
|
||||
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.26-alpine3.23 AS build-env
|
||||
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.25-alpine3.22 AS build-env
|
||||
|
||||
ARG GOPROXY
|
||||
ENV GOPROXY=${GOPROXY:-https://proxy.golang.org,direct}
|
||||
ENV GOPROXY=${GOPROXY:-direct}
|
||||
|
||||
ARG RELEASE_VERSION
|
||||
ARG TAGS="sqlite sqlite_unlock_notify"
|
||||
|
|
@ -33,10 +33,10 @@ RUN apk --no-cache add build-base git nodejs npm
|
|||
COPY . ${GOPATH}/src/forgejo.org
|
||||
WORKDIR ${GOPATH}/src/forgejo.org
|
||||
|
||||
RUN make clean-no-bindata
|
||||
RUN make clean
|
||||
RUN make frontend
|
||||
RUN go build contrib/environment-to-ini/environment-to-ini.go && xx-verify environment-to-ini
|
||||
RUN LDFLAGS="-buildid=" make FORGEJO_GENERATE_SKIP_HASH=true RELEASE_VERSION=$RELEASE_VERSION GOFLAGS="-trimpath" go-check generate-backend static-executable && xx-verify gitea
|
||||
RUN LDFLAGS="-buildid=" make RELEASE_VERSION=$RELEASE_VERSION GOFLAGS="-trimpath" go-check generate-backend static-executable && xx-verify gitea
|
||||
|
||||
# Copy local files
|
||||
COPY docker/root /tmp/local
|
||||
|
|
@ -51,7 +51,7 @@ RUN chmod 755 /tmp/local/usr/bin/entrypoint \
|
|||
/go/src/forgejo.org/environment-to-ini
|
||||
RUN chmod 644 /go/src/forgejo.org/contrib/autocompletion/bash_autocomplete
|
||||
|
||||
FROM data.forgejo.org/oci/alpine:3.23
|
||||
FROM data.forgejo.org/oci/alpine:3.22
|
||||
ARG RELEASE_VERSION
|
||||
LABEL maintainer="contact@forgejo.org" \
|
||||
org.opencontainers.image.authors="Forgejo" \
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/xx AS xx
|
||||
|
||||
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.26-alpine3.23 AS build-env
|
||||
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.25-alpine3.22 AS build-env
|
||||
|
||||
ARG GOPROXY
|
||||
ENV GOPROXY=${GOPROXY:-https://proxy.golang.org,direct}
|
||||
ENV GOPROXY=${GOPROXY:-direct}
|
||||
|
||||
ARG RELEASE_VERSION
|
||||
ARG TAGS="sqlite sqlite_unlock_notify"
|
||||
|
|
@ -33,10 +33,10 @@ RUN apk --no-cache add build-base git nodejs npm
|
|||
COPY . ${GOPATH}/src/forgejo.org
|
||||
WORKDIR ${GOPATH}/src/forgejo.org
|
||||
|
||||
RUN make clean-no-bindata
|
||||
RUN make clean
|
||||
RUN make frontend
|
||||
RUN go build contrib/environment-to-ini/environment-to-ini.go && xx-verify environment-to-ini
|
||||
RUN make FORGEJO_GENERATE_SKIP_HASH=true RELEASE_VERSION=$RELEASE_VERSION go-check generate-backend static-executable && xx-verify gitea
|
||||
RUN make RELEASE_VERSION=$RELEASE_VERSION go-check generate-backend static-executable && xx-verify gitea
|
||||
|
||||
# Copy local files
|
||||
COPY docker/rootless /tmp/local
|
||||
|
|
@ -49,7 +49,7 @@ RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \
|
|||
/go/src/forgejo.org/environment-to-ini
|
||||
RUN chmod 644 /go/src/forgejo.org/contrib/autocompletion/bash_autocomplete
|
||||
|
||||
FROM data.forgejo.org/oci/alpine:3.23
|
||||
FROM data.forgejo.org/oci/alpine:3.22
|
||||
ARG RELEASE_VERSION
|
||||
LABEL maintainer="contact@forgejo.org" \
|
||||
org.opencontainers.image.authors="Forgejo" \
|
||||
|
|
@ -86,8 +86,8 @@ RUN addgroup \
|
|||
-G git \
|
||||
git
|
||||
|
||||
RUN mkdir -p /var/lib/gitea
|
||||
RUN chown git:git /var/lib/gitea
|
||||
RUN mkdir -p /var/lib/gitea /etc/gitea
|
||||
RUN chown git:git /var/lib/gitea /etc/gitea
|
||||
|
||||
COPY --from=build-env /tmp/local /
|
||||
RUN cd /usr/local/bin ; ln -s gitea forgejo
|
||||
|
|
@ -103,9 +103,13 @@ ENV GITEA_CUSTOM=/var/lib/gitea/custom
|
|||
ENV GITEA_TEMP=/tmp/gitea
|
||||
ENV TMPDIR=/tmp/gitea
|
||||
|
||||
# Legacy config file for backwards compatibility
|
||||
# TODO: remove on next major version release
|
||||
ENV GITEA_APP_INI_LEGACY=/etc/gitea/app.ini
|
||||
|
||||
ENV GITEA_APP_INI=${GITEA_CUSTOM}/conf/app.ini
|
||||
ENV HOME="/var/lib/gitea/git"
|
||||
VOLUME ["/var/lib/gitea"]
|
||||
VOLUME ["/var/lib/gitea", "/etc/gitea"]
|
||||
WORKDIR /var/lib/gitea
|
||||
|
||||
ENTRYPOINT ["/usr/bin/dumb-init", "--", "/usr/local/bin/docker-entrypoint.sh"]
|
||||
|
|
|
|||
306
Makefile
306
Makefile
|
|
@ -37,18 +37,19 @@ endif
|
|||
XGO_VERSION := go-1.21.x
|
||||
|
||||
AIR_PACKAGE ?= github.com/air-verse/air@v1 # renovate: datasource=go
|
||||
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.6.1 # renovate: datasource=go
|
||||
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2 # renovate: datasource=go
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4 # renovate: datasource=go
|
||||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 # renovate: datasource=go
|
||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.2 # renovate: datasource=go
|
||||
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.2.1 # renovate: datasource=go
|
||||
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.7.0 # renovate: datasource=go
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.7 # renovate: datasource=go
|
||||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11 # renovate: datasource=go
|
||||
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.6.0 # renovate: datasource=go
|
||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1 # renovate: datasource=go
|
||||
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
|
||||
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses/v2@v2.0.1 # renovate: datasource=go
|
||||
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1.6.0 # renovate: datasource=go
|
||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 # renovate: datasource=go
|
||||
DEADCODE_PACKAGE ?= golang.org/x/tools/cmd/deadcode@v0.45.0 # renovate: datasource=go
|
||||
ERRORTYPE_PACKAGE ?= fillmore-labs.com/errortype@v0.0.11 # renovate: datasource=go
|
||||
RENOVATE_NPM_PACKAGE ?= renovate@43.170.20 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate
|
||||
MOCKERY_PACKAGE ?= github.com/vektra/mockery/v3@v3.7.0 # renovate: datasource=go
|
||||
DEADCODE_PACKAGE ?= golang.org/x/tools/cmd/deadcode@v0.31.0 # renovate: datasource=go
|
||||
GOMOCK_PACKAGE ?= go.uber.org/mock/mockgen@v0.4.0 # renovate: datasource=go
|
||||
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.20.0 # renovate: datasource=go
|
||||
RENOVATE_NPM_PACKAGE ?= renovate@39.212.0 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate
|
||||
|
||||
# https://github.com/disposable-email-domains/disposable-email-domains/commits/main/
|
||||
DISPOSABLE_EMAILS_SHA ?= 0c27e671231d27cf66370034d7f6818037416989 # renovate: ...
|
||||
|
|
@ -91,22 +92,29 @@ else
|
|||
FORGEJO_VERSION_API ?= $(GITEA_VERSION)+${GITEA_COMPATIBILITY}
|
||||
else
|
||||
# drop the "g" prefix prepended by git describe to the commit hash
|
||||
FORGEJO_VERSION ?= $(shell git describe --exclude '*-test' --tags --always 2>/dev/null | sed 's/^v//' | sed 's/\-g/-/')
|
||||
ifneq ($(FORGEJO_VERSION),)
|
||||
ifeq ($(findstring $(GITEA_COMPATIBILITY),$(FORGEJO_VERSION)),)
|
||||
FORGEJO_VERSION := $(FORGEJO_VERSION)+$(GITEA_COMPATIBILITY)
|
||||
endif
|
||||
endif
|
||||
FORGEJO_VERSION ?= $(shell git describe --exclude '*-test' --tags --always | sed 's/^v//' | sed 's/\-g/-/')+${GITEA_COMPATIBILITY}
|
||||
endif
|
||||
endif
|
||||
FORGEJO_VERSION_MAJOR=$(shell echo $(FORGEJO_VERSION) | sed -e 's/\..*//')
|
||||
FORGEJO_VERSION_MINOR=$(shell echo $(FORGEJO_VERSION) | sed -E -e 's/^([0-9]+\.[0-9]+).*/\1/')
|
||||
|
||||
show-version-full:
|
||||
@echo ${FORGEJO_VERSION}
|
||||
|
||||
show-version-major:
|
||||
@echo ${FORGEJO_VERSION_MAJOR}
|
||||
|
||||
show-version-minor:
|
||||
@echo ${FORGEJO_VERSION_MINOR}
|
||||
|
||||
RELEASE_VERSION ?= ${FORGEJO_VERSION}
|
||||
VERSION ?= ${RELEASE_VERSION}
|
||||
|
||||
FORGEJO_VERSION_API ?= ${FORGEJO_VERSION}
|
||||
|
||||
show-version-api:
|
||||
@echo ${FORGEJO_VERSION_API}
|
||||
|
||||
# Strip binaries by default to reduce size, allow overriding for debugging
|
||||
STRIP ?= 1
|
||||
ifeq ($(STRIP),1)
|
||||
|
|
@ -116,6 +124,9 @@ LDFLAGS := $(LDFLAGS) -X "main.ReleaseVersion=$(RELEASE_VERSION)" -X "main.MakeV
|
|||
|
||||
LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64
|
||||
|
||||
ifeq ($(HAS_GO), yes)
|
||||
GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list forgejo.org/models/migrations/...) $(shell $(GO) list forgejo.org/models/forgejo_migrations/...) forgejo.org/tests/integration/migration-test forgejo.org/tests forgejo.org/tests/integration forgejo.org/tests/e2e,$(shell $(GO) list ./...))
|
||||
endif
|
||||
REMOTE_CACHER_MODULES ?= cache nosql session queue
|
||||
GO_TEST_REMOTE_CACHER_PACKAGES ?= $(addprefix forgejo.org/modules/,$(REMOTE_CACHER_MODULES))
|
||||
|
||||
|
|
@ -126,7 +137,7 @@ WEBPACK_CONFIGS := webpack.config.js tailwind.config.js
|
|||
WEBPACK_DEST := public/assets/js/index.js public/assets/css/index.css
|
||||
WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts
|
||||
|
||||
BINDATA_DEST := modules/migration/bindata.go modules/public/bindata.go modules/options/bindata.go modules/templates/bindata.go
|
||||
BINDATA_DEST := modules/public/bindata.go modules/options/bindata.go modules/templates/bindata.go
|
||||
BINDATA_HASH := $(addsuffix .hash,$(BINDATA_DEST))
|
||||
|
||||
GENERATED_GO_DEST := modules/charset/invisible_gen.go modules/charset/ambiguous_gen.go
|
||||
|
|
@ -157,6 +168,9 @@ GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go" ! -path modules/optio
|
|||
GO_SOURCES += $(GENERATED_GO_DEST)
|
||||
GO_SOURCES_NO_BINDATA := $(GO_SOURCES)
|
||||
|
||||
ifeq ($(HAS_GO), yes)
|
||||
MIGRATION_PACKAGES := $(shell $(GO) list forgejo.org/models/migrations/... forgejo.org/models/forgejo_migrations/...)
|
||||
endif
|
||||
|
||||
ifeq ($(filter $(TAGS_SPLIT),bindata),bindata)
|
||||
GO_SOURCES += $(BINDATA_DEST)
|
||||
|
|
@ -194,7 +208,7 @@ all: build
|
|||
.PHONY: help
|
||||
help:
|
||||
@echo "Make Routines:"
|
||||
@echo " - \"\" equivalent to \"build\""
|
||||
@echo " - \"\" equivalent to \"build\""
|
||||
@echo " - build build everything"
|
||||
@echo " - frontend build frontend files"
|
||||
@echo " - backend build backend files"
|
||||
|
|
@ -207,22 +221,31 @@ help:
|
|||
@echo " - deps-frontend install frontend dependencies"
|
||||
@echo " - deps-backend install backend dependencies"
|
||||
@echo " - deps-tools install tool dependencies"
|
||||
@echo " - deps-py install python dependencies"
|
||||
@echo " - lint lint everything"
|
||||
@echo " - lint-fix lint everything and fix issues"
|
||||
@echo " - lint-frontend lint frontend files"
|
||||
@echo " - lint-frontend-fix lint frontend files and fix issues"
|
||||
@echo " - lint-backend lint backend files"
|
||||
@echo " - lint-backend-fix lint backend files and fix issues"
|
||||
@echo " - lint-codespell lint typos"
|
||||
@echo " - lint-codespell-fix lint typos and fix them automatically"
|
||||
@echo " - lint-codespell-fix-i lint typos and fix them interactively"
|
||||
@echo " - lint-go lint go files"
|
||||
@echo " - lint-go-fix lint go files and fix issues"
|
||||
@echo " - lint-go-vet lint go files with vet"
|
||||
@echo " - lint-go-gopls lint go files with gopls"
|
||||
@echo " - lint-js lint js files"
|
||||
@echo " - lint-js-fix lint js files and fix issues"
|
||||
@echo " - lint-css lint css files"
|
||||
@echo " - lint-css-fix lint css files and fix issues"
|
||||
@echo " - lint-md lint markdown files"
|
||||
@echo " - lint-swagger lint swagger files"
|
||||
@echo " - lint-templates lint template files"
|
||||
@echo " - lint-renovate lint renovate files"
|
||||
@echo " - lint-yaml lint yaml files"
|
||||
@echo " - lint-spell lint spelling"
|
||||
@echo " - lint-spell-fix lint spelling and fix issues"
|
||||
@echo " - checks run various consistency checks"
|
||||
@echo " - checks-frontend check frontend files"
|
||||
@echo " - checks-backend check backend files"
|
||||
|
|
@ -233,9 +256,6 @@ help:
|
|||
@echo " - test-frontend-coverage test frontend files and display code coverage"
|
||||
@echo " - test-backend test backend files"
|
||||
@echo " - test-remote-cacher test backend files that use a remote cache"
|
||||
@echo " - coverage-run* test and collect coverages in the coverage/data directory"
|
||||
@echo " - coverage-show-html display coverage-run results in an HTML page"
|
||||
@echo " - coverage-show-percent display coverage-run per package coverage percentage"
|
||||
@echo " - test-e2e-sqlite[\#name.test.e2e] test end to end using playwright and sqlite"
|
||||
@echo " - webpack build webpack files"
|
||||
@echo " - svg build svg files"
|
||||
|
|
@ -245,7 +265,7 @@ help:
|
|||
@echo " - generate-license update license files"
|
||||
@echo " - generate-gitignore update gitignore files"
|
||||
@echo " - generate-manpage generate manpage"
|
||||
@echo " - generate-mockery generate mockery files"
|
||||
@echo " - generate-gomock generate gomock files"
|
||||
@echo " - generate-forgejo-api generate the forgejo API from spec"
|
||||
@echo " - forgejo-api-validate check if the forgejo API matches the specs"
|
||||
@echo " - generate-swagger generate the swagger spec from code comments"
|
||||
|
|
@ -256,48 +276,6 @@ help:
|
|||
@echo " - test-sqlite[\#TestSpecificName] run integration test for sqlite"
|
||||
@echo " - reproduce-build\#version build a reproducible binary for the specified release version"
|
||||
|
||||
.PHONY: verify-version
|
||||
verify-version:
|
||||
ifeq ($(FORGEJO_VERSION),)
|
||||
@echo "Error: Could not determine FORGEJO_VERSION; version file $(STORED_VERSION_FILE) not present and no suitable git tag found"
|
||||
@echo 'In most cases this likely means you forgot to fetch git tags, you can fix this by executing `git fetch --tags`. If this is not possible and this is part of a custom build process, then you can set a specific version by writing it to $(STORED_VERSION_FILE) (This must be a semver compatible version).'
|
||||
@false
|
||||
endif
|
||||
|
||||
.PHONY: show-version-full
|
||||
show-version-full: verify-version
|
||||
@echo ${FORGEJO_VERSION}
|
||||
|
||||
.PHONY: show-version-major
|
||||
show-version-major: verify-version
|
||||
@echo ${FORGEJO_VERSION_MAJOR}
|
||||
|
||||
.PHONY: show-version-minor
|
||||
show-version-minor: verify-version
|
||||
@echo ${FORGEJO_VERSION_MINOR}
|
||||
|
||||
.PHONY: show-version-api
|
||||
show-version-api: verify-version
|
||||
@echo ${FORGEJO_VERSION_API}
|
||||
|
||||
###
|
||||
# Package computation targets
|
||||
###
|
||||
|
||||
# Target to compute GO_TEST_PACKAGES - only runs when needed
|
||||
.PHONY: compute-go-test-packages
|
||||
compute-go-test-packages:
|
||||
ifeq ($(HAS_GO), yes)
|
||||
$(eval GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list forgejo.org/models/gitea_migrations/...) $(shell $(GO) list forgejo.org/models/forgejo_migrations_legacy/...) $(shell $(GO) list forgejo.org/models/forgejo_migrations/...) forgejo.org/tests/integration/migration-test forgejo.org/tests forgejo.org/tests/integration forgejo.org/tests/e2e,$(shell $(GO) list ./...)))
|
||||
endif
|
||||
|
||||
# Target to compute MIGRATION_PACKAGES - only runs when needed
|
||||
.PHONY: compute-migration-packages
|
||||
compute-migration-packages:
|
||||
ifeq ($(HAS_GO), yes)
|
||||
$(eval MIGRATION_PACKAGES := $(shell $(GO) list forgejo.org/models/gitea_migrations/... forgejo.org/models/forgejo_migrations_legacy/... forgejo.org/models/forgejo_migrations/...))
|
||||
endif
|
||||
|
||||
###
|
||||
# Check system and environment requirements
|
||||
###
|
||||
|
|
@ -323,7 +301,7 @@ git-check:
|
|||
node-check:
|
||||
$(eval MIN_NODE_VERSION_STR := $(shell grep -Eo '"node":.*[0-9.]+"' package.json | sed -n 's/.*[^0-9.]\([0-9.]*\)"/\1/p'))
|
||||
$(eval MIN_NODE_VERSION := $(shell printf "%03d%03d%03d" $(shell echo '$(MIN_NODE_VERSION_STR)' | tr '.' ' ')))
|
||||
$(eval NODE_VERSION := $(shell printf "%03d%03d%03d" $(shell node -v | cut -c2- | sed 's:-.*::' | tr '.' ' ');))
|
||||
$(eval NODE_VERSION := $(shell printf "%03d%03d%03d" $(shell node -v | cut -c2- | tr '.' ' ');))
|
||||
$(eval NPM_MISSING := $(shell hash npm > /dev/null 2>&1 || echo 1))
|
||||
@if [ "$(NODE_VERSION)" -lt "$(MIN_NODE_VERSION)" -o "$(NPM_MISSING)" = "1" ]; then \
|
||||
echo "Forgejo requires Node.js $(MIN_NODE_VERSION_STR) or greater and npm to build. You can get it at https://nodejs.org/en/download/"; \
|
||||
|
|
@ -339,12 +317,8 @@ clean-all: clean
|
|||
rm -rf $(WEBPACK_DEST_ENTRIES) node_modules
|
||||
|
||||
.PHONY: clean
|
||||
clean: clean-no-bindata
|
||||
rm -rf $(BINDATA_DEST) $(BINDATA_HASH)
|
||||
|
||||
.PHONY: clean-no-bindata
|
||||
clean-no-bindata:
|
||||
rm -rf $(EXECUTABLE) $(DIST) \
|
||||
clean:
|
||||
rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST) $(BINDATA_HASH) \
|
||||
integrations*.test \
|
||||
e2e*.test \
|
||||
tests/integration/gitea-integration-* \
|
||||
|
|
@ -427,13 +401,13 @@ checks-frontend: lockfile-check svg-check
|
|||
checks-backend: tidy-check swagger-check fmt-check swagger-validate security-check
|
||||
|
||||
.PHONY: lint
|
||||
lint: lint-frontend lint-backend
|
||||
lint: lint-frontend lint-backend lint-spell
|
||||
|
||||
.PHONY: lint-fix
|
||||
lint-fix: lint-frontend-fix lint-backend-fix
|
||||
lint-fix: lint-frontend-fix lint-backend-fix lint-spell-fix
|
||||
|
||||
.PHONY: lint-frontend
|
||||
lint-frontend: lint-js tsc lint-css
|
||||
lint-frontend: lint-js lint-css
|
||||
|
||||
.PHONY: lint-frontend-fix
|
||||
lint-frontend-fix: lint-js-fix lint-css-fix
|
||||
|
|
@ -444,6 +418,18 @@ lint-backend: lint-go lint-go-vet lint-editorconfig lint-renovate lint-locale li
|
|||
.PHONY: lint-backend-fix
|
||||
lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig lint-disposable-emails-fix
|
||||
|
||||
.PHONY: lint-codespell
|
||||
lint-codespell: deps-py
|
||||
@poetry run codespell
|
||||
|
||||
.PHONY: lint-codespell-fix
|
||||
lint-codespell-fix: deps-py
|
||||
@poetry run codespell -w
|
||||
|
||||
.PHONY: lint-codespell-fix-i
|
||||
lint-codespell-fix-i: deps-py
|
||||
@poetry run codespell -w -i 3 -C 2
|
||||
|
||||
.PHONY: lint-js
|
||||
lint-js: node_modules
|
||||
npx eslint --color --max-warnings=0
|
||||
|
|
@ -466,8 +452,8 @@ lint-swagger: node_modules
|
|||
|
||||
.PHONY: lint-renovate
|
||||
lint-renovate: node_modules
|
||||
npx --yes --package $(RENOVATE_NPM_PACKAGE) -- renovate-config-validator --no-global .forgejo/renovate.json > .lint-renovate 2>&1 || true
|
||||
@if grep --quiet --extended-regexp -e '^( ERROR:)' .lint-renovate ; then cat .lint-renovate ; rm .lint-renovate ; exit 1 ; fi
|
||||
npx --yes --package $(RENOVATE_NPM_PACKAGE) -- renovate-config-validator --strict > .lint-renovate 2>&1 || true
|
||||
@if grep --quiet --extended-regexp -e '^( WARN:|ERROR:)' .lint-renovate ; then cat .lint-renovate ; rm .lint-renovate ; exit 1 ; fi
|
||||
@rm .lint-renovate
|
||||
|
||||
.PHONY: lint-locale
|
||||
|
|
@ -476,33 +462,28 @@ lint-locale:
|
|||
|
||||
.PHONY: lint-locale-usage
|
||||
lint-locale-usage:
|
||||
$(GO) run ./build/lint-locale-usage/bin --allow-masked-usages-from=build/lint-locale-usage/allowed-masked-usage.txt
|
||||
$(GO) run build/lint-locale-usage/lint-locale-usage.go --allow-missing-msgids
|
||||
|
||||
.PHONY: lint-md
|
||||
lint-md: node_modules
|
||||
npx markdownlint docs *.md
|
||||
|
||||
.PHONY: lint-spell
|
||||
lint-spell: lint-codespell
|
||||
@go run $(MISSPELL_PACKAGE) -error $(SPELLCHECK_FILES)
|
||||
|
||||
.PHONY: lint-spell-fix
|
||||
lint-spell-fix: lint-codespell-fix
|
||||
@go run $(MISSPELL_PACKAGE) -w $(SPELLCHECK_FILES)
|
||||
|
||||
RUN_DEADCODE = $(GO) run $(DEADCODE_PACKAGE) -generated=false -f='{{println .Path}}{{range .Funcs}}{{printf "\t%s\n" .Name}}{{end}}{{println}}' -test forgejo.org
|
||||
|
||||
.PHONY: lint-go
|
||||
lint-go:
|
||||
$(GO) run $(GOLANGCI_LINT_PACKAGE) run $(GOLANGCI_LINT_ARGS) \
|
||||
|| (code=$$?; echo "Please run 'make lint-go-fix' and commit the result"; exit $${code})
|
||||
$(GO) run $(GOLANGCI_LINT_PACKAGE) run $(GOLANGCI_LINT_ARGS)
|
||||
$(RUN_DEADCODE) > .cur-deadcode-out
|
||||
@$(DIFF) .deadcode-out .cur-deadcode-out >.deadcode.diff || true
|
||||
@if grep -qE '^[+][^+]' .deadcode.diff ; then \
|
||||
cat .deadcode.diff ; \
|
||||
echo "Looks like you added dead code, please evaluate and remove or use it."; \
|
||||
echo "If you are sure the dead code should stay around, please run 'make lint-go-fix',"; \
|
||||
echo "commit the result and explain the reason in the commit message / PR description."; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if grep -qE '^[-][^-]' .deadcode.diff ; then \
|
||||
cat .deadcode.diff ; \
|
||||
echo "Looks like you removed dead code. Thank you!"; \
|
||||
echo "Run 'make lint-go-fix' and commit the result to accept."; \
|
||||
fi
|
||||
$(GO) run $(ERRORTYPE_PACKAGE) ./...
|
||||
@$(DIFF) .deadcode-out .cur-deadcode-out \
|
||||
|| (code=$$?; echo "Please run 'make lint-go-fix' and commit the result"; exit $${code})
|
||||
|
||||
.PHONY: lint-go-fix
|
||||
lint-go-fix:
|
||||
|
|
@ -514,6 +495,11 @@ lint-go-vet:
|
|||
@echo "Running go vet..."
|
||||
@$(GO) vet ./...
|
||||
|
||||
.PHONY: lint-go-gopls
|
||||
lint-go-gopls:
|
||||
@echo "Running gopls check..."
|
||||
@GO=$(GO) GOPLS_PACKAGE=$(GOPLS_PACKAGE) tools/lint-go-gopls.sh $(GO_SOURCES_NO_BINDATA)
|
||||
|
||||
.PHONY: lint-editorconfig
|
||||
lint-editorconfig:
|
||||
$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) templates .forgejo/workflows
|
||||
|
|
@ -526,18 +512,18 @@ lint-disposable-emails:
|
|||
lint-disposable-emails-fix:
|
||||
$(GO) run build/generate-disposable-email.go -r $(DISPOSABLE_EMAILS_SHA)
|
||||
|
||||
.PHONY: lint-templates
|
||||
lint-templates: .venv node_modules
|
||||
@node tools/lint-templates-svg.js
|
||||
@poetry run djlint $(shell find templates -type f -iname '*.tmpl')
|
||||
|
||||
.PHONY: lint-yaml
|
||||
lint-yaml: .venv
|
||||
@poetry run yamllint -s .
|
||||
|
||||
.PHONY: security-check
|
||||
security-check:
|
||||
$(GO) run $(GOVULNCHECK_PACKAGE) -show color ./...
|
||||
|
||||
.PHONY: tsc
|
||||
tsc: node_modules
|
||||
npx tsc --noEmit
|
||||
|
||||
# target for PRs to be pushed. Mandatory to succeed in CI
|
||||
.PHONY: pr-go
|
||||
pr-go: deps-backend deps-tools lint-backend tidy-check swagger-check lint-swagger fmt-check swagger-validate
|
||||
TAGS=bindata $(MAKE) backend
|
||||
$(GO) run $(GOVULNCHECK_PACKAGE) ./...
|
||||
|
||||
###
|
||||
# Development and testing targets
|
||||
|
|
@ -560,14 +546,14 @@ watch-backend: go-check
|
|||
test: test-frontend test-backend
|
||||
|
||||
.PHONY: test-backend
|
||||
test-backend: | compute-go-test-packages
|
||||
test-backend:
|
||||
@echo "Running go test with $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..."
|
||||
@TZ=UTC GITEA_ROOT="$(CURDIR)" $(GOTEST) $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_TEST_PACKAGES)
|
||||
@$(GOTEST) $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_TEST_PACKAGES)
|
||||
|
||||
.PHONY: test-remote-cacher
|
||||
test-remote-cacher:
|
||||
@echo "Running go test with $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..."
|
||||
GITEA_ROOT="$(CURDIR)" $(GOTEST) $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_TEST_REMOTE_CACHER_PACKAGES)
|
||||
@$(GOTEST) $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_TEST_REMOTE_CACHER_PACKAGES)
|
||||
|
||||
.PHONY: test-frontend
|
||||
test-frontend: node_modules
|
||||
|
|
@ -590,39 +576,20 @@ test-check:
|
|||
fi
|
||||
|
||||
.PHONY: test\#%
|
||||
test\#%: | compute-go-test-packages
|
||||
@echo "Running go test with $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..."
|
||||
@TZ=UTC GITEA_ROOT="$(CURDIR)" $(GOTEST) $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -run $(subst .,/,$*) $(GO_TEST_PACKAGES)
|
||||
test\#%:
|
||||
@echo "Running go test with -tags '$(TEST_TAGS)'..."
|
||||
@$(GOTEST) $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -run $(subst .,/,$*) $(GO_TEST_PACKAGES)
|
||||
|
||||
coverage-merge:
|
||||
rm -fr coverage/merged ; mkdir -p coverage/merged
|
||||
$(GO) tool covdata merge -i `find coverage/data -name 'covmeta.*' | sed -e 's|/covmeta.*|,|' | tr -d '\n' | sed -e 's/,$$//'` -o coverage/merged
|
||||
.PHONY: coverage
|
||||
coverage:
|
||||
grep '^\(mode: .*\)\|\(.*:[0-9]\+\.[0-9]\+,[0-9]\+\.[0-9]\+ [0-9]\+ [0-9]\+\)$$' coverage.out > coverage-bodged.out
|
||||
grep '^\(mode: .*\)\|\(.*:[0-9]\+\.[0-9]\+,[0-9]\+\.[0-9]\+ [0-9]\+ [0-9]\+\)$$' integration.coverage.out > integration.coverage-bodged.out
|
||||
$(GO) run build/gocovmerge.go integration.coverage-bodged.out coverage-bodged.out > coverage.all
|
||||
|
||||
coverage-convert: coverage-merge
|
||||
$(GO) tool covdata textfmt -i=coverage/merged -o=coverage/textfmt.out
|
||||
|
||||
coverage-show-html: coverage-convert
|
||||
( cd coverage ; $(GO) tool cover -html=textfmt.out -o coverage.html )
|
||||
xdg-open coverage/coverage.html
|
||||
|
||||
coverage-show-percentage: coverage-convert
|
||||
go tool cover -func=coverage/textfmt.out
|
||||
|
||||
coverage-run: | compute-go-test-packages
|
||||
contrib/coverage-helper.sh test_packages $(COVERAGE_TEST_PACKAGES)
|
||||
|
||||
coverage-run-%: generate-ini-% | compute-migration-packages
|
||||
#
|
||||
# Migration tests go first
|
||||
#
|
||||
$(MAKE) GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/$*.ini COVERAGE_TEST_ARGS= COVERAGE_TEST_PACKAGES=forgejo.org/tests/integration/migration-test coverage-run
|
||||
for pkg in $(MIGRATION_PACKAGES); do \
|
||||
$(MAKE) GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/$*.ini COVERAGE_TEST_DATABASE=$* COVERAGE_TEST_ARGS= COVERAGE_TEST_PACKAGES=$$pkg coverage-run ; \
|
||||
done
|
||||
#
|
||||
# All other integration tests follow
|
||||
#
|
||||
$(MAKE) GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/$*.ini COVERAGE_TEST_DATABASE=$* COVERAGE_TEST_PACKAGES=forgejo.org/tests/integration coverage-run
|
||||
.PHONY: unit-test-coverage
|
||||
unit-test-coverage:
|
||||
@echo "Running unit-test-coverage $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..."
|
||||
@$(GOTEST) $(GOTESTFLAGS) -timeout=20m -tags='$(TEST_TAGS)' -cover -coverprofile coverage.out $(GO_TEST_PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1
|
||||
|
||||
.PHONY: tidy
|
||||
tidy:
|
||||
|
|
@ -695,7 +662,6 @@ generate-ini-pgsql:
|
|||
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
|
||||
-e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
|
||||
-e 's|{{TEST_STORAGE_TYPE}}|$(or $(TEST_STORAGE_TYPE),minio)|g' \
|
||||
-e 's|{{TEST_S3_HOST}}|$(or $(TEST_S3_HOST),minio:9000)|g' \
|
||||
tests/pgsql.ini.tmpl > tests/pgsql.ini
|
||||
|
||||
.PHONY: test-pgsql
|
||||
|
|
@ -742,7 +708,7 @@ test-e2e-mysql\#%: playwright e2e.mysql.test generate-ini-mysql
|
|||
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini $(GOTESTCOMPILEDRUNPREFIX) ./e2e.mysql.test $(GOTESTCOMPILEDRUNSUFFIX) -test.run TestE2e/$*
|
||||
|
||||
.PHONY: test-e2e-pgsql
|
||||
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.initest-e2e-pgsql: playwright e2e.pgsql.test generate-ini-pgsql
|
||||
test-e2e-pgsql: playwright e2e.pgsql.test generate-ini-pgsql
|
||||
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GOTESTCOMPILEDRUNPREFIX) ./e2e.pgsql.test $(GOTESTCOMPILEDRUNSUFFIX) -test.run TestE2e
|
||||
|
||||
.PHONY: test-e2e-pgsql\#%
|
||||
|
|
@ -765,6 +731,14 @@ bench-mysql: integrations.mysql.test generate-ini-mysql
|
|||
bench-pgsql: integrations.pgsql.test generate-ini-pgsql
|
||||
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./integrations.pgsql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
|
||||
|
||||
.PHONY: integration-test-coverage
|
||||
integration-test-coverage: integrations.cover.test generate-ini-mysql
|
||||
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./integrations.cover.test -test.coverprofile=integration.coverage.out
|
||||
|
||||
.PHONY: integration-test-coverage-sqlite
|
||||
integration-test-coverage-sqlite: integrations.cover.sqlite.test generate-ini-sqlite
|
||||
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./integrations.cover.sqlite.test -test.coverprofile=integration.coverage.out
|
||||
|
||||
integrations.mysql.test: git-check $(GO_SOURCES)
|
||||
$(GOTEST) $(GOTESTFLAGS) -c forgejo.org/tests/integration -o integrations.mysql.test
|
||||
|
||||
|
|
@ -774,6 +748,12 @@ integrations.pgsql.test: git-check $(GO_SOURCES)
|
|||
integrations.sqlite.test: git-check $(GO_SOURCES)
|
||||
$(GOTEST) $(GOTESTFLAGS) -c forgejo.org/tests/integration -o integrations.sqlite.test -tags '$(TEST_TAGS)'
|
||||
|
||||
integrations.cover.test: git-check $(GO_SOURCES)
|
||||
$(GOTEST) $(GOTESTFLAGS) -c forgejo.org/tests/integration -coverpkg $(shell echo $(GO_TEST_PACKAGES) | tr ' ' ',') -o integrations.cover.test
|
||||
|
||||
integrations.cover.sqlite.test: git-check $(GO_SOURCES)
|
||||
$(GOTEST) $(GOTESTFLAGS) -c forgejo.org/tests/integration -coverpkg $(shell echo $(GO_TEST_PACKAGES) | tr ' ' ',') -o integrations.cover.sqlite.test -tags '$(TEST_TAGS)'
|
||||
|
||||
.PHONY: migrations.mysql.test
|
||||
migrations.mysql.test: $(GO_SOURCES) generate-ini-mysql
|
||||
$(GOTEST) $(GOTESTFLAGS) -c forgejo.org/tests/integration/migration-test -o migrations.mysql.test
|
||||
|
|
@ -790,34 +770,34 @@ migrations.sqlite.test: $(GO_SOURCES) generate-ini-sqlite
|
|||
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GOTESTCOMPILEDRUNPREFIX) ./migrations.sqlite.test $(GOTESTCOMPILEDRUNSUFFIX)
|
||||
|
||||
.PHONY: migrations.individual.mysql.test
|
||||
migrations.individual.mysql.test: $(GO_SOURCES) | compute-migration-packages
|
||||
migrations.individual.mysql.test: $(GO_SOURCES)
|
||||
for pkg in $(MIGRATION_PACKAGES); do \
|
||||
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini $(GOTEST) $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg || exit 1; \
|
||||
done
|
||||
|
||||
.PHONY: migrations.individual.sqlite.test\#%
|
||||
migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite
|
||||
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GOTEST) $(GOTESTFLAGS) -tags '$(TEST_TAGS)' forgejo.org/models/gitea_migrations/$*
|
||||
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GOTEST) $(GOTESTFLAGS) -tags '$(TEST_TAGS)' forgejo.org/models/migrations/$*
|
||||
|
||||
.PHONY: migrations.individual.pgsql.test
|
||||
migrations.individual.pgsql.test: $(GO_SOURCES) | compute-migration-packages
|
||||
migrations.individual.pgsql.test: $(GO_SOURCES)
|
||||
for pkg in $(MIGRATION_PACKAGES); do \
|
||||
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GOTEST) $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg || exit 1;\
|
||||
done
|
||||
|
||||
.PHONY: migrations.individual.pgsql.test\#%
|
||||
migrations.individual.pgsql.test\#%: $(GO_SOURCES) generate-ini-pgsql
|
||||
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GOTEST) $(GOTESTFLAGS) -tags '$(TEST_TAGS)' forgejo.org/models/gitea_migrations/$*
|
||||
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GOTEST) $(GOTESTFLAGS) -tags '$(TEST_TAGS)' forgejo.org/models/migrations/$*
|
||||
|
||||
.PHONY: migrations.individual.sqlite.test
|
||||
migrations.individual.sqlite.test: $(GO_SOURCES) generate-ini-sqlite | compute-migration-packages
|
||||
migrations.individual.sqlite.test: $(GO_SOURCES) generate-ini-sqlite
|
||||
for pkg in $(MIGRATION_PACKAGES); do \
|
||||
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GOTEST) $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg || exit 1; \
|
||||
done
|
||||
|
||||
.PHONY: migrations.individual.sqlite.test\#%
|
||||
migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite
|
||||
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GOTEST) $(GOTESTFLAGS) -tags '$(TEST_TAGS)' forgejo.org/models/gitea_migrations/$*
|
||||
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GOTEST) $(GOTESTFLAGS) -tags '$(TEST_TAGS)' forgejo.org/models/migrations/$*
|
||||
|
||||
e2e.mysql.test: $(GO_SOURCES)
|
||||
$(GOTEST) $(GOTESTFLAGS) -c forgejo.org/tests/e2e -o e2e.mysql.test
|
||||
|
|
@ -836,7 +816,7 @@ check: test
|
|||
###
|
||||
|
||||
.PHONY: install $(TAGS_PREREQ)
|
||||
install: $(wildcard *.go) | verify-version
|
||||
install: $(wildcard *.go)
|
||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) install -v -tags '$(TAGS)' -ldflags '$(LDFLAGS)'
|
||||
|
||||
.PHONY: build
|
||||
|
|
@ -864,13 +844,13 @@ generate-go: $(TAGS_PREREQ)
|
|||
merge-locales:
|
||||
@echo "NOT NEEDED: THIS IS A NOOP AS OF Forgejo 7.0 BUT KEPT FOR BACKWARD COMPATIBILITY"
|
||||
|
||||
$(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ) | verify-version
|
||||
$(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
|
||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $@
|
||||
|
||||
forgejo: $(EXECUTABLE)
|
||||
ln -f $(EXECUTABLE) forgejo
|
||||
|
||||
static-executable: $(GO_SOURCES) $(TAGS_PREREQ) | verify-version
|
||||
static-executable: $(GO_SOURCES) $(TAGS_PREREQ)
|
||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -o $(EXECUTABLE)
|
||||
|
||||
.PHONY: release
|
||||
|
|
@ -883,18 +863,18 @@ $(DIST_DIRS):
|
|||
mkdir -p $(DIST_DIRS)
|
||||
|
||||
.PHONY: release-linux
|
||||
release-linux: | $(DIST_DIRS) verify-version
|
||||
release-linux: | $(DIST_DIRS)
|
||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out forgejo-$(VERSION) .
|
||||
ifeq ($(CI),true)
|
||||
cp /build/* $(DIST)/binaries
|
||||
endif
|
||||
|
||||
.PHONY: release-darwin
|
||||
release-darwin: | $(DIST_DIRS) verify-version
|
||||
release-darwin: | $(DIST_DIRS)
|
||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'darwin-10.12/amd64,darwin-10.12/arm64' -out gitea-$(VERSION) .
|
||||
|
||||
.PHONY: release-freebsd
|
||||
release-freebsd: | $(DIST_DIRS) verify-version
|
||||
release-freebsd: | $(DIST_DIRS)
|
||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'freebsd/amd64' -out gitea-$(VERSION) .
|
||||
|
||||
.PHONY: release-copy
|
||||
|
|
@ -948,7 +928,10 @@ reproduce-build\#%:
|
|||
###
|
||||
|
||||
.PHONY: deps
|
||||
deps: deps-frontend deps-backend deps-tools
|
||||
deps: deps-frontend deps-backend deps-tools deps-py
|
||||
|
||||
.PHONY: deps-py
|
||||
deps-py: .venv
|
||||
|
||||
.PHONY: deps-frontend
|
||||
deps-frontend: node_modules
|
||||
|
|
@ -964,24 +947,28 @@ deps-tools:
|
|||
$(GO) install $(GOFUMPT_PACKAGE)
|
||||
$(GO) install $(GOLANGCI_LINT_PACKAGE)
|
||||
$(GO) install $(GXZ_PACKAGE)
|
||||
$(GO) install $(MISSPELL_PACKAGE)
|
||||
$(GO) install $(SWAGGER_PACKAGE)
|
||||
$(GO) install $(XGO_PACKAGE)
|
||||
$(GO) install $(GO_LICENSES_PACKAGE)
|
||||
$(GO) install $(GOVULNCHECK_PACKAGE)
|
||||
$(GO) install $(ERRORTYPE_PACKAGE)
|
||||
$(GO) install $(MOCKERY_PACKAGE)
|
||||
$(GO) install $(GOMOCK_PACKAGE)
|
||||
$(GO) install $(GOPLS_PACKAGE)
|
||||
|
||||
node_modules: package-lock.json
|
||||
npm install --no-save
|
||||
@touch node_modules
|
||||
|
||||
.venv: poetry.lock
|
||||
poetry install
|
||||
@touch .venv
|
||||
|
||||
.PHONY: fomantic
|
||||
fomantic:
|
||||
rm -rf $(FOMANTIC_WORK_DIR)/build
|
||||
cd $(FOMANTIC_WORK_DIR) && npm install --no-save
|
||||
cp -f $(FOMANTIC_WORK_DIR)/theme.config.less $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/theme.config
|
||||
cp -rf $(FOMANTIC_WORK_DIR)/_site $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/
|
||||
rm -rf $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/themes/default/modules/dropdown.overrides
|
||||
$(SED_INPLACE) -e 's/ overrideBrowserslist\r/ overrideBrowserslist: ["defaults"]\r/g' $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/tasks/config/tasks.js
|
||||
cd $(FOMANTIC_WORK_DIR) && npx gulp -f node_modules/fomantic-ui/gulpfile.js build
|
||||
# fomantic uses "touchstart" as click event for some browsers, it's not ideal, so we force fomantic to always use "click" as click event
|
||||
|
|
@ -1024,13 +1011,14 @@ generate-license:
|
|||
generate-gitignore:
|
||||
$(GO) run build/generate-gitignores.go
|
||||
|
||||
.PHONY: generate-mockery
|
||||
generate-mockery:
|
||||
$(GO) run $(MOCKERY_PACKAGE)
|
||||
.PHONY: generate-gomock
|
||||
generate-gomock:
|
||||
$(GO) run $(GOMOCK_PACKAGE) -package mock -destination ./modules/queue/mock/redisuniversalclient.go forgejo.org/modules/nosql RedisClient
|
||||
|
||||
.PHONY: generate-images
|
||||
generate-images: | node_modules
|
||||
node tools/generate-images.js
|
||||
npm install --no-save fabric@6 imagemin-zopfli@7
|
||||
node tools/generate-images.js $(TAGS)
|
||||
|
||||
.PHONY: generate-manpage
|
||||
generate-manpage:
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
Hi there! Tired of big platforms playing monopoly?
|
||||
Providing Git hosting for your project, friends, company or community?
|
||||
**Forgejo** (/for'd͡ʒe.jo/ inspired by forĝejo – the Esperanto word for *forge*) has you covered with its intuitive interface,
|
||||
light and easy hosting and a lot of built-in functionality.
|
||||
light and easy hosting and a lot of builtin functionality.
|
||||
|
||||
Forgejo was [created in 2022](https://forgejo.org/2022-12-15-hello-forgejo/)
|
||||
because we think that the project should be owned by an independent community.
|
||||
|
|
@ -15,6 +15,11 @@ Our promise: **Independent Free/Libre Software forever!**
|
|||
|
||||
## What does Forgejo offer?
|
||||
|
||||
<!-- If you want to know what Forgejo is like,
|
||||
you can check out public instances,
|
||||
e.g. [Codeberg.org](https://codeberg.org).
|
||||
-->
|
||||
|
||||
If you like any of the following, Forgejo is literally meant for you:
|
||||
|
||||
- Lightweight: Forgejo can easily be hosted on nearly **every machine**.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ A minor or major Forgejo release is published every [three months](https://forge
|
|||
|
||||
A [patch or minor release](https://semver.org/spec/v2.0.0.html) (e.g. upgrading from v7.0.0 to v7.0.1 or v7.1.0) does not require manual intervention. But [major releases](https://semver.org/spec/v2.0.0.html#spec-item-8) where the first version number changes (e.g. upgrading from v1.21 to v7.0) contain breaking changes and the release notes explain how to deal with them.
|
||||
|
||||
The release notes of each release [are available in the release-notes-published directory of this repository](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/release-notes-published), starting with [Forgejo 7.0.7](release-notes-published/7.0.7.md) and [Forgejo 8.0.1](release-notes-published/8.0.1.md).
|
||||
The release notes of each release [are available in the release-notes-published directory of this repository](release-notes-published), starting with [Forgejo 7.0.7](release-notes-published/7.0.7.md) and [Forgejo 8.0.1](release-notes-published/8.0.1.md).
|
||||
|
||||
## 9.0.2
|
||||
|
||||
|
|
@ -130,10 +130,6 @@ A [companion blog post](https://forgejo.org/2024-07-release-v8-0/) provides addi
|
|||
- [PR](https://codeberg.org/forgejo/forgejo/pulls/3334): <!--number 3334 --><!--number--><!--description Added support for the `workflow_dispatch` workflow trigger-->added support for the [`workflow_dispatch` trigger](https://forgejo.org/docs/v8.0/user/actions/#onworkflow_dispatch) in Forgejo Actions.<!--description-->
|
||||
- [PR](https://codeberg.org/forgejo/forgejo/pulls/3307): <!--number 3307 --><!--number--><!--description Support [Proof Key for Code Exchange (PKCE - RFC7636)](https://www.rfc-editor.org/rfc/rfc7636) for external login using the OpenID Connect authentication source.-->support [Proof Key for Code Exchange (PKCE - RFC7636)](https://www.rfc-editor.org/rfc/rfc7636) for external login using the OpenID Connect authentication source.<!--description-->
|
||||
- [PR](https://codeberg.org/forgejo/forgejo/pulls/3139): <!--number 3139 --><!--number--><!--description Allow hiding auto generated release archives-->allow hiding auto generated release archives.<!--description-->
|
||||
- [PR](https://codeberg.org/forgejo/forgejo/pulls/3952): Update of Chroma from v2.13.0: to v2.14.0:
|
||||
- [`1e983e7`](https://github.com/alecthomas/chroma/commit/1e983e7) lexers/cue: support CUE attributes ([#​961](https://github.com/alecthomas/chroma/issues/961))
|
||||
- [`9347b55`](https://github.com/alecthomas/chroma/commit/9347b55) Add Gleam syntax highlighting ([#​959](https://github.com/alecthomas/chroma/issues/959))
|
||||
- [`2580aaa`](https://github.com/alecthomas/chroma/commit/2580aaa) Add Bazel bzlmod support into Python lexer ([#​947](https://github.com/alecthomas/chroma/issues/947))
|
||||
- **Bug fixes**
|
||||
- [PR](https://codeberg.org/forgejo/forgejo/pulls/4732) ([backported from](https://codeberg.org/forgejo/forgejo/pulls/4715)): <!--number 4732 --><!--number--><!--description -->Show the AGit label on merged pull requests.<!--description-->
|
||||
- [PR](https://codeberg.org/forgejo/forgejo/pulls/4689) ([backported from](https://codeberg.org/forgejo/forgejo/pulls/4687)): <!--number 4689 --><!--number--><!--description -->Fixed: issue state change via the API is not idempotent.<!--description-->
|
||||
|
|
@ -150,9 +146,6 @@ A [companion blog post](https://forgejo.org/2024-07-release-v8-0/) provides addi
|
|||
- [PR](https://codeberg.org/forgejo/forgejo/pulls/3442): <!--number 3442 --><!--number--><!--description Save updated empty comments instead of skipping the update silently, [which prevented the removal of attachments of such comments](https://codeberg.org/forgejo/forgejo/issues/3424).-->Fixed: it is not possible to remove attachments from an empty comment.<!--description-->
|
||||
- [PR](https://codeberg.org/forgejo/forgejo/pulls/3430): <!--number 3430 --><!--number--><!--description Fixed a bug where the `/api/v1/repos/{owner}/{repo}/wiki` API endpoints were using a hardcoded "master" branch for the wiki, rather than the branch they really use.-->Fixed: the `/api/v1/repos/{owner}/{repo}/wiki` API endpoints is using a hardcoded "master" branch for the wiki, rather than the branch they really use.<!--description-->
|
||||
- [PR](https://codeberg.org/forgejo/forgejo/pulls/3379): <!--number 3379 --><!--number--><!--description -->Fixed: using the API to search for users, the results are not paged by default an the default paging limits are not respected.<!--description-->
|
||||
- [PR](https://codeberg.org/forgejo/forgejo/pulls/3952): Update of Chroma from v2.13.0: to v2.14.0:
|
||||
- [`736c0ea`](https://github.com/alecthomas/chroma/commit/736c0ea) Typescript: Several fixes ([#​952](https://github.com/alecthomas/chroma/issues/952))
|
||||
- [`e5c25d0`](https://github.com/alecthomas/chroma/commit/e5c25d0) Org: Keep all newlines ([#​951](https://github.com/alecthomas/chroma/issues/951))
|
||||
- **Localization**
|
||||
- [PR](https://codeberg.org/forgejo/forgejo/pulls/4661) ([backported from](https://codeberg.org/forgejo/forgejo/pulls/4568)): <!--number 4661 --><!--number--><!--description -->24 July updates<!--description-->
|
||||
- [PR](https://codeberg.org/forgejo/forgejo/pulls/4565) ([backported from](https://codeberg.org/forgejo/forgejo/pulls/4451)): <!--number 4565 --><!--number--><!--description -->19 July updates<!--description-->
|
||||
|
|
|
|||
473
assets/go-licenses.json
generated
473
assets/go-licenses.json
generated
File diff suppressed because one or more lines are too long
14
build.go
Normal file
14
build.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build vendor
|
||||
|
||||
package main
|
||||
|
||||
// Libraries that are included to vendor utilities used during build.
|
||||
// These libraries will not be included in a normal compilation.
|
||||
|
||||
import (
|
||||
// for embed
|
||||
_ "github.com/shurcooL/vfsgen"
|
||||
)
|
||||
|
|
@ -74,7 +74,7 @@ func newFileCollector(fileFilter string, batchSize int) (*fileCollector, error)
|
|||
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`tests/integration/migration-test`))
|
||||
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`modules/git/tests`))
|
||||
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`models/fixtures`))
|
||||
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`models/gitea_migrations/fixtures`))
|
||||
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`models/migrations/fixtures`))
|
||||
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`services/gitdiff/testdata`))
|
||||
}
|
||||
|
||||
|
|
@ -181,7 +181,7 @@ func parseArgs() (mainOptions map[string]string, subCmd string, subArgs []string
|
|||
break
|
||||
}
|
||||
}
|
||||
return mainOptions, subCmd, subArgs
|
||||
return
|
||||
}
|
||||
|
||||
func showUsage() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build ignore
|
||||
|
||||
|
|
@ -8,40 +7,30 @@ package main
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"text/template"
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/shurcooL/vfsgen"
|
||||
)
|
||||
|
||||
func fileExists(filename string) bool {
|
||||
_, err := os.Stat(filename)
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
func needsUpdate(dir, filename string) (bool, []byte) {
|
||||
needRegen := !fileExists(filename)
|
||||
needRegen := false
|
||||
_, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
needRegen = true
|
||||
}
|
||||
|
||||
oldHash, err := os.ReadFile(filename + ".hash")
|
||||
if err != nil {
|
||||
oldHash = []byte{}
|
||||
}
|
||||
|
||||
hasher := sha256.New()
|
||||
hasher := sha1.New()
|
||||
|
||||
err = filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
|
|
@ -62,7 +51,7 @@ func needsUpdate(dir, filename string) (bool, []byte) {
|
|||
|
||||
newHash := hasher.Sum([]byte{})
|
||||
|
||||
if !bytes.Equal(oldHash, newHash) {
|
||||
if bytes.Compare(oldHash, newHash) != 0 {
|
||||
return true, newHash
|
||||
}
|
||||
|
||||
|
|
@ -80,280 +69,24 @@ func main() {
|
|||
useGlobalModTime, _ = strconv.ParseBool(os.Args[4])
|
||||
}
|
||||
|
||||
if os.Getenv("FORGEJO_GENERATE_SKIP_HASH") == "true" && fileExists(filename) {
|
||||
fmt.Printf("bindata %s already exists and FORGEJO_GENERATE_SKIP_HASH=true\n", packageName)
|
||||
return
|
||||
}
|
||||
|
||||
update, newHash := needsUpdate(dir, filename)
|
||||
|
||||
if !update {
|
||||
fmt.Printf("bindata %s already exists and the checksum is a match\n", packageName)
|
||||
fmt.Printf("bindata for %s already up-to-date\n", packageName)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("generating bindata for %s\n", packageName)
|
||||
|
||||
root, err := os.OpenRoot(dir)
|
||||
var fsTemplates http.FileSystem = http.Dir(dir)
|
||||
err := vfsgen.Generate(fsTemplates, vfsgen.Options{
|
||||
PackageName: packageName,
|
||||
BuildTags: "bindata",
|
||||
VariableName: "Assets",
|
||||
Filename: filename,
|
||||
UseGlobalModTime: useGlobalModTime,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
out, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
if err := generate(root.FS(), packageName, useGlobalModTime, out); err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("%v\n", err)
|
||||
}
|
||||
_ = os.WriteFile(filename+".hash", newHash, 0o666)
|
||||
}
|
||||
|
||||
type file struct {
|
||||
Path string
|
||||
Name string
|
||||
UncompressedSize int
|
||||
CompressedData []byte
|
||||
UncompressedData []byte
|
||||
}
|
||||
|
||||
type direntry struct {
|
||||
Name string
|
||||
IsDir bool
|
||||
}
|
||||
|
||||
func generate(fsRoot fs.FS, packageName string, globalTime bool, output io.Writer) error {
|
||||
enc, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(zstd.SpeedBestCompression))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
files := []file{}
|
||||
|
||||
dirs := map[string][]direntry{}
|
||||
|
||||
if err := fs.WalkDir(fsRoot, ".", func(filePath string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
entries, err := fs.ReadDir(fsRoot, filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dirEntries := make([]direntry, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
dirEntries = append(dirEntries, direntry{Name: entry.Name(), IsDir: entry.IsDir()})
|
||||
}
|
||||
dirs[filePath] = dirEntries
|
||||
return nil
|
||||
}
|
||||
|
||||
src, err := fs.ReadFile(fsRoot, filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dst := enc.EncodeAll(src, nil)
|
||||
if len(dst) < len(src) {
|
||||
files = append(files, file{
|
||||
Path: filePath,
|
||||
Name: path.Base(filePath),
|
||||
UncompressedSize: len(src),
|
||||
CompressedData: dst,
|
||||
})
|
||||
} else {
|
||||
files = append(files, file{
|
||||
Path: filePath,
|
||||
Name: path.Base(filePath),
|
||||
UncompressedData: src,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return generatedTmpl.Execute(output, map[string]any{
|
||||
"Packagename": packageName,
|
||||
"GlobalTime": globalTime,
|
||||
"Files": files,
|
||||
"Dirs": dirs,
|
||||
})
|
||||
}
|
||||
|
||||
var generatedTmpl = template.Must(template.New("").Parse(`// Code generated by efs-gen. DO NOT EDIT.
|
||||
|
||||
//go:build bindata
|
||||
|
||||
package {{.Packagename}}
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"time"
|
||||
"io"
|
||||
"io/fs"
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
)
|
||||
|
||||
type normalFile struct {
|
||||
name string
|
||||
content []byte
|
||||
}
|
||||
|
||||
type compressedFile struct {
|
||||
name string
|
||||
uncompressedSize int64
|
||||
data []byte
|
||||
}
|
||||
|
||||
var files = map[string]any{
|
||||
{{- range .Files}}
|
||||
"{{.Path}}": {{if .CompressedData}}compressedFile{"{{.Name}}", {{.UncompressedSize}}, []byte({{printf "%+q" .CompressedData}})}{{else}}normalFile{"{{.Name}}", []byte({{printf "%+q" .UncompressedData}})}{{end}},
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
var dirs = map[string][]fs.DirEntry{
|
||||
{{- range $key, $entry := .Dirs}}
|
||||
"{{$key}}": {
|
||||
{{- range $entry}}
|
||||
direntry{"{{.Name}}", {{.IsDir}}},
|
||||
{{- end}}
|
||||
},
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
type assets struct{}
|
||||
|
||||
var Assets = assets{}
|
||||
|
||||
func (a assets) Open(name string) (fs.File, error) {
|
||||
f, ok := files[name]
|
||||
if !ok {
|
||||
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
|
||||
}
|
||||
|
||||
switch f := f.(type) {
|
||||
case normalFile:
|
||||
return file{name: f.name, size: int64(len(f.content)), data: bytes.NewReader(f.content)}, nil
|
||||
case compressedFile:
|
||||
r, _ := zstd.NewReader(bytes.NewReader(f.data))
|
||||
return &compressFile{name: f.name, size: f.uncompressedSize, data: r, content: f.data}, nil
|
||||
default:
|
||||
panic("unknown file type")
|
||||
}
|
||||
}
|
||||
|
||||
func (a assets) ReadDir(name string) ([]fs.DirEntry, error) {
|
||||
d, ok := dirs[name]
|
||||
if !ok {
|
||||
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
type file struct {
|
||||
name string
|
||||
size int64
|
||||
data io.ReadSeeker
|
||||
}
|
||||
|
||||
var _ io.ReadSeeker = (*file)(nil)
|
||||
|
||||
func (f file) Stat() (fs.FileInfo, error) {
|
||||
return fileinfo{name: f.name, size: f.size}, nil
|
||||
}
|
||||
|
||||
func (f file) Read(p []byte) (int, error) {
|
||||
return f.data.Read(p)
|
||||
}
|
||||
|
||||
func (f file) Seek(offset int64, whence int) (int64, error) {
|
||||
return f.data.Seek(offset, whence)
|
||||
}
|
||||
|
||||
func (f file) Close() error { return nil }
|
||||
|
||||
type compressFile struct {
|
||||
name string
|
||||
size int64
|
||||
data *zstd.Decoder
|
||||
content []byte
|
||||
zstdPos int64
|
||||
seekPos int64
|
||||
}
|
||||
|
||||
var _ io.ReadSeeker = (*compressFile)(nil)
|
||||
|
||||
func (f *compressFile) Stat() (fs.FileInfo, error) {
|
||||
return fileinfo{name: f.name, size: f.size}, nil
|
||||
}
|
||||
|
||||
func (f *compressFile) Read(p []byte) (int, error) {
|
||||
if f.zstdPos > f.seekPos {
|
||||
if err := f.data.Reset(bytes.NewReader(f.content)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
f.zstdPos = 0
|
||||
}
|
||||
if f.zstdPos < f.seekPos {
|
||||
if _, err := io.CopyN(io.Discard, f.data, f.seekPos - f.zstdPos); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
f.zstdPos = f.seekPos
|
||||
}
|
||||
n, err := f.data.Read(p)
|
||||
f.zstdPos += int64(n)
|
||||
f.seekPos = f.zstdPos
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (f *compressFile) Seek(offset int64, whence int) (int64, error) {
|
||||
switch whence {
|
||||
case io.SeekStart:
|
||||
f.seekPos = 0 + offset
|
||||
case io.SeekCurrent:
|
||||
f.seekPos += offset
|
||||
case io.SeekEnd:
|
||||
f.seekPos = f.size + offset
|
||||
}
|
||||
return f.seekPos, nil
|
||||
}
|
||||
|
||||
func (f *compressFile) Close() error {
|
||||
f.data.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *compressFile) ZstdBytes() []byte { return f.content }
|
||||
|
||||
type fileinfo struct {
|
||||
name string
|
||||
size int64
|
||||
}
|
||||
|
||||
func (f fileinfo) Name() string { return f.name }
|
||||
func (f fileinfo) Size() int64 { return f.size }
|
||||
func (f fileinfo) Mode() fs.FileMode { return 0o444 }
|
||||
func (f fileinfo) ModTime() time.Time { return {{if .GlobalTime}}GlobalModTime(f.name){{else}}time.Unix(0, 0){{end}} }
|
||||
func (f fileinfo) IsDir() bool { return false }
|
||||
func (f fileinfo) Sys() any { return nil }
|
||||
|
||||
type direntry struct {
|
||||
name string
|
||||
isDir bool
|
||||
}
|
||||
|
||||
func (d direntry) Name() string { return d.name }
|
||||
func (d direntry) IsDir() bool { return d.isDir }
|
||||
func (d direntry) Type() fs.FileMode {
|
||||
if d.isDir {
|
||||
return 0o755 | fs.ModeDir
|
||||
}
|
||||
return 0o444
|
||||
}
|
||||
func (direntry) Info() (fs.FileInfo, error) { return nil, fs.ErrNotExist }
|
||||
`))
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
# translation tooling test keys
|
||||
meta.last_line
|
||||
translation_meta.test
|
||||
|
||||
# models/admin/task.go: instances of $TranslatableMessage.Format
|
||||
# this also gets instantiated as a Messenger once
|
||||
repo.migrate.migrating_failed.error
|
||||
|
||||
# modules/setting/ui.go
|
||||
themes.names.
|
||||
|
||||
# services/context/context.go
|
||||
relativetime.
|
||||
|
||||
# templates/repo/settings/webhook/link_menu.tmpl, templates/webhook/new.tmpl: repo.settings.web_hook_name_
|
||||
# tests/integration/repo_archive_text_test.go
|
||||
repo.settings.
|
||||
|
|
@ -1,208 +0,0 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
goParser "go/parser"
|
||||
"go/token"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
llu "forgejo.org/build/lint-locale-usage"
|
||||
lluAsymKey "forgejo.org/models/asymkey/lint-locale-usage"
|
||||
lluUnit "forgejo.org/models/unit/lint-locale-usage"
|
||||
lluMigrate "forgejo.org/services/migrations/lint-locale-usage"
|
||||
)
|
||||
|
||||
// the `Handle*File` functions follow the following calling convention:
|
||||
// * `fname` is the name of the input file
|
||||
// * `src` is either `nil` (then the function invokes `ReadFile` to read the file)
|
||||
// or the contents of the file as {`[]byte`, or a `string`}
|
||||
|
||||
func HandleGoFile(handler llu.Handler, fname string, src any) error {
|
||||
fset := token.NewFileSet()
|
||||
node, err := goParser.ParseFile(fset, fname, src, goParser.SkipObjectResolution|goParser.ParseComments)
|
||||
if err != nil {
|
||||
return llu.LocatedError{
|
||||
Location: fname,
|
||||
Kind: "Go parser",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
ast.Inspect(node, func(n ast.Node) bool {
|
||||
return HandleGoNode(handler, fset, fname, n)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func HandleGoNode(handler llu.Handler, fset *token.FileSet, fname string, n ast.Node) bool {
|
||||
// search for function calls of the form `anything.Tr(any-string-lit, ...)`
|
||||
|
||||
switch n2 := n.(type) {
|
||||
case *ast.CallExpr:
|
||||
if len(n2.Args) == 0 {
|
||||
return true
|
||||
}
|
||||
funSel, ok := n2.Fun.(*ast.SelectorExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
ltf, ok := handler.LocaleTrFunctions[funSel.Sel.Name]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
var gotUnexpectedInvoke *int
|
||||
|
||||
for _, argNum := range ltf {
|
||||
if len(n2.Args) <= int(argNum) {
|
||||
argc := len(n2.Args)
|
||||
gotUnexpectedInvoke = &argc
|
||||
} else {
|
||||
handler.HandleGoTrArgument(fset, n2.Args[int(argNum)], "")
|
||||
}
|
||||
}
|
||||
|
||||
if gotUnexpectedInvoke != nil {
|
||||
handler.OnUnexpectedInvoke(fset, funSel.Sel.NamePos, funSel.Sel.Name, *gotUnexpectedInvoke)
|
||||
}
|
||||
|
||||
case *ast.CompositeLit:
|
||||
if strings.HasSuffix(fname, "models/unit/unit.go") {
|
||||
lluUnit.HandleCompositeUnit(handler, fset, n2)
|
||||
} else if strings.Contains(fname, "models/asymkey/") {
|
||||
lluAsymKey.HandleCompositeErrorReason(handler, fset, n2)
|
||||
}
|
||||
|
||||
case *ast.FuncDecl:
|
||||
if matchInsPrefix := handler.HandleGoCommentGroup(fset, n2.Doc, "llu:returnsTrKeyWeak"); matchInsPrefix != nil {
|
||||
results := n2.Type.Results.List
|
||||
if len(results) != 1 {
|
||||
handler.OnWarning(fset, n2.Type.Func, fmt.Sprintf("function %s has unexpected return type; expected single return value", n2.Name.Name))
|
||||
return true
|
||||
}
|
||||
|
||||
ast.Inspect(n2.Body, func(n ast.Node) bool {
|
||||
// search for return stmts
|
||||
// TODO: what about nested functions?
|
||||
if ret, ok := n.(*ast.ReturnStmt); ok {
|
||||
for _, res := range ret.Results {
|
||||
ast.Inspect(res, func(n ast.Node) bool {
|
||||
if expr, ok := n.(ast.Expr); ok {
|
||||
handler.HandleGoTrArgument(fset, expr, *matchInsPrefix)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
if matchInsPrefix := handler.HandleGoCommentGroup(fset, n2.Doc, "llu:returnsTrKey"); matchInsPrefix != nil {
|
||||
results := n2.Type.Results.List
|
||||
if len(results) != 1 {
|
||||
handler.OnWarning(fset, n2.Type.Func, fmt.Sprintf("function %s has unexpected return type; expected single return value", n2.Name.Name))
|
||||
return true
|
||||
}
|
||||
|
||||
ast.Inspect(n2.Body, func(n ast.Node) bool {
|
||||
// search for return stmts
|
||||
if ret, ok := n.(*ast.ReturnStmt); ok {
|
||||
for _, res := range ret.Results {
|
||||
handler.HandleGoTrArgument(fset, res, *matchInsPrefix)
|
||||
}
|
||||
return false
|
||||
} else if _, ok := n.(*ast.FuncDecl); ok {
|
||||
ast.Inspect(n, func(n2 ast.Node) bool {
|
||||
return HandleGoNode(handler, fset, fname, n2)
|
||||
})
|
||||
// don't search inside nested functions for return stmts
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
if strings.HasSuffix(fname, "services/migrations/migrate.go") {
|
||||
lluMigrate.HandleMessengerInFunc(handler, fset, n2)
|
||||
}
|
||||
return true
|
||||
case *ast.GenDecl:
|
||||
switch n2.Tok {
|
||||
case token.CONST, token.VAR:
|
||||
matchInsPrefix := handler.HandleGoCommentGroup(fset, n2.Doc, " llu:TrKeys")
|
||||
if matchInsPrefix == nil {
|
||||
return true
|
||||
}
|
||||
for _, spec := range n2.Specs {
|
||||
// interpret all contained strings as message IDs
|
||||
ast.Inspect(spec, func(n ast.Node) bool {
|
||||
if argLit, ok := n.(*ast.BasicLit); ok {
|
||||
handler.HandleGoTrBasicLit(fset, argLit, *matchInsPrefix)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
case token.TYPE:
|
||||
// modules/web/middleware/binding.go:Validate uses the convention that structs
|
||||
// entries can have tags.
|
||||
// In particular, `locale:$msgid` should be handled; any fields with `form:-` shouldn't.
|
||||
// Problem: we don't know which structs are forms, actually.
|
||||
|
||||
for _, spec := range n2.Specs {
|
||||
tspec := spec.(*ast.TypeSpec)
|
||||
structNode, ok := tspec.Type.(*ast.StructType)
|
||||
if !ok || !(strings.HasSuffix(tspec.Name.Name, "Form") ||
|
||||
(tspec.Doc != nil &&
|
||||
slices.ContainsFunc(tspec.Doc.List, func(c *ast.Comment) bool {
|
||||
return c.Text == "// swagger:model"
|
||||
}))) {
|
||||
continue
|
||||
}
|
||||
for _, field := range structNode.Fields.List {
|
||||
if field.Names == nil {
|
||||
continue
|
||||
}
|
||||
if len(field.Names) != 1 {
|
||||
handler.OnWarning(fset, field.Type.Pos(), "unsupported multiple field names")
|
||||
continue
|
||||
}
|
||||
msgidPos := field.Names[0].NamePos
|
||||
msgid := "form." + field.Names[0].Name
|
||||
if field.Tag != nil && field.Tag.Kind == token.STRING {
|
||||
rawTag, err := strconv.Unquote(field.Tag.Value)
|
||||
if err != nil {
|
||||
handler.OnWarning(fset, field.Tag.ValuePos, "invalid tag value encountered")
|
||||
continue
|
||||
}
|
||||
tag := reflect.StructTag(rawTag)
|
||||
if tag.Get("form") == "-" {
|
||||
continue
|
||||
}
|
||||
tmp := tag.Get("locale")
|
||||
if len(tmp) != 0 {
|
||||
msgidPos = field.Tag.ValuePos
|
||||
msgid = tmp
|
||||
}
|
||||
}
|
||||
handler.OnMsgid(fset, msgidPos, msgid, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
@ -1,404 +0,0 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/token"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
llu "forgejo.org/build/lint-locale-usage"
|
||||
"forgejo.org/modules/container"
|
||||
"forgejo.org/modules/translation/localeiter"
|
||||
)
|
||||
|
||||
// this works by first gathering all valid source string IDs from `en-US` reference files
|
||||
// and then checking if all used source strings are actually defined
|
||||
|
||||
func InitLocaleTrFunctions() map[string][]uint {
|
||||
ret := make(map[string][]uint)
|
||||
|
||||
f0 := []uint{0}
|
||||
ret["Tr"] = f0
|
||||
ret["TrString"] = f0
|
||||
ret["TrHTML"] = f0
|
||||
|
||||
ret["TrPluralString"] = []uint{1}
|
||||
ret["TrN"] = []uint{1, 2}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
type StringTrie interface {
|
||||
Matches(key []string) bool
|
||||
}
|
||||
|
||||
type StringTrieMap map[string]StringTrie
|
||||
|
||||
func printfPatternToRegex(key string) (string, bool) {
|
||||
parts := strings.Split(key, "%")
|
||||
if len(parts) < 2 {
|
||||
return key, false
|
||||
}
|
||||
var pattern strings.Builder
|
||||
pattern.WriteString("^")
|
||||
pattern.WriteString(parts[0])
|
||||
skip := false
|
||||
for _, part := range parts[1:] {
|
||||
if skip {
|
||||
skip = false
|
||||
continue
|
||||
}
|
||||
if len(part) == 0 {
|
||||
// "%%"
|
||||
pattern.WriteString("%")
|
||||
continue
|
||||
}
|
||||
switch part[0] {
|
||||
case 'd':
|
||||
pattern.WriteString("[0-9]+")
|
||||
default:
|
||||
pattern.WriteString("[A-Za-z0-9]*")
|
||||
}
|
||||
pattern.WriteString(part[1:])
|
||||
}
|
||||
pattern.WriteString("$")
|
||||
return pattern.String(), true
|
||||
}
|
||||
|
||||
func (m StringTrieMap) Matches(key []string) bool {
|
||||
if len(key) == 0 || m == nil {
|
||||
return true
|
||||
}
|
||||
value, ok := m[key[0]]
|
||||
if !ok {
|
||||
for altKey, value := range m {
|
||||
// TODO: cache mapping $printfFormatString -> $regexpCompileOutput
|
||||
pattern, found := printfPatternToRegex(altKey)
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
matched, err := regexp.MatchString(pattern, key[0])
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("unable to compile regexp '%s': %s", pattern, err.Error()))
|
||||
}
|
||||
if matched && (value == nil || value.Matches(key[1:])) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if value == nil {
|
||||
return true
|
||||
}
|
||||
return value.Matches(key[1:])
|
||||
}
|
||||
|
||||
func (m StringTrieMap) Insert(key []string) {
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch len(key) {
|
||||
case 0:
|
||||
return
|
||||
|
||||
case 1:
|
||||
m[key[0]] = nil
|
||||
|
||||
default:
|
||||
if value, ok := m[key[0]]; ok {
|
||||
if value == nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
m[key[0]] = make(StringTrieMap)
|
||||
}
|
||||
m[key[0]].(StringTrieMap).Insert(key[1:])
|
||||
}
|
||||
}
|
||||
|
||||
func ParseAllowedMaskedUsages(fname string, usedMsgids container.Set[string], allowedMaskedPrefixes StringTrieMap, chkMsgid func(msgid string) bool) error {
|
||||
file, err := os.Open(fname)
|
||||
if err != nil {
|
||||
return llu.LocatedError{
|
||||
Location: fname,
|
||||
Kind: "Open",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
lno := 0
|
||||
for scanner.Scan() {
|
||||
lno++
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
if linePrefix, found := strings.CutSuffix(line, "."); found || strings.Contains(line, "%") {
|
||||
allowedMaskedPrefixes.Insert(strings.Split(linePrefix, "."))
|
||||
} else {
|
||||
if !chkMsgid(line) {
|
||||
return llu.LocatedError{
|
||||
Location: fmt.Sprintf("%s: line %d", fname, lno),
|
||||
Kind: "undefined msgid",
|
||||
Err: errors.New(line),
|
||||
}
|
||||
}
|
||||
usedMsgids.Add(line)
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return llu.LocatedError{
|
||||
Location: fname,
|
||||
Kind: "Scanner",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Usage() {
|
||||
outp := flag.CommandLine.Output()
|
||||
fmt.Fprintf(outp, "Usage of %s:\n", os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
|
||||
fmt.Fprintf(outp, "\nThis command assumes that it gets started from the project root directory.\n")
|
||||
|
||||
fmt.Fprintf(outp, "\nExit codes:\n")
|
||||
for _, i := range []string{
|
||||
"0\tsuccess, no issues found",
|
||||
"1\tunable to walk directory tree",
|
||||
"2\tunable to parse locale ini/json files",
|
||||
"3\tunable to parse go or text/template files",
|
||||
"4\tfound missing message IDs",
|
||||
"5\tfound unused message IDs",
|
||||
} {
|
||||
fmt.Fprintf(outp, "\t%s\n", i)
|
||||
}
|
||||
|
||||
fmt.Fprintf(outp, "\nSpecial Go doc comments:\n")
|
||||
for _, i := range []string{
|
||||
"//llu:returnsTrKeyWeak",
|
||||
"\tcan be used in front of functions to indicate",
|
||||
"\tthat the function returns message IDs (allows nesting inside complicated function calls)",
|
||||
"\tWARNING: this currently doesn't support nested functions properly",
|
||||
"",
|
||||
"//llu:returnsTrKey",
|
||||
"\tcan be used in front of functions to indicate",
|
||||
"\tthat the function returns message IDs (doesn't allow nesting inside complicated function calls)",
|
||||
"\tWARNING: this currently doesn't support nested functions properly",
|
||||
"",
|
||||
"//llu:returnsTrKeySuffix prefix.",
|
||||
"\tsimilar to llu:returnsTrKey, but the given prefix is prepended",
|
||||
"\tto the found strings before interpreting them as msgids",
|
||||
"",
|
||||
"// llu:TrKeys",
|
||||
"\tcan be used in front of 'const' and 'var' blocks",
|
||||
"\tin order to mark all contained strings as message IDs",
|
||||
"",
|
||||
"// llu:TrKeysSuffix prefix.",
|
||||
"\tlike llu:returnsTrKeySuffix, but for 'const' and 'var' blocks",
|
||||
} {
|
||||
if i == "" {
|
||||
fmt.Fprintf(outp, "\n")
|
||||
} else {
|
||||
fmt.Fprintf(outp, "\t%s\n", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:forbidigo
|
||||
func main() {
|
||||
allowMissingMsgids := false
|
||||
allowUnusedMsgids := false
|
||||
allowWeakMissingMsgids := true
|
||||
usedMsgids := make(container.Set[string])
|
||||
allowedMaskedPrefixes := make(StringTrieMap)
|
||||
|
||||
// It's possible for execl to hand us an empty os.Args.
|
||||
if len(os.Args) == 0 {
|
||||
flag.CommandLine = flag.NewFlagSet("lint-locale-usage", flag.ExitOnError)
|
||||
} else {
|
||||
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
|
||||
}
|
||||
flag.CommandLine.Usage = Usage
|
||||
flag.Usage = Usage
|
||||
|
||||
flag.BoolVar(
|
||||
&allowMissingMsgids,
|
||||
"allow-missing-msgids",
|
||||
false,
|
||||
"don't return an error code if missing message IDs are found",
|
||||
)
|
||||
flag.BoolVar(
|
||||
&allowWeakMissingMsgids,
|
||||
"allow-weak-missing-msgids",
|
||||
true,
|
||||
"Don't return an error code if missing 'weak' (e.g. \"form.$msgid\") message IDs are found",
|
||||
)
|
||||
flag.BoolVar(
|
||||
&allowUnusedMsgids,
|
||||
"allow-unused-msgids",
|
||||
false,
|
||||
"don't return an error code if unused message IDs are found",
|
||||
)
|
||||
|
||||
msgids := make(container.Set[string])
|
||||
|
||||
localeFile := filepath.Join(filepath.Join("options", "locale"), "locale_en-US.ini")
|
||||
localeContent, err := os.ReadFile(localeFile)
|
||||
if err != nil {
|
||||
fmt.Printf("%s:\tERROR: %s\n", localeFile, err.Error())
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if err = localeiter.IterateMessagesContent(localeContent, func(trKey, trValue string) error {
|
||||
msgids[trKey] = struct{}{}
|
||||
return nil
|
||||
}); err != nil {
|
||||
fmt.Printf("%s:\tERROR: %s\n", localeFile, err.Error())
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
localeFile = filepath.Join(filepath.Join("options", "locale_next"), "locale_en-US.json")
|
||||
localeContent, err = os.ReadFile(localeFile)
|
||||
if err != nil {
|
||||
fmt.Printf("%s:\tERROR: %s\n", localeFile, err.Error())
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if err := localeiter.IterateMessagesNextContent(localeContent, func(trKey, pluralForm, trValue string) error {
|
||||
// ignore plural form
|
||||
msgids[trKey] = struct{}{}
|
||||
return nil
|
||||
}); err != nil {
|
||||
fmt.Printf("%s:\tERROR: %s\n", localeFile, err.Error())
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
gotAnyMsgidError := false
|
||||
|
||||
flag.Func(
|
||||
"allow-masked-usages-from",
|
||||
"supply a file containing a newline-separated list of allowed masked usages",
|
||||
func(argval string) error {
|
||||
return ParseAllowedMaskedUsages(argval, usedMsgids, allowedMaskedPrefixes, func(msgid string) bool {
|
||||
return msgids.Contains(msgid)
|
||||
})
|
||||
},
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
onError := func(err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(3)
|
||||
}
|
||||
|
||||
handler := llu.Handler{
|
||||
OnMsgidPattern: func(fset *token.FileSet, pos token.Pos, msgidPattern string) {
|
||||
msgidPatternSplit := strings.Split(msgidPattern, ".")
|
||||
allowedMaskedPrefixes.Insert(msgidPatternSplit)
|
||||
},
|
||||
OnMsgidPrefix: func(fset *token.FileSet, pos token.Pos, msgidPrefix string, truncated bool) {
|
||||
msgidPrefixSplit := strings.Split(msgidPrefix, ".")
|
||||
if !truncated {
|
||||
allowedMaskedPrefixes.Insert(msgidPrefixSplit)
|
||||
} else if !allowedMaskedPrefixes.Matches(msgidPrefixSplit) {
|
||||
gotAnyMsgidError = true
|
||||
fmt.Printf("%s:\tmissing msgid prefix: %s\n", fset.Position(pos).String(), msgidPrefix)
|
||||
}
|
||||
},
|
||||
OnMsgid: func(fset *token.FileSet, pos token.Pos, msgid string, weak bool) {
|
||||
if strings.Contains(msgid, "%") {
|
||||
fmt.Printf("%s:\tunexpected msgid pattern: %s\n", fset.Position(pos).String(), msgid)
|
||||
return
|
||||
}
|
||||
if !msgids.Contains(msgid) {
|
||||
if weak && allowWeakMissingMsgids {
|
||||
return
|
||||
}
|
||||
gotAnyMsgidError = true
|
||||
fmt.Printf("%s:\tmissing msgid: %s\n", fset.Position(pos).String(), msgid)
|
||||
} else {
|
||||
usedMsgids.Add(msgid)
|
||||
}
|
||||
},
|
||||
OnUnexpectedInvoke: func(fset *token.FileSet, pos token.Pos, funcname string, argc int) {
|
||||
gotAnyMsgidError = true
|
||||
fmt.Printf("%s:\tunexpected invocation of %s with %d arguments\n", fset.Position(pos).String(), funcname, argc)
|
||||
},
|
||||
OnWarning: func(fset *token.FileSet, pos token.Pos, msg string) {
|
||||
fmt.Printf("%s:\tWARNING: %s\n", fset.Position(pos).String(), msg)
|
||||
},
|
||||
LocaleTrFunctions: llu.InitLocaleTrFunctions(),
|
||||
}
|
||||
|
||||
if err := filepath.WalkDir(".", func(fpath string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
name := d.Name()
|
||||
if d.IsDir() {
|
||||
if name == "docker" || name == ".git" || name == "node_modules" {
|
||||
return fs.SkipDir
|
||||
}
|
||||
} else if name == "bindata.go" || fpath == "modules/translation/i18n/i18n_test.go" || fpath == "modules/translation/i18n/i18n_ini_test.go" {
|
||||
// skip false positives
|
||||
} else if strings.HasSuffix(name, ".go") {
|
||||
onError(HandleGoFile(handler, fpath, nil))
|
||||
} else if strings.HasSuffix(name, ".tmpl") {
|
||||
if strings.HasPrefix(fpath, "tests") && strings.HasSuffix(name, ".ini.tmpl") {
|
||||
// skip false positives
|
||||
} else {
|
||||
onError(handler.HandleTemplateFile(fpath, nil))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
fmt.Printf("walkdir ERROR: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
unusedMsgids := []string{}
|
||||
|
||||
for msgid := range msgids {
|
||||
if !usedMsgids.Contains(msgid) && !allowedMaskedPrefixes.Matches(strings.Split(msgid, ".")) {
|
||||
unusedMsgids = append(unusedMsgids, msgid)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(unusedMsgids)
|
||||
|
||||
if len(unusedMsgids) != 0 {
|
||||
fmt.Printf("=== unused msgids (%d): ===\n", len(unusedMsgids))
|
||||
for _, msgid := range unusedMsgids {
|
||||
fmt.Printf("- %s\n", msgid)
|
||||
}
|
||||
}
|
||||
|
||||
if !allowMissingMsgids && gotAnyMsgidError {
|
||||
os.Exit(4)
|
||||
}
|
||||
if !allowUnusedMsgids && len(unusedMsgids) != 0 {
|
||||
os.Exit(5)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package lintLocaleUsage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (handler Handler) HandleGoTrBasicLit(fset *token.FileSet, argLit *ast.BasicLit, prefix string) {
|
||||
if argLit.Kind == token.STRING {
|
||||
// extract string content
|
||||
arg, err := strconv.Unquote(argLit.Value)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// found interesting strings
|
||||
arg = prefix + arg
|
||||
if strings.HasSuffix(arg, ".") || strings.HasSuffix(arg, "_") {
|
||||
prep, trunc := PrepareMsgidPrefix(arg)
|
||||
if trunc {
|
||||
handler.OnWarning(fset, argLit.ValuePos, fmt.Sprintf("needed to truncate message id prefix: %s", arg))
|
||||
}
|
||||
handler.OnMsgidPrefix(fset, argLit.ValuePos, prep, trunc)
|
||||
} else {
|
||||
handler.OnMsgid(fset, argLit.ValuePos, arg, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (handler Handler) HandleGoTrArgument(fset *token.FileSet, n ast.Expr, prefix string) {
|
||||
switch n := n.(type) {
|
||||
case *ast.BasicLit:
|
||||
handler.HandleGoTrBasicLit(fset, n, prefix)
|
||||
|
||||
case *ast.BinaryExpr:
|
||||
if n.Op != token.ADD {
|
||||
// pass
|
||||
} else if argLit, ok := n.X.(*ast.BasicLit); ok && argLit.Kind == token.STRING {
|
||||
// extract string content
|
||||
arg, err := strconv.Unquote(argLit.Value)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// found interesting strings
|
||||
arg = prefix + arg
|
||||
prep, trunc := PrepareMsgidPrefix(arg)
|
||||
if trunc {
|
||||
handler.OnWarning(fset, argLit.ValuePos, fmt.Sprintf("needed to truncate message id prefix: %s", arg))
|
||||
}
|
||||
handler.OnMsgidPrefix(fset, argLit.ValuePos, prep, trunc)
|
||||
}
|
||||
|
||||
case *ast.CallExpr:
|
||||
if selExpr, ok := n.Fun.(*ast.SelectorExpr); ok {
|
||||
if xIdent, xok := selExpr.X.(*ast.Ident); !xok || xIdent.Name != "fmt" {
|
||||
return
|
||||
}
|
||||
if selExpr.Sel.Name != "Sprintf" {
|
||||
handler.OnWarning(fset, selExpr.Sel.NamePos, fmt.Sprintf("unexpected formatting function encountered: %s", selExpr.Sel.Name))
|
||||
return
|
||||
}
|
||||
if len(n.Args) == 0 {
|
||||
handler.OnWarning(fset, selExpr.Sel.NamePos, fmt.Sprintf("unexpected formatting function invocation (no arguments) of '%s'", selExpr.Sel.Name))
|
||||
return
|
||||
}
|
||||
|
||||
if argLit, ok := n.Args[0].(*ast.BasicLit); ok && argLit.Kind == token.STRING {
|
||||
// extract string content
|
||||
arg, err := strconv.Unquote(argLit.Value)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if strings.Contains(arg, " ") {
|
||||
handler.OnWarning(fset, argLit.ValuePos, fmt.Sprintf(
|
||||
"formatting function invocation of '%s' with weird msgid format string: %s",
|
||||
selExpr.Sel.Name,
|
||||
arg,
|
||||
))
|
||||
return
|
||||
}
|
||||
// found interesting strings
|
||||
handler.OnMsgidPattern(fset, argLit.ValuePos, prefix+arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (handler Handler) HandleGoCommentGroup(fset *token.FileSet, cg *ast.CommentGroup, commentPrefix string) *string {
|
||||
if cg == nil {
|
||||
return nil
|
||||
}
|
||||
var matches []token.Pos
|
||||
matchInsPrefix := ""
|
||||
commentPrefix = "//" + commentPrefix
|
||||
for _, comment := range cg.List {
|
||||
ctxt := strings.TrimSpace(comment.Text)
|
||||
if ctxt == commentPrefix {
|
||||
matches = append(matches, comment.Slash)
|
||||
} else if after, found := strings.CutPrefix(ctxt, commentPrefix+"Suffix "); found {
|
||||
matches = append(matches, comment.Slash)
|
||||
matchInsPrefix = strings.TrimSpace(after)
|
||||
}
|
||||
}
|
||||
switch len(matches) {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
return &matchInsPrefix
|
||||
default:
|
||||
handler.OnWarning(
|
||||
fset,
|
||||
matches[0],
|
||||
fmt.Sprintf("encountered multiple %s... directives, ignoring", strings.TrimSpace(commentPrefix)),
|
||||
)
|
||||
return &matchInsPrefix
|
||||
}
|
||||
}
|
||||
|
|
@ -1,244 +0,0 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package lintLocaleUsage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/token"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
tmplParser "text/template/parse"
|
||||
|
||||
fjTemplates "forgejo.org/modules/templates"
|
||||
"forgejo.org/modules/util"
|
||||
)
|
||||
|
||||
// derived from source: modules/templates/scopedtmpl/scopedtmpl.go, L169-L213
|
||||
func (handler Handler) handleTemplateNode(fset *token.FileSet, node tmplParser.Node) {
|
||||
switch node.Type() {
|
||||
case tmplParser.NodeAction:
|
||||
handler.handleTemplatePipeNode(fset, node.(*tmplParser.ActionNode).Pipe)
|
||||
case tmplParser.NodeList:
|
||||
nodeList := node.(*tmplParser.ListNode)
|
||||
handler.handleTemplateFileNodes(fset, nodeList.Nodes)
|
||||
case tmplParser.NodePipe:
|
||||
handler.handleTemplatePipeNode(fset, node.(*tmplParser.PipeNode))
|
||||
case tmplParser.NodeTemplate:
|
||||
handler.handleTemplatePipeNode(fset, node.(*tmplParser.TemplateNode).Pipe)
|
||||
case tmplParser.NodeIf:
|
||||
nodeIf := node.(*tmplParser.IfNode)
|
||||
handler.handleTemplateBranchNode(fset, nodeIf.BranchNode)
|
||||
case tmplParser.NodeRange:
|
||||
nodeRange := node.(*tmplParser.RangeNode)
|
||||
handler.handleTemplateBranchNode(fset, nodeRange.BranchNode)
|
||||
case tmplParser.NodeWith:
|
||||
nodeWith := node.(*tmplParser.WithNode)
|
||||
handler.handleTemplateBranchNode(fset, nodeWith.BranchNode)
|
||||
|
||||
case tmplParser.NodeCommand:
|
||||
nodeCommand := node.(*tmplParser.CommandNode)
|
||||
|
||||
handler.handleTemplateFileNodes(fset, nodeCommand.Args)
|
||||
|
||||
if len(nodeCommand.Args) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
funcname := ""
|
||||
switch nodeCommand.Args[0].Type() {
|
||||
case tmplParser.NodeChain:
|
||||
nodeChain := nodeCommand.Args[0].(*tmplParser.ChainNode)
|
||||
if nodeIdent, ok := nodeChain.Node.(*tmplParser.IdentifierNode); ok {
|
||||
if nodeIdent.Ident != "ctx" || len(nodeChain.Field) != 2 || nodeChain.Field[0] != "Locale" {
|
||||
return
|
||||
}
|
||||
funcname = nodeChain.Field[1]
|
||||
}
|
||||
|
||||
case tmplParser.NodeField:
|
||||
nodeField := nodeCommand.Args[0].(*tmplParser.FieldNode)
|
||||
if len(nodeField.Ident) != 2 || nodeField.Ident[0] != "locale" {
|
||||
return
|
||||
}
|
||||
resolvedPos := fset.PositionFor(token.Pos(nodeCommand.Pos), false)
|
||||
if !strings.Contains(resolvedPos.Filename, "templates/mail/") {
|
||||
handler.OnWarning(fset, token.Pos(nodeCommand.Pos), "encountered unexpected .locale usage")
|
||||
}
|
||||
funcname = nodeField.Ident[1]
|
||||
|
||||
case tmplParser.NodeVariable:
|
||||
nodeVar := nodeCommand.Args[0].(*tmplParser.VariableNode)
|
||||
if len(nodeVar.Ident) != 3 || !(nodeVar.Ident[0] == "$" && nodeVar.Ident[1] == "locale") {
|
||||
return
|
||||
}
|
||||
funcname = nodeVar.Ident[2]
|
||||
}
|
||||
|
||||
var gotUnexpectedInvoke *int
|
||||
ltf, ok := handler.LocaleTrFunctions[funcname]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for _, argNum := range ltf {
|
||||
if len(nodeCommand.Args) >= int(argNum+2) {
|
||||
handler.handleTemplateMsgid(fset, nodeCommand.Args[int(argNum+1)])
|
||||
} else {
|
||||
argc := len(nodeCommand.Args) - 1
|
||||
gotUnexpectedInvoke = &argc
|
||||
}
|
||||
}
|
||||
|
||||
if gotUnexpectedInvoke != nil {
|
||||
handler.OnUnexpectedInvoke(fset, token.Pos(nodeCommand.Pos), funcname, *gotUnexpectedInvoke)
|
||||
}
|
||||
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (handler Handler) handleTemplateMsgid(fset *token.FileSet, node tmplParser.Node) {
|
||||
// the column numbers are a bit "off", but much better than nothing
|
||||
pos := token.Pos(node.Position())
|
||||
|
||||
switch node.Type() {
|
||||
case tmplParser.NodeString:
|
||||
nodeString := node.(*tmplParser.StringNode)
|
||||
// found interesting strings
|
||||
handler.OnMsgid(fset, pos, nodeString.Text, false)
|
||||
|
||||
case tmplParser.NodePipe:
|
||||
nodePipe := node.(*tmplParser.PipeNode)
|
||||
handler.handleTemplatePipeNode(fset, nodePipe)
|
||||
|
||||
if len(nodePipe.Cmds) == 0 {
|
||||
handler.OnWarning(fset, pos, fmt.Sprintf("unsupported invocation of locate function (no commands): %s", node.String()))
|
||||
} else if len(nodePipe.Cmds) != 1 {
|
||||
handler.OnWarning(fset, pos, fmt.Sprintf("unsupported invocation of locate function (too many commands): %s", node.String()))
|
||||
return
|
||||
}
|
||||
nodeCommand := nodePipe.Cmds[0]
|
||||
if len(nodeCommand.Args) < 2 {
|
||||
handler.OnWarning(fset, pos, fmt.Sprintf("unsupported invocation of locate function (not enough arguments): %s", node.String()))
|
||||
return
|
||||
}
|
||||
|
||||
nodeIdent, ok := nodeCommand.Args[0].(*tmplParser.IdentifierNode)
|
||||
if !ok || (nodeIdent.Ident != "print" && nodeIdent.Ident != "printf") {
|
||||
// handler.OnWarning(fset, pos, fmt.Sprintf("unsupported invocation of locate function (bad command): %s", node.String()))
|
||||
return
|
||||
}
|
||||
|
||||
nodeString, ok := nodeCommand.Args[1].(*tmplParser.StringNode)
|
||||
if !ok {
|
||||
//handler.OnWarning(
|
||||
// fset,
|
||||
// pos,
|
||||
// fmt.Sprintf("unsupported invocation of locate function (string should be first argument to %s): %s", nodeIdent.Ident, node.String()),
|
||||
//)
|
||||
return
|
||||
}
|
||||
|
||||
msgidPrefix := nodeString.Text
|
||||
stringPos := token.Pos(nodeString.Pos)
|
||||
|
||||
if len(nodeCommand.Args) == 2 {
|
||||
// found interesting strings
|
||||
handler.OnMsgid(fset, stringPos, msgidPrefix, false)
|
||||
} else {
|
||||
if nodeIdent.Ident == "printf" {
|
||||
// found interesting strings
|
||||
if !(strings.HasSuffix(msgidPrefix, ".%s") && strings.Count(msgidPrefix, "%") == 1) {
|
||||
handler.OnMsgidPattern(fset, stringPos, msgidPrefix)
|
||||
return
|
||||
}
|
||||
msgidPrefix = strings.TrimSuffix(msgidPrefix, "%s")
|
||||
}
|
||||
|
||||
msgidPrefixFin, truncated := PrepareMsgidPrefix(msgidPrefix)
|
||||
if truncated {
|
||||
handler.OnWarning(fset, stringPos, fmt.Sprintf("needed to truncate message id prefix: %s", msgidPrefix))
|
||||
}
|
||||
|
||||
// found interesting strings
|
||||
handler.OnMsgidPrefix(fset, stringPos, msgidPrefixFin, truncated)
|
||||
}
|
||||
|
||||
default:
|
||||
// handler.OnWarning(fset, pos, fmt.Sprintf("unknown invocation of locate function: %s", node.String()))
|
||||
}
|
||||
}
|
||||
|
||||
func (handler Handler) handleTemplatePipeNode(fset *token.FileSet, pipeNode *tmplParser.PipeNode) {
|
||||
if pipeNode == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// NOTE: we can't pass `pipeNode.Cmds` to handleTemplateFileNodes due to incompatible argument types
|
||||
for _, node := range pipeNode.Cmds {
|
||||
handler.handleTemplateNode(fset, node)
|
||||
}
|
||||
}
|
||||
|
||||
func (handler Handler) handleTemplateBranchNode(fset *token.FileSet, branchNode tmplParser.BranchNode) {
|
||||
handler.handleTemplatePipeNode(fset, branchNode.Pipe)
|
||||
handler.handleTemplateFileNodes(fset, branchNode.List.Nodes)
|
||||
if branchNode.ElseList != nil {
|
||||
handler.handleTemplateFileNodes(fset, branchNode.ElseList.Nodes)
|
||||
}
|
||||
}
|
||||
|
||||
func (handler Handler) handleTemplateFileNodes(fset *token.FileSet, nodes []tmplParser.Node) {
|
||||
for _, node := range nodes {
|
||||
handler.handleTemplateNode(fset, node)
|
||||
}
|
||||
}
|
||||
|
||||
// the `Handle*File` functions follow the following calling convention:
|
||||
// * `fname` is the name of the input file
|
||||
// * `src` is either `nil` (then the function invokes `ReadFile` to read the file)
|
||||
// or the contents of the file as {`[]byte`, or a `string`}
|
||||
|
||||
func (handler Handler) HandleTemplateFile(fname string, src any) error {
|
||||
var tmplContent []byte
|
||||
switch src2 := src.(type) {
|
||||
case nil:
|
||||
var err error
|
||||
tmplContent, err = os.ReadFile(fname)
|
||||
if err != nil {
|
||||
return LocatedError{
|
||||
Location: fname,
|
||||
Kind: "ReadFile",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
case []byte:
|
||||
tmplContent = src2
|
||||
case string:
|
||||
// SAFETY: we do not modify tmplContent below
|
||||
tmplContent = util.UnsafeStringToBytes(src2)
|
||||
default:
|
||||
panic("invalid type for 'src'")
|
||||
}
|
||||
|
||||
fset := token.NewFileSet()
|
||||
fset.AddFile(fname, 1, len(tmplContent)).SetLinesForContent(tmplContent)
|
||||
// SAFETY: we do not modify tmplContent2 below
|
||||
tmplContent2 := util.UnsafeBytesToString(tmplContent)
|
||||
|
||||
tmpl := template.New(fname)
|
||||
tmpl.Funcs(fjTemplates.NewFuncMap())
|
||||
tmplParsed, err := tmpl.Parse(tmplContent2)
|
||||
if err != nil {
|
||||
return LocatedError{
|
||||
Location: fname,
|
||||
Kind: "Template parser",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
handler.handleTemplateFileNodes(fset, tmplParsed.Root.Nodes)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package lintLocaleUsage
|
||||
|
||||
import (
|
||||
"go/token"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type LocatedError struct {
|
||||
Location string
|
||||
Kind string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e LocatedError) Error() string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(e.Location)
|
||||
sb.WriteString(":\t")
|
||||
if e.Kind != "" {
|
||||
sb.WriteString(e.Kind)
|
||||
sb.WriteString(": ")
|
||||
}
|
||||
sb.WriteString("ERROR: ")
|
||||
sb.WriteString(e.Err.Error())
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func InitLocaleTrFunctions() map[string][]uint {
|
||||
ret := make(map[string][]uint)
|
||||
|
||||
f0 := []uint{0}
|
||||
ret["Tr"] = f0
|
||||
ret["TrString"] = f0
|
||||
ret["TrHTML"] = f0
|
||||
|
||||
ret["TrPluralString"] = []uint{1}
|
||||
ret["TrN"] = []uint{1, 2}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
OnMsgid func(fset *token.FileSet, pos token.Pos, msgid string, weak bool)
|
||||
OnMsgidPrefix func(fset *token.FileSet, pos token.Pos, msgidPrefix string, truncated bool)
|
||||
OnMsgidPattern func(fset *token.FileSet, pos token.Pos, msgidPattern string)
|
||||
OnUnexpectedInvoke func(fset *token.FileSet, pos token.Pos, funcname string, argc int)
|
||||
OnWarning func(fset *token.FileSet, pos token.Pos, msg string)
|
||||
LocaleTrFunctions map[string][]uint
|
||||
}
|
||||
|
||||
// Truncating a message id prefix to the last dot
|
||||
func PrepareMsgidPrefix(s string) (string, bool) {
|
||||
index := strings.LastIndexByte(s, 0x2e)
|
||||
if index == -1 {
|
||||
return "", true
|
||||
}
|
||||
return s[:index], index != len(s)-1
|
||||
}
|
||||
331
build/lint-locale-usage/lint-locale-usage.go
Normal file
331
build/lint-locale-usage/lint-locale-usage.go
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
goParser "go/parser"
|
||||
"go/token"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
tmplParser "text/template/parse"
|
||||
|
||||
"forgejo.org/modules/container"
|
||||
"forgejo.org/modules/locale"
|
||||
fjTemplates "forgejo.org/modules/templates"
|
||||
"forgejo.org/modules/util"
|
||||
)
|
||||
|
||||
// this works by first gathering all valid source string IDs from `en-US` reference files
|
||||
// and then checking if all used source strings are actually defined
|
||||
|
||||
type OnMsgidHandler func(fset *token.FileSet, pos token.Pos, msgid string)
|
||||
|
||||
type LocatedError struct {
|
||||
Location string
|
||||
Kind string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e LocatedError) Error() string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(e.Location)
|
||||
sb.WriteString(":\t")
|
||||
if e.Kind != "" {
|
||||
sb.WriteString(e.Kind)
|
||||
sb.WriteString(": ")
|
||||
}
|
||||
sb.WriteString("ERROR: ")
|
||||
sb.WriteString(e.Err.Error())
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func isLocaleTrFunction(funcname string) bool {
|
||||
return funcname == "Tr" || funcname == "TrN"
|
||||
}
|
||||
|
||||
// the `Handle*File` functions follow the following calling convention:
|
||||
// * `fname` is the name of the input file
|
||||
// * `src` is either `nil` (then the function invokes `ReadFile` to read the file)
|
||||
// or the contents of the file as {`[]byte`, or a `string`}
|
||||
|
||||
func (omh OnMsgidHandler) HandleGoFile(fname string, src any) error {
|
||||
fset := token.NewFileSet()
|
||||
node, err := goParser.ParseFile(fset, fname, src, goParser.SkipObjectResolution)
|
||||
if err != nil {
|
||||
return LocatedError{
|
||||
Location: fname,
|
||||
Kind: "Go parser",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
ast.Inspect(node, func(n ast.Node) bool {
|
||||
// search for function calls of the form `anything.Tr(any-string-lit)`
|
||||
|
||||
call, ok := n.(*ast.CallExpr)
|
||||
if !ok || len(call.Args) != 1 {
|
||||
return true
|
||||
}
|
||||
|
||||
funSel, ok := call.Fun.(*ast.SelectorExpr)
|
||||
if (!ok) || !isLocaleTrFunction(funSel.Sel.Name) {
|
||||
return true
|
||||
}
|
||||
|
||||
argLit, ok := call.Args[0].(*ast.BasicLit)
|
||||
if (!ok) || argLit.Kind != token.STRING {
|
||||
return true
|
||||
}
|
||||
|
||||
// extract string content
|
||||
arg, err := strconv.Unquote(argLit.Value)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// found interesting string
|
||||
omh(fset, argLit.ValuePos, arg)
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// derived from source: modules/templates/scopedtmpl/scopedtmpl.go, L169-L213
|
||||
func (omh OnMsgidHandler) handleTemplateNode(fset *token.FileSet, node tmplParser.Node) {
|
||||
switch node.Type() {
|
||||
case tmplParser.NodeAction:
|
||||
omh.handleTemplatePipeNode(fset, node.(*tmplParser.ActionNode).Pipe)
|
||||
case tmplParser.NodeList:
|
||||
nodeList := node.(*tmplParser.ListNode)
|
||||
omh.handleTemplateFileNodes(fset, nodeList.Nodes)
|
||||
case tmplParser.NodePipe:
|
||||
omh.handleTemplatePipeNode(fset, node.(*tmplParser.PipeNode))
|
||||
case tmplParser.NodeTemplate:
|
||||
omh.handleTemplatePipeNode(fset, node.(*tmplParser.TemplateNode).Pipe)
|
||||
case tmplParser.NodeIf:
|
||||
nodeIf := node.(*tmplParser.IfNode)
|
||||
omh.handleTemplateBranchNode(fset, nodeIf.BranchNode)
|
||||
case tmplParser.NodeRange:
|
||||
nodeRange := node.(*tmplParser.RangeNode)
|
||||
omh.handleTemplateBranchNode(fset, nodeRange.BranchNode)
|
||||
case tmplParser.NodeWith:
|
||||
nodeWith := node.(*tmplParser.WithNode)
|
||||
omh.handleTemplateBranchNode(fset, nodeWith.BranchNode)
|
||||
|
||||
case tmplParser.NodeCommand:
|
||||
nodeCommand := node.(*tmplParser.CommandNode)
|
||||
|
||||
omh.handleTemplateFileNodes(fset, nodeCommand.Args)
|
||||
|
||||
if len(nodeCommand.Args) != 2 {
|
||||
return
|
||||
}
|
||||
|
||||
nodeChain, ok := nodeCommand.Args[0].(*tmplParser.ChainNode)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
nodeString, ok := nodeCommand.Args[1].(*tmplParser.StringNode)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
nodeIdent, ok := nodeChain.Node.(*tmplParser.IdentifierNode)
|
||||
if !ok || nodeIdent.Ident != "ctx" {
|
||||
return
|
||||
}
|
||||
|
||||
if len(nodeChain.Field) != 2 || nodeChain.Field[0] != "Locale" || !isLocaleTrFunction(nodeChain.Field[1]) {
|
||||
return
|
||||
}
|
||||
|
||||
// found interesting string
|
||||
// the column numbers are a bit "off", but much better than nothing
|
||||
omh(fset, token.Pos(nodeString.Pos), nodeString.Text)
|
||||
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (omh OnMsgidHandler) handleTemplatePipeNode(fset *token.FileSet, pipeNode *tmplParser.PipeNode) {
|
||||
if pipeNode == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// NOTE: we can't pass `pipeNode.Cmds` to handleTemplateFileNodes due to incompatible argument types
|
||||
for _, node := range pipeNode.Cmds {
|
||||
omh.handleTemplateNode(fset, node)
|
||||
}
|
||||
}
|
||||
|
||||
func (omh OnMsgidHandler) handleTemplateBranchNode(fset *token.FileSet, branchNode tmplParser.BranchNode) {
|
||||
omh.handleTemplatePipeNode(fset, branchNode.Pipe)
|
||||
omh.handleTemplateFileNodes(fset, branchNode.List.Nodes)
|
||||
if branchNode.ElseList != nil {
|
||||
omh.handleTemplateFileNodes(fset, branchNode.ElseList.Nodes)
|
||||
}
|
||||
}
|
||||
|
||||
func (omh OnMsgidHandler) handleTemplateFileNodes(fset *token.FileSet, nodes []tmplParser.Node) {
|
||||
for _, node := range nodes {
|
||||
omh.handleTemplateNode(fset, node)
|
||||
}
|
||||
}
|
||||
|
||||
func (omh OnMsgidHandler) HandleTemplateFile(fname string, src any) error {
|
||||
var tmplContent []byte
|
||||
switch src2 := src.(type) {
|
||||
case nil:
|
||||
var err error
|
||||
tmplContent, err = os.ReadFile(fname)
|
||||
if err != nil {
|
||||
return LocatedError{
|
||||
Location: fname,
|
||||
Kind: "ReadFile",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
case []byte:
|
||||
tmplContent = src2
|
||||
case string:
|
||||
// SAFETY: we do not modify tmplContent below
|
||||
tmplContent = util.UnsafeStringToBytes(src2)
|
||||
default:
|
||||
panic("invalid type for 'src'")
|
||||
}
|
||||
|
||||
fset := token.NewFileSet()
|
||||
fset.AddFile(fname, 1, len(tmplContent)).SetLinesForContent(tmplContent)
|
||||
// SAFETY: we do not modify tmplContent2 below
|
||||
tmplContent2 := util.UnsafeBytesToString(tmplContent)
|
||||
|
||||
tmpl := template.New(fname)
|
||||
tmpl.Funcs(fjTemplates.NewFuncMap())
|
||||
tmplParsed, err := tmpl.Parse(tmplContent2)
|
||||
if err != nil {
|
||||
return LocatedError{
|
||||
Location: fname,
|
||||
Kind: "Template parser",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
omh.handleTemplateFileNodes(fset, tmplParsed.Tree.Root.Nodes)
|
||||
return nil
|
||||
}
|
||||
|
||||
// This command assumes that we get started from the project root directory
|
||||
//
|
||||
// Possible command line flags:
|
||||
//
|
||||
// --allow-missing-msgids don't return an error code if missing message IDs are found
|
||||
//
|
||||
// EXIT CODES:
|
||||
//
|
||||
// 0 success, no issues found
|
||||
// 1 unable to walk directory tree
|
||||
// 2 unable to parse locale ini/json files
|
||||
// 3 unable to parse go or text/template files
|
||||
// 4 found missing message IDs
|
||||
//
|
||||
//nolint:forbidigo
|
||||
func main() {
|
||||
allowMissingMsgids := false
|
||||
for _, arg := range os.Args[1:] {
|
||||
if arg == "--allow-missing-msgids" {
|
||||
allowMissingMsgids = true
|
||||
}
|
||||
}
|
||||
|
||||
onError := func(err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(3)
|
||||
}
|
||||
|
||||
msgids := make(container.Set[string])
|
||||
onMsgid := func(trKey, trValue string) error {
|
||||
msgids[trKey] = struct{}{}
|
||||
return nil
|
||||
}
|
||||
|
||||
localeFile := filepath.Join(filepath.Join("options", "locale"), "locale_en-US.ini")
|
||||
localeContent, err := os.ReadFile(localeFile)
|
||||
if err != nil {
|
||||
fmt.Printf("%s:\tERROR: %s\n", localeFile, err.Error())
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if err = locale.IterateMessagesContent(localeContent, onMsgid); err != nil {
|
||||
fmt.Printf("%s:\tERROR: %s\n", localeFile, err.Error())
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
localeFile = filepath.Join(filepath.Join("options", "locale_next"), "locale_en-US.json")
|
||||
localeContent, err = os.ReadFile(localeFile)
|
||||
if err != nil {
|
||||
fmt.Printf("%s:\tERROR: %s\n", localeFile, err.Error())
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if err := locale.IterateMessagesNextContent(localeContent, onMsgid); err != nil {
|
||||
fmt.Printf("%s:\tERROR: %s\n", localeFile, err.Error())
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
gotAnyMsgidError := false
|
||||
|
||||
omh := OnMsgidHandler(func(fset *token.FileSet, pos token.Pos, msgid string) {
|
||||
if !msgids.Contains(msgid) {
|
||||
gotAnyMsgidError = true
|
||||
fmt.Printf("%s:\tmissing msgid: %s\n", fset.Position(pos).String(), msgid)
|
||||
}
|
||||
})
|
||||
|
||||
if err := filepath.WalkDir(".", func(fpath string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
name := d.Name()
|
||||
if d.IsDir() {
|
||||
if name == "docker" || name == ".git" || name == "node_modules" {
|
||||
return fs.SkipDir
|
||||
}
|
||||
} else if name == "bindata.go" {
|
||||
// skip false positives
|
||||
} else if strings.HasSuffix(name, ".go") {
|
||||
onError(omh.HandleGoFile(fpath, nil))
|
||||
} else if strings.HasSuffix(name, ".tmpl") {
|
||||
if strings.HasPrefix(fpath, "tests") && strings.HasSuffix(name, ".ini.tmpl") {
|
||||
// skip false positives
|
||||
} else {
|
||||
onError(omh.HandleTemplateFile(fpath, nil))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
fmt.Printf("walkdir ERROR: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if !allowMissingMsgids && gotAnyMsgidError {
|
||||
os.Exit(4)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,45 +7,37 @@ import (
|
|||
"go/token"
|
||||
"testing"
|
||||
|
||||
llu "forgejo.org/build/lint-locale-usage"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func buildHandler(ret *[]string) llu.Handler {
|
||||
return llu.Handler{
|
||||
OnMsgid: func(fset *token.FileSet, pos token.Pos, msgid string, weak bool) {
|
||||
*ret = append(*ret, msgid)
|
||||
},
|
||||
OnUnexpectedInvoke: func(fset *token.FileSet, pos token.Pos, funcname string, argc int) {},
|
||||
LocaleTrFunctions: llu.InitLocaleTrFunctions(),
|
||||
}
|
||||
}
|
||||
|
||||
func HandleGoFileWrapped(t *testing.T, fname, src string) []string {
|
||||
var ret []string
|
||||
handler := buildHandler(&ret)
|
||||
require.NoError(t, HandleGoFile(handler, fname, src))
|
||||
omh := OnMsgidHandler(func(fset *token.FileSet, pos token.Pos, msgid string) {
|
||||
ret = append(ret, msgid)
|
||||
})
|
||||
require.NoError(t, omh.HandleGoFile(fname, src))
|
||||
return ret
|
||||
}
|
||||
|
||||
func HandleTemplateFileWrapped(t *testing.T, fname, src string) []string {
|
||||
var ret []string
|
||||
handler := buildHandler(&ret)
|
||||
require.NoError(t, handler.HandleTemplateFile(fname, src))
|
||||
omh := OnMsgidHandler(func(fset *token.FileSet, pos token.Pos, msgid string) {
|
||||
ret = append(ret, msgid)
|
||||
})
|
||||
require.NoError(t, omh.HandleTemplateFile(fname, src))
|
||||
return ret
|
||||
}
|
||||
|
||||
func TestUsagesParser(t *testing.T) {
|
||||
t.Run("go, simple", func(t *testing.T) {
|
||||
assert.Equal(t,
|
||||
assert.EqualValues(t,
|
||||
[]string{"what.an.example"},
|
||||
HandleGoFileWrapped(t, "<g1>", "package main\nfunc Render(ctx *context.Context) string { return ctx.Tr(\"what.an.example\"); }\n"))
|
||||
})
|
||||
|
||||
t.Run("template, simple", func(t *testing.T) {
|
||||
assert.Equal(t,
|
||||
assert.EqualValues(t,
|
||||
[]string{"what.an.example"},
|
||||
HandleTemplateFileWrapped(t, "<t1>", "{{ ctx.Locale.Tr \"what.an.example\" }}\n"))
|
||||
})
|
||||
|
|
@ -14,7 +14,7 @@ import (
|
|||
"slices"
|
||||
"strings"
|
||||
|
||||
"forgejo.org/modules/translation/localeiter"
|
||||
"forgejo.org/modules/locale"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
|
|
@ -52,7 +52,7 @@ func initBlueMondayPolicy() {
|
|||
policy.AllowAttrs("id").Matching(positionalPlaceholderRe).OnElements("code")
|
||||
|
||||
// Allowed elements with no attributes. Must be a recognized tagname.
|
||||
policy.AllowElements("strong", "br", "b", "strike", "code", "i", "kbd")
|
||||
policy.AllowElements("strong", "br", "b", "strike", "code", "i")
|
||||
|
||||
// TODO: Remove <c> in `actions.workflow.dispatch.trigger_found`.
|
||||
policy.AllowNoAttrs().OnElements("c")
|
||||
|
|
@ -100,7 +100,7 @@ func checkValue(trKey, value string) []string {
|
|||
func checkLocaleContent(localeContent []byte) []string {
|
||||
errors := []string{}
|
||||
|
||||
if err := localeiter.IterateMessagesContent(localeContent, func(trKey, trValue string) error {
|
||||
if err := locale.IterateMessagesContent(localeContent, func(trKey, trValue string) error {
|
||||
errors = append(errors, checkValue(trKey, trValue)...)
|
||||
return nil
|
||||
}); err != nil {
|
||||
|
|
@ -113,12 +113,8 @@ func checkLocaleContent(localeContent []byte) []string {
|
|||
func checkLocaleNextContent(localeContent []byte) []string {
|
||||
errors := []string{}
|
||||
|
||||
if err := localeiter.IterateMessagesNextContent(localeContent, func(trKey, pluralForm, trValue string) error {
|
||||
fullKey := trKey
|
||||
if pluralForm != "" {
|
||||
fullKey = trKey + "." + pluralForm
|
||||
}
|
||||
errors = append(errors, checkValue(fullKey, trValue)...)
|
||||
if err := locale.IterateMessagesNextContent(localeContent, func(trKey, trValue string) error {
|
||||
errors = append(errors, checkValue(trKey, trValue)...)
|
||||
return nil
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ func TestLocalizationPolicy(t *testing.T) {
|
|||
t.Run("Remove tags", func(t *testing.T) {
|
||||
assert.Empty(t, checkLocaleContent([]byte(`hidden_comment_types_description = Comment types checked here will not be shown inside issue pages. Checking "Label" for example removes all "<user> added/removed <label>" comments.`)))
|
||||
|
||||
assert.Equal(t, []string{"key: \x1b[31m<not-an-allowed-key>\x1b[0m REPLACED-TAG"}, checkLocaleContent([]byte(`key = "<not-an-allowed-key> <label>"`)))
|
||||
assert.Equal(t, []string{"key: \x1b[31m<user@example.com>\x1b[0m REPLACED-TAG"}, checkLocaleContent([]byte(`key = "<user@example.com> <email@example.com>"`)))
|
||||
assert.Equal(t, []string{"key: \x1b[31m<tag>\x1b[0m REPLACED-TAG \x1b[31m</tag>\x1b[0m"}, checkLocaleContent([]byte(`key = "<tag> <email@example.com> </tag>"`)))
|
||||
assert.EqualValues(t, []string{"key: \x1b[31m<not-an-allowed-key>\x1b[0m REPLACED-TAG"}, checkLocaleContent([]byte(`key = "<not-an-allowed-key> <label>"`)))
|
||||
assert.EqualValues(t, []string{"key: \x1b[31m<user@example.com>\x1b[0m REPLACED-TAG"}, checkLocaleContent([]byte(`key = "<user@example.com> <email@example.com>"`)))
|
||||
assert.EqualValues(t, []string{"key: \x1b[31m<tag>\x1b[0m REPLACED-TAG \x1b[31m</tag>\x1b[0m"}, checkLocaleContent([]byte(`key = "<tag> <email@example.com> </tag>"`)))
|
||||
})
|
||||
|
||||
t.Run("Specific exception", func(t *testing.T) {
|
||||
|
|
@ -25,11 +25,11 @@ func TestLocalizationPolicy(t *testing.T) {
|
|||
assert.Empty(t, checkLocaleContent([]byte(`pulls.title_desc_one = wants to merge %[1]d commit from <code>%[2]s</code> into <code id="%[4]s">%[3]s</code>`)))
|
||||
assert.Empty(t, checkLocaleContent([]byte(`editor.commit_directly_to_this_branch = Commit directly to the <strong class="%[2]s">%[1]s</strong> branch.`)))
|
||||
|
||||
assert.Equal(t, []string{"workflow.dispatch.trigger_found: This workflow has a \x1b[31m<d>\x1b[0mworkflow_dispatch\x1b[31m</d>\x1b[0m event trigger."}, checkLocaleContent([]byte(`workflow.dispatch.trigger_found = This workflow has a <d>workflow_dispatch</d> event trigger.`)))
|
||||
assert.Equal(t, []string{"key: <code\x1b[31m id=\"branch_targe\"\x1b[0m>%[3]s</code>"}, checkLocaleContent([]byte(`key = <code id="branch_targe">%[3]s</code>`)))
|
||||
assert.Equal(t, []string{"key: <a\x1b[31m class=\"ui sh\"\x1b[0m href=\"https://TO-BE-REPLACED.COM\">"}, checkLocaleContent([]byte(`key = <a class="ui sh" href="%[3]s">`)))
|
||||
assert.Equal(t, []string{"key: <a\x1b[31m class=\"js-click-me\"\x1b[0m href=\"https://TO-BE-REPLACED.COM\">"}, checkLocaleContent([]byte(`key = <a class="js-click-me" href="%[3]s">`)))
|
||||
assert.Equal(t, []string{"key: <strong\x1b[31m class=\"branch-target\"\x1b[0m>%[1]s</strong>"}, checkLocaleContent([]byte(`key = <strong class="branch-target">%[1]s</strong>`)))
|
||||
assert.EqualValues(t, []string{"workflow.dispatch.trigger_found: This workflow has a \x1b[31m<d>\x1b[0mworkflow_dispatch\x1b[31m</d>\x1b[0m event trigger."}, checkLocaleContent([]byte(`workflow.dispatch.trigger_found = This workflow has a <d>workflow_dispatch</d> event trigger.`)))
|
||||
assert.EqualValues(t, []string{"key: <code\x1b[31m id=\"branch_targe\"\x1b[0m>%[3]s</code>"}, checkLocaleContent([]byte(`key = <code id="branch_targe">%[3]s</code>`)))
|
||||
assert.EqualValues(t, []string{"key: <a\x1b[31m class=\"ui sh\"\x1b[0m href=\"https://TO-BE-REPLACED.COM\">"}, checkLocaleContent([]byte(`key = <a class="ui sh" href="%[3]s">`)))
|
||||
assert.EqualValues(t, []string{"key: <a\x1b[31m class=\"js-click-me\"\x1b[0m href=\"https://TO-BE-REPLACED.COM\">"}, checkLocaleContent([]byte(`key = <a class="js-click-me" href="%[3]s">`)))
|
||||
assert.EqualValues(t, []string{"key: <strong\x1b[31m class=\"branch-target\"\x1b[0m>%[1]s</strong>"}, checkLocaleContent([]byte(`key = <strong class="branch-target">%[1]s</strong>`)))
|
||||
})
|
||||
|
||||
t.Run("General safe tags", func(t *testing.T) {
|
||||
|
|
@ -37,9 +37,8 @@ func TestLocalizationPolicy(t *testing.T) {
|
|||
assert.Empty(t, checkLocaleContent([]byte("teams.specific_repositories_helper = Members will only have access to repositories explicitly added to the team. Selecting this <strong>will not</strong> automatically remove repositories already added with <i>All repositories</i>.")))
|
||||
assert.Empty(t, checkLocaleContent([]byte("sqlite_helper = File path for the SQLite3 database.<br>Enter an absolute path if you run Forgejo as a service.")))
|
||||
assert.Empty(t, checkLocaleContent([]byte("hi_user_x = Hi <b>%s</b>,")))
|
||||
assert.Empty(t, checkLocaleContent([]byte("key = Press <kbd>Shift</kbd>")))
|
||||
|
||||
assert.Equal(t, []string{"error404: The page you are trying to reach either <strong\x1b[31m title='aaa'\x1b[0m>does not exist</strong> or <strong>you are not authorized</strong> to view it."}, checkLocaleContent([]byte("error404 = The page you are trying to reach either <strong title='aaa'>does not exist</strong> or <strong>you are not authorized</strong> to view it.")))
|
||||
assert.EqualValues(t, []string{"error404: The page you are trying to reach either <strong\x1b[31m title='aaa'\x1b[0m>does not exist</strong> or <strong>you are not authorized</strong> to view it."}, checkLocaleContent([]byte("error404 = The page you are trying to reach either <strong title='aaa'>does not exist</strong> or <strong>you are not authorized</strong> to view it.")))
|
||||
})
|
||||
|
||||
t.Run("<a>", func(t *testing.T) {
|
||||
|
|
@ -48,20 +47,20 @@ func TestLocalizationPolicy(t *testing.T) {
|
|||
assert.Empty(t, checkLocaleContent([]byte(`webauthn_desc = Security keys are hardware devices containing cryptographic keys. They can be used for two-factor authentication. Security keys must support the <a rel="noreferrer" target="_blank" href="%s">WebAuthn Authenticator</a> standard.`)))
|
||||
assert.Empty(t, checkLocaleContent([]byte("issues.closed_at = `closed this issue <a id=\"%[1]s\" href=\"#%[1]s\">%[2]s</a>`")))
|
||||
|
||||
assert.Equal(t, []string{"key: \x1b[31m<a href=\"https://example.com\">\x1b[0m"}, checkLocaleContent([]byte(`key = <a href="https://example.com">`)))
|
||||
assert.Equal(t, []string{"key: \x1b[31m<a href=\"javascript:alert('1')\">\x1b[0m"}, checkLocaleContent([]byte(`key = <a href="javascript:alert('1')">`)))
|
||||
assert.Equal(t, []string{"key: <a href=\"https://TO-BE-REPLACED.COM\"\x1b[31m download\x1b[0m>"}, checkLocaleContent([]byte(`key = <a href="%s" download>`)))
|
||||
assert.Equal(t, []string{"key: <a href=\"https://TO-BE-REPLACED.COM\"\x1b[31m target=\"_self\"\x1b[0m>"}, checkLocaleContent([]byte(`key = <a href="%s" target="_self">`)))
|
||||
assert.Equal(t, []string{"key: \x1b[31m<a href=\"https://example.com/%s\">\x1b[0m"}, checkLocaleContent([]byte(`key = <a href="https://example.com/%s">`)))
|
||||
assert.Equal(t, []string{"key: \x1b[31m<a href=\"https://example.com/?q=%s\">\x1b[0m"}, checkLocaleContent([]byte(`key = <a href="https://example.com/?q=%s">`)))
|
||||
assert.Equal(t, []string{"key: \x1b[31m<a href=\"%s/open-redirect\">\x1b[0m"}, checkLocaleContent([]byte(`key = <a href="%s/open-redirect">`)))
|
||||
assert.Equal(t, []string{"key: \x1b[31m<a href=\"%s?q=open-redirect\">\x1b[0m"}, checkLocaleContent([]byte(`key = <a href="%s?q=open-redirect">`)))
|
||||
assert.EqualValues(t, []string{"key: \x1b[31m<a href=\"https://example.com\">\x1b[0m"}, checkLocaleContent([]byte(`key = <a href="https://example.com">`)))
|
||||
assert.EqualValues(t, []string{"key: \x1b[31m<a href=\"javascript:alert('1')\">\x1b[0m"}, checkLocaleContent([]byte(`key = <a href="javascript:alert('1')">`)))
|
||||
assert.EqualValues(t, []string{"key: <a href=\"https://TO-BE-REPLACED.COM\"\x1b[31m download\x1b[0m>"}, checkLocaleContent([]byte(`key = <a href="%s" download>`)))
|
||||
assert.EqualValues(t, []string{"key: <a href=\"https://TO-BE-REPLACED.COM\"\x1b[31m target=\"_self\"\x1b[0m>"}, checkLocaleContent([]byte(`key = <a href="%s" target="_self">`)))
|
||||
assert.EqualValues(t, []string{"key: \x1b[31m<a href=\"https://example.com/%s\">\x1b[0m"}, checkLocaleContent([]byte(`key = <a href="https://example.com/%s">`)))
|
||||
assert.EqualValues(t, []string{"key: \x1b[31m<a href=\"https://example.com/?q=%s\">\x1b[0m"}, checkLocaleContent([]byte(`key = <a href="https://example.com/?q=%s">`)))
|
||||
assert.EqualValues(t, []string{"key: \x1b[31m<a href=\"%s/open-redirect\">\x1b[0m"}, checkLocaleContent([]byte(`key = <a href="%s/open-redirect">`)))
|
||||
assert.EqualValues(t, []string{"key: \x1b[31m<a href=\"%s?q=open-redirect\">\x1b[0m"}, checkLocaleContent([]byte(`key = <a href="%s?q=open-redirect">`)))
|
||||
})
|
||||
|
||||
t.Run("Escaped HTML characters", func(t *testing.T) {
|
||||
assert.Empty(t, checkLocaleContent([]byte("activity.git_stats_push_to_branch = `إلى %s و\"`")))
|
||||
|
||||
assert.Equal(t, []string{"key: و\x1b[31m \x1b[0m\x1b[32m\u00a0\x1b[0m"}, checkLocaleContent([]byte(`key = و `)))
|
||||
assert.EqualValues(t, []string{"key: و\x1b[31m \x1b[0m\x1b[32m\u00a0\x1b[0m"}, checkLocaleContent([]byte(`key = و `)))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +75,7 @@ func TestNextLocalizationPolicy(t *testing.T) {
|
|||
}
|
||||
}`)))
|
||||
|
||||
assert.Equal(t, []string{"settings.hidden_comment_types_description: \"\x1b[31m<not-an-allowed-key>\x1b[0m REPLACED-TAG\""}, checkLocaleNextContent([]byte(`{
|
||||
assert.EqualValues(t, []string{"settings.hidden_comment_types_description: \"\x1b[31m<not-an-allowed-key>\x1b[0m REPLACED-TAG\""}, checkLocaleNextContent([]byte(`{
|
||||
"settings": {
|
||||
"hidden_comment_types_description": "\"<not-an-allowed-key> <label>\""
|
||||
}
|
||||
|
|
@ -88,20 +87,8 @@ func TestNextLocalizationPolicy(t *testing.T) {
|
|||
"settings.hidden_comment_types_description": "Comment types checked here will not be shown inside issue pages. Checking \"Label\" for example removes all \"<user> added/removed <label>\" comments."
|
||||
}`)))
|
||||
|
||||
assert.Equal(t, []string{"settings.hidden_comment_types_description: \"\x1b[31m<not-an-allowed-key>\x1b[0m REPLACED-TAG\""}, checkLocaleNextContent([]byte(`{
|
||||
assert.EqualValues(t, []string{"settings.hidden_comment_types_description: \"\x1b[31m<not-an-allowed-key>\x1b[0m REPLACED-TAG\""}, checkLocaleNextContent([]byte(`{
|
||||
"settings.hidden_comment_types_description": "\"<not-an-allowed-key> <label>\""
|
||||
}`)))
|
||||
})
|
||||
|
||||
t.Run("Plural form", func(t *testing.T) {
|
||||
assert.Equal(t, []string{"repo.pulls.title_desc: key = \x1b[31m<a href=\"https://example.com\">\x1b[0m"}, checkLocaleNextContent([]byte(`{"repo.pulls.title_desc": {
|
||||
"few": "key = <a href=\"%s\">",
|
||||
"other": "key = <a href=\"https://example.com\">"
|
||||
}}`)))
|
||||
|
||||
assert.Equal(t, []string{"repo.pulls.title_desc.few: key = \x1b[31m<a href=\"https://example.com\">\x1b[0m"}, checkLocaleNextContent([]byte(`{"repo.pulls.title_desc": {
|
||||
"few": "key = <a href=\"https://example.com\">",
|
||||
"other": "key = <a href=\"%s\">"
|
||||
}}`)))
|
||||
})
|
||||
}
|
||||
|
|
|
|||
52
build/update-locales.sh
Executable file
52
build/update-locales.sh
Executable file
|
|
@ -0,0 +1,52 @@
|
|||
#!/bin/sh
|
||||
|
||||
# this script runs in alpine image which only has `sh` shell
|
||||
|
||||
set +e
|
||||
if sed --version 2>/dev/null | grep -q GNU; then
|
||||
SED_INPLACE="sed -i"
|
||||
else
|
||||
SED_INPLACE="sed -i ''"
|
||||
fi
|
||||
set -e
|
||||
|
||||
if [ ! -f ./options/locale/locale_en-US.ini ]; then
|
||||
echo "please run this script in the root directory of the project"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mv ./options/locale/locale_en-US.ini ./options/
|
||||
|
||||
# the "ini" library for locale has many quirks, its behavior is different from Crowdin.
|
||||
# see i18n_test.go for more details
|
||||
|
||||
# this script helps to unquote the Crowdin outputs for the quirky ini library
|
||||
# * find all `key="...\"..."` lines
|
||||
# * remove the leading quote
|
||||
# * remove the trailing quote
|
||||
# * unescape the quotes
|
||||
# * eg: key="...\"..." => key=..."...
|
||||
$SED_INPLACE -r -e '/^[-.A-Za-z0-9_]+[ ]*=[ ]*".*"$/ {
|
||||
s/^([-.A-Za-z0-9_]+)[ ]*=[ ]*"/\1=/
|
||||
s/"$//
|
||||
s/\\"/"/g
|
||||
}' ./options/locale/*.ini
|
||||
|
||||
# * if the escaped line is incomplete like `key="...` or `key=..."`, quote it with backticks
|
||||
# * eg: key="... => key=`"...`
|
||||
# * eg: key=..." => key=`..."`
|
||||
$SED_INPLACE -r -e 's/^([-.A-Za-z0-9_]+)[ ]*=[ ]*(".*[^"])$/\1=`\2`/' ./options/locale/*.ini
|
||||
$SED_INPLACE -r -e 's/^([-.A-Za-z0-9_]+)[ ]*=[ ]*([^"].*")$/\1=`\2`/' ./options/locale/*.ini
|
||||
|
||||
# Remove translation under 25% of en_us
|
||||
baselines=$(wc -l "./options/locale_en-US.ini" | cut -d" " -f1)
|
||||
baselines=$((baselines / 4))
|
||||
for filename in ./options/locale/*.ini; do
|
||||
lines=$(wc -l "$filename" | cut -d" " -f1)
|
||||
if [ $lines -lt $baselines ]; then
|
||||
echo "Removing $filename: $lines/$baselines"
|
||||
rm "$filename"
|
||||
fi
|
||||
done
|
||||
|
||||
mv ./options/locale_en-US.ini ./options/locale/
|
||||
|
|
@ -4,31 +4,27 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"forgejo.org/modules/private"
|
||||
"forgejo.org/modules/setting"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// CmdActions represents the available actions sub-commands.
|
||||
func cmdActions() *cli.Command {
|
||||
return &cli.Command{
|
||||
var (
|
||||
// CmdActions represents the available actions sub-commands.
|
||||
CmdActions = &cli.Command{
|
||||
Name: "actions",
|
||||
Usage: "Manage Forgejo Actions",
|
||||
Commands: []*cli.Command{
|
||||
subcmdActionsGenRunnerToken(),
|
||||
Subcommands: []*cli.Command{
|
||||
subcmdActionsGenRunnerToken,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func subcmdActionsGenRunnerToken() *cli.Command {
|
||||
return &cli.Command{
|
||||
subcmdActionsGenRunnerToken = &cli.Command{
|
||||
Name: "generate-runner-token",
|
||||
Usage: "Generate a new token for a runner to use to register with the server",
|
||||
Before: noDanglingArgs,
|
||||
Action: runGenerateActionsRunnerToken,
|
||||
Aliases: []string{"grt"},
|
||||
Flags: []cli.Flag{
|
||||
|
|
@ -40,10 +36,10 @@ func subcmdActionsGenRunnerToken() *cli.Command {
|
|||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
func runGenerateActionsRunnerToken(ctx context.Context, c *cli.Command) error {
|
||||
ctx, cancel := installSignals(ctx)
|
||||
func runGenerateActionsRunnerToken(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
setting.MustInstalled()
|
||||
|
|
|
|||
75
cmd/admin.go
75
cmd/admin.go
|
|
@ -15,69 +15,58 @@ import (
|
|||
"forgejo.org/modules/log"
|
||||
repo_module "forgejo.org/modules/repository"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// CmdAdmin represents the available admin sub-command.
|
||||
func cmdAdmin() *cli.Command {
|
||||
return &cli.Command{
|
||||
var (
|
||||
// CmdAdmin represents the available admin sub-command.
|
||||
CmdAdmin = &cli.Command{
|
||||
Name: "admin",
|
||||
Usage: "Perform common administrative operations",
|
||||
Commands: []*cli.Command{
|
||||
subcmdUser(),
|
||||
subcmdRepoSyncReleases(),
|
||||
subcmdRegenerate(),
|
||||
subcmdAuth(),
|
||||
subcmdSendMail(),
|
||||
Subcommands: []*cli.Command{
|
||||
subcmdUser,
|
||||
subcmdRepoSyncReleases,
|
||||
subcmdRegenerate,
|
||||
subcmdAuth,
|
||||
subcmdSendMail,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func subcmdRepoSyncReleases() *cli.Command {
|
||||
return &cli.Command{
|
||||
subcmdRepoSyncReleases = &cli.Command{
|
||||
Name: "repo-sync-releases",
|
||||
Usage: "Synchronize repository releases with tags",
|
||||
Before: noDanglingArgs,
|
||||
Action: runRepoSyncReleases,
|
||||
}
|
||||
}
|
||||
|
||||
func subcmdRegenerate() *cli.Command {
|
||||
return &cli.Command{
|
||||
subcmdRegenerate = &cli.Command{
|
||||
Name: "regenerate",
|
||||
Usage: "Regenerate specific files",
|
||||
Commands: []*cli.Command{
|
||||
Subcommands: []*cli.Command{
|
||||
microcmdRegenHooks,
|
||||
microcmdRegenKeys,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func subcmdAuth() *cli.Command {
|
||||
return &cli.Command{
|
||||
subcmdAuth = &cli.Command{
|
||||
Name: "auth",
|
||||
Usage: "Modify external auth providers",
|
||||
Commands: []*cli.Command{
|
||||
microcmdAuthAddOauth(),
|
||||
microcmdAuthUpdateOauth(),
|
||||
microcmdAuthAddLdapBindDn(),
|
||||
microcmdAuthUpdateLdapBindDn(),
|
||||
microcmdAuthAddLdapSimpleAuth(),
|
||||
microcmdAuthUpdateLdapSimpleAuth(),
|
||||
microcmdAuthAddPAM(),
|
||||
microcmdAuthUpdatePAM(),
|
||||
microcmdAuthAddSMTP(),
|
||||
microcmdAuthUpdateSMTP(),
|
||||
microcmdAuthList(),
|
||||
microcmdAuthDelete(),
|
||||
Subcommands: []*cli.Command{
|
||||
microcmdAuthAddOauth,
|
||||
microcmdAuthUpdateOauth,
|
||||
microcmdAuthAddLdapBindDn,
|
||||
microcmdAuthUpdateLdapBindDn,
|
||||
microcmdAuthAddLdapSimpleAuth,
|
||||
microcmdAuthUpdateLdapSimpleAuth,
|
||||
microcmdAuthAddSMTP,
|
||||
microcmdAuthUpdateSMTP,
|
||||
microcmdAuthList,
|
||||
microcmdAuthDelete,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func subcmdSendMail() *cli.Command {
|
||||
return &cli.Command{
|
||||
subcmdSendMail = &cli.Command{
|
||||
Name: "sendmail",
|
||||
Usage: "Send a message to all users",
|
||||
Before: noDanglingArgs,
|
||||
Action: runSendMail,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
|
|
@ -97,17 +86,15 @@ func subcmdSendMail() *cli.Command {
|
|||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func idFlag() *cli.Int64Flag {
|
||||
return &cli.Int64Flag{
|
||||
idFlag = &cli.Int64Flag{
|
||||
Name: "id",
|
||||
Usage: "ID of authentication source",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
func runRepoSyncReleases(ctx context.Context, c *cli.Command) error {
|
||||
ctx, cancel := installSignals(ctx)
|
||||
func runRepoSyncReleases(_ *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
|
@ -14,33 +13,19 @@ import (
|
|||
"forgejo.org/models/db"
|
||||
auth_service "forgejo.org/services/auth"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
type (
|
||||
authService struct {
|
||||
initDB func(ctx context.Context) error
|
||||
createAuthSource func(context.Context, *auth_model.Source) error
|
||||
updateAuthSource func(context.Context, *auth_model.Source) error
|
||||
getAuthSourceByID func(ctx context.Context, id int64) (*auth_model.Source, error)
|
||||
}
|
||||
)
|
||||
|
||||
func microcmdAuthDelete() *cli.Command {
|
||||
return &cli.Command{
|
||||
var (
|
||||
microcmdAuthDelete = &cli.Command{
|
||||
Name: "delete",
|
||||
Usage: "Delete specific auth source",
|
||||
Flags: []cli.Flag{idFlag()},
|
||||
Before: noDanglingArgs,
|
||||
Flags: []cli.Flag{idFlag},
|
||||
Action: runDeleteAuth,
|
||||
}
|
||||
}
|
||||
|
||||
func microcmdAuthList() *cli.Command {
|
||||
return &cli.Command{
|
||||
microcmdAuthList = &cli.Command{
|
||||
Name: "list",
|
||||
Usage: "List auth sources",
|
||||
Before: noDanglingArgs,
|
||||
Action: runListAuth,
|
||||
Flags: []cli.Flag{
|
||||
&cli.IntFlag{
|
||||
|
|
@ -69,20 +54,10 @@ func microcmdAuthList() *cli.Command {
|
|||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// newAuthService creates a service with default functions.
|
||||
func newAuthService() *authService {
|
||||
return &authService{
|
||||
initDB: initDB,
|
||||
createAuthSource: auth_model.CreateSource,
|
||||
updateAuthSource: auth_model.UpdateSource,
|
||||
getAuthSourceByID: auth_model.GetSourceByID,
|
||||
}
|
||||
}
|
||||
|
||||
func runListAuth(ctx context.Context, c *cli.Command) error {
|
||||
ctx, cancel := installSignals(ctx)
|
||||
func runListAuth(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
|
|
@ -106,7 +81,7 @@ func runListAuth(ctx context.Context, c *cli.Command) error {
|
|||
|
||||
// loop through each source and print
|
||||
w := tabwriter.NewWriter(os.Stdout, c.Int("min-width"), c.Int("tab-width"), c.Int("padding"), padChar, flags)
|
||||
fmt.Fprint(w, "ID\tName\tType\tEnabled\n")
|
||||
fmt.Fprintf(w, "ID\tName\tType\tEnabled\n")
|
||||
for _, source := range authSources {
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", source.ID, source.Name, source.Type.String(), source.IsActive)
|
||||
}
|
||||
|
|
@ -115,12 +90,12 @@ func runListAuth(ctx context.Context, c *cli.Command) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func runDeleteAuth(ctx context.Context, c *cli.Command) error {
|
||||
func runDeleteAuth(c *cli.Context) error {
|
||||
if !c.IsSet("id") {
|
||||
return errors.New("--id flag is missing")
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals(ctx)
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
|
|
|
|||
|
|
@ -11,11 +11,20 @@ import (
|
|||
"forgejo.org/models/auth"
|
||||
"forgejo.org/services/auth/source/ldap"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func commonLdapCLIFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
type (
|
||||
authService struct {
|
||||
initDB func(ctx context.Context) error
|
||||
createAuthSource func(context.Context, *auth.Source) error
|
||||
updateAuthSource func(context.Context, *auth.Source) error
|
||||
getAuthSourceByID func(ctx context.Context, id int64) (*auth.Source, error)
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
commonLdapCLIFlags = []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "Authentication name.",
|
||||
|
|
@ -93,10 +102,8 @@ func commonLdapCLIFlags() []cli.Flag {
|
|||
Usage: "The attribute of the user’s LDAP record containing the user’s avatar.",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ldapBindDnCLIFlags() []cli.Flag {
|
||||
return append(commonLdapCLIFlags(),
|
||||
ldapBindDnCLIFlags = append(commonLdapCLIFlags,
|
||||
&cli.StringFlag{
|
||||
Name: "bind-dn",
|
||||
Usage: "The DN to bind to the LDAP server with when searching for the user.",
|
||||
|
|
@ -121,66 +128,62 @@ func ldapBindDnCLIFlags() []cli.Flag {
|
|||
Name: "page-size",
|
||||
Usage: "Search page size.",
|
||||
})
|
||||
}
|
||||
|
||||
func ldapSimpleAuthCLIFlags() []cli.Flag {
|
||||
return append(commonLdapCLIFlags(),
|
||||
ldapSimpleAuthCLIFlags = append(commonLdapCLIFlags,
|
||||
&cli.StringFlag{
|
||||
Name: "user-dn",
|
||||
Usage: "The user's DN.",
|
||||
})
|
||||
}
|
||||
|
||||
func microcmdAuthAddLdapBindDn() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "add-ldap",
|
||||
Usage: "Add new LDAP (via Bind DN) authentication source",
|
||||
Before: noDanglingArgs,
|
||||
Action: func(ctx context.Context, cli *cli.Command) error {
|
||||
return newAuthService().addLdapBindDn(ctx, cli)
|
||||
microcmdAuthAddLdapBindDn = &cli.Command{
|
||||
Name: "add-ldap",
|
||||
Usage: "Add new LDAP (via Bind DN) authentication source",
|
||||
Action: func(c *cli.Context) error {
|
||||
return newAuthService().addLdapBindDn(c)
|
||||
},
|
||||
Flags: ldapBindDnCLIFlags(),
|
||||
Flags: ldapBindDnCLIFlags,
|
||||
}
|
||||
}
|
||||
|
||||
func microcmdAuthUpdateLdapBindDn() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "update-ldap",
|
||||
Usage: "Update existing LDAP (via Bind DN) authentication source",
|
||||
Before: noDanglingArgs,
|
||||
Action: func(ctx context.Context, cli *cli.Command) error {
|
||||
return newAuthService().updateLdapBindDn(ctx, cli)
|
||||
microcmdAuthUpdateLdapBindDn = &cli.Command{
|
||||
Name: "update-ldap",
|
||||
Usage: "Update existing LDAP (via Bind DN) authentication source",
|
||||
Action: func(c *cli.Context) error {
|
||||
return newAuthService().updateLdapBindDn(c)
|
||||
},
|
||||
Flags: append([]cli.Flag{idFlag()}, ldapBindDnCLIFlags()...),
|
||||
Flags: append([]cli.Flag{idFlag}, ldapBindDnCLIFlags...),
|
||||
}
|
||||
}
|
||||
|
||||
func microcmdAuthAddLdapSimpleAuth() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "add-ldap-simple",
|
||||
Usage: "Add new LDAP (simple auth) authentication source",
|
||||
Before: noDanglingArgs,
|
||||
Action: func(ctx context.Context, cli *cli.Command) error {
|
||||
return newAuthService().addLdapSimpleAuth(ctx, cli)
|
||||
microcmdAuthAddLdapSimpleAuth = &cli.Command{
|
||||
Name: "add-ldap-simple",
|
||||
Usage: "Add new LDAP (simple auth) authentication source",
|
||||
Action: func(c *cli.Context) error {
|
||||
return newAuthService().addLdapSimpleAuth(c)
|
||||
},
|
||||
Flags: ldapSimpleAuthCLIFlags(),
|
||||
Flags: ldapSimpleAuthCLIFlags,
|
||||
}
|
||||
}
|
||||
|
||||
func microcmdAuthUpdateLdapSimpleAuth() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "update-ldap-simple",
|
||||
Usage: "Update existing LDAP (simple auth) authentication source",
|
||||
Before: noDanglingArgs,
|
||||
Action: func(ctx context.Context, cli *cli.Command) error {
|
||||
return newAuthService().updateLdapSimpleAuth(ctx, cli)
|
||||
microcmdAuthUpdateLdapSimpleAuth = &cli.Command{
|
||||
Name: "update-ldap-simple",
|
||||
Usage: "Update existing LDAP (simple auth) authentication source",
|
||||
Action: func(c *cli.Context) error {
|
||||
return newAuthService().updateLdapSimpleAuth(c)
|
||||
},
|
||||
Flags: append([]cli.Flag{idFlag()}, ldapSimpleAuthCLIFlags()...),
|
||||
Flags: append([]cli.Flag{idFlag}, ldapSimpleAuthCLIFlags...),
|
||||
}
|
||||
)
|
||||
|
||||
// newAuthService creates a service with default functions.
|
||||
func newAuthService() *authService {
|
||||
return &authService{
|
||||
initDB: initDB,
|
||||
createAuthSource: auth.CreateSource,
|
||||
updateAuthSource: auth.UpdateSource,
|
||||
getAuthSourceByID: auth.GetSourceByID,
|
||||
}
|
||||
}
|
||||
|
||||
// parseAuthSource assigns values on authSource according to command line flags.
|
||||
func parseAuthSource(c *cli.Command, authSource *auth.Source) {
|
||||
func parseAuthSource(c *cli.Context, authSource *auth.Source) {
|
||||
if c.IsSet("name") {
|
||||
authSource.Name = c.String("name")
|
||||
}
|
||||
|
|
@ -199,7 +202,7 @@ func parseAuthSource(c *cli.Command, authSource *auth.Source) {
|
|||
}
|
||||
|
||||
// parseLdapConfig assigns values on config according to command line flags.
|
||||
func parseLdapConfig(c *cli.Command, config *ldap.Source) error {
|
||||
func parseLdapConfig(c *cli.Context, config *ldap.Source) error {
|
||||
if c.IsSet("name") {
|
||||
config.Name = c.String("name")
|
||||
}
|
||||
|
|
@ -286,7 +289,7 @@ func findLdapSecurityProtocolByName(name string) (ldap.SecurityProtocol, bool) {
|
|||
|
||||
// getAuthSource gets the login source by its id defined in the command line flags.
|
||||
// It returns an error if the id is not set, does not match any source or if the source is not of expected type.
|
||||
func (a *authService) getAuthSource(ctx context.Context, c *cli.Command, authType auth.Type) (*auth.Source, error) {
|
||||
func (a *authService) getAuthSource(ctx context.Context, c *cli.Context, authType auth.Type) (*auth.Source, error) {
|
||||
if err := argsSet(c, "id"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -304,12 +307,12 @@ func (a *authService) getAuthSource(ctx context.Context, c *cli.Command, authTyp
|
|||
}
|
||||
|
||||
// addLdapBindDn adds a new LDAP via Bind DN authentication source.
|
||||
func (a *authService) addLdapBindDn(ctx context.Context, c *cli.Command) error {
|
||||
func (a *authService) addLdapBindDn(c *cli.Context) error {
|
||||
if err := argsSet(c, "name", "security-protocol", "host", "port", "user-search-base", "user-filter", "email-attribute"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals(ctx)
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := a.initDB(ctx); err != nil {
|
||||
|
|
@ -333,8 +336,8 @@ func (a *authService) addLdapBindDn(ctx context.Context, c *cli.Command) error {
|
|||
}
|
||||
|
||||
// updateLdapBindDn updates a new LDAP via Bind DN authentication source.
|
||||
func (a *authService) updateLdapBindDn(ctx context.Context, c *cli.Command) error {
|
||||
ctx, cancel := installSignals(ctx)
|
||||
func (a *authService) updateLdapBindDn(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := a.initDB(ctx); err != nil {
|
||||
|
|
@ -355,12 +358,12 @@ func (a *authService) updateLdapBindDn(ctx context.Context, c *cli.Command) erro
|
|||
}
|
||||
|
||||
// addLdapSimpleAuth adds a new LDAP (simple auth) authentication source.
|
||||
func (a *authService) addLdapSimpleAuth(ctx context.Context, c *cli.Command) error {
|
||||
func (a *authService) addLdapSimpleAuth(c *cli.Context) error {
|
||||
if err := argsSet(c, "name", "security-protocol", "host", "port", "user-dn", "user-filter", "email-attribute"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals(ctx)
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := a.initDB(ctx); err != nil {
|
||||
|
|
@ -384,8 +387,8 @@ func (a *authService) addLdapSimpleAuth(ctx context.Context, c *cli.Command) err
|
|||
}
|
||||
|
||||
// updateLdapSimpleAuth updates a new LDAP (simple auth) authentication source.
|
||||
func (a *authService) updateLdapSimpleAuth(ctx context.Context, c *cli.Command) error {
|
||||
ctx, cancel := installSignals(ctx)
|
||||
func (a *authService) updateLdapSimpleAuth(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := a.initDB(ctx); err != nil {
|
||||
|
|
|
|||
|
|
@ -8,17 +8,18 @@ import (
|
|||
"testing"
|
||||
|
||||
"forgejo.org/models/auth"
|
||||
"forgejo.org/modules/test"
|
||||
"forgejo.org/services/auth/source/ldap"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestAddLdapBindDn(t *testing.T) {
|
||||
// Mock cli functions to do not exit on error
|
||||
defer test.MockVariableValue(&cli.OsExiter, func(code int) {})()
|
||||
osExiter := cli.OsExiter
|
||||
defer func() { cli.OsExiter = osExiter }()
|
||||
cli.OsExiter = func(code int) {}
|
||||
|
||||
// Test cases
|
||||
cases := []struct {
|
||||
|
|
@ -215,22 +216,22 @@ func TestAddLdapBindDn(t *testing.T) {
|
|||
return nil
|
||||
},
|
||||
updateAuthSource: func(ctx context.Context, authSource *auth.Source) error {
|
||||
assert.FailNow(t, "should not call updateAuthSource", "case: %d", n)
|
||||
assert.FailNow(t, "case %d: should not call updateAuthSource", n)
|
||||
return nil
|
||||
},
|
||||
getAuthSourceByID: func(ctx context.Context, id int64) (*auth.Source, error) {
|
||||
assert.FailNow(t, "should not call getAuthSourceByID", "case: %d", n)
|
||||
assert.FailNow(t, "case %d: should not call getAuthSourceByID", n)
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
// Create a copy of command to test
|
||||
app := cli.Command{}
|
||||
app.Flags = microcmdAuthAddLdapBindDn().Flags
|
||||
app := cli.NewApp()
|
||||
app.Flags = microcmdAuthAddLdapBindDn.Flags
|
||||
app.Action = service.addLdapBindDn
|
||||
|
||||
// Run it
|
||||
err := app.Run(t.Context(), c.args)
|
||||
err := app.Run(c.args)
|
||||
if c.errMsg != "" {
|
||||
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
|
||||
} else {
|
||||
|
|
@ -242,7 +243,9 @@ func TestAddLdapBindDn(t *testing.T) {
|
|||
|
||||
func TestAddLdapSimpleAuth(t *testing.T) {
|
||||
// Mock cli functions to do not exit on error
|
||||
defer test.MockVariableValue(&cli.OsExiter, func(code int) {})()
|
||||
osExiter := cli.OsExiter
|
||||
defer func() { cli.OsExiter = osExiter }()
|
||||
cli.OsExiter = func(code int) {}
|
||||
|
||||
// Test cases
|
||||
cases := []struct {
|
||||
|
|
@ -444,22 +447,22 @@ func TestAddLdapSimpleAuth(t *testing.T) {
|
|||
return nil
|
||||
},
|
||||
updateAuthSource: func(ctx context.Context, authSource *auth.Source) error {
|
||||
assert.FailNow(t, "should not call updateAuthSource", "case: %d", n)
|
||||
assert.FailNow(t, "case %d: should not call updateAuthSource", n)
|
||||
return nil
|
||||
},
|
||||
getAuthSourceByID: func(ctx context.Context, id int64) (*auth.Source, error) {
|
||||
assert.FailNow(t, "should not call getAuthSourceByID", "case: %d", n)
|
||||
assert.FailNow(t, "case %d: should not call getAuthSourceByID", n)
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
// Create a copy of command to test
|
||||
app := cli.Command{}
|
||||
app.Flags = microcmdAuthAddLdapSimpleAuth().Flags
|
||||
app := cli.NewApp()
|
||||
app.Flags = microcmdAuthAddLdapSimpleAuth.Flags
|
||||
app.Action = service.addLdapSimpleAuth
|
||||
|
||||
// Run it
|
||||
err := app.Run(t.Context(), c.args)
|
||||
err := app.Run(c.args)
|
||||
if c.errMsg != "" {
|
||||
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
|
||||
} else {
|
||||
|
|
@ -471,7 +474,9 @@ func TestAddLdapSimpleAuth(t *testing.T) {
|
|||
|
||||
func TestUpdateLdapBindDn(t *testing.T) {
|
||||
// Mock cli functions to do not exit on error
|
||||
defer test.MockVariableValue(&cli.OsExiter, func(code int) {})()
|
||||
osExiter := cli.OsExiter
|
||||
defer func() { cli.OsExiter = osExiter }()
|
||||
cli.OsExiter = func(code int) {}
|
||||
|
||||
// Test cases
|
||||
cases := []struct {
|
||||
|
|
@ -893,7 +898,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
|
|||
return nil
|
||||
},
|
||||
createAuthSource: func(ctx context.Context, authSource *auth.Source) error {
|
||||
assert.FailNow(t, "should not call createAuthSource", "case: %d", n)
|
||||
assert.FailNow(t, "case %d: should not call createAuthSource", n)
|
||||
return nil
|
||||
},
|
||||
updateAuthSource: func(ctx context.Context, authSource *auth.Source) error {
|
||||
|
|
@ -915,12 +920,12 @@ func TestUpdateLdapBindDn(t *testing.T) {
|
|||
}
|
||||
|
||||
// Create a copy of command to test
|
||||
app := cli.Command{}
|
||||
app.Flags = microcmdAuthUpdateLdapBindDn().Flags
|
||||
app := cli.NewApp()
|
||||
app.Flags = microcmdAuthUpdateLdapBindDn.Flags
|
||||
app.Action = service.updateLdapBindDn
|
||||
|
||||
// Run it
|
||||
err := app.Run(t.Context(), c.args)
|
||||
err := app.Run(c.args)
|
||||
if c.errMsg != "" {
|
||||
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
|
||||
} else {
|
||||
|
|
@ -932,7 +937,9 @@ func TestUpdateLdapBindDn(t *testing.T) {
|
|||
|
||||
func TestUpdateLdapSimpleAuth(t *testing.T) {
|
||||
// Mock cli functions to do not exit on error
|
||||
defer test.MockVariableValue(&cli.OsExiter, func(code int) {})()
|
||||
osExiter := cli.OsExiter
|
||||
defer func() { cli.OsExiter = osExiter }()
|
||||
cli.OsExiter = func(code int) {}
|
||||
|
||||
// Test cases
|
||||
cases := []struct {
|
||||
|
|
@ -1281,7 +1288,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
|
|||
return nil
|
||||
},
|
||||
createAuthSource: func(ctx context.Context, authSource *auth.Source) error {
|
||||
assert.FailNow(t, "should not call createAuthSource", "case: %d", n)
|
||||
assert.FailNow(t, "case %d: should not call createAuthSource", n)
|
||||
return nil
|
||||
},
|
||||
updateAuthSource: func(ctx context.Context, authSource *auth.Source) error {
|
||||
|
|
@ -1303,12 +1310,12 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
|
|||
}
|
||||
|
||||
// Create a copy of command to test
|
||||
app := cli.Command{}
|
||||
app.Flags = microcmdAuthUpdateLdapSimpleAuth().Flags
|
||||
app := cli.NewApp()
|
||||
app.Flags = microcmdAuthUpdateLdapSimpleAuth.Flags
|
||||
app.Action = service.updateLdapSimpleAuth
|
||||
|
||||
// Run it
|
||||
err := app.Run(t.Context(), c.args)
|
||||
err := app.Run(c.args)
|
||||
if c.errMsg != "" {
|
||||
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
|
@ -12,11 +11,11 @@ import (
|
|||
auth_model "forgejo.org/models/auth"
|
||||
"forgejo.org/services/auth/source/oauth2"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func oauthCLIFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
var (
|
||||
oauthCLIFlags = []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Value: "",
|
||||
|
|
@ -86,11 +85,6 @@ func oauthCLIFlags() []cli.Flag {
|
|||
Value: nil,
|
||||
Usage: "Scopes to request when to authenticate against this OAuth2 source",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "attribute-ssh-public-key",
|
||||
Value: "",
|
||||
Usage: "Claim name providing SSH public keys for this source",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "required-claim-name",
|
||||
Value: "",
|
||||
|
|
@ -125,48 +119,24 @@ func oauthCLIFlags() []cli.Flag {
|
|||
Name: "group-team-map-removal",
|
||||
Usage: "Activate automatic team membership removal depending on groups",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "allow-username-change",
|
||||
Usage: "Allow users to change their username",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "quota-group-claim-name",
|
||||
Value: "",
|
||||
Usage: "Claim name providing quota group names for this source",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "quota-group-map",
|
||||
Value: "",
|
||||
Usage: "JSON mapping between groups and quota groups",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "quota-group-map-removal",
|
||||
Usage: "Activate automatic quota group removal depending on groups",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func microcmdAuthAddOauth() *cli.Command {
|
||||
return &cli.Command{
|
||||
microcmdAuthAddOauth = &cli.Command{
|
||||
Name: "add-oauth",
|
||||
Usage: "Add new Oauth authentication source",
|
||||
Before: noDanglingArgs,
|
||||
Action: newAuthService().addOauth,
|
||||
Flags: oauthCLIFlags(),
|
||||
Action: runAddOauth,
|
||||
Flags: oauthCLIFlags,
|
||||
}
|
||||
}
|
||||
|
||||
func microcmdAuthUpdateOauth() *cli.Command {
|
||||
return &cli.Command{
|
||||
microcmdAuthUpdateOauth = &cli.Command{
|
||||
Name: "update-oauth",
|
||||
Usage: "Update existing Oauth authentication source",
|
||||
Before: noDanglingArgs,
|
||||
Action: newAuthService().updateOauth,
|
||||
Flags: append(oauthCLIFlags()[:1], append([]cli.Flag{idFlag()}, oauthCLIFlags()[1:]...)...),
|
||||
Action: runUpdateOauth,
|
||||
Flags: append(oauthCLIFlags[:1], append([]cli.Flag{idFlag}, oauthCLIFlags[1:]...)...),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
func parseOAuth2Config(_ context.Context, c *cli.Command) *oauth2.Source {
|
||||
func parseOAuth2Config(c *cli.Context) *oauth2.Source {
|
||||
var customURLMapping *oauth2.CustomURLMapping
|
||||
if c.IsSet("use-custom-urls") {
|
||||
customURLMapping = &oauth2.CustomURLMapping{
|
||||
|
|
@ -188,7 +158,6 @@ func parseOAuth2Config(_ context.Context, c *cli.Command) *oauth2.Source {
|
|||
IconURL: c.String("icon-url"),
|
||||
SkipLocalTwoFA: c.Bool("skip-local-2fa"),
|
||||
Scopes: c.StringSlice("scopes"),
|
||||
AttributeSSHPublicKey: c.String("attribute-ssh-public-key"),
|
||||
RequiredClaimName: c.String("required-claim-name"),
|
||||
RequiredClaimValue: c.String("required-claim-value"),
|
||||
GroupClaimName: c.String("group-claim-name"),
|
||||
|
|
@ -196,22 +165,18 @@ func parseOAuth2Config(_ context.Context, c *cli.Command) *oauth2.Source {
|
|||
RestrictedGroup: c.String("restricted-group"),
|
||||
GroupTeamMap: c.String("group-team-map"),
|
||||
GroupTeamMapRemoval: c.Bool("group-team-map-removal"),
|
||||
AllowUsernameChange: c.Bool("allow-username-change"),
|
||||
QuotaGroupClaimName: c.String("quota-group-claim-name"),
|
||||
QuotaGroupMap: c.String("quota-group-map"),
|
||||
QuotaGroupMapRemoval: c.Bool("quota-group-map-removal"),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *authService) addOauth(ctx context.Context, c *cli.Command) error {
|
||||
ctx, cancel := installSignals(ctx)
|
||||
func runAddOauth(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := a.initDB(ctx); err != nil {
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := parseOAuth2Config(ctx, c)
|
||||
config := parseOAuth2Config(c)
|
||||
if config.Provider == "openidConnect" {
|
||||
discoveryURL, err := url.Parse(config.OpenIDConnectAutoDiscoveryURL)
|
||||
if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") {
|
||||
|
|
@ -219,7 +184,7 @@ func (a *authService) addOauth(ctx context.Context, c *cli.Command) error {
|
|||
}
|
||||
}
|
||||
|
||||
return a.createAuthSource(ctx, &auth_model.Source{
|
||||
return auth_model.CreateSource(ctx, &auth_model.Source{
|
||||
Type: auth_model.OAuth2,
|
||||
Name: c.String("name"),
|
||||
IsActive: true,
|
||||
|
|
@ -227,19 +192,19 @@ func (a *authService) addOauth(ctx context.Context, c *cli.Command) error {
|
|||
})
|
||||
}
|
||||
|
||||
func (a *authService) updateOauth(ctx context.Context, c *cli.Command) error {
|
||||
func runUpdateOauth(c *cli.Context) error {
|
||||
if !c.IsSet("id") {
|
||||
return errors.New("--id flag is missing")
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals(ctx)
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := a.initDB(ctx); err != nil {
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
source, err := a.getAuthSourceByID(ctx, c.Int64("id"))
|
||||
source, err := auth_model.GetSourceByID(ctx, c.Int64("id"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -274,10 +239,6 @@ func (a *authService) updateOauth(ctx context.Context, c *cli.Command) error {
|
|||
oAuth2Config.Scopes = c.StringSlice("scopes")
|
||||
}
|
||||
|
||||
if c.IsSet("attribute-ssh-public-key") {
|
||||
oAuth2Config.AttributeSSHPublicKey = c.String("attribute-ssh-public-key")
|
||||
}
|
||||
|
||||
if c.IsSet("required-claim-name") {
|
||||
oAuth2Config.RequiredClaimName = c.String("required-claim-name")
|
||||
}
|
||||
|
|
@ -300,19 +261,6 @@ func (a *authService) updateOauth(ctx context.Context, c *cli.Command) error {
|
|||
if c.IsSet("group-team-map-removal") {
|
||||
oAuth2Config.GroupTeamMapRemoval = c.Bool("group-team-map-removal")
|
||||
}
|
||||
if c.IsSet("quota-group-claim-name") {
|
||||
oAuth2Config.QuotaGroupClaimName = c.String("quota-group-claim-name")
|
||||
}
|
||||
if c.IsSet("quota-group-map") {
|
||||
oAuth2Config.QuotaGroupMap = c.String("quota-group-map")
|
||||
}
|
||||
if c.IsSet("quota-group-map-removal") {
|
||||
oAuth2Config.QuotaGroupMapRemoval = c.Bool("quota-group-map-removal")
|
||||
}
|
||||
|
||||
if c.IsSet("allow-username-change") {
|
||||
oAuth2Config.AllowUsernameChange = c.Bool("allow-username-change")
|
||||
}
|
||||
|
||||
// update custom URL mapping
|
||||
customURLMapping := &oauth2.CustomURLMapping{}
|
||||
|
|
@ -347,5 +295,5 @@ func (a *authService) updateOauth(ctx context.Context, c *cli.Command) error {
|
|||
oAuth2Config.CustomURLMapping = customURLMapping
|
||||
source.Cfg = oAuth2Config
|
||||
|
||||
return a.updateAuthSource(ctx, source)
|
||||
return auth_model.UpdateSource(ctx, source)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,886 +0,0 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"forgejo.org/models/auth"
|
||||
"forgejo.org/modules/test"
|
||||
"forgejo.org/services/auth/source/oauth2"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func TestAddOauth(t *testing.T) {
|
||||
// Mock cli functions to do not exit on error
|
||||
defer test.MockVariableValue(&cli.OsExiter, func(code int) {})()
|
||||
|
||||
// Test cases
|
||||
cases := []struct {
|
||||
args []string
|
||||
source *auth.Source
|
||||
errMsg string
|
||||
}{
|
||||
// case 0
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--name", "oauth2 (via openidConnect) source full",
|
||||
"--provider", "openidConnect",
|
||||
"--key", "client id",
|
||||
"--secret", "client secret",
|
||||
"--auto-discover-url", "https://example.com/.well-known/openid-configuration",
|
||||
"--use-custom-urls", "",
|
||||
"--custom-tenant-id", "tenant id",
|
||||
"--custom-auth-url", "https://example.com/auth",
|
||||
"--custom-token-url", "https://example.com/token",
|
||||
"--custom-profile-url", "https://example.com/profile",
|
||||
"--custom-email-url", "https://example.com/email",
|
||||
"--icon-url", "https://example.com/icon.svg",
|
||||
"--skip-local-2fa",
|
||||
"--scopes", "address",
|
||||
"--scopes", "email",
|
||||
"--scopes", "phone",
|
||||
"--scopes", "profile",
|
||||
"--attribute-ssh-public-key", "ssh_public_key",
|
||||
"--required-claim-name", "can_access",
|
||||
"--required-claim-value", "yes",
|
||||
"--group-claim-name", "groups",
|
||||
"--admin-group", "admin",
|
||||
"--restricted-group", "restricted",
|
||||
"--group-team-map", `{"org_a_team_1": {"organization-a": ["Team 1"]}, "org_a_all_teams": {"organization-a": ["Team 1", "Team 2", "Team 3"]}}`,
|
||||
"--group-team-map-removal",
|
||||
"--allow-username-change",
|
||||
"--quota-group-claim-name", "quota_groups",
|
||||
"--quota-group-map", `{"oauth_group_1": ["quota_group_1"], "oauth_group_2": ["quota_group_2"]}`,
|
||||
"--quota-group-map-removal",
|
||||
},
|
||||
source: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Name: "oauth2 (via openidConnect) source full",
|
||||
IsActive: true,
|
||||
Cfg: &oauth2.Source{
|
||||
Provider: "openidConnect",
|
||||
ClientID: "client id",
|
||||
ClientSecret: "client secret",
|
||||
OpenIDConnectAutoDiscoveryURL: "https://example.com/.well-known/openid-configuration",
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{
|
||||
AuthURL: "https://example.com/auth",
|
||||
TokenURL: "https://example.com/token",
|
||||
ProfileURL: "https://example.com/profile",
|
||||
EmailURL: "https://example.com/email",
|
||||
Tenant: "tenant id",
|
||||
},
|
||||
IconURL: "https://example.com/icon.svg",
|
||||
Scopes: []string{"address", "email", "phone", "profile"},
|
||||
AttributeSSHPublicKey: "ssh_public_key",
|
||||
RequiredClaimName: "can_access",
|
||||
RequiredClaimValue: "yes",
|
||||
GroupClaimName: "groups",
|
||||
AdminGroup: "admin",
|
||||
GroupTeamMap: `{"org_a_team_1": {"organization-a": ["Team 1"]}, "org_a_all_teams": {"organization-a": ["Team 1", "Team 2", "Team 3"]}}`,
|
||||
GroupTeamMapRemoval: true,
|
||||
QuotaGroupClaimName: "quota_groups",
|
||||
QuotaGroupMap: `{"oauth_group_1": ["quota_group_1"], "oauth_group_2": ["quota_group_2"]}`,
|
||||
QuotaGroupMapRemoval: true,
|
||||
RestrictedGroup: "restricted",
|
||||
SkipLocalTwoFA: true,
|
||||
AllowUsernameChange: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 1
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--name", "oauth2 (via openidConnect) source min",
|
||||
"--provider", "openidConnect",
|
||||
"--auto-discover-url", "https://example.com/.well-known/openid-configuration",
|
||||
},
|
||||
source: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Name: "oauth2 (via openidConnect) source min",
|
||||
IsActive: true,
|
||||
Cfg: &oauth2.Source{
|
||||
Provider: "openidConnect",
|
||||
OpenIDConnectAutoDiscoveryURL: "https://example.com/.well-known/openid-configuration",
|
||||
Scopes: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 2
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--name", "oauth2 (via openidConnect) source `--use-custom-urls` required for `--custom-*` flags",
|
||||
"--custom-tenant-id", "tenant id",
|
||||
"--custom-auth-url", "https://example.com/auth",
|
||||
"--custom-token-url", "https://example.com/token",
|
||||
"--custom-profile-url", "https://example.com/profile",
|
||||
"--custom-email-url", "https://example.com/email",
|
||||
},
|
||||
source: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Name: "oauth2 (via openidConnect) source `--use-custom-urls` required for `--custom-*` flags",
|
||||
IsActive: true,
|
||||
Cfg: &oauth2.Source{
|
||||
Scopes: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 3
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--name", "oauth2 (via openidConnect) source `--scopes` aggregates multiple uses",
|
||||
"--provider", "openidConnect",
|
||||
"--auto-discover-url", "https://example.com/.well-known/openid-configuration",
|
||||
"--scopes", "address",
|
||||
"--scopes", "email",
|
||||
"--scopes", "phone",
|
||||
"--scopes", "profile",
|
||||
},
|
||||
source: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Name: "oauth2 (via openidConnect) source `--scopes` aggregates multiple uses",
|
||||
IsActive: true,
|
||||
Cfg: &oauth2.Source{
|
||||
Provider: "openidConnect",
|
||||
OpenIDConnectAutoDiscoveryURL: "https://example.com/.well-known/openid-configuration",
|
||||
Scopes: []string{"address", "email", "phone", "profile"},
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 4
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--name", "oauth2 (via openidConnect) source `--scopes` supports commas as separators",
|
||||
"--provider", "openidConnect",
|
||||
"--auto-discover-url", "https://example.com/.well-known/openid-configuration",
|
||||
"--scopes", "address,email,phone,profile",
|
||||
},
|
||||
source: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Name: "oauth2 (via openidConnect) source `--scopes` supports commas as separators",
|
||||
IsActive: true,
|
||||
Cfg: &oauth2.Source{
|
||||
Provider: "openidConnect",
|
||||
OpenIDConnectAutoDiscoveryURL: "https://example.com/.well-known/openid-configuration",
|
||||
Scopes: []string{"address", "email", "phone", "profile"},
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 5
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--name", "oauth2 (via openidConnect) source",
|
||||
"--provider", "openidConnect",
|
||||
},
|
||||
errMsg: "invalid Auto Discovery URL: (this must be a valid URL starting with http:// or https://)",
|
||||
},
|
||||
// case 6
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--name", "oauth2 (via openidConnect) source",
|
||||
"--provider", "openidConnect",
|
||||
"--auto-discover-url", "example.com",
|
||||
},
|
||||
errMsg: "invalid Auto Discovery URL: example.com (this must be a valid URL starting with http:// or https://)",
|
||||
},
|
||||
// case 7
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--name", "oauth2 source with quota group claim name",
|
||||
"--provider", "openidConnect",
|
||||
"--auto-discover-url", "https://example.com/.well-known/openid-configuration",
|
||||
"--quota-group-claim-name", "quota_groups",
|
||||
},
|
||||
source: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Name: "oauth2 source with quota group claim name",
|
||||
IsActive: true,
|
||||
Cfg: &oauth2.Source{
|
||||
Provider: "openidConnect",
|
||||
OpenIDConnectAutoDiscoveryURL: "https://example.com/.well-known/openid-configuration",
|
||||
Scopes: []string{},
|
||||
QuotaGroupClaimName: "quota_groups",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 8
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--name", "oauth2 source with quota group map",
|
||||
"--provider", "openidConnect",
|
||||
"--auto-discover-url", "https://example.com/.well-known/openid-configuration",
|
||||
"--quota-group-map", `{"oauth_group_1": ["quota_group_1"], "oauth_group_2": ["quota_group_2"]}`,
|
||||
},
|
||||
source: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Name: "oauth2 source with quota group map",
|
||||
IsActive: true,
|
||||
Cfg: &oauth2.Source{
|
||||
Provider: "openidConnect",
|
||||
OpenIDConnectAutoDiscoveryURL: "https://example.com/.well-known/openid-configuration",
|
||||
Scopes: []string{},
|
||||
QuotaGroupMap: `{"oauth_group_1": ["quota_group_1"], "oauth_group_2": ["quota_group_2"]}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 9
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--name", "oauth2 source with quota group map removal",
|
||||
"--provider", "openidConnect",
|
||||
"--auto-discover-url", "https://example.com/.well-known/openid-configuration",
|
||||
"--quota-group-map-removal",
|
||||
},
|
||||
source: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Name: "oauth2 source with quota group map removal",
|
||||
IsActive: true,
|
||||
Cfg: &oauth2.Source{
|
||||
Provider: "openidConnect",
|
||||
OpenIDConnectAutoDiscoveryURL: "https://example.com/.well-known/openid-configuration",
|
||||
Scopes: []string{},
|
||||
QuotaGroupMapRemoval: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 10
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--name", "oauth2 source with all quota group fields",
|
||||
"--provider", "openidConnect",
|
||||
"--auto-discover-url", "https://example.com/.well-known/openid-configuration",
|
||||
"--quota-group-claim-name", "quota_groups",
|
||||
"--quota-group-map", `{"developers": ["dev_quota"], "admins": ["admin_quota"]}`,
|
||||
"--quota-group-map-removal",
|
||||
},
|
||||
source: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Name: "oauth2 source with all quota group fields",
|
||||
IsActive: true,
|
||||
Cfg: &oauth2.Source{
|
||||
Provider: "openidConnect",
|
||||
OpenIDConnectAutoDiscoveryURL: "https://example.com/.well-known/openid-configuration",
|
||||
Scopes: []string{},
|
||||
QuotaGroupClaimName: "quota_groups",
|
||||
QuotaGroupMap: `{"developers": ["dev_quota"], "admins": ["admin_quota"]}`,
|
||||
QuotaGroupMapRemoval: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for n, c := range cases {
|
||||
// Mock functions.
|
||||
var createdAuthSource *auth.Source
|
||||
service := &authService{
|
||||
initDB: func(context.Context) error {
|
||||
return nil
|
||||
},
|
||||
createAuthSource: func(ctx context.Context, authSource *auth.Source) error {
|
||||
createdAuthSource = authSource
|
||||
return nil
|
||||
},
|
||||
updateAuthSource: func(ctx context.Context, authSource *auth.Source) error {
|
||||
assert.FailNow(t, "should not call updateAuthSource", "case: %d", n)
|
||||
return nil
|
||||
},
|
||||
getAuthSourceByID: func(ctx context.Context, id int64) (*auth.Source, error) {
|
||||
assert.FailNow(t, "should not call getAuthSourceByID", "case: %d", n)
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
// Create a copy of command to test
|
||||
app := cli.Command{}
|
||||
app.Flags = microcmdAuthAddOauth().Flags
|
||||
app.Action = service.addOauth
|
||||
|
||||
// Run it
|
||||
err := app.Run(t.Context(), c.args)
|
||||
if c.errMsg != "" {
|
||||
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
|
||||
} else {
|
||||
require.NoError(t, err, "case %d: should have no errors", n)
|
||||
assert.Equal(t, c.source, createdAuthSource, "case %d: wrong authSource", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateOauth(t *testing.T) {
|
||||
// Mock cli functions to do not exit on error
|
||||
defer test.MockVariableValue(&cli.OsExiter, func(code int) {})()
|
||||
|
||||
// Test cases
|
||||
cases := []struct {
|
||||
args []string
|
||||
id int64
|
||||
existingAuthSource *auth.Source
|
||||
authSource *auth.Source
|
||||
errMsg string
|
||||
}{
|
||||
// case 0
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "23",
|
||||
"--name", "oauth2 (via openidConnect) source full",
|
||||
"--provider", "openidConnect",
|
||||
"--key", "client id",
|
||||
"--secret", "client secret",
|
||||
"--auto-discover-url", "https://example.com/.well-known/openid-configuration",
|
||||
"--use-custom-urls", "",
|
||||
"--custom-tenant-id", "tenant id",
|
||||
"--custom-auth-url", "https://example.com/auth",
|
||||
"--custom-token-url", "https://example.com/token",
|
||||
"--custom-profile-url", "https://example.com/profile",
|
||||
"--custom-email-url", "https://example.com/email",
|
||||
"--icon-url", "https://example.com/icon.svg",
|
||||
"--skip-local-2fa",
|
||||
"--scopes", "address",
|
||||
"--scopes", "email",
|
||||
"--scopes", "phone",
|
||||
"--scopes", "profile",
|
||||
"--attribute-ssh-public-key", "ssh_public_key",
|
||||
"--required-claim-name", "can_access",
|
||||
"--required-claim-value", "yes",
|
||||
"--group-claim-name", "groups",
|
||||
"--admin-group", "admin",
|
||||
"--restricted-group", "restricted",
|
||||
"--group-team-map", `{"org_a_team_1": {"organization-a": ["Team 1"]}, "org_a_all_teams": {"organization-a": ["Team 1", "Team 2", "Team 3"]}}`,
|
||||
"--group-team-map-removal",
|
||||
},
|
||||
id: 23,
|
||||
existingAuthSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{},
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Name: "oauth2 (via openidConnect) source full",
|
||||
Cfg: &oauth2.Source{
|
||||
Provider: "openidConnect",
|
||||
ClientID: "client id",
|
||||
ClientSecret: "client secret",
|
||||
OpenIDConnectAutoDiscoveryURL: "https://example.com/.well-known/openid-configuration",
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{
|
||||
AuthURL: "https://example.com/auth",
|
||||
TokenURL: "https://example.com/token",
|
||||
ProfileURL: "https://example.com/profile",
|
||||
EmailURL: "https://example.com/email",
|
||||
Tenant: "tenant id",
|
||||
},
|
||||
IconURL: "https://example.com/icon.svg",
|
||||
Scopes: []string{"address", "email", "phone", "profile"},
|
||||
AttributeSSHPublicKey: "ssh_public_key",
|
||||
RequiredClaimName: "can_access",
|
||||
RequiredClaimValue: "yes",
|
||||
GroupClaimName: "groups",
|
||||
AdminGroup: "admin",
|
||||
GroupTeamMap: `{"org_a_team_1": {"organization-a": ["Team 1"]}, "org_a_all_teams": {"organization-a": ["Team 1", "Team 2", "Team 3"]}}`,
|
||||
GroupTeamMapRemoval: true,
|
||||
RestrictedGroup: "restricted",
|
||||
// `--skip-local-2fa` is currently ignored.
|
||||
// SkipLocalTwoFA: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 1
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "1",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 2
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "1",
|
||||
"--name", "oauth2 (via openidConnect) source full",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Name: "oauth2 (via openidConnect) source full",
|
||||
Cfg: &oauth2.Source{
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 3
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "1",
|
||||
"--provider", "openidConnect",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{
|
||||
Provider: "openidConnect",
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 4
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "1",
|
||||
"--key", "client id",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{
|
||||
ClientID: "client id",
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 5
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "1",
|
||||
"--secret", "client secret",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{
|
||||
ClientSecret: "client secret",
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 6
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "1",
|
||||
"--auto-discover-url", "https://example.com/.well-known/openid-configuration",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{
|
||||
OpenIDConnectAutoDiscoveryURL: "https://example.com/.well-known/openid-configuration",
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 7
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "1",
|
||||
"--use-custom-urls", "",
|
||||
"--custom-tenant-id", "tenant id",
|
||||
"--custom-auth-url", "https://example.com/auth",
|
||||
"--custom-token-url", "https://example.com/token",
|
||||
"--custom-profile-url", "https://example.com/profile",
|
||||
"--custom-email-url", "https://example.com/email",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{
|
||||
AuthURL: "https://example.com/auth",
|
||||
TokenURL: "https://example.com/token",
|
||||
ProfileURL: "https://example.com/profile",
|
||||
EmailURL: "https://example.com/email",
|
||||
Tenant: "tenant id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 8
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "1",
|
||||
"--name", "oauth2 (via openidConnect) source `--use-custom-urls` required for `--custom-*` flags",
|
||||
"--custom-tenant-id", "tenant id",
|
||||
"--custom-auth-url", "https://example.com/auth",
|
||||
"--custom-token-url", "https://example.com/token",
|
||||
"--custom-profile-url", "https://example.com/profile",
|
||||
"--custom-email-url", "https://example.com/email",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Name: "oauth2 (via openidConnect) source `--use-custom-urls` required for `--custom-*` flags",
|
||||
Cfg: &oauth2.Source{
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 9
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "1",
|
||||
"--icon-url", "https://example.com/icon.svg",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
IconURL: "https://example.com/icon.svg",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 10
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "1",
|
||||
"--name", "oauth2 (via openidConnect) source `--skip-local-2fa` is currently ignored",
|
||||
"--skip-local-2fa",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Name: "oauth2 (via openidConnect) source `--skip-local-2fa` is currently ignored",
|
||||
Cfg: &oauth2.Source{
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
// `--skip-local-2fa` is currently ignored.
|
||||
// SkipLocalTwoFA: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 11
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "1",
|
||||
"--name", "oauth2 (via openidConnect) source `--scopes` aggregates multiple uses",
|
||||
"--scopes", "address",
|
||||
"--scopes", "email",
|
||||
"--scopes", "phone",
|
||||
"--scopes", "profile",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Name: "oauth2 (via openidConnect) source `--scopes` aggregates multiple uses",
|
||||
Cfg: &oauth2.Source{
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
Scopes: []string{"address", "email", "phone", "profile"},
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 12
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "1",
|
||||
"--name", "oauth2 (via openidConnect) source `--scopes` supports commas as separators",
|
||||
"--scopes", "address,email,phone,profile",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Name: "oauth2 (via openidConnect) source `--scopes` supports commas as separators",
|
||||
Cfg: &oauth2.Source{
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
Scopes: []string{"address", "email", "phone", "profile"},
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 13
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "1",
|
||||
"--attribute-ssh-public-key", "ssh_public_key",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
AttributeSSHPublicKey: "ssh_public_key",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 14
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "1",
|
||||
"--required-claim-name", "can_access",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
RequiredClaimName: "can_access",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 15
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "1",
|
||||
"--required-claim-value", "yes",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
RequiredClaimValue: "yes",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 16
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "1",
|
||||
"--group-claim-name", "groups",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
GroupClaimName: "groups",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 17
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "1",
|
||||
"--admin-group", "admin",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
AdminGroup: "admin",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 18
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "1",
|
||||
"--restricted-group", "restricted",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
RestrictedGroup: "restricted",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 19
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "1",
|
||||
"--group-team-map", `{"org_a_team_1": {"organization-a": ["Team 1"]}, "org_a_all_teams": {"organization-a": ["Team 1", "Team 2", "Team 3"]}}`,
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
GroupTeamMap: `{"org_a_team_1": {"organization-a": ["Team 1"]}, "org_a_all_teams": {"organization-a": ["Team 1", "Team 2", "Team 3"]}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 20
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "1",
|
||||
"--group-team-map-removal",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
GroupTeamMapRemoval: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 21
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "23",
|
||||
"--group-team-map-removal=false",
|
||||
},
|
||||
id: 23,
|
||||
existingAuthSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{
|
||||
GroupTeamMapRemoval: true,
|
||||
},
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
GroupTeamMapRemoval: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 22
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
},
|
||||
errMsg: "--id flag is missing",
|
||||
},
|
||||
// case 23
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "1",
|
||||
"--quota-group-claim-name", "quota_groups",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
QuotaGroupClaimName: "quota_groups",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 24
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "1",
|
||||
"--quota-group-map", `{"oauth_group_1": ["quota_group_1"], "oauth_group_2": ["quota_group_2"]}`,
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
QuotaGroupMap: `{"oauth_group_1": ["quota_group_1"], "oauth_group_2": ["quota_group_2"]}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 25
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "1",
|
||||
"--quota-group-map-removal",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
QuotaGroupMapRemoval: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 26
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "24",
|
||||
"--quota-group-map-removal=false",
|
||||
},
|
||||
id: 24,
|
||||
existingAuthSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{
|
||||
QuotaGroupMapRemoval: true,
|
||||
},
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
QuotaGroupMapRemoval: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 27
|
||||
{
|
||||
args: []string{
|
||||
"oauth-test",
|
||||
"--id", "1",
|
||||
"--quota-group-claim-name", "quota_groups",
|
||||
"--quota-group-map", `{"developers": ["dev_quota"], "admins": ["admin_quota"]}`,
|
||||
"--quota-group-map-removal",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{
|
||||
CustomURLMapping: &oauth2.CustomURLMapping{},
|
||||
QuotaGroupClaimName: "quota_groups",
|
||||
QuotaGroupMap: `{"developers": ["dev_quota"], "admins": ["admin_quota"]}`,
|
||||
QuotaGroupMapRemoval: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for n, c := range cases {
|
||||
// Mock functions.
|
||||
var updatedAuthSource *auth.Source
|
||||
service := &authService{
|
||||
initDB: func(context.Context) error {
|
||||
return nil
|
||||
},
|
||||
createAuthSource: func(ctx context.Context, authSource *auth.Source) error {
|
||||
assert.FailNow(t, "should not call createAuthSource", "case: %d", n)
|
||||
return nil
|
||||
},
|
||||
updateAuthSource: func(ctx context.Context, authSource *auth.Source) error {
|
||||
updatedAuthSource = authSource
|
||||
return nil
|
||||
},
|
||||
getAuthSourceByID: func(ctx context.Context, id int64) (*auth.Source, error) {
|
||||
if c.id != 0 {
|
||||
assert.Equal(t, c.id, id, "case %d: wrong id", n)
|
||||
}
|
||||
if c.existingAuthSource != nil {
|
||||
return c.existingAuthSource, nil
|
||||
}
|
||||
return &auth.Source{
|
||||
Type: auth.OAuth2,
|
||||
Cfg: &oauth2.Source{},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
// Create a copy of command to test
|
||||
app := cli.Command{}
|
||||
app.Flags = microcmdAuthUpdateOauth().Flags
|
||||
app.Action = service.updateOauth
|
||||
|
||||
// Run it
|
||||
err := app.Run(t.Context(), c.args)
|
||||
if c.errMsg != "" {
|
||||
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
|
||||
} else {
|
||||
require.NoError(t, err, "case %d: should have no errors", n)
|
||||
assert.Equal(t, c.authSource, updatedAuthSource, "case %d: wrong authSource", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
"forgejo.org/services/auth/source/pam"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func pamCLIFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Value: "",
|
||||
Usage: "Application Name",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "service-name",
|
||||
Value: "PLAIN",
|
||||
Usage: "PAM service name",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "email-domain",
|
||||
Value: "",
|
||||
Usage: "PAM email domain",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-local-2fa",
|
||||
Usage: "Skip 2FA to log on.",
|
||||
Value: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "active",
|
||||
Usage: "This Authentication Source is Activated.",
|
||||
Value: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func microcmdAuthAddPAM() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "add-pam",
|
||||
Usage: "Add new PAM authentication source",
|
||||
Before: noDanglingArgs,
|
||||
Action: newAuthService().addPAM,
|
||||
Flags: pamCLIFlags(),
|
||||
}
|
||||
}
|
||||
|
||||
func microcmdAuthUpdatePAM() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "update-pam",
|
||||
Usage: "Update existing PAM authentication source",
|
||||
Before: noDanglingArgs,
|
||||
Action: newAuthService().updatePAM,
|
||||
Flags: append(pamCLIFlags()[:1], append([]cli.Flag{idFlag()}, pamCLIFlags()[1:]...)...),
|
||||
}
|
||||
}
|
||||
|
||||
func parsePAMConfig(_ context.Context, c *cli.Command) *pam.Source {
|
||||
return &pam.Source{
|
||||
ServiceName: c.String("service-name"),
|
||||
EmailDomain: c.String("email-domain"),
|
||||
SkipLocalTwoFA: c.Bool("skip-local-2fa"),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *authService) addPAM(ctx context.Context, c *cli.Command) error {
|
||||
ctx, cancel := installSignals(ctx)
|
||||
defer cancel()
|
||||
|
||||
if err := a.initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !c.IsSet("name") || len(c.String("name")) == 0 {
|
||||
return errors.New("name must be set")
|
||||
}
|
||||
if !c.IsSet("service-name") || len(c.String("service-name")) == 0 {
|
||||
return errors.New("service-name must be set")
|
||||
}
|
||||
active := true
|
||||
if c.IsSet("active") {
|
||||
active = c.Bool("active")
|
||||
}
|
||||
|
||||
config := parsePAMConfig(ctx, c)
|
||||
|
||||
return a.createAuthSource(ctx, &auth_model.Source{
|
||||
Type: auth_model.PAM,
|
||||
Name: c.String("name"),
|
||||
IsActive: active,
|
||||
Cfg: config,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *authService) updatePAM(ctx context.Context, c *cli.Command) error {
|
||||
if !c.IsSet("id") {
|
||||
return errors.New("--id flag is missing")
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals(ctx)
|
||||
defer cancel()
|
||||
|
||||
if err := a.initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
source, err := a.getAuthSource(ctx, c, auth_model.PAM)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pamConfig := source.Cfg.(*pam.Source)
|
||||
|
||||
if c.IsSet("name") {
|
||||
source.Name = c.String("name")
|
||||
}
|
||||
|
||||
if c.IsSet("service-name") {
|
||||
pamConfig.ServiceName = c.String("service-name")
|
||||
}
|
||||
|
||||
if c.IsSet("email-domain") {
|
||||
pamConfig.EmailDomain = c.String("email-domain")
|
||||
}
|
||||
|
||||
if c.IsSet("skip-local-2fa") {
|
||||
pamConfig.SkipLocalTwoFA = c.Bool("skip-local-2fa")
|
||||
}
|
||||
|
||||
if c.IsSet("active") {
|
||||
source.IsActive = c.Bool("active")
|
||||
}
|
||||
|
||||
source.Cfg = pamConfig
|
||||
|
||||
return a.updateAuthSource(ctx, source)
|
||||
}
|
||||
|
|
@ -1,293 +0,0 @@
|
|||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"forgejo.org/models/auth"
|
||||
"forgejo.org/modules/test"
|
||||
"forgejo.org/services/auth/source/pam"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func TestPamService(t *testing.T) {
|
||||
// Mock cli functions to do not exit on error
|
||||
defer test.MockVariableValue(&cli.OsExiter, func(code int) {})()
|
||||
|
||||
// Test cases
|
||||
cases := []struct {
|
||||
args []string
|
||||
source *auth.Source
|
||||
errMsg string
|
||||
}{
|
||||
// case 0
|
||||
{
|
||||
args: []string{
|
||||
"pam-test",
|
||||
"--name", "Pam Service",
|
||||
"--service-name", "myservice",
|
||||
},
|
||||
source: &auth.Source{
|
||||
Type: auth.PAM,
|
||||
Name: "Pam Service",
|
||||
IsActive: true,
|
||||
Cfg: &pam.Source{
|
||||
ServiceName: "myservice",
|
||||
EmailDomain: "",
|
||||
SkipLocalTwoFA: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 1
|
||||
{
|
||||
args: []string{
|
||||
"pam-test",
|
||||
"--name", "Pam Service",
|
||||
"--service-name", "myservice",
|
||||
"--email-domain", "testdomain.org",
|
||||
"--skip-local-2fa",
|
||||
},
|
||||
source: &auth.Source{
|
||||
Type: auth.PAM,
|
||||
Name: "Pam Service",
|
||||
IsActive: true,
|
||||
Cfg: &pam.Source{
|
||||
ServiceName: "myservice",
|
||||
EmailDomain: "testdomain.org",
|
||||
SkipLocalTwoFA: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 2
|
||||
{
|
||||
args: []string{
|
||||
"pam-test",
|
||||
"--service-name", "myservice",
|
||||
"--email-domain", "testdomain.org",
|
||||
"--skip-local-2fa", "false",
|
||||
"--active", "true",
|
||||
},
|
||||
errMsg: "name must be set",
|
||||
},
|
||||
// case 3
|
||||
{
|
||||
args: []string{
|
||||
"pam-test",
|
||||
"--name", "Pam Service",
|
||||
"--email-domain", "testdomain.org",
|
||||
"--skip-local-2fa", "false",
|
||||
"--active", "true",
|
||||
},
|
||||
errMsg: "service-name must be set",
|
||||
},
|
||||
}
|
||||
|
||||
for n, c := range cases {
|
||||
// Mock functions.
|
||||
var createdAuthSource *auth.Source
|
||||
service := &authService{
|
||||
initDB: func(context.Context) error {
|
||||
return nil
|
||||
},
|
||||
createAuthSource: func(ctx context.Context, authSource *auth.Source) error {
|
||||
createdAuthSource = authSource
|
||||
return nil
|
||||
},
|
||||
updateAuthSource: func(ctx context.Context, authSource *auth.Source) error {
|
||||
assert.FailNow(t, "should not call updateAuthSource", "case: %d", n)
|
||||
return nil
|
||||
},
|
||||
getAuthSourceByID: func(ctx context.Context, id int64) (*auth.Source, error) {
|
||||
assert.FailNow(t, "should not call getAuthSourceByID", "case: %d", n)
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
// Create a copy of command to test
|
||||
app := cli.Command{}
|
||||
app.Flags = microcmdAuthAddPAM().Flags
|
||||
app.Action = service.addPAM
|
||||
|
||||
// Run it
|
||||
err := app.Run(t.Context(), c.args)
|
||||
if c.errMsg != "" {
|
||||
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
|
||||
} else {
|
||||
require.NoError(t, err, "case %d: should have no errors", n)
|
||||
assert.Equal(t, c.source, createdAuthSource, "case %d: wrong authSource", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatePAM(t *testing.T) {
|
||||
// Mock cli functions to do not exit on error
|
||||
defer test.MockVariableValue(&cli.OsExiter, func(code int) {})()
|
||||
|
||||
// Test cases
|
||||
cases := []struct {
|
||||
args []string
|
||||
id int64
|
||||
existingAuthSource *auth.Source
|
||||
authSource *auth.Source
|
||||
errMsg string
|
||||
}{
|
||||
// case 0
|
||||
{
|
||||
args: []string{
|
||||
"pam-test",
|
||||
"--id", "23",
|
||||
"--name", "PAM Service",
|
||||
"--service-name", "myservice",
|
||||
},
|
||||
id: 23,
|
||||
existingAuthSource: &auth.Source{
|
||||
Type: auth.PAM,
|
||||
IsActive: true,
|
||||
Cfg: &pam.Source{},
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.PAM,
|
||||
Name: "PAM Service",
|
||||
IsActive: true,
|
||||
Cfg: &pam.Source{
|
||||
ServiceName: "myservice",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 1
|
||||
{
|
||||
args: []string{
|
||||
"pam-test",
|
||||
"--id", "1",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.PAM,
|
||||
Cfg: &pam.Source{},
|
||||
},
|
||||
},
|
||||
// case 2
|
||||
{
|
||||
args: []string{
|
||||
"pam-test",
|
||||
"--id", "1",
|
||||
"--name", "pam service",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.PAM,
|
||||
Name: "pam service",
|
||||
Cfg: &pam.Source{},
|
||||
},
|
||||
},
|
||||
// case 3
|
||||
{
|
||||
args: []string{
|
||||
"pam-test",
|
||||
"--id", "1",
|
||||
"--active=false",
|
||||
},
|
||||
existingAuthSource: &auth.Source{
|
||||
Type: auth.PAM,
|
||||
IsActive: true,
|
||||
Cfg: &pam.Source{},
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.PAM,
|
||||
IsActive: false,
|
||||
Cfg: &pam.Source{},
|
||||
},
|
||||
},
|
||||
// case 4
|
||||
{
|
||||
args: []string{
|
||||
"pam-test",
|
||||
"--id", "1",
|
||||
"--service-name", "myservice",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.PAM,
|
||||
Cfg: &pam.Source{
|
||||
ServiceName: "myservice",
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 5
|
||||
{
|
||||
args: []string{
|
||||
"pam-test",
|
||||
"--id", "1",
|
||||
"--skip-local-2fa=false",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.PAM,
|
||||
Cfg: &pam.Source{
|
||||
SkipLocalTwoFA: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
// case 6
|
||||
{
|
||||
args: []string{
|
||||
"pam-test",
|
||||
"--id", "1",
|
||||
"--email-domain", "testdomain.org",
|
||||
},
|
||||
authSource: &auth.Source{
|
||||
Type: auth.PAM,
|
||||
Cfg: &pam.Source{
|
||||
EmailDomain: "testdomain.org",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for n, c := range cases {
|
||||
// Mock functions.
|
||||
var updatedAuthSource *auth.Source
|
||||
service := &authService{
|
||||
initDB: func(context.Context) error {
|
||||
return nil
|
||||
},
|
||||
createAuthSource: func(ctx context.Context, authSource *auth.Source) error {
|
||||
assert.FailNow(t, "should not call createAuthSource", "case: %d", n)
|
||||
return nil
|
||||
},
|
||||
updateAuthSource: func(ctx context.Context, authSource *auth.Source) error {
|
||||
updatedAuthSource = authSource
|
||||
return nil
|
||||
},
|
||||
getAuthSourceByID: func(ctx context.Context, id int64) (*auth.Source, error) {
|
||||
if c.id != 0 {
|
||||
assert.Equal(t, c.id, id, "case %d: wrong id", n)
|
||||
}
|
||||
if c.existingAuthSource != nil {
|
||||
return c.existingAuthSource, nil
|
||||
}
|
||||
return &auth.Source{
|
||||
Type: auth.PAM,
|
||||
Cfg: &pam.Source{},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
// Create a copy of command to test
|
||||
app := cli.Command{}
|
||||
app.Flags = microcmdAuthUpdatePAM().Flags
|
||||
app.Action = service.updatePAM
|
||||
|
||||
// Run it
|
||||
err := app.Run(t.Context(), c.args)
|
||||
if c.errMsg != "" {
|
||||
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
|
||||
} else {
|
||||
require.NoError(t, err, "case %d: should have no errors", n)
|
||||
assert.Equal(t, c.authSource, updatedAuthSource, "case %d: wrong authSource", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
|
|
@ -12,11 +11,11 @@ import (
|
|||
"forgejo.org/modules/util"
|
||||
"forgejo.org/services/auth/source/smtp"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func smtpCLIFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
var (
|
||||
smtpCLIFlags = []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Value: "",
|
||||
|
|
@ -72,29 +71,23 @@ func smtpCLIFlags() []cli.Flag {
|
|||
Value: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func microcmdAuthAddSMTP() *cli.Command {
|
||||
return &cli.Command{
|
||||
microcmdAuthAddSMTP = &cli.Command{
|
||||
Name: "add-smtp",
|
||||
Usage: "Add new SMTP authentication source",
|
||||
Before: noDanglingArgs,
|
||||
Action: runAddSMTP,
|
||||
Flags: smtpCLIFlags(),
|
||||
Flags: smtpCLIFlags,
|
||||
}
|
||||
}
|
||||
|
||||
func microcmdAuthUpdateSMTP() *cli.Command {
|
||||
return &cli.Command{
|
||||
microcmdAuthUpdateSMTP = &cli.Command{
|
||||
Name: "update-smtp",
|
||||
Usage: "Update existing SMTP authentication source",
|
||||
Before: noDanglingArgs,
|
||||
Action: runUpdateSMTP,
|
||||
Flags: append(smtpCLIFlags()[:1], append([]cli.Flag{idFlag()}, smtpCLIFlags()[1:]...)...),
|
||||
Flags: append(smtpCLIFlags[:1], append([]cli.Flag{idFlag}, smtpCLIFlags[1:]...)...),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
func parseSMTPConfig(c *cli.Command, conf *smtp.Source) error {
|
||||
func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error {
|
||||
if c.IsSet("auth-type") {
|
||||
conf.Auth = c.String("auth-type")
|
||||
validAuthTypes := []string{"PLAIN", "LOGIN", "CRAM-MD5"}
|
||||
|
|
@ -130,8 +123,8 @@ func parseSMTPConfig(c *cli.Command, conf *smtp.Source) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func runAddSMTP(ctx context.Context, c *cli.Command) error {
|
||||
ctx, cancel := installSignals(ctx)
|
||||
func runAddSMTP(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
|
|
@ -170,12 +163,12 @@ func runAddSMTP(ctx context.Context, c *cli.Command) error {
|
|||
})
|
||||
}
|
||||
|
||||
func runUpdateSMTP(ctx context.Context, c *cli.Command) error {
|
||||
func runUpdateSMTP(c *cli.Context) error {
|
||||
if !c.IsSet("id") {
|
||||
return errors.New("--id flag is missing")
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals(ctx)
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
|
|
@ -4,22 +4,39 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
asymkey_model "forgejo.org/models/asymkey"
|
||||
"forgejo.org/modules/graceful"
|
||||
repo_service "forgejo.org/services/repository"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var microcmdRegenKeys = &cli.Command{
|
||||
Name: "keys",
|
||||
Usage: "Regenerate authorized_keys file",
|
||||
Before: noDanglingArgs,
|
||||
Action: runRegenerateKeys,
|
||||
var (
|
||||
microcmdRegenHooks = &cli.Command{
|
||||
Name: "hooks",
|
||||
Usage: "Regenerate git-hooks",
|
||||
Action: runRegenerateHooks,
|
||||
}
|
||||
|
||||
microcmdRegenKeys = &cli.Command{
|
||||
Name: "keys",
|
||||
Usage: "Regenerate authorized_keys file",
|
||||
Action: runRegenerateKeys,
|
||||
}
|
||||
)
|
||||
|
||||
func runRegenerateHooks(_ *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return repo_service.SyncRepositoryHooks(graceful.GetManager().ShutdownContext())
|
||||
}
|
||||
|
||||
func runRegenerateKeys(ctx context.Context, c *cli.Command) error {
|
||||
ctx, cancel := installSignals(ctx)
|
||||
func runRegenerateKeys(_ *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
|
|
|
|||
|
|
@ -4,22 +4,18 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func subcmdUser() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "user",
|
||||
Usage: "Modify users",
|
||||
Commands: []*cli.Command{
|
||||
microcmdUserCreate(),
|
||||
microcmdUserList(),
|
||||
microcmdUserChangePassword(),
|
||||
microcmdUserDelete(),
|
||||
microcmdUserGenerateAccessToken(),
|
||||
microcmdUserCreateAuthorizedIntegration(),
|
||||
microcmdUserMustChangePassword(),
|
||||
microcmdUserResetMFA(),
|
||||
},
|
||||
}
|
||||
var subcmdUser = &cli.Command{
|
||||
Name: "user",
|
||||
Usage: "Modify users",
|
||||
Subcommands: []*cli.Command{
|
||||
microcmdUserCreate,
|
||||
microcmdUserList,
|
||||
microcmdUserChangePassword,
|
||||
microcmdUserDelete,
|
||||
microcmdUserGenerateAccessToken,
|
||||
microcmdUserMustChangePassword,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
|
|
@ -14,43 +13,40 @@ import (
|
|||
"forgejo.org/modules/setting"
|
||||
user_service "forgejo.org/services/user"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func microcmdUserChangePassword() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "change-password",
|
||||
Usage: "Change a user's password",
|
||||
Before: noDanglingArgs,
|
||||
Action: runChangePassword,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Aliases: []string{"u"},
|
||||
Value: "",
|
||||
Usage: "The user to change password for",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "password",
|
||||
Aliases: []string{"p"},
|
||||
Value: "",
|
||||
Usage: "New password to set for user",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "must-change-password",
|
||||
Usage: "User must change password",
|
||||
Value: true,
|
||||
},
|
||||
var microcmdUserChangePassword = &cli.Command{
|
||||
Name: "change-password",
|
||||
Usage: "Change a user's password",
|
||||
Action: runChangePassword,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Aliases: []string{"u"},
|
||||
Value: "",
|
||||
Usage: "The user to change password for",
|
||||
},
|
||||
}
|
||||
&cli.StringFlag{
|
||||
Name: "password",
|
||||
Aliases: []string{"p"},
|
||||
Value: "",
|
||||
Usage: "New password to set for user",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "must-change-password",
|
||||
Usage: "User must change password",
|
||||
Value: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func runChangePassword(ctx context.Context, c *cli.Command) error {
|
||||
func runChangePassword(c *cli.Context) error {
|
||||
if err := argsSet(c, "username", "password"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals(ctx)
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,8 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
"forgejo.org/models/db"
|
||||
|
|
@ -16,77 +14,61 @@ import (
|
|||
"forgejo.org/modules/optional"
|
||||
"forgejo.org/modules/setting"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func microcmdUserCreate() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "create",
|
||||
Usage: "Create a new user in database",
|
||||
Before: noDanglingArgs,
|
||||
Action: runCreateUser,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "Username. DEPRECATED: use username instead",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Usage: "Username",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "password",
|
||||
Usage: "User password",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "email",
|
||||
Usage: "User email address",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "admin",
|
||||
Usage: "User is an admin",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "random-password",
|
||||
Usage: "Generate a random password for the user",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "must-change-password",
|
||||
Usage: "Set this option to false to prevent forcing the user to change their password after initial login",
|
||||
Value: true,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "random-password-length",
|
||||
Usage: "Length of the random password to be generated",
|
||||
Value: 12,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "access-token",
|
||||
Usage: "Generate access token for the user",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "access-token-name",
|
||||
Usage: `Name of the generated access token`,
|
||||
Value: "gitea-admin",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "access-token-scopes",
|
||||
Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`,
|
||||
Value: "all",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "restricted",
|
||||
Usage: "Make a restricted user account",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "fullname",
|
||||
Usage: `The full, human-readable name of the user`,
|
||||
},
|
||||
var microcmdUserCreate = &cli.Command{
|
||||
Name: "create",
|
||||
Usage: "Create a new user in database",
|
||||
Action: runCreateUser,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "Username. DEPRECATED: use username instead",
|
||||
},
|
||||
}
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Usage: "Username",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "password",
|
||||
Usage: "User password",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "email",
|
||||
Usage: "User email address",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "admin",
|
||||
Usage: "User is an admin",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "random-password",
|
||||
Usage: "Generate a random password for the user",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "must-change-password",
|
||||
Usage: "Set this option to false to prevent forcing the user to change their password after initial login",
|
||||
Value: true,
|
||||
DisableDefaultText: true,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "random-password-length",
|
||||
Usage: "Length of the random password to be generated",
|
||||
Value: 12,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "access-token",
|
||||
Usage: "Generate access token for the user",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "restricted",
|
||||
Usage: "Make a restricted user account",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func runCreateUser(ctx context.Context, c *cli.Command) error {
|
||||
func runCreateUser(c *cli.Context) error {
|
||||
// this command highly depends on the many setting options (create org, visibility, etc.), so it must have a full setting load first
|
||||
// duplicate setting loading should be safe at the moment, but it should be refactored & improved in the future.
|
||||
setting.LoadSettings()
|
||||
|
|
@ -111,10 +93,10 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
|
|||
username = c.String("username")
|
||||
} else {
|
||||
username = c.String("name")
|
||||
_, _ = fmt.Fprint(c.Root().ErrWriter, "--name flag is deprecated. Use --username instead.\n")
|
||||
_, _ = fmt.Fprintf(c.App.ErrWriter, "--name flag is deprecated. Use --username instead.\n")
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals(ctx)
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
|
|
@ -168,7 +150,6 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
|
|||
IsAdmin: isAdmin,
|
||||
MustChangePassword: mustChangePassword,
|
||||
Visibility: visibility,
|
||||
FullName: c.String("fullname"),
|
||||
}
|
||||
|
||||
overwriteDefault := &user_model.CreateUserOverwriteOptions{
|
||||
|
|
@ -176,48 +157,23 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
|
|||
IsRestricted: restricted,
|
||||
}
|
||||
|
||||
var accessTokenName string
|
||||
var accessTokenScope auth_model.AccessTokenScope
|
||||
if c.IsSet("access-token") {
|
||||
accessTokenName = strings.TrimSpace(c.String("access-token-name"))
|
||||
if accessTokenName == "" {
|
||||
return errors.New("access-token-name cannot be empty")
|
||||
}
|
||||
var err error
|
||||
accessTokenScope, err = auth_model.AccessTokenScope(c.String("access-token-scopes")).Normalize()
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid access token scope provided: %w", err)
|
||||
}
|
||||
if !accessTokenScope.HasPermissionScope() {
|
||||
return errors.New("access token does not have any permission")
|
||||
}
|
||||
} else if c.IsSet("access-token-name") || c.IsSet("access-token-scopes") {
|
||||
return errors.New("access-token-name and access-token-scopes flags are only valid when access-token flag is set")
|
||||
}
|
||||
|
||||
// arguments should be prepared before creating the user & access token, in case there is anything wrong
|
||||
|
||||
// create the user
|
||||
if err := user_model.CreateUser(ctx, u, overwriteDefault); err != nil {
|
||||
return fmt.Errorf("CreateUser: %w", err)
|
||||
}
|
||||
fmt.Printf("New user '%s' has been successfully created!\n", username)
|
||||
|
||||
// create the access token
|
||||
if accessTokenScope != "" {
|
||||
if c.Bool("access-token") {
|
||||
t := &auth_model.AccessToken{
|
||||
Name: accessTokenName,
|
||||
UID: u.ID,
|
||||
Scope: accessTokenScope,
|
||||
|
||||
// maintain legacy behaviour until new CLI options are added -- token has access to all resources, is not
|
||||
// fine-grained
|
||||
ResourceAllRepos: true,
|
||||
Name: "gitea-admin",
|
||||
UID: u.ID,
|
||||
}
|
||||
|
||||
if err := auth_model.NewAccessToken(ctx, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Access token was successfully created... %s\n", t.Token)
|
||||
}
|
||||
|
||||
fmt.Printf("New user '%s' has been successfully created!\n", username)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
|
@ -13,44 +12,41 @@ import (
|
|||
"forgejo.org/modules/storage"
|
||||
user_service "forgejo.org/services/user"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func microcmdUserDelete() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "delete",
|
||||
Usage: "Delete specific user by id, name or email",
|
||||
Flags: []cli.Flag{
|
||||
&cli.Int64Flag{
|
||||
Name: "id",
|
||||
Usage: "ID of user of the user to delete",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "Username of the user to delete",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "email",
|
||||
Aliases: []string{"e"},
|
||||
Usage: "Email of the user to delete",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "purge",
|
||||
Usage: "Purge user, all their repositories, organizations and comments",
|
||||
},
|
||||
var microcmdUserDelete = &cli.Command{
|
||||
Name: "delete",
|
||||
Usage: "Delete specific user by id, name or email",
|
||||
Flags: []cli.Flag{
|
||||
&cli.Int64Flag{
|
||||
Name: "id",
|
||||
Usage: "ID of user of the user to delete",
|
||||
},
|
||||
Before: noDanglingArgs,
|
||||
Action: runDeleteUser,
|
||||
}
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "Username of the user to delete",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "email",
|
||||
Aliases: []string{"e"},
|
||||
Usage: "Email of the user to delete",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "purge",
|
||||
Usage: "Purge user, all their repositories, organizations and comments",
|
||||
},
|
||||
},
|
||||
Action: runDeleteUser,
|
||||
}
|
||||
|
||||
func runDeleteUser(ctx context.Context, c *cli.Command) error {
|
||||
func runDeleteUser(c *cli.Context) error {
|
||||
if !c.IsSet("id") && !c.IsSet("username") && !c.IsSet("email") {
|
||||
return errors.New("You must provide the id, username or email of a user to delete")
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals(ctx)
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
|
|
|
|||
|
|
@ -4,53 +4,49 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
user_model "forgejo.org/models/user"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func microcmdUserGenerateAccessToken() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "generate-access-token",
|
||||
Usage: "Generate an access token for a specific user",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "Username",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "token-name",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "Token name",
|
||||
Value: "gitea-admin",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "raw",
|
||||
Usage: "Display only the token value",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "scopes",
|
||||
Value: "all",
|
||||
Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`,
|
||||
},
|
||||
var microcmdUserGenerateAccessToken = &cli.Command{
|
||||
Name: "generate-access-token",
|
||||
Usage: "Generate an access token for a specific user",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "Username",
|
||||
},
|
||||
Before: noDanglingArgs,
|
||||
Action: runGenerateAccessToken,
|
||||
}
|
||||
&cli.StringFlag{
|
||||
Name: "token-name",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "Token name",
|
||||
Value: "gitea-admin",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "raw",
|
||||
Usage: "Display only the token value",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "scopes",
|
||||
Value: "",
|
||||
Usage: "Comma separated list of scopes to apply to access token",
|
||||
},
|
||||
},
|
||||
Action: runGenerateAccessToken,
|
||||
}
|
||||
|
||||
func runGenerateAccessToken(ctx context.Context, c *cli.Command) error {
|
||||
func runGenerateAccessToken(c *cli.Context) error {
|
||||
if !c.IsSet("username") {
|
||||
return errors.New("you must provide a username to generate a token for")
|
||||
return errors.New("You must provide a username to generate a token for")
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals(ctx)
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
|
|
@ -81,15 +77,8 @@ func runGenerateAccessToken(ctx context.Context, c *cli.Command) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("invalid access token scope provided: %w", err)
|
||||
}
|
||||
if !accessTokenScope.HasPermissionScope() {
|
||||
return errors.New("access token does not have any permission")
|
||||
}
|
||||
t.Scope = accessTokenScope
|
||||
|
||||
// maintain legacy behaviour until new CLI options are added -- token has access to all resources, is not
|
||||
// fine-grained
|
||||
t.ResourceAllRepos = true
|
||||
|
||||
// create the token
|
||||
if err := auth_model.NewAccessToken(ctx, t); err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -1,269 +0,0 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
"forgejo.org/models/db"
|
||||
"forgejo.org/models/repo"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/modules/json"
|
||||
"forgejo.org/services/authz"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func microcmdUserCreateAuthorizedIntegration() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "create-authorized-integration",
|
||||
Description: `Creates an authorized integration. Authorized integrations allow Forgejo to
|
||||
receive JWTs from external sources, validate their claims against
|
||||
user-defined rules, and grant access to Forgejo's API on behalf of a user.
|
||||
|
||||
The issuer may be set to "urn:forgejo:authorized-integrations:actions"
|
||||
to support JWTs from the local instance's Forgejo Actions, utilizing the
|
||||
enable-openid-connect flag in a workflow.`,
|
||||
|
||||
// `--claim-in sub=v1,v2,v3` needs to be parsed as a single parameter so that we can comma-split the value into
|
||||
// an array. To accomplish this, we disable urfave 's slice flag separator, which would cause this to be
|
||||
// treated as "sub=v1", "v2=?", and "v3=?", resulting in an error of missing values.
|
||||
DisableSliceFlagSeparator: true,
|
||||
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "Username",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "Name of the authorized integration for later identification",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "description",
|
||||
Usage: "Optional description for the authorized integration",
|
||||
},
|
||||
|
||||
// JWT validation:
|
||||
&cli.StringFlag{
|
||||
Name: "issuer",
|
||||
Usage: `JWT issuer ('iss' claim), example: https://forgejo.example.org/api/actions`,
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringMapFlag{
|
||||
Name: "claim-eq",
|
||||
Value: map[string]string{},
|
||||
Usage: `Zero-or-more claim equality checks, formatted as claim=value, example: "actor=someuser"`,
|
||||
},
|
||||
&cli.StringMapFlag{
|
||||
Name: "claim-in",
|
||||
Value: map[string]string{},
|
||||
Usage: `Zero-or-more claim equality in list checks, formatted as claim=value1,value2,... example: "actor=user1,user2"`,
|
||||
},
|
||||
&cli.StringMapFlag{
|
||||
Name: "claim-glob",
|
||||
Value: map[string]string{},
|
||||
Usage: `Zero-or-more claim glob checks, formatted as claim=value, example: "sub=repo:forgejo/*:pull_request"`,
|
||||
},
|
||||
&cli.StringMapFlag{
|
||||
Name: "claim-glob-in",
|
||||
Value: map[string]string{},
|
||||
Usage: `Zero-or-more claim glob in list checks, formatted as claim=va*ue1,va*ue2,... example: "sub=repo:*/*:pull_request,repo:*/*:refs:*"`,
|
||||
},
|
||||
// nested claim support omitted for now -- pretty complex for a CLI
|
||||
|
||||
// Permissions available on successful auth:
|
||||
&cli.StringSliceFlag{
|
||||
Name: "scope",
|
||||
Value: []string{"all"},
|
||||
Usage: `One-or-more scopes to apply to access token, examples: "all", "read:issue", "write:repository"`,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "repo",
|
||||
Value: []string{"all"},
|
||||
Usage: `Zero-or-more specific repositories that can be accessed, or "all" to allow access to all repositories, example: "owner1/repo1"`,
|
||||
},
|
||||
},
|
||||
Before: noDanglingArgs,
|
||||
Action: runCreateAuthorizedIntegration,
|
||||
}
|
||||
}
|
||||
|
||||
func runCreateAuthorizedIntegration(ctx context.Context, c *cli.Command) error {
|
||||
if !c.IsSet("username") {
|
||||
return errors.New("you must provide a username to generate a token for")
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals(ctx)
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := user_model.GetUserByName(ctx, c.String("username"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ai := &auth_model.AuthorizedIntegration{
|
||||
UserID: user.ID,
|
||||
Name: c.String("name"),
|
||||
Description: c.String("description"),
|
||||
}
|
||||
|
||||
var rules []auth_model.ClaimRule
|
||||
ai.Issuer = c.String("issuer")
|
||||
for claim, value := range c.StringMap("claim-eq") {
|
||||
rules = append(rules, auth_model.ClaimRule{
|
||||
Claim: claim,
|
||||
Comparison: auth_model.ClaimEqual,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
for claim, value := range c.StringMap("claim-in") {
|
||||
values := []string{}
|
||||
for s := range strings.SplitSeq(value, ",") {
|
||||
values = append(values, strings.TrimSpace(s))
|
||||
}
|
||||
rules = append(rules, auth_model.ClaimRule{
|
||||
Claim: claim,
|
||||
Comparison: auth_model.ClaimIn,
|
||||
Values: values,
|
||||
})
|
||||
}
|
||||
for claim, value := range c.StringMap("claim-glob") {
|
||||
rules = append(rules, auth_model.ClaimRule{
|
||||
Claim: claim,
|
||||
Comparison: auth_model.ClaimGlob,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
for claim, value := range c.StringMap("claim-glob-in") {
|
||||
values := []string{}
|
||||
for s := range strings.SplitSeq(value, ",") {
|
||||
values = append(values, strings.TrimSpace(s))
|
||||
}
|
||||
rules = append(rules, auth_model.ClaimRule{
|
||||
Claim: claim,
|
||||
Comparison: auth_model.ClaimGlobIn,
|
||||
Values: values,
|
||||
})
|
||||
}
|
||||
ai.ClaimRules = &auth_model.ClaimRules{Rules: rules}
|
||||
|
||||
scopes := strings.Join(c.StringSlice("scope"), ",")
|
||||
accessTokenScope, err := auth_model.AccessTokenScope(scopes).Normalize()
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid access token scope provided: %w", err)
|
||||
}
|
||||
ai.Scope = accessTokenScope
|
||||
|
||||
allRepos := false
|
||||
repos := []*repo.Repository{}
|
||||
for _, repoName := range c.StringSlice("repo") {
|
||||
if repoName == "all" {
|
||||
allRepos = true
|
||||
} else {
|
||||
split := strings.Split(repoName, "/")
|
||||
if len(split) != 2 {
|
||||
return fmt.Errorf("invalid repo name: %q", split)
|
||||
}
|
||||
owner := split[0]
|
||||
name := split[1]
|
||||
repo, err := repo.GetRepositoryByOwnerAndName(ctx, owner, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repos = append(repos, repo)
|
||||
}
|
||||
}
|
||||
ai.ResourceAllRepos = allRepos
|
||||
|
||||
rr := make([]*auth_model.AuthorizedIntegResourceRepo, len(repos))
|
||||
for i := range repos {
|
||||
rr[i] = &auth_model.AuthorizedIntegResourceRepo{RepoID: repos[i].ID}
|
||||
}
|
||||
if err := authz.ValidateAuthorizedIntegration(ai, rr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.WithTx(ctx, func(ctx context.Context) error {
|
||||
if err := auth_model.InsertAuthorizedIntegration(ctx, ai); err != nil {
|
||||
return err
|
||||
}
|
||||
if !allRepos {
|
||||
if err := auth_model.InsertAuthorizedIntegrationResourceRepos(ctx, ai.ID, rr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type ClaimRuleDescription struct {
|
||||
Description string `json:"description"`
|
||||
Claim string `json:"claim"`
|
||||
Comparison auth_model.ClaimComparison `json:"compare"`
|
||||
Value string `json:"value,omitempty"`
|
||||
Values []string `json:"values,omitempty"`
|
||||
}
|
||||
output := struct {
|
||||
Message string `json:"message"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Issuer string `json:"issuer"`
|
||||
Audience string `json:"audience"`
|
||||
ClaimRules []ClaimRuleDescription `json:"claim_rules"`
|
||||
}{
|
||||
Message: "Authorized integration was successfully created.",
|
||||
Name: ai.Name,
|
||||
Description: ai.Description,
|
||||
Issuer: ai.Issuer,
|
||||
Audience: ai.Audience,
|
||||
}
|
||||
for _, cr := range ai.ClaimRules.Rules {
|
||||
var description string
|
||||
switch cr.Comparison {
|
||||
case auth_model.ClaimEqual:
|
||||
description = fmt.Sprintf("%q = %q", cr.Claim, cr.Value)
|
||||
case auth_model.ClaimIn:
|
||||
description = fmt.Sprintf("%q in %q", cr.Claim, cr.Values)
|
||||
case auth_model.ClaimGlob:
|
||||
description = fmt.Sprintf("%q matches %q", cr.Claim, cr.Value)
|
||||
case auth_model.ClaimGlobIn:
|
||||
description = fmt.Sprintf("%q matches in %q", cr.Claim, cr.Values)
|
||||
}
|
||||
output.ClaimRules = append(output.ClaimRules, ClaimRuleDescription{
|
||||
Description: description,
|
||||
Claim: cr.Claim,
|
||||
Comparison: cr.Comparison,
|
||||
Value: cr.Value,
|
||||
Values: cr.Values,
|
||||
})
|
||||
}
|
||||
|
||||
raw, err := json.Marshal(output)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var indent bytes.Buffer
|
||||
if err := json.Indent(&indent, raw, "", " "); err != nil {
|
||||
return err
|
||||
}
|
||||
os.Stdout.Write(indent.Bytes())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -4,33 +4,29 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
user_model "forgejo.org/models/user"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func microcmdUserList() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "list",
|
||||
Usage: "List users",
|
||||
Before: noDanglingArgs,
|
||||
Action: runListUsers,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "admin",
|
||||
Usage: "List only admin users",
|
||||
},
|
||||
var microcmdUserList = &cli.Command{
|
||||
Name: "list",
|
||||
Usage: "List users",
|
||||
Action: runListUsers,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "admin",
|
||||
Usage: "List only admin users",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func runListUsers(ctx context.Context, c *cli.Command) error {
|
||||
ctx, cancel := installSignals(ctx)
|
||||
func runListUsers(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
|
|
@ -45,7 +41,7 @@ func runListUsers(ctx context.Context, c *cli.Command) error {
|
|||
w := tabwriter.NewWriter(os.Stdout, 5, 0, 1, ' ', 0)
|
||||
|
||||
if c.IsSet("admin") {
|
||||
fmt.Fprint(w, "ID\tUsername\tEmail\tIsActive\n")
|
||||
fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\n")
|
||||
for _, u := range users {
|
||||
if u.IsAdmin {
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", u.ID, u.Name, u.Email, u.IsActive)
|
||||
|
|
@ -53,7 +49,7 @@ func runListUsers(ctx context.Context, c *cli.Command) error {
|
|||
}
|
||||
} else {
|
||||
twofa := user_model.UserList(users).GetTwoFaStatus(ctx)
|
||||
fmt.Fprint(w, "ID\tUsername\tEmail\tIsActive\tIsAdmin\t2FA\n")
|
||||
fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\tIsAdmin\t2FA\n")
|
||||
for _, u := range users {
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\t%t\t%t\t%t\n", u.ID, u.Name, u.Email, u.IsActive, u.IsAdmin, twofa[u.ID])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,41 +4,38 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
user_model "forgejo.org/models/user"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func microcmdUserMustChangePassword() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "must-change-password",
|
||||
Usage: "Set the must change password flag for the provided users or all users",
|
||||
Action: runMustChangePassword,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "all",
|
||||
Aliases: []string{"A"},
|
||||
Usage: "All users must change password, except those explicitly excluded with --exclude",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "exclude",
|
||||
Aliases: []string{"e"},
|
||||
Usage: "Do not change the must-change-password flag for these users",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "unset",
|
||||
Usage: "Instead of setting the must-change-password flag, unset it",
|
||||
},
|
||||
var microcmdUserMustChangePassword = &cli.Command{
|
||||
Name: "must-change-password",
|
||||
Usage: "Set the must change password flag for the provided users or all users",
|
||||
Action: runMustChangePassword,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "all",
|
||||
Aliases: []string{"A"},
|
||||
Usage: "All users must change password, except those explicitly excluded with --exclude",
|
||||
},
|
||||
}
|
||||
&cli.StringSliceFlag{
|
||||
Name: "exclude",
|
||||
Aliases: []string{"e"},
|
||||
Usage: "Do not change the must-change-password flag for these users",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "unset",
|
||||
Usage: "Instead of setting the must-change-password flag, unset it",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func runMustChangePassword(ctx context.Context, c *cli.Command) error {
|
||||
ctx, cancel := installSignals(ctx)
|
||||
func runMustChangePassword(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if c.NArg() == 0 && !c.IsSet("all") {
|
||||
|
|
|
|||
|
|
@ -1,74 +0,0 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
user_model "forgejo.org/models/user"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func microcmdUserResetMFA() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "reset-mfa",
|
||||
Usage: "Remove all two-factor authentication configurations for a user",
|
||||
Before: noDanglingArgs,
|
||||
Action: runResetMFA,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Aliases: []string{"u"},
|
||||
Value: "",
|
||||
Usage: "The user to update",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runResetMFA(ctx context.Context, c *cli.Command) error {
|
||||
if err := argsSet(c, "username"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals(ctx)
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := user_model.GetUserByName(ctx, c.String("username"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
webAuthnList, err := auth_model.GetWebAuthnCredentialsByUID(ctx, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, credential := range webAuthnList {
|
||||
if _, err := auth_model.DeleteCredential(ctx, credential.ID, user.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
tfaModes, err := auth_model.GetTwoFactorByUID(ctx, user.ID)
|
||||
if err == nil && tfaModes != nil {
|
||||
if err := auth_model.DeleteTwoFactorByID(ctx, tfaModes.ID, user.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if _, is := err.(auth_model.ErrTwoFactorNotEnrolled); !is {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("%s's two-factor authentication settings have been removed!\n", user.Name)
|
||||
return nil
|
||||
}
|
||||
82
cmd/cert.go
82
cmd/cert.go
|
|
@ -6,7 +6,6 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
|
|
@ -21,50 +20,47 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// CmdCert represents the available cert sub-command.
|
||||
func cmdCert() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "cert",
|
||||
Usage: "Generate self-signed certificate",
|
||||
Description: `Generate a self-signed X.509 certificate for a TLS server.
|
||||
var CmdCert = &cli.Command{
|
||||
Name: "cert",
|
||||
Usage: "Generate self-signed certificate",
|
||||
Description: `Generate a self-signed X.509 certificate for a TLS server.
|
||||
Outputs to 'cert.pem' and 'key.pem' and will overwrite existing files.`,
|
||||
Before: noDanglingArgs,
|
||||
Action: runCert,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "host",
|
||||
Value: "",
|
||||
Usage: "Comma-separated hostnames and IPs to generate a certificate for",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ecdsa-curve",
|
||||
Value: "",
|
||||
Usage: "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "rsa-bits",
|
||||
Value: 3072,
|
||||
Usage: "Size of RSA key to generate. Ignored if --ecdsa-curve is set",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "start-date",
|
||||
Value: "",
|
||||
Usage: "Creation date formatted as Jan 1 15:04:05 2011",
|
||||
},
|
||||
&cli.DurationFlag{
|
||||
Name: "duration",
|
||||
Value: 365 * 24 * time.Hour,
|
||||
Usage: "Duration that certificate is valid for",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "ca",
|
||||
Usage: "whether this cert should be its own Certificate Authority",
|
||||
},
|
||||
Action: runCert,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "host",
|
||||
Value: "",
|
||||
Usage: "Comma-separated hostnames and IPs to generate a certificate for",
|
||||
},
|
||||
}
|
||||
&cli.StringFlag{
|
||||
Name: "ecdsa-curve",
|
||||
Value: "",
|
||||
Usage: "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "rsa-bits",
|
||||
Value: 3072,
|
||||
Usage: "Size of RSA key to generate. Ignored if --ecdsa-curve is set",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "start-date",
|
||||
Value: "",
|
||||
Usage: "Creation date formatted as Jan 1 15:04:05 2011",
|
||||
},
|
||||
&cli.DurationFlag{
|
||||
Name: "duration",
|
||||
Value: 365 * 24 * time.Hour,
|
||||
Usage: "Duration that certificate is valid for",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "ca",
|
||||
Usage: "whether this cert should be its own Certificate Authority",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func publicKey(priv any) any {
|
||||
|
|
@ -93,7 +89,7 @@ func pemBlockForKey(priv any) *pem.Block {
|
|||
}
|
||||
}
|
||||
|
||||
func runCert(ctx context.Context, c *cli.Command) error {
|
||||
func runCert(c *cli.Context) error {
|
||||
if err := argsSet(c, "host"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -150,8 +146,8 @@ func runCert(ctx context.Context, c *cli.Command) error {
|
|||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
hosts := strings.SplitSeq(c.String("host"), ",")
|
||||
for h := range hosts {
|
||||
hosts := strings.Split(c.String("host"), ",")
|
||||
for _, h := range hosts {
|
||||
if ip := net.ParseIP(h); ip != nil {
|
||||
template.IPAddresses = append(template.IPAddresses, ip)
|
||||
} else {
|
||||
|
|
|
|||
75
cmd/cmd.go
75
cmd/cmd.go
|
|
@ -1,5 +1,4 @@
|
|||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package cmd provides subcommands to the gitea binary - such as "web" or
|
||||
|
|
@ -13,7 +12,6 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
|
|
@ -22,39 +20,24 @@ import (
|
|||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/util"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// argsSet checks that all the required arguments are set. args is a list of
|
||||
// arguments that must be set in the passed Context.
|
||||
func argsSet(c *cli.Command, args ...string) error {
|
||||
func argsSet(c *cli.Context, args ...string) error {
|
||||
for _, a := range args {
|
||||
if !c.IsSet(a) {
|
||||
return errors.New(a + " is not set")
|
||||
}
|
||||
|
||||
if s, ok := c.Value(a).(string); ok {
|
||||
if util.IsEmptyString(s) {
|
||||
return errors.New(a + " is required")
|
||||
}
|
||||
if util.IsEmptyString(c.String(a)) {
|
||||
return errors.New(a + " is required")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// When a CLI command is intended to be used only with flags and no other arbitrary args, noDanglingArgs will validate
|
||||
// the end-user's usage.
|
||||
func noDanglingArgs(ctx context.Context, c *cli.Command) (context.Context, error) {
|
||||
if c.Args().Len() != 0 {
|
||||
args := c.Args().Slice()
|
||||
if slices.Contains(args, "false") {
|
||||
println("Hint: boolean false must be specified as a single arg, eg. '--restricted=false', not '--restricted false'")
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected arguments: %s", strings.Join(c.Args().Slice(), ", "))
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// confirm waits for user input which confirms an action
|
||||
func confirm() (bool, error) {
|
||||
var response string
|
||||
|
|
@ -90,9 +73,26 @@ If this is the intended configuration file complete the [database] section.`, se
|
|||
return nil
|
||||
}
|
||||
|
||||
// installSignals returns a context that's cancelled on the SIGINT and SIGTERM signals or if the passed ctx is cancelled.
|
||||
func installSignals(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
return signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
|
||||
func installSignals() (context.Context, context.CancelFunc) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
// install notify
|
||||
signalChannel := make(chan os.Signal, 1)
|
||||
|
||||
signal.Notify(
|
||||
signalChannel,
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
)
|
||||
select {
|
||||
case <-signalChannel:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
cancel()
|
||||
signal.Reset()
|
||||
}()
|
||||
|
||||
return ctx, cancel
|
||||
}
|
||||
|
||||
func setupConsoleLogger(level log.Level, colorize bool, out io.Writer) {
|
||||
|
|
@ -109,7 +109,7 @@ func setupConsoleLogger(level log.Level, colorize bool, out io.Writer) {
|
|||
log.GetManager().GetLogger(log.DEFAULT).ReplaceAllWriters(writer)
|
||||
}
|
||||
|
||||
func globalBool(c *cli.Command, name string) bool {
|
||||
func globalBool(c *cli.Context, name string) bool {
|
||||
for _, ctx := range c.Lineage() {
|
||||
if ctx.Bool(name) {
|
||||
return true
|
||||
|
|
@ -120,31 +120,16 @@ func globalBool(c *cli.Command, name string) bool {
|
|||
|
||||
// PrepareConsoleLoggerLevel by default, use INFO level for console logger, but some sub-commands (for git/ssh protocol) shouldn't output any log to stdout.
|
||||
// Any log appears in git stdout pipe will break the git protocol, eg: client can't push and hangs forever.
|
||||
func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(ctx context.Context, cli *cli.Command) (context.Context, error) {
|
||||
return func(ctx context.Context, cli *cli.Command) (context.Context, error) {
|
||||
func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(*cli.Context) error {
|
||||
return func(c *cli.Context) error {
|
||||
level := defaultLevel
|
||||
if globalBool(cli, "quiet") {
|
||||
if globalBool(c, "quiet") {
|
||||
level = log.FATAL
|
||||
}
|
||||
if globalBool(cli, "debug") || globalBool(cli, "verbose") {
|
||||
if globalBool(c, "debug") || globalBool(c, "verbose") {
|
||||
level = log.TRACE
|
||||
}
|
||||
log.SetConsoleLogger(log.DEFAULT, "console-default", level)
|
||||
return ctx, nil
|
||||
}
|
||||
}
|
||||
|
||||
func multipleBefore(beforeFuncs ...cli.BeforeFunc) cli.BeforeFunc {
|
||||
return func(ctx context.Context, cli *cli.Command) (context.Context, error) {
|
||||
for _, beforeFunc := range beforeFuncs {
|
||||
bctx, err := beforeFunc(ctx, cli)
|
||||
if err != nil {
|
||||
return bctx, err
|
||||
}
|
||||
if bctx != nil {
|
||||
ctx = bctx
|
||||
}
|
||||
}
|
||||
return ctx, nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Test_installSignals(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skipf("Windows does not terminate in an awaitable manner")
|
||||
return
|
||||
}
|
||||
|
||||
for _, s := range []syscall.Signal{syscall.SIGTERM, syscall.SIGINT} {
|
||||
t.Run(fmt.Sprintf("Context is terminated on %s", s), func(t *testing.T) {
|
||||
// Register the signal handler. context.Background() is chosen deliberately,
|
||||
// because unlike t.Context(), we can be sure that it's not cancelled by a
|
||||
// different handler.
|
||||
ctx, cancel := installSignals(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
// Send the signal in the background.
|
||||
go syscall.Kill(syscall.Getpid(), s)
|
||||
|
||||
select {
|
||||
case <-time.Tick(time.Second * 10):
|
||||
t.Fatalf("Context not cancelled via signal after 10 seconds")
|
||||
case <-ctx.Done():
|
||||
t.Logf("Context was cancelled")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
65
cmd/docs.go
Normal file
65
cmd/docs.go
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// CmdDocs represents the available docs sub-command.
|
||||
var CmdDocs = &cli.Command{
|
||||
Name: "docs",
|
||||
Usage: "Output CLI documentation",
|
||||
Description: "A command to output Forgejo's CLI documentation, optionally to a file.",
|
||||
Action: runDocs,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "man",
|
||||
Usage: "Output man pages instead",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "output",
|
||||
Aliases: []string{"o"},
|
||||
Usage: "Path to output to instead of stdout (will overwrite if exists)",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func runDocs(ctx *cli.Context) error {
|
||||
docs, err := ctx.App.ToMarkdown()
|
||||
if ctx.Bool("man") {
|
||||
docs, err = ctx.App.ToMan()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !ctx.Bool("man") {
|
||||
// Clean up markdown. The following bug was fixed in v2, but is present in v1.
|
||||
// It affects markdown output (even though the issue is referring to man pages)
|
||||
// https://github.com/urfave/cli/issues/1040
|
||||
firstHashtagIndex := strings.Index(docs, "#")
|
||||
|
||||
if firstHashtagIndex > 0 {
|
||||
docs = docs[firstHashtagIndex:]
|
||||
}
|
||||
}
|
||||
|
||||
out := os.Stdout
|
||||
if ctx.String("output") != "" {
|
||||
fi, err := os.Create(ctx.String("output"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fi.Close()
|
||||
out = fi
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintln(out, docs)
|
||||
return err
|
||||
}
|
||||
321
cmd/doctor.go
321
cmd/doctor.go
|
|
@ -4,10 +4,7 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
golog "log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
|
@ -16,112 +13,91 @@ import (
|
|||
|
||||
"forgejo.org/models/db"
|
||||
git_model "forgejo.org/models/git"
|
||||
"forgejo.org/models/gitea_migrations"
|
||||
migrate_base "forgejo.org/models/gitea_migrations/base"
|
||||
repo_model "forgejo.org/models/repo"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/models/migrations"
|
||||
migrate_base "forgejo.org/models/migrations/base"
|
||||
"forgejo.org/modules/container"
|
||||
"forgejo.org/modules/log"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/storage"
|
||||
"forgejo.org/services/doctor"
|
||||
|
||||
exif_terminator "code.superseriousbusiness.org/exif-terminator"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// CmdDoctor represents the available doctor sub-command.
|
||||
func cmdDoctor() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "doctor",
|
||||
Usage: "Diagnose and optionally fix problems, convert or re-create database tables",
|
||||
Description: "A command to diagnose problems with the current Forgejo instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.",
|
||||
var CmdDoctor = &cli.Command{
|
||||
Name: "doctor",
|
||||
Usage: "Diagnose and optionally fix problems, convert or re-create database tables",
|
||||
Description: "A command to diagnose problems with the current Forgejo instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.",
|
||||
|
||||
Commands: []*cli.Command{
|
||||
cmdDoctorCheck(),
|
||||
cmdRecreateTable(),
|
||||
cmdDoctorConvert(),
|
||||
cmdAvatarStripExif(),
|
||||
cmdCleanupCommitStatuses(),
|
||||
},
|
||||
}
|
||||
Subcommands: []*cli.Command{
|
||||
cmdDoctorCheck,
|
||||
cmdRecreateTable,
|
||||
cmdDoctorConvert,
|
||||
cmdCleanupCommitStatuses,
|
||||
},
|
||||
}
|
||||
|
||||
func cmdDoctorCheck() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "check",
|
||||
Usage: "Diagnose and optionally fix problems",
|
||||
Description: "A command to diagnose problems with the current Forgejo instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.",
|
||||
Before: noDanglingArgs,
|
||||
Action: runDoctorCheck,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "list",
|
||||
Usage: "List the available checks",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "default",
|
||||
Usage: "Run the default checks (if neither --run or --all is set, this is the default behaviour)",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "run",
|
||||
Usage: "Run the provided checks - (if --default is set, the default checks will also run)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "all",
|
||||
Usage: "Run all the available checks",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "fix",
|
||||
Usage: "Automatically fix what we can",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "log-file",
|
||||
Usage: `Name of the log file (no verbose log output by default). Set to "-" to output to stdout`,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "color",
|
||||
Aliases: []string{"H"},
|
||||
Usage: "Use color for outputted information",
|
||||
},
|
||||
var cmdDoctorCheck = &cli.Command{
|
||||
Name: "check",
|
||||
Usage: "Diagnose and optionally fix problems",
|
||||
Description: "A command to diagnose problems with the current Forgejo instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.",
|
||||
Action: runDoctorCheck,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "list",
|
||||
Usage: "List the available checks",
|
||||
},
|
||||
}
|
||||
&cli.BoolFlag{
|
||||
Name: "default",
|
||||
Usage: "Run the default checks (if neither --run or --all is set, this is the default behaviour)",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "run",
|
||||
Usage: "Run the provided checks - (if --default is set, the default checks will also run)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "all",
|
||||
Usage: "Run all the available checks",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "fix",
|
||||
Usage: "Automatically fix what we can",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "log-file",
|
||||
Usage: `Name of the log file (no verbose log output by default). Set to "-" to output to stdout`,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "color",
|
||||
Aliases: []string{"H"},
|
||||
Usage: "Use color for outputted information",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func cmdRecreateTable() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "recreate-table",
|
||||
Usage: "Recreate tables from XORM definitions and copy the data.",
|
||||
ArgsUsage: "[TABLE]... : (TABLEs to recreate - leave blank for all)",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "debug",
|
||||
Usage: "Print SQL commands sent",
|
||||
},
|
||||
var cmdRecreateTable = &cli.Command{
|
||||
Name: "recreate-table",
|
||||
Usage: "Recreate tables from XORM definitions and copy the data.",
|
||||
ArgsUsage: "[TABLE]... : (TABLEs to recreate - leave blank for all)",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "debug",
|
||||
Usage: "Print SQL commands sent",
|
||||
},
|
||||
Description: `The database definitions Forgejo uses change across versions, sometimes changing default values and leaving old unused columns.
|
||||
},
|
||||
Description: `The database definitions Forgejo uses change across versions, sometimes changing default values and leaving old unused columns.
|
||||
|
||||
This command will cause Xorm to recreate tables, copying over the data and deleting the old table.
|
||||
|
||||
You should back-up your database before doing this and ensure that your database is up-to-date first.`,
|
||||
Action: runRecreateTable,
|
||||
}
|
||||
Action: runRecreateTable,
|
||||
}
|
||||
|
||||
func cmdAvatarStripExif() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "avatar-strip-exif",
|
||||
Usage: "Strip EXIF metadata from all images in the avatar storage",
|
||||
Before: noDanglingArgs,
|
||||
Action: runAvatarStripExif,
|
||||
}
|
||||
}
|
||||
|
||||
func cmdCleanupCommitStatuses() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "cleanup-commit-status",
|
||||
Usage: "Cleanup extra records in commit_status table",
|
||||
Description: `Forgejo suffered from a bug which caused the creation of more entries in the
|
||||
var cmdCleanupCommitStatuses = &cli.Command{
|
||||
Name: "cleanup-commit-status",
|
||||
Usage: "Cleanup extra records in commit_status table",
|
||||
Description: `Forgejo suffered from a bug which caused the creation of more entries in the
|
||||
"commit_status" table than necessary. This operation removes the redundant
|
||||
data caused by the bug. Removing this data is almost always safe.
|
||||
These redundant records can be accessed by users through the API, making it
|
||||
|
|
@ -137,36 +113,35 @@ memory is required for every 100,000 records in the buffer.
|
|||
Bug reference: https://codeberg.org/forgejo/forgejo/issues/10671
|
||||
`,
|
||||
|
||||
Before: multipleBefore(noDanglingArgs, PrepareConsoleLoggerLevel(log.INFO)),
|
||||
Action: runCleanupCommitStatus,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "verbose",
|
||||
Aliases: []string{"V"},
|
||||
Usage: "Show process details",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "dry-run",
|
||||
Usage: "Report statistics from the operation but do not modify the database",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "buffer-size",
|
||||
Usage: "Record count per query while iterating records; larger values are typically faster but use more memory",
|
||||
// See IterateByKeyset's documentation for performance notes which led to the choice of the default
|
||||
// buffer size for this operation.
|
||||
Value: 100000,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "delete-chunk-size",
|
||||
Usage: "Number of records to delete per DELETE query",
|
||||
Value: 1000,
|
||||
},
|
||||
Before: PrepareConsoleLoggerLevel(log.INFO),
|
||||
Action: runCleanupCommitStatus,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "verbose",
|
||||
Aliases: []string{"V"},
|
||||
Usage: "Show process details",
|
||||
},
|
||||
}
|
||||
&cli.BoolFlag{
|
||||
Name: "dry-run",
|
||||
Usage: "Report statistics from the operation but do not modify the database",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "buffer-size",
|
||||
Usage: "Record count per query while iterating records; larger values are typically faster but use more memory",
|
||||
// See IterateByKeyset's documentation for performance notes which led to the choice of the default
|
||||
// buffer size for this operation.
|
||||
Value: 100000,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "delete-chunk-size",
|
||||
Usage: "Number of records to delete per DELETE query",
|
||||
Value: 1000,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func runRecreateTable(stdCtx context.Context, ctx *cli.Command) error {
|
||||
stdCtx, cancel := installSignals(stdCtx)
|
||||
func runRecreateTable(ctx *cli.Context) error {
|
||||
stdCtx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
// Redirect the default golog to here
|
||||
|
|
@ -193,7 +168,7 @@ func runRecreateTable(stdCtx context.Context, ctx *cli.Command) error {
|
|||
|
||||
args := ctx.Args()
|
||||
names := make([]string, 0, ctx.NArg())
|
||||
for i := range ctx.NArg() {
|
||||
for i := 0; i < ctx.NArg(); i++ {
|
||||
names = append(names, args.Get(i))
|
||||
}
|
||||
|
||||
|
|
@ -203,31 +178,24 @@ func runRecreateTable(stdCtx context.Context, ctx *cli.Command) error {
|
|||
}
|
||||
recreateTables := migrate_base.RecreateTables(beans...)
|
||||
|
||||
return db.InitEngineWithMigration(stdCtx, func(x db.Engine) error {
|
||||
engine, err := db.GetMasterEngine(x)
|
||||
if err != nil {
|
||||
return db.InitEngineWithMigration(stdCtx, func(x *xorm.Engine) error {
|
||||
if err := migrations.EnsureUpToDate(x); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gitea_migrations.EnsureUpToDate(engine); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return recreateTables(engine)
|
||||
return recreateTables(x)
|
||||
})
|
||||
}
|
||||
|
||||
func setupDoctorDefaultLogger(ctx *cli.Command, colorize bool) {
|
||||
func setupDoctorDefaultLogger(ctx *cli.Context, colorize bool) {
|
||||
// Silence the default loggers
|
||||
setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr)
|
||||
|
||||
logFile := ctx.String("log-file")
|
||||
switch logFile {
|
||||
case "":
|
||||
if logFile == "" {
|
||||
return // if no doctor log-file is set, do not show any log from default logger
|
||||
case "-":
|
||||
} else if logFile == "-" {
|
||||
setupConsoleLogger(log.TRACE, colorize, os.Stdout)
|
||||
default:
|
||||
} else {
|
||||
logFile, _ = filepath.Abs(logFile)
|
||||
writeMode := log.WriterMode{Level: log.TRACE, WriterOption: log.WriterFileOption{FileName: logFile}}
|
||||
writer, err := log.NewEventWriter("console-to-file", "file", writeMode)
|
||||
|
|
@ -239,8 +207,8 @@ func setupDoctorDefaultLogger(ctx *cli.Command, colorize bool) {
|
|||
}
|
||||
}
|
||||
|
||||
func runDoctorCheck(stdCtx context.Context, ctx *cli.Command) error {
|
||||
stdCtx, cancel := installSignals(stdCtx)
|
||||
func runDoctorCheck(ctx *cli.Context) error {
|
||||
stdCtx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
colorize := log.CanColorStdout
|
||||
|
|
@ -298,93 +266,18 @@ func runDoctorCheck(stdCtx context.Context, ctx *cli.Command) error {
|
|||
return doctor.RunChecks(stdCtx, colorize, ctx.Bool("fix"), checks)
|
||||
}
|
||||
|
||||
func runAvatarStripExif(ctx context.Context, c *cli.Command) error {
|
||||
ctx, cancel := installSignals(ctx)
|
||||
func runCleanupCommitStatus(ctx *cli.Context) error {
|
||||
stdCtx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := storage.Init(); err != nil {
|
||||
if err := initDB(stdCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type HasCustomAvatarRelativePath interface {
|
||||
CustomAvatarRelativePath() string
|
||||
}
|
||||
|
||||
doExifStrip := func(obj HasCustomAvatarRelativePath, name string, target_storage storage.ObjectStorage) error {
|
||||
if obj.CustomAvatarRelativePath() == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Info("Stripping avatar for %s...", name)
|
||||
|
||||
avatarFile, err := target_storage.Open(obj.CustomAvatarRelativePath())
|
||||
if err != nil {
|
||||
return fmt.Errorf("storage.Avatars.Open: %w", err)
|
||||
}
|
||||
_, imgType, err := image.DecodeConfig(avatarFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("image.DecodeConfig: %w", err)
|
||||
}
|
||||
|
||||
// reset io.Reader for exif termination scan
|
||||
_, err = avatarFile.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return fmt.Errorf("avatarFile.Seek: %w", err)
|
||||
}
|
||||
|
||||
cleanedData, err := exif_terminator.Terminate(avatarFile, imgType)
|
||||
if err != nil && strings.Contains(err.Error(), "cannot be processed") {
|
||||
// expected error for an image type that isn't supported by exif_terminator
|
||||
log.Info("... image type %s is not supported by exif_terminator, skipping.", imgType)
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error cleaning exif data: %w", err)
|
||||
}
|
||||
|
||||
if err := storage.SaveFrom(target_storage, obj.CustomAvatarRelativePath(), func(w io.Writer) error {
|
||||
_, err := io.Copy(w, cleanedData)
|
||||
return err
|
||||
}); err != nil {
|
||||
return fmt.Errorf("Failed to create dir %s: %w", obj.CustomAvatarRelativePath(), err)
|
||||
}
|
||||
|
||||
log.Info("... completed %s.", name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
err := db.Iterate(ctx, nil, func(ctx context.Context, user *user_model.User) error {
|
||||
return doExifStrip(user, fmt.Sprintf("user %s", user.Name), storage.Avatars)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.Iterate(ctx, nil, func(ctx context.Context, repo *repo_model.Repository) error {
|
||||
return doExifStrip(repo, fmt.Sprintf("repo %s", repo.Name), storage.RepoAvatars)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runCleanupCommitStatus(ctx context.Context, cli *cli.Command) error {
|
||||
ctx, cancel := installSignals(ctx)
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bufferSize := cli.Int("buffer-size")
|
||||
deleteChunkSize := cli.Int("delete-chunk-size")
|
||||
dryRun := cli.Bool("dry-run")
|
||||
bufferSize := ctx.Int("buffer-size")
|
||||
deleteChunkSize := ctx.Int("delete-chunk-size")
|
||||
dryRun := ctx.Bool("dry-run")
|
||||
log.Debug("bufferSize = %d, deleteChunkSize = %d, dryRun = %v", bufferSize, deleteChunkSize, dryRun)
|
||||
|
||||
return git_model.CleanupCommitStatus(ctx, bufferSize, deleteChunkSize, dryRun)
|
||||
return git_model.CleanupCommitStatus(stdCtx, bufferSize, deleteChunkSize, dryRun)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,29 +4,25 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"forgejo.org/models/db"
|
||||
"forgejo.org/modules/log"
|
||||
"forgejo.org/modules/setting"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// cmdDoctorConvert represents the available convert sub-command.
|
||||
func cmdDoctorConvert() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "convert",
|
||||
Usage: "Convert the database",
|
||||
Description: "A command to convert an existing MySQL database from utf8 to utf8mb4",
|
||||
Before: noDanglingArgs,
|
||||
Action: runDoctorConvert,
|
||||
}
|
||||
var cmdDoctorConvert = &cli.Command{
|
||||
Name: "convert",
|
||||
Usage: "Convert the database",
|
||||
Description: "A command to convert an existing MySQL database from utf8 to utf8mb4",
|
||||
Action: runDoctorConvert,
|
||||
}
|
||||
|
||||
func runDoctorConvert(stdCtx context.Context, ctx *cli.Command) error {
|
||||
stdCtx, cancel := installSignals(stdCtx)
|
||||
func runDoctorConvert(ctx *cli.Context) error {
|
||||
stdCtx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(stdCtx); err != nil {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import (
|
|||
"forgejo.org/services/doctor"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestDoctorRun(t *testing.T) {
|
||||
|
|
@ -22,12 +22,12 @@ func TestDoctorRun(t *testing.T) {
|
|||
|
||||
SkipDatabaseInitialization: true,
|
||||
})
|
||||
app := cli.Command{}
|
||||
app.Commands = []*cli.Command{cmdDoctorCheck()}
|
||||
err := app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check"})
|
||||
app := cli.NewApp()
|
||||
app.Commands = []*cli.Command{cmdDoctorCheck}
|
||||
err := app.Run([]string{"./gitea", "check", "--run", "test-check"})
|
||||
require.NoError(t, err)
|
||||
err = app.Run(t.Context(), []string{"./gitea", "check", "--run", "no-such"})
|
||||
err = app.Run([]string{"./gitea", "check", "--run", "no-such"})
|
||||
require.ErrorContains(t, err, `unknown checks: "no-such"`)
|
||||
err = app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check,no-such"})
|
||||
err = app.Run([]string{"./gitea", "check", "--run", "test-check,no-such"})
|
||||
require.ErrorContains(t, err, `unknown checks: "no-such"`)
|
||||
}
|
||||
|
|
|
|||
210
cmd/dump.go
210
cmd/dump.go
|
|
@ -5,14 +5,11 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -26,7 +23,7 @@ import (
|
|||
|
||||
"code.forgejo.org/go-chi/session"
|
||||
"github.com/mholt/archives"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func addObject(archiveJobs chan archives.ArchiveAsyncJob, object fs.File, customName string, verbose bool) error {
|
||||
|
|
@ -84,18 +81,16 @@ func (o outputType) Join() string {
|
|||
}
|
||||
|
||||
func (o *outputType) Set(value string) error {
|
||||
if slices.Contains(o.Enum, value) {
|
||||
o.selected = value
|
||||
return nil
|
||||
for _, enum := range o.Enum {
|
||||
if enum == value {
|
||||
o.selected = value
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("allowed values are %s", o.Join())
|
||||
}
|
||||
|
||||
func (o *outputType) Get() any {
|
||||
return o.String()
|
||||
}
|
||||
|
||||
func (o outputType) String() string {
|
||||
if o.selected == "" {
|
||||
return o.Default
|
||||
|
|
@ -112,10 +107,7 @@ func getArchiverByType(outType string) (archives.ArchiverAsync, error) {
|
|||
var archiver archives.ArchiverAsync
|
||||
switch outType {
|
||||
case "zip":
|
||||
archiver = archives.Zip{
|
||||
Compression: 8,
|
||||
SelectiveCompression: false,
|
||||
}
|
||||
archiver = archives.Zip{}
|
||||
case "tar":
|
||||
archiver = archives.Tar{}
|
||||
case "tar.sz":
|
||||
|
|
@ -160,82 +152,80 @@ func getArchiverByType(outType string) (archives.ArchiverAsync, error) {
|
|||
}
|
||||
|
||||
// CmdDump represents the available dump sub-command.
|
||||
func cmdDump() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "dump",
|
||||
Usage: "Dump Forgejo files and database",
|
||||
Description: `Dump compresses all related files and database into zip file.
|
||||
var CmdDump = &cli.Command{
|
||||
Name: "dump",
|
||||
Usage: "Dump Forgejo files and database",
|
||||
Description: `Dump compresses all related files and database into zip file.
|
||||
It can be used for backup and capture Forgejo server image to send to maintainer`,
|
||||
Before: noDanglingArgs,
|
||||
Action: runDump,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "file",
|
||||
Aliases: []string{"f"},
|
||||
Value: fmt.Sprintf("forgejo-dump-%d.zip", time.Now().Unix()),
|
||||
Usage: "Name of the dump file which will be created. Supply '-' for stdout. See type for available types.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "verbose",
|
||||
Aliases: []string{"V"},
|
||||
Usage: "Show process details",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "quiet",
|
||||
Aliases: []string{"q"},
|
||||
Usage: "Only display warnings and errors",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "tempdir",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "Temporary dir path",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "database",
|
||||
Aliases: []string{"d"},
|
||||
Usage: "Specify the database SQL syntax: sqlite3, mysql, postgres",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-repository",
|
||||
Aliases: []string{"R"},
|
||||
Usage: "Skip repositories",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-log",
|
||||
Aliases: []string{"L"},
|
||||
Usage: "Skip logs",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-custom-dir",
|
||||
Usage: "Skip custom directory",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-lfs-data",
|
||||
Usage: "Skip LFS data",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-attachment-data",
|
||||
Usage: "Skip attachment data",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-package-data",
|
||||
Usage: "Skip package data",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-index",
|
||||
Usage: "Skip bleve index data",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-repo-archives",
|
||||
Usage: "Skip repository archives",
|
||||
},
|
||||
&cli.GenericFlag{
|
||||
Name: "type",
|
||||
Value: outputTypeEnum,
|
||||
Usage: fmt.Sprintf("Dump output format: %s", outputTypeEnum.Join()),
|
||||
},
|
||||
Action: runDump,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "file",
|
||||
Aliases: []string{"f"},
|
||||
Value: fmt.Sprintf("forgejo-dump-%d.zip", time.Now().Unix()),
|
||||
Usage: "Name of the dump file which will be created. Supply '-' for stdout. See type for available types.",
|
||||
},
|
||||
}
|
||||
&cli.BoolFlag{
|
||||
Name: "verbose",
|
||||
Aliases: []string{"V"},
|
||||
Usage: "Show process details",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "quiet",
|
||||
Aliases: []string{"q"},
|
||||
Usage: "Only display warnings and errors",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "tempdir",
|
||||
Aliases: []string{"t"},
|
||||
Value: os.TempDir(),
|
||||
Usage: "Temporary dir path",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "database",
|
||||
Aliases: []string{"d"},
|
||||
Usage: "Specify the database SQL syntax: sqlite3, mysql, postgres",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-repository",
|
||||
Aliases: []string{"R"},
|
||||
Usage: "Skip repositories",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-log",
|
||||
Aliases: []string{"L"},
|
||||
Usage: "Skip logs",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-custom-dir",
|
||||
Usage: "Skip custom directory",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-lfs-data",
|
||||
Usage: "Skip LFS data",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-attachment-data",
|
||||
Usage: "Skip attachment data",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-package-data",
|
||||
Usage: "Skip package data",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-index",
|
||||
Usage: "Skip bleve index data",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-repo-archives",
|
||||
Usage: "Skip repository archives",
|
||||
},
|
||||
&cli.GenericFlag{
|
||||
Name: "type",
|
||||
Value: outputTypeEnum,
|
||||
Usage: fmt.Sprintf("Dump output format: %s", outputTypeEnum.Join()),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func fatal(format string, args ...any) {
|
||||
|
|
@ -243,7 +233,7 @@ func fatal(format string, args ...any) {
|
|||
log.Fatal(format, args...)
|
||||
}
|
||||
|
||||
func runDump(stdCtx context.Context, ctx *cli.Command) error {
|
||||
func runDump(ctx *cli.Context) error {
|
||||
var file *os.File
|
||||
fileName := ctx.String("file")
|
||||
outType := ctx.String("type")
|
||||
|
|
@ -252,8 +242,8 @@ func runDump(stdCtx context.Context, ctx *cli.Command) error {
|
|||
setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr)
|
||||
} else {
|
||||
for _, suffix := range outputTypeEnum.Enum {
|
||||
if before, ok := strings.CutSuffix(fileName, "."+suffix); ok {
|
||||
fileName = before
|
||||
if strings.HasSuffix(fileName, "."+suffix) {
|
||||
fileName = strings.TrimSuffix(fileName, "."+suffix)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -279,16 +269,16 @@ func runDump(stdCtx context.Context, ctx *cli.Command) error {
|
|||
|
||||
if !setting.InstallLock {
|
||||
log.Error("Is '%s' really the right config path?\n", setting.CustomConf)
|
||||
return errors.New("forgejo is not initialized")
|
||||
return fmt.Errorf("forgejo is not initialized")
|
||||
}
|
||||
setting.LoadSettings() // cannot access session settings otherwise
|
||||
|
||||
verbose := ctx.Bool("verbose")
|
||||
if verbose && ctx.Bool("quiet") {
|
||||
return errors.New("--quiet and --verbose cannot both be set")
|
||||
return fmt.Errorf("--quiet and --verbose cannot both be set")
|
||||
}
|
||||
|
||||
stdCtx, cancel := installSignals(stdCtx)
|
||||
stdCtx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
err := db.InitEngine(stdCtx)
|
||||
|
|
@ -332,12 +322,14 @@ func runDump(stdCtx context.Context, ctx *cli.Command) error {
|
|||
go dumpDatabase(ctx, archiveJobs, &wg, verbose)
|
||||
|
||||
if len(setting.CustomConf) > 0 {
|
||||
wg.Go(func() {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
log.Info("Adding custom configuration file from %s", setting.CustomConf)
|
||||
if err := addFile(archiveJobs, "app.ini", setting.CustomConf, verbose); err != nil {
|
||||
fatal("Failed to include specified app.ini: %v", err)
|
||||
}
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
if ctx.IsSet("skip-custom-dir") && ctx.Bool("skip-custom-dir") {
|
||||
|
|
@ -361,13 +353,15 @@ func runDump(stdCtx context.Context, ctx *cli.Command) error {
|
|||
if ctx.IsSet("skip-attachment-data") && ctx.Bool("skip-attachment-data") {
|
||||
log.Info("Skipping attachment data")
|
||||
} else {
|
||||
wg.Go(func() {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := storage.Attachments.IterateObjects("", func(objPath string, object storage.Object) error {
|
||||
return addObject(archiveJobs, object, path.Join("data", "attachments", objPath), verbose)
|
||||
}); err != nil {
|
||||
fatal("Failed to dump attachments: %v", err)
|
||||
}
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
if ctx.IsSet("skip-package-data") && ctx.Bool("skip-package-data") {
|
||||
|
|
@ -375,13 +369,15 @@ func runDump(stdCtx context.Context, ctx *cli.Command) error {
|
|||
} else if !setting.Packages.Enabled {
|
||||
log.Info("Package registry not enabled - skipping")
|
||||
} else {
|
||||
wg.Go(func() {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := storage.Packages.IterateObjects("", func(objPath string, object storage.Object) error {
|
||||
return addObject(archiveJobs, object, path.Join("data", "packages", objPath), verbose)
|
||||
}); err != nil {
|
||||
fatal("Failed to dump packages: %v", err)
|
||||
}
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
// Doesn't check if LogRootPath exists before processing --skip-log intentionally,
|
||||
|
|
@ -395,11 +391,13 @@ func runDump(stdCtx context.Context, ctx *cli.Command) error {
|
|||
log.Error("Failed to check if %s exists: %v", setting.Log.RootPath, err)
|
||||
}
|
||||
if isExist {
|
||||
wg.Go(func() {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := addRecursiveExclude(archiveJobs, "log", setting.Log.RootPath, []string{absFileName}, verbose); err != nil {
|
||||
fatal("Failed to include log: %v", err)
|
||||
}
|
||||
})
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -429,7 +427,7 @@ func runDump(stdCtx context.Context, ctx *cli.Command) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func dumpData(ctx *cli.Command, archiveJobs chan archives.ArchiveAsyncJob, wg *sync.WaitGroup, absFileName string, verbose bool) {
|
||||
func dumpData(ctx *cli.Context, archiveJobs chan archives.ArchiveAsyncJob, wg *sync.WaitGroup, absFileName string, verbose bool) {
|
||||
defer wg.Done()
|
||||
|
||||
var excludes []string
|
||||
|
|
@ -480,7 +478,7 @@ func dumpCustom(archiveJobs chan archives.ArchiveAsyncJob, wg *sync.WaitGroup, a
|
|||
}
|
||||
}
|
||||
|
||||
func dumpDatabase(ctx *cli.Command, archiveJobs chan archives.ArchiveAsyncJob, wg *sync.WaitGroup, verbose bool) {
|
||||
func dumpDatabase(ctx *cli.Context, archiveJobs chan archives.ArchiveAsyncJob, wg *sync.WaitGroup, verbose bool) {
|
||||
defer wg.Done()
|
||||
|
||||
var err error
|
||||
|
|
@ -529,7 +527,7 @@ func dumpDatabase(ctx *cli.Command, archiveJobs chan archives.ArchiveAsyncJob, w
|
|||
}
|
||||
}
|
||||
|
||||
func dumpRepos(ctx *cli.Command, archiveJobs chan archives.ArchiveAsyncJob, wg *sync.WaitGroup, absFileName string, verbose bool) {
|
||||
func dumpRepos(ctx *cli.Context, archiveJobs chan archives.ArchiveAsyncJob, wg *sync.WaitGroup, absFileName string, verbose bool) {
|
||||
defer wg.Done()
|
||||
|
||||
if err := addRecursiveExclude(archiveJobs, "repos", setting.RepoRootPath, []string{absFileName}, verbose); err != nil {
|
||||
|
|
|
|||
124
cmd/dump_repo.go
124
cmd/dump_repo.go
|
|
@ -19,76 +19,68 @@ import (
|
|||
"forgejo.org/services/convert"
|
||||
"forgejo.org/services/migrations"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// CmdDumpRepository represents the available dump repository sub-command.
|
||||
func cmdDumpRepository() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "dump-repo",
|
||||
Usage: "Dump the repository from git/github/gitea/gitlab",
|
||||
Description: "This is a command for dumping the repository data.",
|
||||
Before: noDanglingArgs,
|
||||
Action: runDumpRepository,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "git_service",
|
||||
Value: "",
|
||||
Usage: "Git service, git, github, gitea, gitlab. If clone_addr could be recognized, this could be ignored.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "repo_dir",
|
||||
Aliases: []string{"r"},
|
||||
Value: "./data",
|
||||
Usage: "Repository dir path to store the data",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "clone_addr",
|
||||
Value: "",
|
||||
Usage: "The URL will be clone, currently could be a git/github/gitea/gitlab http/https URL",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "auth_username",
|
||||
Value: "",
|
||||
Usage: "The username to visit the clone_addr",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "auth_password",
|
||||
Value: "",
|
||||
Usage: "The password to visit the clone_addr",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "auth_token",
|
||||
Value: "",
|
||||
Usage: "The personal token to visit the clone_addr",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "owner_name",
|
||||
Value: "",
|
||||
Usage: "The data will be stored on a directory with owner name if not empty",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "repo_name",
|
||||
Value: "",
|
||||
Usage: "The data will be stored on a directory with repository name if not empty",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "units",
|
||||
Value: "",
|
||||
Usage: `Which items will be migrated, one or more units should be separated as comma.
|
||||
wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units.`,
|
||||
},
|
||||
var CmdDumpRepository = &cli.Command{
|
||||
Name: "dump-repo",
|
||||
Usage: "Dump the repository from git/github/gitea/gitlab",
|
||||
Description: "This is a command for dumping the repository data.",
|
||||
Action: runDumpRepository,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "git_service",
|
||||
Value: "",
|
||||
Usage: "Git service, git, github, gitea, gitlab. If clone_addr could be recognized, this could be ignored.",
|
||||
},
|
||||
}
|
||||
&cli.StringFlag{
|
||||
Name: "repo_dir",
|
||||
Aliases: []string{"r"},
|
||||
Value: "./data",
|
||||
Usage: "Repository dir path to store the data",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "clone_addr",
|
||||
Value: "",
|
||||
Usage: "The URL will be clone, currently could be a git/github/gitea/gitlab http/https URL",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "auth_username",
|
||||
Value: "",
|
||||
Usage: "The username to visit the clone_addr",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "auth_password",
|
||||
Value: "",
|
||||
Usage: "The password to visit the clone_addr",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "auth_token",
|
||||
Value: "",
|
||||
Usage: "The personal token to visit the clone_addr",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "owner_name",
|
||||
Value: "",
|
||||
Usage: "The data will be stored on a directory with owner name if not empty",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "repo_name",
|
||||
Value: "",
|
||||
Usage: "The data will be stored on a directory with repository name if not empty",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "units",
|
||||
Value: "",
|
||||
Usage: `Which items will be migrated, one or more units should be separated as comma.
|
||||
wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func runDumpRepository(stdCtx context.Context, ctx *cli.Command) error {
|
||||
setupConsoleLogger(log.INFO, log.CanColorStderr, os.Stderr)
|
||||
|
||||
// setting.DisableLoggerInit()
|
||||
setting.LoadSettings() // cannot access skip_tls_verify settings otherwise
|
||||
|
||||
stdCtx, cancel := installSignals(stdCtx)
|
||||
func runDumpRepository(ctx *cli.Context) error {
|
||||
stdCtx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(stdCtx); err != nil {
|
||||
|
|
@ -143,8 +135,8 @@ func runDumpRepository(stdCtx context.Context, ctx *cli.Command) error {
|
|||
opts.PullRequests = true
|
||||
opts.ReleaseAssets = true
|
||||
} else {
|
||||
units := strings.SplitSeq(ctx.String("units"), ",")
|
||||
for unit := range units {
|
||||
units := strings.Split(ctx.String("units"), ",")
|
||||
for _, unit := range units {
|
||||
switch strings.ToLower(strings.TrimSpace(unit)) {
|
||||
case "":
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
|
@ -20,25 +19,23 @@ import (
|
|||
"forgejo.org/modules/util"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// CmdEmbedded represents the available extract sub-command.
|
||||
func cmdEmbedded() *cli.Command {
|
||||
return &cli.Command{
|
||||
var (
|
||||
CmdEmbedded = &cli.Command{
|
||||
Name: "embedded",
|
||||
Usage: "Extract embedded resources",
|
||||
Description: "A command for extracting embedded resources, like templates and images",
|
||||
Commands: []*cli.Command{
|
||||
subcmdList(),
|
||||
subcmdView(),
|
||||
subcmdExtract(),
|
||||
Subcommands: []*cli.Command{
|
||||
subcmdList,
|
||||
subcmdView,
|
||||
subcmdExtract,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func subcmdList() *cli.Command {
|
||||
return &cli.Command{
|
||||
subcmdList = &cli.Command{
|
||||
Name: "list",
|
||||
Usage: "List files matching the given pattern",
|
||||
Action: runList,
|
||||
|
|
@ -50,10 +47,8 @@ func subcmdList() *cli.Command {
|
|||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func subcmdView() *cli.Command {
|
||||
return &cli.Command{
|
||||
subcmdView = &cli.Command{
|
||||
Name: "view",
|
||||
Usage: "View a file matching the given pattern",
|
||||
Action: runView,
|
||||
|
|
@ -65,10 +60,8 @@ func subcmdView() *cli.Command {
|
|||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func subcmdExtract() *cli.Command {
|
||||
return &cli.Command{
|
||||
subcmdExtract = &cli.Command{
|
||||
Name: "extract",
|
||||
Usage: "Extract resources",
|
||||
Action: runExtract,
|
||||
|
|
@ -97,9 +90,9 @@ func subcmdExtract() *cli.Command {
|
|||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var matchedAssetFiles []assetFile
|
||||
matchedAssetFiles []assetFile
|
||||
)
|
||||
|
||||
type assetFile struct {
|
||||
fs *assetfs.LayeredFS
|
||||
|
|
@ -107,7 +100,7 @@ type assetFile struct {
|
|||
path string
|
||||
}
|
||||
|
||||
func initEmbeddedExtractor(_ context.Context, c *cli.Command) error {
|
||||
func initEmbeddedExtractor(c *cli.Context) error {
|
||||
setupConsoleLogger(log.ERROR, log.CanColorStderr, os.Stderr)
|
||||
|
||||
patterns, err := compileCollectPatterns(c.Args().Slice())
|
||||
|
|
@ -122,32 +115,32 @@ func initEmbeddedExtractor(_ context.Context, c *cli.Command) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func runList(ctx context.Context, c *cli.Command) error {
|
||||
if err := runListDo(ctx, c); err != nil {
|
||||
func runList(c *cli.Context) error {
|
||||
if err := runListDo(c); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runView(ctx context.Context, c *cli.Command) error {
|
||||
if err := runViewDo(ctx, c); err != nil {
|
||||
func runView(c *cli.Context) error {
|
||||
if err := runViewDo(c); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runExtract(ctx context.Context, c *cli.Command) error {
|
||||
if err := runExtractDo(ctx, c); err != nil {
|
||||
func runExtract(c *cli.Context) error {
|
||||
if err := runExtractDo(c); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runListDo(ctx context.Context, c *cli.Command) error {
|
||||
if err := initEmbeddedExtractor(ctx, c); err != nil {
|
||||
func runListDo(c *cli.Context) error {
|
||||
if err := initEmbeddedExtractor(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -158,8 +151,8 @@ func runListDo(ctx context.Context, c *cli.Command) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func runViewDo(ctx context.Context, c *cli.Command) error {
|
||||
if err := initEmbeddedExtractor(ctx, c); err != nil {
|
||||
func runViewDo(c *cli.Context) error {
|
||||
if err := initEmbeddedExtractor(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -181,8 +174,8 @@ func runViewDo(ctx context.Context, c *cli.Command) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func runExtractDo(ctx context.Context, c *cli.Command) error {
|
||||
if err := initEmbeddedExtractor(ctx, c); err != nil {
|
||||
func runExtractDo(c *cli.Context) error {
|
||||
if err := initEmbeddedExtractor(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -278,7 +271,7 @@ func extractAsset(d string, a assetFile, overwrite, rename bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func collectAssetFilesByPattern(c *cli.Command, globs []glob.Glob, path string, layer *assetfs.Layer) {
|
||||
func collectAssetFilesByPattern(c *cli.Context, globs []glob.Glob, path string, layer *assetfs.Layer) {
|
||||
fs := assetfs.Layered(layer)
|
||||
files, err := fs.ListAllFiles(".", true)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ package forgejo
|
|||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
|
@ -17,14 +16,14 @@ import (
|
|||
"forgejo.org/modules/setting"
|
||||
private_routers "forgejo.org/routers/private"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func CmdActions(ctx context.Context) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "actions",
|
||||
Usage: "Commands for managing Forgejo Actions",
|
||||
Commands: []*cli.Command{
|
||||
Subcommands: []*cli.Command{
|
||||
SubcmdActionsGenerateRunnerToken(ctx),
|
||||
SubcmdActionsGenerateRunnerSecret(ctx),
|
||||
SubcmdActionsRegister(ctx),
|
||||
|
|
@ -37,7 +36,7 @@ func SubcmdActionsGenerateRunnerToken(ctx context.Context) *cli.Command {
|
|||
Name: "generate-runner-token",
|
||||
Usage: "Generate a new token for a runner to use to register with the server",
|
||||
Before: prepareWorkPathAndCustomConf(ctx),
|
||||
Action: RunGenerateActionsRunnerToken,
|
||||
Action: func(cliCtx *cli.Context) error { return RunGenerateActionsRunnerToken(ctx, cliCtx) },
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "scope",
|
||||
|
|
@ -53,7 +52,7 @@ func SubcmdActionsGenerateRunnerSecret(ctx context.Context) *cli.Command {
|
|||
return &cli.Command{
|
||||
Name: "generate-secret",
|
||||
Usage: "Generate a secret suitable for input to the register subcommand",
|
||||
Action: RunGenerateSecret,
|
||||
Action: func(cliCtx *cli.Context) error { return RunGenerateSecret(ctx, cliCtx) },
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -62,7 +61,7 @@ func SubcmdActionsRegister(ctx context.Context) *cli.Command {
|
|||
Name: "register",
|
||||
Usage: "Idempotent registration of a runner using a shared secret",
|
||||
Before: prepareWorkPathAndCustomConf(ctx),
|
||||
Action: RunRegister,
|
||||
Action: func(cliCtx *cli.Context) error { return RunRegister(ctx, cliCtx) },
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "secret",
|
||||
|
|
@ -102,35 +101,30 @@ func SubcmdActionsRegister(ctx context.Context) *cli.Command {
|
|||
Value: "",
|
||||
Usage: "version of the runner (not required since v1.21)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "ephemeral",
|
||||
Value: false,
|
||||
Usage: "instruct Forgejo to permanently unregister this runner after it has run one job",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func readSecret(ctx context.Context, cli *cli.Command) (string, error) {
|
||||
if cli.IsSet("secret") {
|
||||
return cli.String("secret"), nil
|
||||
func readSecret(ctx context.Context, cliCtx *cli.Context) (string, error) {
|
||||
if cliCtx.IsSet("secret") {
|
||||
return cliCtx.String("secret"), nil
|
||||
}
|
||||
if cli.IsSet("secret-stdin") {
|
||||
if cliCtx.IsSet("secret-stdin") {
|
||||
buf, err := io.ReadAll(ContextGetStdin(ctx))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
if cli.IsSet("secret-file") {
|
||||
path := cli.String("secret-file")
|
||||
if cliCtx.IsSet("secret-file") {
|
||||
path := cliCtx.String("secret-file")
|
||||
buf, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
return "", errors.New("at least one of the --secret, --secret-stdin, --secret-file options is required")
|
||||
return "", fmt.Errorf("at least one of the --secret, --secret-stdin, --secret-file options is required")
|
||||
}
|
||||
|
||||
func validateSecret(secret string) error {
|
||||
|
|
@ -144,18 +138,18 @@ func validateSecret(secret string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func getLabels(cli *cli.Command) (*[]string, error) {
|
||||
if !cli.Bool("keep-labels") {
|
||||
lblValue := strings.Split(cli.String("labels"), ",")
|
||||
func getLabels(cliCtx *cli.Context) (*[]string, error) {
|
||||
if !cliCtx.Bool("keep-labels") {
|
||||
lblValue := strings.Split(cliCtx.String("labels"), ",")
|
||||
return &lblValue, nil
|
||||
}
|
||||
if cli.String("labels") != "" {
|
||||
return nil, errors.New("--labels and --keep-labels should not be used together")
|
||||
if cliCtx.String("labels") != "" {
|
||||
return nil, fmt.Errorf("--labels and --keep-labels should not be used together")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func RunRegister(ctx context.Context, cli *cli.Command) error {
|
||||
func RunRegister(ctx context.Context, cliCtx *cli.Context) error {
|
||||
var cancel context.CancelFunc
|
||||
if !ContextGetNoInit(ctx) {
|
||||
ctx, cancel = installSignals(ctx)
|
||||
|
|
@ -167,18 +161,17 @@ func RunRegister(ctx context.Context, cli *cli.Command) error {
|
|||
}
|
||||
setting.MustInstalled()
|
||||
|
||||
secret, err := readSecret(ctx, cli)
|
||||
secret, err := readSecret(ctx, cliCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateSecret(secret); err != nil {
|
||||
return err
|
||||
}
|
||||
scope := cli.String("scope")
|
||||
name := cli.String("name")
|
||||
version := cli.String("version")
|
||||
ephemeral := cli.Bool("ephemeral")
|
||||
labels, err := getLabels(cli)
|
||||
scope := cliCtx.String("scope")
|
||||
name := cliCtx.String("name")
|
||||
version := cliCtx.String("version")
|
||||
labels, err := getLabels(cliCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -205,7 +198,7 @@ func RunRegister(ctx context.Context, cli *cli.Command) error {
|
|||
return err
|
||||
}
|
||||
|
||||
runner, err := actions_model.RegisterRunner(ctx, owner, repo, secret, labels, name, version, ephemeral)
|
||||
runner, err := actions_model.RegisterRunner(ctx, owner, repo, secret, labels, name, version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while registering runner: %v", err)
|
||||
}
|
||||
|
|
@ -216,16 +209,18 @@ func RunRegister(ctx context.Context, cli *cli.Command) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func RunGenerateSecret(ctx context.Context, cli *cli.Command) error {
|
||||
func RunGenerateSecret(ctx context.Context, cliCtx *cli.Context) error {
|
||||
runner := actions_model.ActionRunner{}
|
||||
runner.GenerateToken()
|
||||
if err := runner.GenerateToken(); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := fmt.Fprintf(ContextGetStdout(ctx), "%s", runner.Token); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func RunGenerateActionsRunnerToken(ctx context.Context, cli *cli.Command) error {
|
||||
func RunGenerateActionsRunnerToken(ctx context.Context, cliCtx *cli.Context) error {
|
||||
if !ContextGetNoInit(ctx) {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = installSignals(ctx)
|
||||
|
|
@ -234,7 +229,7 @@ func RunGenerateActionsRunnerToken(ctx context.Context, cli *cli.Command) error
|
|||
|
||||
setting.MustInstalled()
|
||||
|
||||
scope := cli.String("scope")
|
||||
scope := cliCtx.String("scope")
|
||||
|
||||
respText, extra := private.GenerateActionsRunnerToken(ctx, scope)
|
||||
if extra.HasError() {
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@
|
|||
package forgejo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"forgejo.org/services/context"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestActions_getLabels(t *testing.T) {
|
||||
|
|
@ -53,21 +54,21 @@ func TestActions_getLabels(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
flags := SubcmdActionsRegister(t.Context()).Flags
|
||||
flags := SubcmdActionsRegister(context.Context{}).Flags
|
||||
for _, c := range cases {
|
||||
t.Run(fmt.Sprintf("args: %v", c.args), func(t *testing.T) {
|
||||
// Create a copy of command to test
|
||||
var result *resultType
|
||||
app := cli.Command{}
|
||||
app := cli.NewApp()
|
||||
app.Flags = flags
|
||||
app.Action = func(_ context.Context, ctx *cli.Command) error {
|
||||
app.Action = func(ctx *cli.Context) error {
|
||||
labels, err := getLabels(ctx)
|
||||
result = &resultType{labels, err}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run it
|
||||
_ = app.Run(t.Context(), c.args)
|
||||
_ = app.Run(c.args)
|
||||
|
||||
// Test the results
|
||||
require.NotNil(t, result)
|
||||
|
|
|
|||
|
|
@ -20,30 +20,29 @@ import (
|
|||
f3_cmd "code.forgejo.org/f3/gof3/v3/cmd"
|
||||
f3_logger "code.forgejo.org/f3/gof3/v3/logger"
|
||||
f3_util "code.forgejo.org/f3/gof3/v3/util"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func CmdF3(ctx context.Context) *cli.Command {
|
||||
ctx = f3_logger.ContextSetLogger(ctx, util.NewF3Logger(nil, log.GetLogger(log.DEFAULT)))
|
||||
return &cli.Command{
|
||||
Name: "f3",
|
||||
Usage: "F3",
|
||||
Commands: []*cli.Command{
|
||||
Subcommands: []*cli.Command{
|
||||
SubcmdF3Mirror(ctx),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func SubcmdF3Mirror(ctx context.Context) *cli.Command {
|
||||
mirrorCmd := f3_cmd.CreateCmdMirror()
|
||||
mirrorCmd := f3_cmd.CreateCmdMirror(ctx)
|
||||
mirrorCmd.Before = prepareWorkPathAndCustomConf(ctx)
|
||||
f3Action := mirrorCmd.Action
|
||||
mirrorCmd.Action = func(ctx context.Context, cli *cli.Command) error {
|
||||
return runMirror(ctx, cli, f3Action)
|
||||
}
|
||||
mirrorCmd.Action = func(c *cli.Context) error { return runMirror(ctx, c, f3Action) }
|
||||
return mirrorCmd
|
||||
}
|
||||
|
||||
func runMirror(ctx context.Context, c *cli.Command, action cli.ActionFunc) error {
|
||||
func runMirror(ctx context.Context, c *cli.Context, action cli.ActionFunc) error {
|
||||
setting.LoadF3Setting()
|
||||
if !setting.F3.Enabled {
|
||||
return errors.New("F3 is disabled, it is not ready to be used and is only present for development purposes")
|
||||
|
|
@ -68,11 +67,9 @@ func runMirror(ctx context.Context, c *cli.Command, action cli.ActionFunc) error
|
|||
if err := models.Init(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx = f3_logger.ContextSetLogger(ctx, util.NewF3Logger(nil, log.GetLogger(log.DEFAULT)))
|
||||
}
|
||||
|
||||
err := action(ctx, c)
|
||||
err := action(c)
|
||||
if panicError, ok := err.(f3_util.PanicError); ok {
|
||||
log.Debug("F3 Stack trace\n%s", panicError.Stack())
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue